diff --git a/suixinkan/Features/TravelAlbum/AI修图接口文档.md b/suixinkan/Features/TravelAlbum/AI修图接口文档.md new file mode 100644 index 0000000..f80fb8f --- /dev/null +++ b/suixinkan/Features/TravelAlbum/AI修图接口文档.md @@ -0,0 +1,524 @@ +# AI 修图接口文档 + +## 接口列表 + +| # | 方法 | 路径 | 用途 | +|---|---|---|---| +| 1 | `GET` | `/api/yf-handset-app/photog/travel-album/material-list` | 获取相册网格素材和预览初始数据 | +| 2 | `GET` | `/api/yf-handset-app/photog/travel-album/material-info` | 获取/刷新当前原图及关联图 | +| 3 | `GET` | `/api/yf-handset-app/photog/travel-album/ai-retouch-options` | 获取 AI 模板、选择规则和剩余额度 | +| 4 | `POST` | `/api/yf-handset-app/photog/travel-album/ai-retouch` | 提交首次修图或重新修图任务 | +| 5 | `GET` | `/api/yf-handset-app/photog/travel-album/ai-retouch-job-info` | 查询 AI 修图任务进度和结果 | +| 6 | `POST` | `/api/yf-handset-app/photog/travel-album/delete-material` | 单张/批量删除素材及关联图 | + +## 接口详情 + +### 获取相册素材列表 + +```http +GET /api/yf-handset-app/photog/travel-album/material-list?user_equity_travel_id=88&limit=30&cursor=opaque_cursor&sort=created_at_desc +``` + +| 参数 | 必填 | 说明 | +|---|---|---| +| `user_equity_travel_id` | 是 | 相册 ID | +| `limit` | 否 | 默认 30,最大 100 | +| `cursor` | 否 | 服务端返回的不透明游标,首页不传 | +| `sort` | 否 | `created_at_desc` 或 `created_at_asc` | +| `material_type` | 否 | `original`、`cover`;不传表示全部 | + +响应: + +```json +{ + "code": 100000, + "msg": "success", + "data": { + "items": [ + { + "id": "2031", + "user_equity_travel_id": "88", + "material_type": "original", + "original_asset": { + "id": "asset_8001", + "url": "https://cdn.example.com/albums/88/IMG_1024.jpg", + "thumbnail_url": "https://cdn.example.com/albums/88/IMG_1024_thumb.jpg", + "file_name": "IMG_1024.jpg", + "mime_type": "image/jpeg", + "file_size": 4821931, + "width": 4032, + "height": 3024, + "version": 1, + "created_at": "2026-08-09T01:00:00.000Z" + }, + "display_status": "uploaded", + "variant_slots": [], + "revision": 7, + "created_at": "2026-08-09T01:00:00.000Z", + "updated_at": "2026-08-11T07:30:18.000Z" + } + ], + "next_cursor": "next_opaque_cursor", + "has_more": true + } +} +``` + +- 只返回网格顶层素材:原图和独立封面。 +- 原图如存在关联图,`variant_slots` 包含预览页 Tab 初始数据。 +- 建议使用游标分页,因为 AI 任务完成时可能实时插入封面素材。 + +### 获取/刷新单个原图项目 + +```http +GET /api/yf-handset-app/photog/travel-album/material-info?user_equity_travel_id=88&material_id=2031 +``` + +查询参数 `user_equity_travel_id` 和 `material_id` 均必填。响应的 `data` 直接返完整 `material_project`,字段与列表 Item 完全一致。该接口供预览页刷新和提交后定向更新使用。 + +如 `material_id` 已删除,返回 `404 MATERIAL_NOT_FOUND`。 + +### 获取 AI 修图选项 + +```http +GET /api/yf-handset-app/photog/travel-album/ai-retouch-options?user_equity_travel_id=88&scope=batch&source_count=4 +``` + +| `scope` | 使用场景 | +|---|---| +| `batch` | 网格多选 AI 修图 | +| `all_variants` | 预览页无 Tab,或当前是原图 Tab | +| `refined_only` | 当前是精修后 Tab | +| `atmosphere_only` | 当前是氛围感 Tab | + +`user_equity_travel_id`、`scope` 和 `source_count` 均必填。`source_count` 用于计算封面模板是否显示和必选。 + +响应: + +```json +{ + "code": 100000, + "msg": "success", + "data": { + "scope": "batch", + "source_count": 4, + "groups": [ + { + "type": "refined", + "title": "原图精修", + "required": true, + "selection_mode": "single", + "quota_cost": { + "mode": "per_source", + "units": 1 + }, + "templates": [ + { + "id": "tpl_refined_12", + "name": "清透精修", + "preview_url": "https://cdn.example.com/templates/refined_12.jpg", + "enabled": true + } + ] + }, + { + "type": "atmosphere", + "title": "氛围感修图", + "required": false, + "selection_mode": "single", + "quota_cost": { + "mode": "per_source", + "units": 1 + }, + "templates": [ + { + "id": "tpl_atmosphere_06", + "name": "暖阳", + "preview_url": "https://cdn.example.com/templates/atmosphere_06.jpg", + "enabled": true + } + ] + }, + { + "type": "cover", + "title": "封面风格模板", + "required": true, + "selection_mode": "single", + "quota_cost": { + "mode": "per_job", + "units": 0 + }, + "templates": [ + { + "id": "tpl_cover_03", + "name": "旅行画册", + "preview_url": "https://cdn.example.com/templates/cover_03.jpg", + "enabled": true + } + ] + } + ], + "quota": { + "unit": "retouch_time", + "remaining": 12, + "reserved": 3, + "display_text": "剩余 12 次", + "updated_at": "2026-08-11T07:29:58.000Z" + }, + "limits": { + "minimum_source_count": 1, + "maximum_source_count": 50, + "cover_minimum_source_count": 4 + } + } +} +``` + +设计要点: + +- 不再让 App 传 `scenic_id`,后端从相册归属景区获取可用模板。 +- 由后端返回 `required` 和可见分组,避免多端各写一套“4 张显示封面”规则。 +- 客户端可默认选中必选分组的第一个可用模板,可选分组默认不选。 +- 此接口用于 UI 配置;提交时后端仍必须根据真实素材 ID 重新校验。 +- 同一响应返回当前用户的剩余可用额度和每类输出的额度单价,弹窗无需再发起第二个额度请求。 +- `quota.remaining` 是扣除其他未完成任务已预占数量后、当前立即可用的额度;弹窗底部使用此值显示“剩余 N 次”。 +- 额度文案位于模板选择弹窗的固定底部区域,不随中间模板列表滚动。 +- `quota.reserved` 是当前用户其他未完成任务的预占额度,仅用于解释账户状态。 +- 客户端预估本次消耗时,对已选输出求和:`per_source` 类型为 `source_count * units`,`per_job` 类型为 `units`。封面始终为 0。 +- 本文档默认精修和氛围感每生成一张各消耗 1 次。如实际商业规则变更,后端只需调整 `quota_cost`,客户端不需改接口。 + +### 创建 AI 修图任务 + +```http +POST /api/yf-handset-app/photog/travel-album/ai-retouch +Idempotency-Key: 9A7820CE-78AB-45C6-9E0F-5DF268995379 +Content-Type: application/json +``` + +#### 5.4.1 网格批量修图 + +```json +{ + "user_equity_travel_id": "88", + "scope": "batch", + "source_material_ids": ["2031", "2032", "2033", "2034"], + "outputs": [ + { + "type": "refined", + "template_id": "tpl_refined_12" + }, + { + "type": "atmosphere", + "template_id": "tpl_atmosphere_06" + }, + { + "type": "cover", + "template_id": "tpl_cover_03" + } + ] +} +``` + +#### 5.4.2 预览页原图 Tab + +```json +{ + "user_equity_travel_id": "88", + "scope": "all_variants", + "source_material_ids": ["2031"], + "outputs": [ + { + "type": "refined", + "template_id": "tpl_refined_15" + }, + { + "type": "atmosphere", + "template_id": "tpl_atmosphere_09" + } + ] +} +``` + +如用户未选氛围感模板,`outputs` 中不传 `atmosphere`。缺失表示“本次不处理”,不表示删除现有氛围感图。 + +#### 5.4.3 只重新精修 + +```json +{ + "user_equity_travel_id": "88", + "scope": "refined_only", + "source_material_ids": ["2031"], + "outputs": [ + { + "type": "refined", + "template_id": "tpl_refined_18" + } + ] +} +``` + +#### 5.4.4 只重新生成氛围感 + +```json +{ + "user_equity_travel_id": "88", + "scope": "atmosphere_only", + "source_material_ids": ["2031"], + "outputs": [ + { + "type": "atmosphere", + "template_id": "tpl_atmosphere_11" + } + ] +} +``` + +#### 5.4.5 提交校验矩阵 + +| `scope` | 原图数 | 精修 | 氛围感 | 封面 | +|---|---:|---|---|---| +| `batch` | 1 至 50 | 必选 | 可选 | 少于 4 张禁止;4 张及以上必选 | +| `all_variants` | 必须为 1 | 必选 | 可选 | 禁止 | +| `refined_only` | 必须为 1 | 必选 | 禁止 | 禁止 | +| `atmosphere_only` | 必须为 1 | 禁止 | 必选 | 禁止 | + +另外: + +- `source_material_ids` 必须去重,且全部为 `original` 类型。 +- 模板必须启用、类型匹配,且适用于相册所属景区。 +- 后端必须使用提交时的最新额度计费规则重新计算消耗,不信任客户端的预估数值。 +- 封面输出不论首次生成、重试或任务结果如何,额度消耗始终为 0。 +- 每个输出槽位均采用 `replace_on_success`:成功后替换旧版本,失败保留旧版本。 +- 批量选中已有 AI 结果的原图时,允许重新生成,只覆盖本次 `outputs` 列出的类型。 +- 同一原图、同一槽位已有 `queued/processing` 任务时,整次请求返回 `409 RETOUCH_TARGET_BUSY`,并列出冲突项。 + +#### 5.4.6 提交响应 + +HTTP 状态码:`202 Accepted` + +```json +{ + "code": 100000, + "msg": "AI 修图任务已提交", + "data": { + "job": { + "id": "job_01K2E4", + "user_equity_travel_id": "88", + "scope": "all_variants", + "status": "queued", + "source_material_ids": ["2031"], + "progress": { + "total": 2, + "queued": 2, + "processing": 0, + "succeeded": 0, + "failed": 0 + }, + "created_at": "2026-08-11T07:30:15.123Z", + "started_at": null, + "finished_at": null + }, + "quota_reservation": { + "before_remaining": 12, + "reserved_units": 2, + "remaining_after_reservation": 10, + "cover_units": 0 + }, + "affected_projects": [ + { + "id": "2031", + "user_equity_travel_id": "88", + "material_type": "original", + "original_asset": { + "id": "asset_8001", + "url": "https://cdn.example.com/albums/88/IMG_1024.jpg", + "thumbnail_url": "https://cdn.example.com/albums/88/IMG_1024_thumb.jpg", + "file_name": "IMG_1024.jpg", + "mime_type": "image/jpeg", + "file_size": 4821931, + "width": 4032, + "height": 3024, + "version": 1, + "created_at": "2026-08-09T01:00:00.000Z" + }, + "display_status": "pending", + "variant_slots": [ + { + "type": "refined", + "asset": null, + "latest_generation": { + "job_id": "job_01K2E4", + "status": "queued", + "is_replacement": false, + "template": { + "id": "tpl_refined_15", + "name": "自然精修" + }, + "requested_at": "2026-08-11T07:30:15.123Z", + "started_at": null, + "finished_at": null, + "error": null + } + }, + { + "type": "atmosphere", + "asset": null, + "latest_generation": { + "job_id": "job_01K2E4", + "status": "queued", + "is_replacement": false, + "template": { + "id": "tpl_atmosphere_09", + "name": "暖阳" + }, + "requested_at": "2026-08-11T07:30:15.123Z", + "started_at": null, + "finished_at": null, + "error": null + } + } + ], + "revision": 8, + "created_at": "2026-08-09T01:00:00.000Z", + "updated_at": "2026-08-11T07:30:15.123Z" + } + ] + } +} +``` + +必须返回 `job.id`、`quota_reservation` 和更新后的 `affected_projects`。客户端用 `remaining_after_reservation` 立即更新剩余额度,并更新 Cell 状态;已有 `asset` 的重修 Tab 可同步显示处理态,首次生成且 `asset = null` 的槽位不显示 Tab。 + +### 查询 AI 修图任务 + +```http +GET /api/yf-handset-app/photog/travel-album/ai-retouch-job-info?user_equity_travel_id=88&job_id=job_01K2E4 +``` + +响应: + +```json +{ + "code": 100000, + "msg": "success", + "data": { + "id": "job_01K2E4", + "user_equity_travel_id": "88", + "scope": "batch", + "status": "partially_succeeded", + "source_material_ids": ["2031", "2032", "2033", "2034"], + "progress": { + "total": 9, + "queued": 0, + "processing": 0, + "succeeded": 8, + "failed": 1 + }, + "quota_settlement": { + "status": "settled", + "reserved_units": 8, + "consumed_units": 7, + "released_units": 1, + "cover_units": 0 + }, + "targets": [ + { + "source_material_id": "2031", + "output_type": "refined", + "status": "succeeded", + "result_asset_id": "asset_9201", + "error": null + }, + { + "source_material_id": "2032", + "output_type": "atmosphere", + "status": "failed", + "result_asset_id": null, + "error": { + "code": "AI_PROVIDER_TIMEOUT", + "message": "AI 服务处理超时,请重试", + "retryable": true + } + }, + { + "source_material_id": null, + "output_type": "cover", + "status": "succeeded", + "result_material_id": "cover_301", + "error": null + } + ], + "created_at": "2026-08-11T07:30:15.123Z", + "started_at": "2026-08-11T07:30:18.000Z", + "finished_at": "2026-08-11T07:33:00.000Z" + } +} +``` + +| 整体状态 | 含义 | +|---|---| +| `queued` | 已受理,尚无子任务开始 | +| `processing` | 至少一个子任务正在执行,且尚未全部完成 | +| `succeeded` | 所有子任务成功 | +| `partially_succeeded` | 子任务全部结束,且既有成功也有失败 | +| `failed` | 子任务全部结束,没有任何成功结果 | +| `canceled` | 任务因素材删除或管理操作被取消 | + +额度结算规则: + +- 任务提交成功时先预占所有非封面子任务的额度,封面不进入预占。 +- 每个精修/氛围感子任务成功后,将对应预占转为实际消耗。 +- 子任务失败或取消时释放对应预占。因此部分成功示例中,预占 8 次、成功消耗 7 次、失败退回 1 次。 +- `quota_settlement.status` 取值为 `reserved | partially_settled | settled`。 + +客户端如只关心当前预览图,优先调用单素材接口;本接口主要用于批量进度、问题排查和任务详情。 + +### 批量删除素材 + +```http +POST /api/yf-handset-app/photog/travel-album/delete-material +Content-Type: application/json +``` + +请求: + +```json +{ + "user_equity_travel_id": "88", + "material_ids": ["2031", "2032", "cover_301"] +} +``` + +为兼容旧客户端,原单张请求体继续有效: + +```json +{ + "id": "2031" +} +``` + +新请求使用 `user_equity_travel_id + material_ids`,旧请求使用 `id`,两组参数不同时传入。 + +响应: + +```json +{ + "code": 100000, + "msg": "删除成功", + "data": { + "deleted_material_ids": ["2031", "2032", "cover_301"], + "deleted_variant_asset_ids": ["asset_9101", "asset_9102"], + "canceled_job_ids": ["job_01K2E4"], + "oss_cleanup_status": "scheduled" + } +} +``` + +语义: + +- 传入原图 ID:级联软删除精修图、氛围感图,并取消该原图的未完成子任务。 +- 传入独立封面 ID:只删除该封面素材。 +- 删除原图不级联删除以它为输入之一的已生成封面。 +- 数据库业务删除采用一个事务,默认全部成功或全部失败。 +- 任一 ID 不属于当前相册或无权删除时,整次请求失败。已删除的 ID 可视为幂等成功。 +- OSS 文件清理异步执行,不影响业务删除成功。 diff --git a/suixinkan/Features/TravelAlbum/AI修图需求.md b/suixinkan/Features/TravelAlbum/AI修图需求.md new file mode 100644 index 0000000..a5ef74f --- /dev/null +++ b/suixinkan/Features/TravelAlbum/AI修图需求.md @@ -0,0 +1,168 @@ +# AI 修图需求文档 + +> 本文档描述旅拍相册中 AI 修图的产品需求和业务规则。后端接口建议见同目录《AI 修图接口文档》。 + +## 1. 目标与范围 + +- 相册管理页面展示用户已在其他流程上传到 OSS、并已在后端登记的图片素材。 +- 用户可从网格页批量发起 AI 修图,也可从单图预览页首次修图或重新修图。 +- 精修图和氛围感图是某张原图的“关联图”;封面图是相册内的独立素材,不归属于任何一张原图。 +- AI 生成为异步长流程,页面必须能分辨已提交、排队、处理、成功和失败。 + +## 2. 核心概念 + +### 2.1 原图项目 + +一张原图与它的精修图、氛围感图共同组成一个原图项目。原图的素材 ID 是项目的稳定标识,OSS URL 不作为业务标识。 + +### 2.2 关联图 + +- `refined`:精修图,一张原图最多保留一个当前有效版本。 +- `atmosphere`:氛围感图,一张原图最多保留一个当前有效版本。 +- 关联图不作为独立 Cell 出现在相册网格中,只在该原图的预览页 Tab 中展示。 + +### 2.3 封面图 + +- 一次选择不少于 4 张原图时,必须选择封面模板。 +- 封面模板使用本次选中的多张原图生成封面图。 +- 封面图不关联某张原图,生成成功后作为 `cover` 类型的独立素材出现在相册网格中。 +- 删除参与生成封面的原图时,不级联删除已生成的独立封面图。 + +## 3. 相册管理页 + +### 3.1 网格列表 + +- 网格展示原图和独立封面图,不直接展示精修图、氛围感图。 +- 列表需支持稳定分页。AI 任务执行期间可能插入新的封面素材,不应造成重复或漏项。 +- 点击原图 Cell 进入图片预览页,可横向滑动切换其他原图项目。 + +### 3.2 Cell 修图状态 + +Cell 左上角展示稳定的修图状态: + +| 状态 | 含义 | +|---|---| +| 已上传 | 原图已登记,从未提交 AI 修图,当前也没有任务 | +| 待处理 | AI 任务已提交,正在排队 | +| 修图中 | AI 任务已开始执行 | +| AI 已修 | 最新任务已成功,存在有效 AI 结果 | +| 失败 | 最新任务执行失败,可能仍保留上一版成功结果 | + +状态以后端返回的枚举代码为准,客户端自行映射中文文案,不依赖可变的中文状态名。 + +### 3.3 批量 AI 修图 + +1. 用户进入多选模式并选择一张或多张原图。 +2. 点击底部“AI 修图”,弹出模板选择页。 +3. 精修模板必选且单选。 +4. 氛围感模板可选且单选,已选时可取消。 +5. 选中原图数量小于 4 时不展示封面模板。 +6. 选中原图数量大于等于 4 时展示封面模板,且必须单选一个。 +7. 提交后,每张原图分别生成精修图,选了氛围感模板时再分别生成氛围感图。 +8. 选了封面模板时,整个批次额外生成独立封面图。 +9. 提交成功指后端已受理任务,不代表图片已生成。 + +### 3.4 修图额度 + +- 模板选择弹窗的底部固定显示当前用户可用的 AI 修图剩余额度,文案例如“剩余 12 次”,不随模板列表滚动消失。 +- 精修和氛围感生成会消耗修图额度;封面模板生成不消耗任何修图额度。 +- 本需求默认按生成结果张数计费:每张原图的精修结果消耗 1 次,每张原图的氛围感结果消耗 1 次,封面结果消耗 0 次。最终单价由后端返回,客户端不硬编码。 +- 重新修图与首次修图使用相同的额度规则。 +- 后端受理任务时预占本次所需额度,避免用户并发提交造成超额。 +- 只对最终生成成功的精修/氛围感结果扣减额度;失败或取消的子任务释放对应预占额度。 +- 如剩余可用额度不足,提交按钮禁用并显示明确提示;后端提交接口仍必须做最终校验。 + +### 3.5 批量删除 + +- 用户多选原图后点击“删除”,删除选中原图及各自的精修图、氛围感图,并取消关联的未完成任务。已有任务记录保留用于审计和排查,但不再对客户端展示已删除素材。 +- 已生成的独立封面图不在级联删除范围内,除非用户明确选中该封面素材本身。 +- 批量删除应只调用一次后端接口,后端应返回整体或逐项结果,不由客户端循环调用单删接口。 + +## 4. 图片预览页 + +### 4.1 横向切换 + +- 从网格点击某张原图进入预览页。 +- 横向滑动在相册内的不同原图项目之间切换。 +- 切换到新的原图项目时,默认显示其“原图”。 + +### 4.2 底部操作 + +页面底部固定显示三个按钮: + +- AI 修图 +- 删除 +- 刷新 + +### 4.3 关联图 Tab + +- 如果当前原图没有任何已生成的关联图,不显示 Tab;首次生成任务排队或处理期间仍不显示。 +- 存在至少一张已成功的关联图时显示 Tab。 +- Tab 最多包含“原图”、“精修后”、“氛围感”;关联图 Tab 只在对应图片已生成时显示。 +- 点击 Tab 切换当前原图项目内的展示图片,不切换原图项目。 +- 某个已有结果的 Tab 重新提交修图后,仍可展示上一版图片,同时通过该结果槽位的任务状态展示“待处理/修图中/失败”。 +- 首次生成尚无图片 URL 时,通过原图 Cell 状态和项目任务状态表达进度,不创建空 Tab。 + +### 4.4 预览页删除 + +- 删除的对象始终是当前原图项目,而不是当前 Tab 上的单张关联图。 +- 删除原图及其所有关联图,并取消关联的未完成任务;历史任务记录保留用于审计。 +- 删除成功后显示下一个原图项目;如果删除的是末项,则回退上一项;无剩余项时关闭预览页。 + +### 4.5 预览页 AI 修图 + +| 当前情况 | 模板要求 | 生成与覆盖规则 | +|---|---|---| +| 不显示 Tab | 精修必选,氛围感可选 | 首次生成关联的精修图,可选生成氛围感图 | +| 已显示 Tab,当前为“原图” | 精修必选,氛围感可选 | 重新生成精修图;如选氛围感则也重新生成。新结果成功后原子替换对应旧结果 | +| 已显示 Tab,当前为“精修后” | 精修必选 | 只重新生成精修图,成功后覆盖旧精修图,不影响氛围感图 | +| 已显示 Tab,当前为“氛围感” | 氛围感必选 | 只重新生成氛围感图,成功后覆盖旧氛围感图,不影响精修图 | + +重新修图期间应保留上一版成功图片可见;只有新结果成功时才原子替换当前版本。失败时继续保留旧版本,并返回可展示的错误信息。 + +### 4.6 刷新 + +- 点击“刷新”只获取当前显示原图项目的最新数据,包括原图、关联图、各结果槽位的任务状态和整体修图状态。 +- 不应为刷新一张图重新拉取整个分页列表。 +- 服务端对已替换的图片应返回新的资源版本或新 URL,避免 CDN/客户端缓存继续显示旧图。 + +## 5. 异步任务规则 + +- 每次提交返回唯一 `job_id`、服务端接收时间和当前状态。 +- 每次提交同时返回额度预占数、提交前剩余额度和预占后可用额度。 +- 任务状态至少包含:`queued`、`processing`、`succeeded`、`partially_succeeded`、`failed`、`canceled`。 +- 精修、氛围感和封面子任务可以独立成功或失败,服务端需保留逐原图、逐结果类型的状态。 +- 客户端重试同一次提交时不得创建重复任务,由幂等键保证。 +- 同一原图、同一结果类型存在未完成任务时,服务端拒绝再次提交,避免旧任务晚完成后覆盖新结果。 + +## 6. 删除与数据一致性 + +- 删除原图项目需在一个后端业务操作中完成:标记原图删除、标记关联图删除、取消未完成任务。 +- 数据库状态成功后再异步清理 OSS 文件,不应因 OSS 删除失败导致客户端删除失败。 +- AI 回调必须检查任务和素材是否已删除或已被新版本替代,过期回调不得恢复已删除素材或覆盖新结果。 + +## 7. 异常与提示 + +- 模板加载失败:保留页面并允许重试。 +- 模板已下线:提交时返回明确错误码,客户端刷新模板列表。 +- 素材不存在、不属于当前相册或不是原图:整次提交不受理,并返回问题素材 ID。 +- 数量不满足封面规则:返回稳定错误码和最小数量。 +- 任务部分成功:保留已成功结果,失败槽位提供错误码、错误信息和是否可重试。 +- 网络超时:客户端使用原幂等键重试,不可产生两个任务。 + +## 8. 权限与校验 + +- 后端必须从登录态校验用户对相册、原图和模板的访问权,不信任客户端传入的用户 ID、景区 ID 或批次 ID。 +- 模板是否适用于相册所属景区,由后端根据相册 ID 推导和校验。 +- 后端负责校验精修、氛围感和封面模板的必选/可选规则,客户端校验只用于交互提示。 + +## 9. 默认产品决策 + +为使接口和异步覆盖行为可实现,本文档采用以下默认决策: + +1. 重新修图不先删旧图;新图成功后才原子切换,失败仍保留旧图。 +2. 在“原图” Tab 重新修图时,若用户未选氛围感模板,保留现有氛围感图,只替换精修图。 +3. 封面图只在生成成功后插入网格;生成前由所选原图的修图状态和任务详情表达进度。 +4. 每个关联图类型只对外暴露一个当前有效版本;历史版本是否长期保留属于后端存储策略,不在当前客户端功能中展示。 + +> 第 2 条是对“氛围感模板可选”的安全解释:未选不等于删除旧结果。如产品希望未选时删除旧氛围感图,应增加明确的“移除氛围感”操作,不建议让后端根据字段缺失隐式删除。