4 Commits
Author SHA1 Message Date
han xin e1175066b9 删除没用的卡片 2026-08-24 11:05:51 +08:00
han xin 5cf4409bad 新增线下收款记录和 ai 修图优化 2026-08-21 15:51:52 +08:00
lujiuyin 6fe9928b49 Merge branch 'dev_1_2_1' into xh_test 2026-08-03 10:48:41 +08:00
XHYourName 6129a1632f 新增ai修图 2026-07-31 16:48:28 +08:00
220 changed files with 8358 additions and 24218 deletions
+45
View File
@@ -0,0 +1,45 @@
# 注销账号页 Design QA
## 对比目标
- source visual truth path: `/Users/hxindemacmini/.codex/generated_images/01a01eb7-eda7-7f73-ae64-60c5195c974a/exec-1ece6a7c-2a8f-4d8e-a33f-aa5a4fc11c80.png`
- implementation screenshot path: `/Users/hxindemacmini/.codex/visualizations/2026/08/20/01a01eb7-eda7-7f73-ae64-60c5195c974a/account-deletion-implementation-v2.png`
- full-view comparison path: `/Users/hxindemacmini/.codex/visualizations/2026/08/20/01a01eb7-eda7-7f73-ae64-60c5195c974a/account-deletion-comparison-v2.png`
- viewport: iPhone 17 Pro 模拟器,402 × 874 pt,竖屏,浅色模式
- source pixels: 852 × 1846,按 @2x 归一化为约 426 × 923 pt
- implementation pixels: 1206 × 2622,@3x,对应 402 × 874 pt
- density normalization: 对比图将实现截图归一化到 852 × 1846,与设计图等像素并排比较
- state: 资产核验加载完成、确认框未勾选、主按钮可点击
## Full-view comparison evidence
信息层级、红色风险提示、四项资产卡片、资产明细入口、注销后果卡片、确认框和蓝色主按钮均与设计方向一致。实现保留 iOS 原生状态栏和安全区;设计图没有状态栏且画布更高,因此较小设备首屏只展示注销后果卡片的前两项,剩余内容通过原生滚动区域查看,底部确认和主按钮始终可操作。
## Focused region comparison evidence
无需额外局部裁图。等尺寸并排图中,标题、资产数字、分割线、风险色、图标、确认框和主按钮均清晰可读,已经足以核对字体、间距、颜色、图标和文案。
## Findings
- 无可执行的 P0、P1 或 P2 差异。
- 字体与排版:使用系统中文字体;标题、资产名、资产值和说明文本的层级、字重与设计一致,无截断。
- 间距与布局:16pt 页面边距、12pt 圆角、资产行节奏和卡片间距一致;原生状态栏及较短屏幕导致后果列表需要滚动,属于平台和视口差异。
- 颜色与视觉令牌:使用项目现有的页面背景、主色、危险色和边框令牌,语义与设计一致。
- 图片与图标:页面没有照片类素材;风险和后果图标使用 SF Symbols,清晰且风格统一。
- 文案与内容:资产名称、数值、注销后果和7天恢复提示与确认稿一致;没有“可放弃”标签。
## Comparison history
1. 第一轮证据:`account-deletion-comparison-v1.png`。发现 [P2] 设计图在未勾选状态仍显示蓝色主按钮,而实现显示灰色禁用按钮。
2. 修正:将主按钮的视觉可点击状态与业务确认校验分离。资产加载完成后按钮保持蓝色;未勾选时点击会提示先确认,不能进入验证页。
3. 第二轮证据:`account-deletion-comparison-v2.png`。按钮状态已与设计一致,勾选保护由单元测试验证,无新增 P0/P1/P2 差异。
## Implementation checklist
- [x] 保持资产数据中性展示,不出现“可放弃”标签
- [x] 保持未勾选时蓝色主按钮视觉
- [x] 未勾选时阻止进入验证流程并提示用户
- [x] 小屏幕使用滚动区域,底部确认与主操作常驻安全区
- [x] 保留原生 iOS 状态栏、导航栏和动态岛安全区
final result: passed
Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 961 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 905 KiB

-53
View File
@@ -1,53 +0,0 @@
# 9.7 线下收款接口调整
本次仅调整以下两个接口,路径保持不变:
- `GET /api/yf-handset-app/photog/offline-pay-collect/statistics`
- `GET /api/yf-handset-app/photog/offline-pay-collect/details`
## 1. statistics
请求:`scenic_id`
在现有 `pending` 中增加所有未补缴日期,按日期升序:
```json
"dates": [
{ "date": "2026-08-23", "unpaid_amount": "230.00", "unpaid_count": 2 }
]
```
- 包含今天及历史所有仍有待补缴数据的日期。
- `date_count`、待补缴总金额和总笔数必须与 `dates` 汇总一致。
- `today.date` 按 `Asia/Shanghai` 返回当前营业日,客户端以此限制未来日期。
## 2. details
请求改为:`scenic_id + date`,删除 `page/page_size`。
`data` 直接返回单日对象:
```json
{
"date": "2026-08-24",
"total_amount": "350.00",
"collect_count": 3,
"paid_amount": "100.00",
"paid_count": 1,
"unpaid_amount": "250.00",
"unpaid_count": 2,
"status": 0,
"status_text": "本日未结清",
"collects": []
}
```
- 无数据日期也返回成功、零值汇总和空 `collects`。
- 明细按登记时间倒序,再按 `collect_no` 保证稳定顺序。
## 不调整的接口
- `POST /register`:继续只接收 `scenic_id/amount/pay_method`,响应保持现状。
- `POST /supplement`:继续只接收 `scenic_id/date`,响应保持现状。
客户端不会向这两个接口传入 `request_id`、补缴金额快照或补缴笔数快照,也不依赖新增错误码。
-282
View File
@@ -1,282 +0,0 @@
# AI 重新修图接口优化需求
## 1. 背景
图片预览页面支持用户在“原图”Tab 上重新选择修图模板。
用户可能只生成过一种关联图片,例如之前仅生成了“原图精修”,尚未生成“氛围感”图片。此时用户重新修图时,可以同时选择:
- 原图精修模板
- 氛围感修图模板
客户端会调用现有 `ai-reretouch` 接口,并通过 `type = 3` 提交两个模板,希望一次创建两个修图输出任务。
## 2. 问题复现
### 前置状态
- 原图素材 ID:`702`
- 原 AI 修图批次 ID:`61`
- 已存在并完成:原图精修
- 从未生成:氛围感图片
### 请求
```http
POST /api/yf-handset-app/photog/travel-album/ai-reretouch
```
```json
{
"ai_retouch_batch_id": 61,
"id": 702,
"type": 3,
"refined_template_id": 2,
"atmosphere_template_id": 14
}
```
字段含义:
| 字段 | 值 | 说明 |
| --- | ---: | --- |
| `ai_retouch_batch_id` | `61` | 原 AI 修图批次 ID |
| `id` | `702` | 原图素材 ID |
| `type` | `3` | 同时处理精修和氛围感 |
| `refined_template_id` | `2` | 本次选择的精修模板 |
| `atmosphere_template_id` | `14` | 本次选择的氛围感模板 |
### 当前响应
```json
{
"code": 100099,
"data": [],
"msg": "仅已完成修图的照片可重新修图",
"time": "2026-08-14 17:41:17"
}
```
## 3. 当前问题
从响应判断,后端可能在 `type = 3` 时要求精修图和氛围感图都已经存在且完成,才允许重新修图。
但本场景中:
- 精修图已经存在,本次需要重新生成,并在成功后覆盖旧精修图。
- 氛围感图从未生成,本次需要创建新的关联图片。
由于氛围感图历史上不存在,接口拒绝了整个请求,导致客户端无法通过一次请求同时完成“覆盖已有精修图”和“新增氛围感图”。
客户端当前请求已经正确传递 `type = 3` 和两个模板 ID,无需修改请求结构。
## 4. 期望接口行为
建议将 `ai-reretouch` 调整为按输出类型分别执行的 **upsert(存在则覆盖,不存在则新增)** 语义。
对于本次请求:
### 原图精修
- 请求传入了 `refined_template_id`。
- 旧精修图已经存在。
- 后端创建新的精修任务。
- 新任务成功后覆盖旧精修图。
- 新任务处理中或失败时保留旧精修图。
### 氛围感修图
- 请求传入了 `atmosphere_template_id`。
- 旧氛围感图不存在。
- 后端创建新的氛围感任务。
- 任务成功后,将结果保存为原图的新关联图片。
### 整体任务
- 一次请求创建两个输出任务:`refined` 和 `atmosphere`。
- 按两个实际创建的输出预占 2 张修图额度。
- 两个输出分别执行、分别记录状态、分别结算额度。
- 单个输出失败不应影响另一个已经成功的输出。
## 5. 建议的后端处理规则
### 5.1 基础校验
建议校验:
- 原图素材存在。
- 原图素材属于指定相册或批次。
- 原图当前状态允许提交 AI 修图任务。
- 请求中至少传入一个与 `type` 匹配的有效模板 ID。
- 用户剩余额度满足本次实际创建的输出数量。
不建议校验:
- `type = 3` 时要求精修和氛围感历史结果都必须存在。
- 某一种关联图从未生成时直接拒绝整个请求。
### 5.2 按模板字段创建任务
后端根据本次实际传入的模板字段创建对应任务:
| 请求情况 | 期望处理 |
| --- | --- |
| 只传 `refined_template_id` | 只创建精修任务 |
| 只传 `atmosphere_template_id` | 只创建氛围感任务 |
| 两个模板 ID 都传 | 同时创建精修和氛围感任务 |
| 某个模板 ID 未传 | 不处理、不覆盖该类型的已有结果 |
### 5.3 按输出类型处理已有结果
对于每个实际提交的输出类型,分别判断:
| 历史状态 | 建议处理 |
| --- | --- |
| 已存在成功结果 | 创建重修任务,新结果成功后替换旧结果 |
| 从未生成 | 创建新任务,成功后新增关联图片 |
| 历史任务失败或已取消 | 允许重新创建任务 |
| 当前已有处理中或排队任务 | 拒绝该类型重复提交,或通过幂等机制复用已有任务 |
### 5.4 替换时机
继续采用现有 `replace_on_success` 语义:
- 新结果成功后,才替换同类型旧结果。
- 新任务排队、处理中或失败时,保留旧结果。
- 精修和氛围感分别替换,互不影响。
## 6. `type` 参数建议语义
保持现有请求类型和接口路径不变:
| `type` | 语义 | 模板规则 |
| ---: | --- | --- |
| `1` | 只处理精修 | 必须传 `refined_template_id`,忽略氛围感模板 |
| `2` | 只处理氛围感 | 必须传 `atmosphere_template_id`,忽略精修模板 |
| `3` | 同时处理精修和氛围感 | 根据实际传入的两个模板分别创建任务 |
对于 `type = 3`:
- 必须传 `refined_template_id`。
- `atmosphere_template_id` 可以选传。
- 传入氛围感模板时创建氛围感任务。
- 未传氛围感模板时,只重修精修图,并保留已有氛围感图。
## 7. 建议处理伪代码
```text
validateOriginalMaterial(request.id)
validateBatch(request.ai_retouch_batch_id)
outputs = []
if request.refined_template_id is not null:
outputs.add(
createTask(
outputType = refined,
templateId = request.refined_template_id,
mode = existingRefinedResult ? replace_on_success : create
)
)
if request.atmosphere_template_id is not null:
outputs.add(
createTask(
outputType = atmosphere,
templateId = request.atmosphere_template_id,
mode = existingAtmosphereResult ? replace_on_success : create
)
)
if outputs is empty:
return invalid_template_error
reserveQuota(outputs.count)
submitTasks(outputs)
```
注意:实际是否存在旧结果,应按照各自的 `output_type` 独立查询,不能用“两个类型都已完成”作为 `type = 3` 的整体前置条件。
## 8. 期望响应
接口成功响应建议继续返回任务批次信息,并明确本次创建的输出类型。例如:
```json
{
"code": 100000,
"data": {
"ai_retouch_batch_id": 61,
"source_material_id": 702,
"outputs": [
{
"type": "refined",
"mode": "replace_on_success",
"status": "queued"
},
{
"type": "atmosphere",
"mode": "create",
"status": "queued"
}
],
"reserved_units": 2
},
"msg": "AI修图任务已提交"
}
```
响应结构可沿用现有实现,不强制增加上述字段;关键要求是一次请求能够成功创建两个对应的修图任务。
## 9. 验收用例
### 用例一:只有旧精修图,同时提交两个模板
- 历史状态:精修已完成,氛围感不存在。
- 请求:`type = 3`,同时传精修和氛围感模板。
- 期望:创建两个任务;精修成功后覆盖,氛围感成功后新增。
### 用例二:只有旧氛围感图,同时提交两个模板
- 历史状态:精修不存在,氛围感已完成。
- 请求:`type = 3`,同时传精修和氛围感模板。
- 期望:创建两个任务;精修成功后新增,氛围感成功后覆盖。
### 用例三:两种旧结果都存在
- 历史状态:精修、氛围感都已完成。
- 请求:`type = 3`,同时传两个模板。
- 期望:创建两个任务,分别在成功后覆盖各自旧结果。
### 用例四:原图 Tab 只选择精修模板
- 历史状态:精修、氛围感均可能存在。
- 请求:`type = 3`,只传 `refined_template_id`。
- 期望:只创建精修任务;不处理、不删除、不覆盖旧氛围感图;预占 1 张额度。
### 用例五:精修后 Tab 重新修图
- 请求:`type = 1`,只传 `refined_template_id`。
- 期望:只创建精修任务,新精修成功后覆盖旧精修图。
### 用例六:氛围感 Tab 重新修图
- 请求:`type = 2`,只传 `atmosphere_template_id`。
- 期望:只创建氛围感任务,新氛围感成功后覆盖旧氛围感图。
### 用例七:其中一个输出失败
- 请求创建精修和氛围感两个任务。
- 精修成功,氛围感失败。
- 期望:保留成功的新精修结果;氛围感旧图存在时继续保留旧图,不存在时不生成关联图;额度分别结算。
### 用例八:防止重复提交
- 同一原图、同一输出类型已经存在排队中或处理中的任务。
- 用户再次提交相同任务。
- 期望:通过幂等机制返回原任务,或仅拒绝重复的输出类型,避免重复扣减额度和重复生成结果。
## 10. 总结
希望 `ai-reretouch` 支持以下统一语义:
> 对本次传入模板对应的每种输出独立处理:已有结果则在新任务成功后覆盖,没有结果则新增;未传模板的类型不处理。不要因为某一种关联图历史上从未生成,就拒绝整个重新修图请求。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="430" viewBox="0 0 1200 430">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
</marker>
<style>
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
.active { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
.action { fill: #F6F8FB; stroke: #D5DEE9; stroke-width: 2; }
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
</style>
</defs>
<rect width="1200" height="430" rx="24" fill="#F7F9FC"/>
<text x="42" y="52" class="title">照片预览与左右切换</text>
<rect x="42" y="92" width="205" height="104" rx="16" class="box"/>
<text x="144" y="126" text-anchor="middle" class="label">相册管理缩略图</text>
<text x="144" y="158" text-anchor="middle" class="small">点击任意照片</text>
<path d="M247 144 H326" class="line"/>
<rect x="326" y="78" width="300" height="132" rx="18" class="active"/>
<text x="476" y="115" text-anchor="middle" class="label">单一返回按钮 + 文件信息</text>
<text x="476" y="146" text-anchor="middle" class="small">文件名 / 文件大小 / 当前照片</text>
<text x="476" y="176" text-anchor="middle" class="small">底部:AI修图、删除、刷新</text>
<path d="M626 144 H706" class="line"/>
<rect x="706" y="78" width="205" height="132" rx="18" class="box"/>
<text x="808" y="115" text-anchor="middle" class="label">左右滑动</text>
<text x="808" y="146" text-anchor="middle" class="small">切换上一张 / 下一张</text>
<text x="808" y="176" text-anchor="middle" class="small">首尾不循环</text>
<path d="M911 144 H990" class="line"/>
<rect x="990" y="92" width="168" height="104" rx="16" class="box"/>
<text x="1074" y="126" text-anchor="middle" class="label">同步刷新信息</text>
<text x="1074" y="158" text-anchor="middle" class="small">名称、大小、结果状态</text>
<rect x="42" y="258" width="344" height="112" rx="16" class="action"/>
<text x="214" y="292" text-anchor="middle" class="label">删除当前照片</text>
<text x="214" y="323" text-anchor="middle" class="small">删除后定位到相邻照片</text>
<text x="214" y="349" text-anchor="middle" class="small">最后一张删除后返回相册管理</text>
<rect x="428" y="258" width="344" height="112" rx="16" class="action"/>
<text x="600" y="292" text-anchor="middle" class="label">刷新当前照片</text>
<text x="600" y="323" text-anchor="middle" class="small">重新加载原图或最新修图结果</text>
<text x="600" y="349" text-anchor="middle" class="small">加载失败保留当前可见内容</text>
<rect x="814" y="258" width="344" height="112" rx="16" class="action"/>
<text x="986" y="292" text-anchor="middle" class="label">预览底部操作</text>
<text x="986" y="323" text-anchor="middle" class="small">保留 AI修图、删除、刷新</text>
<text x="986" y="349" text-anchor="middle" class="small">不展示打印按钮</text>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="430" viewBox="0 0 1200 430">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
</marker>
<style>
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
.active { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
.danger { fill: #FFF1F1; stroke: #E35D6A; stroke-width: 2; }
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
</style>
</defs>
<rect width="1200" height="430" rx="24" fill="#F7F9FC"/>
<text x="42" y="52" class="title">相册管理多选操作栏</text>
<rect x="42" y="92" width="215" height="110" rx="16" class="box"/>
<text x="149" y="127" text-anchor="middle" class="label">普通浏览状态</text>
<text x="149" y="160" text-anchor="middle" class="small">底部显示“上传照片”</text>
<path d="M257 147 H337" class="line"/>
<rect x="337" y="92" width="215" height="110" rx="16" class="active"/>
<text x="444" y="127" text-anchor="middle" class="label">点击右上角选择</text>
<text x="444" y="160" text-anchor="middle" class="small">缩略图出现单选圆圈</text>
<path d="M552 147 H632" class="line"/>
<rect x="632" y="92" width="225" height="110" rx="16" class="active"/>
<text x="744" y="127" text-anchor="middle" class="label">进入多选状态</text>
<text x="744" y="160" text-anchor="middle" class="small">上传按钮隐藏</text>
<text x="744" y="184" text-anchor="middle" class="small">显示 AI修图、删除</text>
<path d="M857 147 H937" class="line"/>
<rect x="937" y="92" width="221" height="110" rx="16" class="box"/>
<text x="1048" y="127" text-anchor="middle" class="label">选择至少 1 张</text>
<text x="1048" y="160" text-anchor="middle" class="small">两个操作按钮恢复可用</text>
<rect x="140" y="268" width="310" height="110" rx="16" class="active"/>
<text x="295" y="304" text-anchor="middle" class="label">点击 AI修图</text>
<text x="295" y="336" text-anchor="middle" class="small">进入批量 AI 修图弹窗</text>
<text x="295" y="360" text-anchor="middle" class="small">包含处理中照片时按钮置灰</text>
<rect x="750" y="268" width="310" height="110" rx="16" class="danger"/>
<text x="905" y="304" text-anchor="middle" class="label">点击删除</text>
<text x="905" y="336" text-anchor="middle" class="small">二次确认后删除选中素材</text>
<text x="905" y="360" text-anchor="middle" class="small">取消确认则保持原选择</text>
<path d="M744 202 V235 H295 V268" class="line"/>
<path d="M744 202 V235 H905 V268" class="line"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="300" viewBox="0 0 1080 300">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#7A8699"/>
</marker>
<style>
.title { font: 600 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #1F2937; }
.detail { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #667085; }
.box { fill: #FFFFFF; stroke: #D9E2EF; stroke-width: 2; rx: 16; }
.active { fill: #EEF5FF; stroke: #1677FF; stroke-width: 2; rx: 16; }
</style>
</defs>
<rect width="1080" height="300" fill="#F7F9FC"/>
<rect class="box" x="40" y="104" width="180" height="92"/>
<text class="title" x="130" y="142" text-anchor="middle">相册管理</text>
<text class="detail" x="130" y="170" text-anchor="middle">点击上传照片</text>
<line x1="220" y1="150" x2="292" y2="150" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
<rect class="active" x="305" y="70" width="230" height="160"/>
<text class="title" x="420" y="108" text-anchor="middle">选择传输模式</text>
<text class="detail" x="420" y="142" text-anchor="middle">边拍边传</text>
<text class="detail" x="420" y="174" text-anchor="middle">拍后传输</text>
<text class="detail" x="420" y="206" text-anchor="middle">取消:留在当前页</text>
<line x1="535" y1="118" x2="622" y2="78" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
<line x1="535" y1="182" x2="622" y2="222" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
<rect class="box" x="635" y="28" width="400" height="100"/>
<text class="title" x="835" y="68" text-anchor="middle">有线传输 · 边拍边传</text>
<text class="detail" x="835" y="96" text-anchor="middle">新照片传入后自动上传当前相册</text>
<rect class="box" x="635" y="172" width="400" height="100"/>
<text class="title" x="835" y="212" text-anchor="middle">有线传输 · 拍后传输</text>
<text class="detail" x="835" y="240" text-anchor="middle">拍摄完成后选择照片批量上传</text>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="470" viewBox="0 0 1200 470">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
</marker>
<style>
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
.active { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
.optional { fill: #FFF8E8; stroke: #F2A93B; stroke-width: 2; }
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
</style>
</defs>
<rect width="1200" height="470" rx="24" fill="#F7F9FC"/>
<text x="42" y="52" class="title">AI 修图弹窗:原图精修、氛围感与结果组合</text>
<rect x="42" y="92" width="190" height="116" rx="16" class="box"/>
<text x="137" y="127" text-anchor="middle" class="label">打开 AI修图</text>
<text x="137" y="158" text-anchor="middle" class="small">显示已选照片数量</text>
<text x="137" y="183" text-anchor="middle" class="small">内容区域可纵向滚动</text>
<path d="M232 150 H272" class="line"/>
<rect x="272" y="78" width="230" height="144" rx="18" class="active"/>
<text x="387" y="116" text-anchor="middle" class="label">原图精修</text>
<text x="387" y="148" text-anchor="middle" class="small">预设文字 + 对应效果图</text>
<text x="387" y="174" text-anchor="middle" class="small">横向滚动,始终单选</text>
<text x="387" y="200" text-anchor="middle" class="small">生成基础“精修后”结果</text>
<path d="M502 150 H542" class="line"/>
<rect x="542" y="78" width="230" height="144" rx="18" class="optional"/>
<text x="657" y="116" text-anchor="middle" class="label">氛围感修图(选填)</text>
<text x="657" y="148" text-anchor="middle" class="small">横向滚动,多样式单选</text>
<text x="657" y="174" text-anchor="middle" class="small">默认不选,重复点击可取消</text>
<text x="657" y="200" text-anchor="middle" class="small">额外生成独立氛围感结果</text>
<path d="M772 150 H812" class="line"/>
<rect x="812" y="92" width="170" height="116" rx="16" class="box"/>
<text x="897" y="127" text-anchor="middle" class="label">只读额度</text>
<text x="897" y="158" text-anchor="middle" class="small">显示剩余 9 张</text>
<text x="897" y="183" text-anchor="middle" class="small">不提供勾选操作</text>
<path d="M982 150 H1014" class="line"/>
<rect x="1014" y="92" width="144" height="116" rx="16" class="active"/>
<text x="1086" y="127" text-anchor="middle" class="label">点击确定</text>
<text x="1086" y="158" text-anchor="middle" class="small">提交样式组合</text>
<text x="1086" y="183" text-anchor="middle" class="small">开始处理</text>
<rect x="42" y="286" width="344" height="120" rx="16" class="box"/>
<text x="214" y="322" text-anchor="middle" class="label">点击取消 / 下拉关闭</text>
<text x="214" y="354" text-anchor="middle" class="small">不启动任务</text>
<text x="214" y="380" text-anchor="middle" class="small">不改变原图、旧结果或额度状态</text>
<rect x="428" y="286" width="344" height="120" rx="16" class="box"/>
<text x="600" y="322" text-anchor="middle" class="label">效果图预览</text>
<text x="600" y="354" text-anchor="middle" class="small">各样式卡片显示“预览”按钮</text>
<text x="600" y="380" text-anchor="middle" class="small">全屏查看,不改变当前选中项</text>
<rect x="814" y="286" width="344" height="120" rx="16" class="box"/>
<text x="986" y="322" text-anchor="middle" class="label">结果独立保存</text>
<text x="986" y="354" text-anchor="middle" class="small">原图 / 精修后 / 氛围感分别查看</text>
<text x="986" y="380" text-anchor="middle" class="small">再次确认后以新组合覆盖旧结果</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="300" viewBox="0 0 1080 300">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#7A8699"/>
</marker>
<style>
.title { font: 600 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #1F2937; }
.detail { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #667085; }
.box { fill: #FFFFFF; stroke: #D9E2EF; stroke-width: 2; rx: 16; }
.active { fill: #EEF5FF; stroke: #1677FF; stroke-width: 2; rx: 16; }
</style>
</defs>
<rect width="1080" height="300" fill="#F7F9FC"/>
<rect class="box" x="40" y="104" width="210" height="92"/>
<text class="title" x="145" y="142" text-anchor="middle">有线传输照片列表</text>
<text class="detail" x="145" y="170" text-anchor="middle">点击任意缩略图</text>
<line x1="250" y1="150" x2="320" y2="150" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
<rect class="active" x="333" y="70" width="260" height="160"/>
<text class="title" x="463" y="108" text-anchor="middle">多图全屏预览</text>
<text class="detail" x="463" y="142" text-anchor="middle">从被点击照片开始</text>
<text class="detail" x="463" y="174" text-anchor="middle">页码显示 当前张 / 总张数</text>
<text class="detail" x="463" y="206" text-anchor="middle">不可预览照片自动跳过</text>
<line x1="593" y1="112" x2="668" y2="74" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
<line x1="593" y1="188" x2="668" y2="226" stroke="#7A8699" stroke-width="2" marker-end="url(#arrow)"/>
<rect class="box" x="681" y="28" width="354" height="100"/>
<text class="title" x="858" y="68" text-anchor="middle">向右滑动</text>
<text class="detail" x="858" y="96" text-anchor="middle">查看上一张,第一张处停止</text>
<rect class="box" x="681" y="172" width="354" height="100"/>
<text class="title" x="858" y="212" text-anchor="middle">向左滑动</text>
<text class="detail" x="858" y="240" text-anchor="middle">查看下一张,最后一张处停止</text>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
</marker>
<style>
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
.processing { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
.success { fill: #EEFAF3; stroke: #37A86B; stroke-width: 2.5; }
.failed { fill: #FFF1F1; stroke: #E35D6A; stroke-width: 2.5; }
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
.retry { fill: none; stroke: #E35D6A; stroke-width: 2.5; marker-end: url(#arrow); }
</style>
</defs>
<rect width="1200" height="500" rx="24" fill="#F7F9FC"/>
<text x="42" y="52" class="title">AI 修图任务生命周期与覆盖规则</text>
<rect x="42" y="104" width="205" height="116" rx="16" class="box"/>
<text x="144" y="140" text-anchor="middle" class="label">可发起</text>
<text x="144" y="172" text-anchor="middle" class="small">未修图或已有旧结果</text>
<text x="144" y="198" text-anchor="middle" class="small">AI修图按钮可点击</text>
<path d="M247 162 H337" class="line"/>
<rect x="337" y="104" width="230" height="116" rx="16" class="processing"/>
<text x="452" y="140" text-anchor="middle" class="label">修图中</text>
<text x="452" y="172" text-anchor="middle" class="small">按钮置灰,禁止重复提交</text>
<text x="452" y="198" text-anchor="middle" class="small">离开页面后任务继续完成</text>
<path d="M567 142 H692" class="line"/>
<path d="M567 182 H692" class="retry"/>
<rect x="692" y="76" width="230" height="116" rx="16" class="success"/>
<text x="807" y="112" text-anchor="middle" class="label">成功</text>
<text x="807" y="144" text-anchor="middle" class="small">分别保存精修与氛围感结果</text>
<text x="807" y="170" text-anchor="middle" class="small">默认展示“精修后”</text>
<rect x="692" y="228" width="230" height="116" rx="16" class="failed"/>
<text x="807" y="264" text-anchor="middle" class="label">失败</text>
<text x="807" y="296" text-anchor="middle" class="small">显示“修图失败”</text>
<text x="807" y="322" text-anchor="middle" class="small">恢复 AI修图按钮,可重试</text>
<path d="M922 134 H1012" class="line"/>
<rect x="1012" y="76" width="146" height="116" rx="16" class="box"/>
<text x="1085" y="112" text-anchor="middle" class="label">再次修图</text>
<text x="1085" y="144" text-anchor="middle" class="small">新组合完成后</text>
<text x="1085" y="170" text-anchor="middle" class="small">整组覆盖旧结果</text>
<path d="M1085 192 V405 H144 V220" class="line"/>
<path d="M807 344 V405 H452 V220" class="retry"/>
<rect x="337" y="380" width="230" height="82" rx="16" class="box"/>
<text x="452" y="414" text-anchor="middle" class="label">还原为原图</text>
<text x="452" y="440" text-anchor="middle" class="small">清除精修、氛围感、封面及失败状态</text>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="500" viewBox="0 0 1200 500">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#4D7CFE"/>
</marker>
<style>
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.label { font: 600 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
.box { fill: #FFFFFF; stroke: #B9C8DA; stroke-width: 2; }
.active { fill: #EEF4FF; stroke: #4D7CFE; stroke-width: 2.5; }
.free { fill: #FFF8E8; stroke: #F2A93B; stroke-width: 2.5; }
.cover { fill: #FFF4E8; stroke: #F59E0B; stroke-width: 2.5; }
.line { fill: none; stroke: #4D7CFE; stroke-width: 2.5; marker-end: url(#arrow); }
</style>
</defs>
<rect width="1200" height="500" rx="24" fill="#F7F9FC"/>
<text x="42" y="52" class="title">多图三类风格选择与封面生成</text>
<rect x="42" y="92" width="220" height="116" rx="16" class="box"/>
<text x="152" y="128" text-anchor="middle" class="label">统计选中照片数</text>
<text x="152" y="160" text-anchor="middle" class="small">进入批量 AI 修图前判断</text>
<text x="152" y="186" text-anchor="middle" class="small">阈值以当前需求为准</text>
<path d="M262 150 H342" class="line"/>
<rect x="342" y="76" width="230" height="148" rx="18" class="active"/>
<text x="457" y="112" text-anchor="middle" class="label">数量是否 ≥ 4 张?</text>
<text x="457" y="147" text-anchor="middle" class="small">否:显示精修 + 氛围感</text>
<text x="457" y="175" text-anchor="middle" class="small">是:再显示封面模板</text>
<text x="457" y="201" text-anchor="middle" class="small">弹窗自动使用更高高度</text>
<path d="M572 150 H652" class="line"/>
<rect x="652" y="76" width="250" height="148" rx="18" class="free"/>
<text x="777" y="112" text-anchor="middle" class="label">显示三个风格模块</text>
<text x="777" y="147" text-anchor="middle" class="small">原图精修:单选基础预设</text>
<text x="777" y="175" text-anchor="middle" class="small">氛围感:横向单选,可取消</text>
<text x="777" y="201" text-anchor="middle" class="small">封面:横向单选,可全屏预览</text>
<path d="M902 150 H982" class="line"/>
<rect x="982" y="92" width="176" height="116" rx="16" class="cover"/>
<text x="1070" y="128" text-anchor="middle" class="label">确认并生成</text>
<text x="1070" y="160" text-anchor="middle" class="small">每张保存独立结果</text>
<text x="1070" y="186" text-anchor="middle" class="small">第一张另作封面</text>
<rect x="42" y="300" width="344" height="126" rx="16" class="box"/>
<text x="214" y="336" text-anchor="middle" class="label">当前免费阶段</text>
<text x="214" y="368" text-anchor="middle" class="small">封面 quotaCost = 0,不减少额度</text>
<text x="214" y="394" text-anchor="middle" class="small">弹窗显示“赠送 · 不占精修张数”</text>
<rect x="428" y="300" width="344" height="126" rx="16" class="box"/>
<text x="600" y="336" text-anchor="middle" class="label">未来收费阶段</text>
<text x="600" y="368" text-anchor="middle" class="small">服务端将 quotaCost 配置为正数</text>
<text x="600" y="394" text-anchor="middle" class="small">确认前展示实际消耗,成功后再扣减</text>
<rect x="814" y="300" width="344" height="126" rx="16" class="box"/>
<text x="986" y="336" text-anchor="middle" class="label">重复选择封面模板</text>
<text x="986" y="368" text-anchor="middle" class="small">新封面效果覆盖旧封面效果</text>
<text x="986" y="394" text-anchor="middle" class="small">失败时不把结果标记为已生成</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="520" viewBox="0 0 1200 520">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#7A8CA5"/>
</marker>
<style>
.title { font: 700 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.label { font: 700 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #182230; }
.small { font: 14px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #5D6B7C; }
.card { fill: #FFFFFF; stroke: #D5DEE9; stroke-width: 2; }
.line { fill: none; stroke: #7A8CA5; stroke-width: 2.3; marker-end: url(#arrow); }
.badge { font: 700 15px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; fill: #FFFFFF; }
</style>
</defs>
<rect width="1200" height="520" rx="24" fill="#F7F9FC"/>
<text x="42" y="52" class="title">相册缩略图结果类型与状态优先级</text>
<rect x="42" y="98" width="190" height="148" rx="18" class="card"/>
<rect x="58" y="114" width="70" height="28" rx="8" fill="#34C759"/>
<text x="93" y="134" text-anchor="middle" class="badge">已上传</text>
<text x="137" y="178" text-anchor="middle" class="label">普通原图</text>
<text x="137" y="210" text-anchor="middle" class="small">显示服务端缩略图</text>
<path d="M232 172 H282" class="line"/>
<rect x="282" y="98" width="190" height="148" rx="18" class="card"/>
<rect x="298" y="114" width="70" height="28" rx="8" fill="#4D7CFE"/>
<text x="333" y="134" text-anchor="middle" class="badge">修图中</text>
<text x="377" y="178" text-anchor="middle" class="label">处理中</text>
<text x="377" y="210" text-anchor="middle" class="small">AI入口同步置灰</text>
<path d="M472 172 H522" class="line"/>
<rect x="522" y="98" width="190" height="148" rx="18" class="card"/>
<rect x="538" y="114" width="70" height="28" rx="8" fill="#6750A4"/>
<text x="573" y="134" text-anchor="middle" class="badge">AI已修</text>
<text x="617" y="178" text-anchor="middle" class="label">普通修图结果</text>
<text x="617" y="210" text-anchor="middle" class="small">缩略图展示最新结果</text>
<path d="M712 172 H762" class="line"/>
<rect x="762" y="98" width="190" height="148" rx="18" class="card"/>
<rect x="778" y="114" width="78" height="28" rx="8" fill="#F59E0B"/>
<text x="817" y="134" text-anchor="middle" class="badge">AI封面</text>
<text x="857" y="178" text-anchor="middle" class="label">封面结果</text>
<text x="857" y="210" text-anchor="middle" class="small">显示封面模板排版图</text>
<path d="M952 172 H1002" class="line"/>
<rect x="1002" y="98" width="156" height="148" rx="18" class="card"/>
<rect x="1018" y="114" width="88" height="28" rx="8" fill="#E53935"/>
<text x="1062" y="134" text-anchor="middle" class="badge">修图失败</text>
<text x="1080" y="178" text-anchor="middle" class="label">失败结果</text>
<text x="1080" y="210" text-anchor="middle" class="small">可再次修图</text>
<rect x="42" y="310" width="1116" height="150" rx="18" fill="#FFFFFF" stroke="#D5DEE9" stroke-width="2"/>
<text x="70" y="346" class="label">状态显示优先级</text>
<text x="70" y="382" class="small">修图中 > 修图失败 > AI封面 > AI已修 > 已上传。高优先级状态先展示,避免处理中或失败被旧成功结果遮盖。</text>
<text x="70" y="416" class="small">成功:缩略图切换为最新修图图或封面图;再次成功:覆盖旧结果;还原原图:回到“已上传”;删除素材:同步清除本地结果。</text>
<text x="70" y="446" class="small">当前 AI 修图、封面结果及角标为本地演示数据,App 进程重启后会清空;服务端接入后应返回结果类型、状态、结果图地址和模板 ID。</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

-85
View File
@@ -1,85 +0,0 @@
# AI 修图完整链路设计稿 V1
## 交付内容
| 文件 | 页面 / 状态 |
|---|---|
| `00-flow-overview.png` | 六张关键屏稿总览 |
| `01-album-multi-selection.png` | 相册管理 · 已选择 4 张照片 |
| `02-template-selection.png` | AI 修图模板选择 Sheet |
| `03-template-preview.png` | 模板全屏预览 |
| `04-result-success.png` | 精修结果 · 成功 |
| `05-result-processing.png` | 精修结果 · 生成新版本中 |
| `06-result-failed.png` | 精修结果 · 新版本生成失败 |
独立屏稿均为 853 × 1844 像素(约 9:19.5),面向 iOS 16+、iPhone 竖屏、浅色模式。
## 设计系统
- 主色:`#1677FF`;辅助渐变:`#5CA8FF`、`#A7D8FF`。
- 页面底色:`#F7FAFF`;主文字:`#172033`;次文字:`#667085`。
- 失败色:`#EF4444`;赠送/免费标签使用橙色,避免与主要操作竞争。
- 字体:SF Pro / 苹方风格;圆角 12–16 pt;以 8 pt 为基础间距。
- 正文置于高不透明度表面;极光光晕只用于背景与重点操作,不承载关键信息。
- 结果页只通过“原图 / 精修后 / 氛围感”标签切换,不使用对比滑杆。
- 重新修图期间及失败后保留上一版成功图片;状态反馈只占局部,不遮挡照片。
## 可复用 ImageGen 提示词
以下提示词使用内置 ImageGen。首屏以 `../travel-album-management-redesign-v2.png` 为风格参考;后续页面以上一张已确认屏稿作为参考,确保视觉连续。
### 通用前缀
```text
Use case: ui-mockup
Asset type: shippable high-fidelity iOS app screen, full portrait canvas, no device frame
Style/medium: realistic production UIKit UI, light Aurora blue, crisp SF Pro / PingFang-like typography, SF Symbols-style icons, 12–16 pt corners, 8 pt spacing grid, subtle borders and restrained shadows.
Color palette: primary #1677FF, gradient accents #5CA8FF and #A7D8FF, background #F7FAFF, text #172033 and #667085, danger #EF4444.
Constraints: iPhone portrait 9:19.5; iOS status bar and home indicator; practical UIKit-buildable hierarchy; accessible contrast; minimum 44 pt touch targets; no dark mode; no watermark; no decorative device frame; no annotations.
Text constraints: render specified Chinese text exactly once; no gibberish, duplicate labels or unrelated copy.
```
### 01 · 相册多选
```text
Create the album management screen in multi-selection mode. Top navigation: “相册管理” and “完成”. Album summary: “旅拍相册”, “138****8000”. Filter row: “全部 128”, “已购 36”, “时间 ↓”. Use a precise three-column grid of nine travel photos. Four photos have blue checkmarks, blue outlines and subtle selected overlays; other photos show empty selectors. Fixed bottom actions: red outline “删除 4 张” and blue gradient “AI修图 4 张”.
```
### 02 · 模板选择
```text
Create a large native page sheet titled “AI修图” with subtitle “已选择 4 张照片”. Show the note “每张照片生成精修结果,氛围感可选;本次可免费生成 1 张封面”. Include three horizontal sections: “原图精修” + “必选”, “氛围感修图” + “选填”, “封面风格” + “赠送 · 不占额度”. Each section has three photographic cards, a clear single selected state and “预览” buttons. Cost card: “AI精修 · 按张收费”, “预计消耗 8 张”, “剩余 12 张”. Fixed actions: “取消” and “确定”.
```
### 03 · 模板预览
```text
Create an immersive full-screen preview for the selected “自然通透” template. Use a dark navy photographic canvas, a circular back button, title “自然通透” and subtitle “双指缩放查看细节”. The large photo shows a woman in a straw hat overlooking Santorini. Add “模板效果预览”. Bottom information panel: “自然通透”, “提亮肤色与画面通透感,保留自然质感”, “当前已选择”. Primary action: “使用此模板”. Do not add a comparison slider.
```
### 04 · 正常结果
```text
Create “照片预览” with counter “1 / 9”. Add exactly three tabs: “原图”, “精修后”, “氛围感”; select “精修后”. Show the same Santorini photo with badge “AI已修”, filename “IMG_8291.JPG” and “自然通透 · 2026/08/13”. Information card: “精修结果”, “自然通透模板 · 已生成最新版本”. Fixed actions: “AI修图”, “删除”, “刷新”. Use tabs only; no slider or press-and-hold comparison.
```
### 05 · 处理中状态编辑
```text
Use case: precise-object-edit. Keep the successful result screen, photo, tabs and layout unchanged. Replace “AI已修” with spinner + “修图中”. Replace the result card with “正在生成新版本” and “当前仍展示上一版结果,完成后自动更新”, plus an indeterminate shimmer line with no percentage. Keep the previous photo unobscured. Disable only the “AI修图” button; leave “删除” and “刷新” available.
```
### 06 · 失败状态编辑
```text
Use case: precise-object-edit. Keep the successful result screen, photo, tabs and layout unchanged. Replace “AI已修” with a local red badge “生成失败”. Replace the result card with pale-red recovery content: “新版本生成失败”, “上一版结果已保留,可重新修图”, and an outline action “重新修图”. Keep the previous photo unobscured and restore the bottom “AI修图” button to its primary blue state. Do not show a modal, stack trace or error code.
```
## QA 记录
- 六张独立屏稿尺寸一致,使用同一旅拍照片体系与组件语言。
- 精修必选、氛围感选填、四张照片触发封面、封面不占额度均已表达。
- 成功、处理中、失败三种状态均保留同一照片与结果标签,便于直接比较。
- 处理中不展示虚构百分比;重复提交按钮禁用,刷新仍可用。
- 失败状态提供“重新修图”恢复路径,且上一版结果保持可见。
- 本目录只包含设计稿与说明,未修改 UIKit、业务逻辑或 Asset Catalog。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

-76
View File
@@ -1,76 +0,0 @@
# AI 修图完整链路设计稿 V2
## 本轮调整
- 第 1、2 张沿用 V1 的浅色“轻盈极光蓝”风格。
- 第 3–6 张改为统一的黑色专业影像工作区,突出照片本身。
- 暗色页面不做简单反色:背景、浮层、边框、文字和按钮均使用独立暗色语义层级。
- V1 完整保留在 `../ai-retouch-v1/`,便于对比和回退。
## 交付内容
| 文件 | 页面 / 状态 | 风格 |
|---|---|---|
| `00-flow-overview-v2.png` | 六张关键屏稿总览 | 混合流程 |
| `01-album-multi-selection.png` | 相册管理 · 已选择 4 张 | 浅色 |
| `02-template-selection.png` | AI 修图模板选择 Sheet | 浅色 |
| `03-template-preview-dark.png` | 模板全屏预览 | 暗色 |
| `04-result-success-dark.png` | 精修结果 · 成功 | 暗色 |
| `05-result-processing-dark.png` | 精修结果 · 生成中 | 暗色 |
| `06-result-failed-dark.png` | 精修结果 · 生成失败 | 暗色 |
## 暗色设计 Token
- 页面背景:`#05070D`,允许向深海军蓝 `#07111F` 做非常轻微的层次过渡。
- 抬升表面:`#111827`;边框:`#263244`。
- 主文字:`#F8FAFC`;次文字:`#9CA3AF`;未选中标签:`#C7CED9`。
- 品牌主色:`#1677FF`;蓝色高光:`#5CA8FF`。
- 失败色:`#EF4444`,仅用于失败徽标、恢复入口和局部边框。
- 图片保持明亮自然,不叠加全屏蒙层;信息区和操作区不出现白色表面。
## 可复用 ImageGen 暗色编辑提示词
以下提示词使用内置 ImageGen,以对应 V1 页面作为 edit target;第 3 张暗色稿确定后,也作为第 4–6 张的风格参考。
### 通用暗色编辑约束
```text
Use case: precise-object-edit
Restyle the target into a cohesive dark professional photography workspace.
Preserve the exact iPhone portrait dimensions, photo content and crop, person identity and pose, all Chinese copy, hierarchy, controls and safe-area spacing.
Use background #05070D, elevated surface #111827, border #263244, primary #1677FF, text #F8FAFC and secondary #9CA3AF.
Keep the bright photo unchanged as the focal point. Use white text and SF Symbols-style white icons. Use brand blue only for selected state and primary actions.
No white page areas, no comparison slider, no device frame, no annotations, no watermark and no additional copy.
```
### 模板预览
```text
Keep “自然通透”, “双指缩放查看细节”, the Santorini photo and “模板效果预览”. Change the lower information panel to charcoal with white heading, muted description and blue “当前已选择”. Change the complete bottom safe area to black and retain the blue “使用此模板” action.
```
### 修图结果
```text
Keep “照片预览”, “1 / 9” and the three tabs “原图 / 精修后 / 氛围感”. Use a charcoal segmented control with “精修后” selected in blue. Keep “AI已修”, the filename metadata, the photo and the result information. Change the result card and secondary actions “删除 / 刷新” to dark surfaces; keep “AI修图” blue.
```
### 处理中状态
```text
Based on the approved dark result screen, change only the badge to spinner + “修图中”. Keep the previous photo visible. Change the card to “正在生成新版本” and “当前仍展示上一版结果,完成后自动更新”, with an indeterminate shimmer line and no percentage. Disable only “AI修图”.
```
### 失败状态
```text
Based on the approved dark result screen, change only the badge to “生成失败”. Keep the previous photo visible. Use a charcoal recovery card with restrained red border, “新版本生成失败”, “上一版结果已保留,可重新修图” and “重新修图”. Keep the main “AI修图” button blue. Do not use a white or pale-red card.
```
## QA 记录
- 第 1、2 张与 V1 像素级复用,没有重新生成。
- 第 3–6 张均为 853 × 1844,暗色背景、表面、文字和操作层级一致。
- 成功、处理中、失败三态保留同一结果标签、照片和底部操作布局。
- 处理中无虚构百分比;失败状态有明确恢复动作;旧结果始终可见。
- 本目录只新增设计稿与说明,未修改 UIKit、业务逻辑或 Asset Catalog。
Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 893 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

+3 -3
View File
@@ -295,7 +295,7 @@ iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比:
| --- | --- | | --- | --- |
| 标题示例 | `付款成功通知` | | 标题示例 | `付款成功通知` |
| 正文示例 | `您的订单 #260722128006 已支付成功,支付金额 ¥0.01` | | 正文示例 | `您的订单 #260722128006 已支付成功,支付金额 ¥0.01` |
| 客户端点击行为 | iOS 进入订单 Tab,恢复默认筛选并刷新;Android 行为以 Android 实现为准 | | 客户端点击行为 | Android、iOS 均进入收款记录页 |
`data` 字段: `data` 字段:
@@ -410,9 +410,9 @@ iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比:
## 7. 客户端解析约定 ## 7. 客户端解析约定
1. Android、iOS 的通知点击路由只根据业务字段 `type` 判断,不读取标题、正文、`data`、`route`、`uri`、`action` 或极光内部字段决定页面。 1. Android、iOS 的通知点击路由只根据业务字段 `type` 判断,不读取标题、正文、`data`、`route`、`uri`、`action` 或极光内部字段决定页面。
2. iOS `type = 1`、`type = 5`:进入订单 Tab 根页面,清空搜索、日期与状态筛选并刷新列表;不进入订单详情。Android 行为以 Android 实现为准。 2. `type = 1`:进入收款记录页。
3. `type = 6`:进入收款详情页。 3. `type = 6`:进入收款详情页。
4. iOS `type = 14`:有效 `ai_retouch_batch_id` 进入 AI 修图任务详情,否则进入任务列表。其余未单独处理的 `type`、缺少 `type` 或类型无法解析:进入消息中心。 4. 其他任意 `type`、缺少 `type`、类型无法解析或 payload 损坏:进入消息中心。
5. Android 从 `notificationExtras` 或厂商通道包装字段中提取 `type`;iOS 优先读取 payload 顶层 `type`,并兼容常见推送包装层中的 `type`。 5. Android 从 `notificationExtras` 或厂商通道包装字段中提取 `type`;iOS 优先读取 payload 顶层 `type`,并兼容常见推送包装层中的 `type`。
6. `data` 仍可按消息类型用于页面内容刷新,但不参与通知点击路由。新增字段应保持向后兼容,客户端应忽略当前版本不认识的额外字段。 6. `data` 仍可按消息类型用于页面内容刷新,但不参与通知点击路由。新增字段应保持向后兼容,客户端应忽略当前版本不认识的额外字段。
7. 金额继续使用字符串;时间戳建议统一为 Unix 秒级整数,并在协议中固定单位。 7. 金额继续使用字符串;时间戳建议统一为 Unix 秒级整数,并在协议中固定单位。
-54
View File
@@ -1,54 +0,0 @@
# 工作周报(2026.08.10—2026.08.14)
## 本周概况
本周围绕旅拍相册和 AI 修图功能开展开发,共完成 **23 次提交**,涉及 **37 个文件**,代码变更约新增 **11,495 行**、删除 **1,120 行**。
## 本周完成
### 1. 完善旅拍相册图片预览
- 新增 iOS 端全屏图片预览。
- 完善预览工具栏、图片索引胶囊、分段控件及关闭按钮样式。
- 支持原图与 AI 修图版本切换,并修复切换时出现黑屏的问题。
- 增加相册管理页下拉刷新。
- 接入旅拍相册批量删除接口。
### 2. 打通 AI 修图业务流程
- 从相册及图片预览页接入 AI 修图入口。
- 完成图片选择、模板选择、模板预览和任务提交流程。
- 优化修图标签、页面布局及提交反馈。
- 新增 AI 修图前后效果对比功能。
### 3. 新增 AI 修图任务中心
- 完成任务列表及任务详情页面。
- 接入 AI 修图任务相关接口和状态模型。
- 支持处理中、成功、失败等任务状态展示。
- 打通消息中心、推送通知与任务详情跳转链路。
### 4. 优化相册数据同步
- 修复从 OTG 页面返回后旅拍相册未及时刷新的问题。
- 优化修图提交后的页面反馈及状态更新。
- 完善异常状态和页面切换场景下的数据刷新逻辑。
### 5. 补充文档与测试
- 补充 AI 修图需求、接口和交互设计文档。
- 完善相册 API、数据模型、ViewModel、消息中心及推送相关单元测试。
- 增加图片预览、任务中心和修图流程的回归测试覆盖。
## 风险与待验证项
- AI 修图任务中心涉及接口、推送、消息中心和页面跳转,需要结合测试环境继续进行全链路验证。
- 需重点回归任务处理中、成功、失败及网络异常等状态。
- OTG 返回刷新、批量删除和图片版本切换需要在真机及大相册场景下继续验证。
## 下周计划
- 联调 AI 修图任务全生命周期及推送跳转。
- 完成旅拍相册与 AI 修图功能的真机回归。
- 优化大图加载、任务刷新和弱网场景下的交互体验。
- 根据测试反馈修复问题,推进功能验收与发布准备。
-63
View File
@@ -1,63 +0,0 @@
# 工作周报(2026.08.17—2026.08.21)
## 本周概况
本周围绕旅拍相册和 AI 重修流程开展功能完善与体验优化,共完成 **7 次提交**,涉及 **20 个文件**,代码及文档变更约新增 **1,101 行**、删除 **240 行**;同时完成版本号升级,为 **1.3.1** 版本发布做准备。
## 本周完成
### 1. 完善 AI 重修业务规则
- 根据是否已有 AI 结果区分首次修图与重新修图流程。
- 已有 AI 结果时,统一支持精修和氛围感模板按需选择,并校验至少选择一种模板。
- 优化单图、多图场景下的模板必选规则、生成数量与额度提示。
- 对齐 AI 重修请求参数、模板范围及结果覆盖规则,确保未选结果类型保持原有版本不变。
### 2. 优化相册素材状态与删除保护
- 将购买状态和 AI 修图状态拆分展示,避免不同业务状态互相覆盖。
- 扩展素材选择能力,支持跨相册 Tab 进入选择模式。
- 增加已购素材删除保护,批量删除时仅提交未购买素材。
- 优化删除确认提示,明确已选、已购及实际可删除数量;全部为已购素材时阻止删除并给出反馈。
### 3. 改进任务详情与图片预览体验
- 优化 AI 修图任务详情中的结果类型标签、状态区域和失败信息布局,提升信息辨识度。
- 调整任务目标标题、模板和操作按钮布局,改善长文本与不同状态下的适配表现。
- 完善图片预览初始版本选择,当前版本不可用时自动回退到有效图片。
- 新增高对比度延迟加载指示器,减少快速加载时的闪烁,并优化未知文件大小的展示。
### 4. 完善相册刷新能力
- 为旅拍相册入口增加下拉刷新。
- 补充刷新状态管理,确保成功、失败及取消场景均能正确结束刷新动画。
### 5. 补充文档与测试覆盖
- 新增 AI 重新修图接口优化需求文档,并同步更新 AI 修图需求及接口说明。
- 完善 AI 修图模板、重修请求、相册素材模型、删除保护及页面布局相关单元测试。
- 增加首次修图、已有结果重修、模板选填和异常状态等关键场景的回归覆盖。
### 6. 推进版本发布准备
- 将项目版本号升级至 **1.3.1**。
- 整理本轮 AI 重修规则、交互调整及测试范围,为后续联调验收提供依据。
## 进行中
- 补充 AI 重修模板占位图的多倍图资源配置。
- 继续验证 AI 重修、批量删除及图片预览在真机和真实数据下的完整表现。
## 风险与待验证项
- AI 重修规则涉及模板范围、额度计算和结果覆盖,需要结合测试环境验证服务端行为是否与最新规则一致。
- 已购与未购素材混合选择时,需要重点回归删除参数、数量提示及刷新后的数据一致性。
- 图片预览需继续验证弱网、大图及不同 AI 结果组合下的加载、回退和切换体验。
- 本周测试代码已补充,仍需在已连接的 iPhone 真机上完成完整测试回归并记录结果。
## 下周计划
- 完成 1.3.1 版本真机测试、问题修复及发布验收。
- 联调 AI 重修模板选择、额度扣减、任务生成与结果覆盖全链路。
- 回归相册下拉刷新、素材状态展示和已购素材删除保护。
- 补齐 AI 重修模板多倍图资源,优化弱网和大图加载体验。
-51
View File
@@ -1,51 +0,0 @@
# 工作周报(2026.08.24—2026.08.28)
## 本周概况
本周围绕随心瞰商家版 iOS 的线下收款、旅拍相册自动修图和门店身份注销开展开发与优化。已完成 **6 次提交**;门店身份注销相关改动仍在工作区,尚未提交,完整终态验收待补齐。
## 本周完成
### 1. 完成线下收款登记与日清功能开发
- 新增线下收款首页卡片、收款登记及日清页面,接入统计、登记、按日明细和补缴接口。
- 支持按日期查看收款与待补缴情况,完善金额校验及快速切换日期时的旧请求结果隔离。
- 优化周/月日历切换、翻页、选中态及未来日期限制,调整收款方式图标和明细展示。
- 补充接口调整说明及金额、日历边界、请求参数和页面相关测试。
### 2. 新增相册自动修图与 OTG 状态预览
- 支持相册级自动修图配置、精修模板选择和传输模式选择,上传完成后自动提交修图任务。
- 在 OTG 页面区分上传与修图状态,增加修图状态角标、精修结果预览及失败重试。
- 优化自动修图设置页,支持同页切换修图方式、三列模板展示和效果对比预览,保留草稿选择及滚动位置。
- 补充配置、任务状态和预览交互测试,并完善 AI 重修占位图资源配置。
## 进行中:门店身份注销
- 已接入现有门店身份注销接口,完成“确认资产 → 手机验证”两步流程,以及条件查询、短信验证、申请和撤销逻辑;注销范围仅限当前门店身份。
- 已实现申请明确成功后退出登录、下次登录取得身份凭证后核验状态,以及受限状态下的业务访问和推送跳转控制。
- 完善身份切换与旧响应隔离、异常状态保护、下拉刷新及统一 Loading,移除普通登录和切换身份时无条件出现的注销提示。
- 已在测试环境验证零资产确认、短信申请、冷静期查询和主动撤销流程,并整理接入说明及脱敏接口记录;正式注销完成、终审阻断及真实多端场景尚未完成验收。
## 测试与质量
- 根据本周已保存的 iPhone 11 真机回归记录,最新全量测试共 **821 项,811 项通过、10 项失败、0 项跳过**,全量尚未全部通过。
- 接入记录显示,**98 项注销相关测试全部通过**,失败用例集合与上一轮一致;这些 Mock 测试不替代真实终态验收。
- 现存失败涉及部分接口解码、头像裁剪和页面布局断言,需继续排查修复。
## 风险与待验证项
- 门店注销仍缺少正式完成、终审阻断的完整接口响应及终态鉴权依据,暂不能按完整功能验收。
- 非零资产、确认过期、错误验证码、重复申请及多端状态变化仍需补充真实场景验证。
- 线下收款和自动修图需继续进行真实数据联调,重点检查补缴统计一致性、弱网重试及前后台恢复。
## 下周计划
- 补齐门店身份注销终态接口契约,推进完整流程与多身份场景验收。
- 排查并修复现有 10 项测试失败,完成受影响模块的真机回归。
- 联调线下收款登记、日清补缴及自动修图全流程,根据反馈完善交互和异常处理。
- 整理未提交改动、测试证据及交付文档,推进本轮功能验收。
---
整理依据:本周 Git 提交、当前工作区改动、门店身份注销接入记录及已保存的真机测试摘要;本次周报整理未重新运行测试。
+281
View File
@@ -0,0 +1,281 @@
# 相册管理与 AI 修图功能说明
> 文档日期:2026-08-20
> 适用范围:新增相册 → 相册任务 → 相册管理 → 照片上传与传输 → 照片预览与 AI 修图
> 数据说明:相册和照片列表、有线相机连接及原图上传继续使用项目已有真实数据能力;传输模式与相册自动修图模板保存在本地。自动修图目前使用本地图像效果演示,修图结果图和任务状态可随 OTG 相册恢复;手动原图精修、氛围感、封面模板和剩余 9 张额度仍为本地演示数据。
## 1. 照片预览、左右滑动与底部操作
### 功能说明
用户在“相册管理”照片网格中点击任意缩略图,进入单张照片预览页。页面只保留一个返回按钮,导航区域展示当前照片文件名和文件大小;照片区域支持左右滑动查看同一相册内的上一张或下一张照片。
预览页统一使用黑色沉浸式自定义头部,不再混用系统导航栏:左侧为圆形返回按钮,中间展示文件名和文件大小,右侧展示“当前张数/总张数”。切换照片时三处信息同步更新。照片预览与原图精修、氛围感、封面模板预览都直接使用同一个全屏模态容器,不进入项目导航栈,因此不会叠加系统白色导航栏或出现两个返回按钮;关闭后回到进入前的相册管理页或修图弹窗。
底部固定展示以下三个操作,不展示打印按钮:
| 操作 | 点击结果 | 操作期间或异常处理 |
| --- | --- | --- |
| AI修图 | 打开当前照片的 AI 修图弹窗 | 当前照片处于“修图中”时按钮置灰,避免重复提交 |
| 删除 | 二次确认后删除当前照片 | 删除成功后自动定位到相邻照片;删除最后一张后返回相册管理 |
| 刷新 | 重新加载当前照片及最新修图结果 | 刷新失败时不清空当前已显示内容,可再次点击 |
左右滑动时同步刷新文件名、文件大小、原图/修图后状态和底部按钮状态。第一张继续向右滑、最后一张继续向左滑时保持当前照片,不循环跳转。
页面底部图标使用项目内 `TravelAlbumPreviewIcons` SVG 资源,确保 AI 修图、删除、刷新三个入口视觉一致。
### 状态流转图
![照片预览与左右切换流程](./assets/travel-album-ai/01-photo-preview-flow.svg)
### 本功能验收
- [ ] 从任意缩略图进入时只显示一个返回入口。
- [ ] 文件名和文件大小与当前照片一致。
- [ ] 导航头部保持黑色沉浸样式,右侧张数随照片切换同步变化。
- [ ] 左右滑动后照片和文件信息同步变化。
- [ ] 首尾照片不会越界或循环。
- [ ] 底部仅显示 AI修图、删除、刷新,不显示打印。
- [ ] 删除当前照片后能正确定位或退出预览。
## 2. 上传照片与传输模式选择
### 功能说明
相册管理处于普通状态时,用户点击底部“上传照片”,先从底部弹出“选择传输模式”,展示“边拍边传”和“拍后传输”两个选项。点击任一选项后弹窗关闭,并进入当前相册的有线传输页面;点击“取消”或向下关闭弹窗时停留在相册管理,不改变现有照片和选择状态。
| 传输模式 | 弹窗说明 | 下一页初始状态 | 后续行为 |
| --- | --- | --- | --- |
| 边拍边传 | 相机拍摄后,照片自动传输并上传到当前相册 | 传输模式显示“边拍边传” | 新传入 App 的照片自动上传 |
| 拍后传输 | 拍摄完成后,再选择相机照片批量传输 | 传输模式显示“拍后传输” | 新照片先保留,用户后续选择并上传 |
本次弹窗选择优先于本机上一次保存的传输模式,并同步成为新的最近使用模式,确保下一页展示和实际传输行为一致。相册 ID、相册名称、联系电话及照片数据继续沿用相册管理页面的真实数据,不创建新的相册或假照片。
### 自动 AI 修图
新建相册时在价格字段下方固定展示“不修图”和“AI 修图”两个选项,默认选中“不修图”。点击“AI 修图”后再从底部弹出模板选择;每个模板同时展示本地演示照片套用该效果后的缩略图和效果说明。未选模板时不会开启 AI 修图。相册创建成功后,配置按真实相册 ID 保存,上传页仅回显当前一级方式“不修图 / AI 修图”。
有线传输页顶部的设置项也仅回显“不修图 / AI 修图”。点击后底部弹窗的第一层始终只有这两个选项;选择“AI 修图”后进入带效果缩略图的模板列表,可返回第一层重新选择。新配置只影响尚未开始的上传任务。每张照片仍先按原流程上传原图,得到服务端素材 ID 后才执行自动修图:
| 阶段 | 列表展示 | 统计归类 | 失败处理 |
| --- | --- | --- | --- |
| 原图上传 | 蓝色“上传中”及进度 | 全部 | 进入上传失败,可重传 |
| 自动修图 | 蓝色“修图中” | 全部 | 不回退原图上传结果 |
| 修图成功 | 绿色“已上传”,缩略图右上角显示紫色“修” | 已上传 | 使用修图后结果预览 |
| 修图失败 | 红色“修图失败” | 已上传 | 更多菜单选择“重新修图”,不重复上传原图 |
修图中退出 App 时,再次进入会恢复为“修图失败”并允许重试,避免任务长期停留在处理中。
进入有线传输页面后,非批量选择状态下点击任意照片缩略图,预览页会使用当前 Tab 筛选后的照片顺序,并从被点击照片开始展示。用户可左右滑动查看上一张或下一张,顶部页码随照片同步变化;没有可用预览地址的照片不进入预览序列,也不占用页码。第一张继续向右滑、最后一张继续向左滑时均停留在边界,不循环跳转。处于批量上传选择状态时,点击照片仍执行选择或取消选择,不打开预览。
### 状态流转图
![上传照片与传输模式选择流程](./assets/travel-album-ai/02-upload-transfer-mode-flow.svg)
![有线传输缩略图预览流程](./assets/travel-album-ai/03-wired-transfer-preview-flow.svg)
### 本功能验收
- [ ] 点击“上传照片”后先显示底部传输模式弹窗,不直接进入下一页。
- [ ] 弹窗同时显示“边拍边传”和“拍后传输”及对应说明。
- [ ] 选择“边拍边传”后,下一页模式显示为“边拍边传”。
- [ ] 选择“拍后传输”后,下一页模式显示为“拍后传输”。
- [ ] 新建相册固定展示“不修图”和“AI 修图”两个选项。
- [ ] 选择“AI 修图”后才弹出模板列表,每个模板都能看到差异化效果图。
- [ ] 上传页一级弹窗只有两种修图方式,新配置仅影响后续上传。
- [ ] 修图中、修图成功“修”角标和修图失败三种状态展示正确。
- [ ] 修图失败仍计入“已上传”,点击“重新修图”不重复上传原图。
- [ ] 本次选择不会被上一次保存的模式覆盖。
- [ ] 取消或关闭弹窗后仍停留在相册管理。
- [ ] 非批量选择状态点击缩略图后,从被点击照片进入全屏预览。
- [ ] 预览可左右滑动查看当前 Tab 中的上一张和下一张,页码同步变化且首尾不循环。
- [ ] 批量选择状态点击照片只改变选择状态,不打开预览。
## 3. 相册管理多选与底部操作栏切换
### 功能说明
相册管理页普通状态下,底部显示“上传照片”。用户点击右上角选择按钮后进入多选状态,每张缩略图右上角显示选择圆圈,底部“上传照片”隐藏,替换为“AI修图”和“删除”。
| 多选状态 | 页面表现 | 可用操作 | 下一状态 |
| --- | --- | --- | --- |
| 未进入多选 | 不显示选择圆圈,底部显示上传照片 | 点击照片进入预览;点击右上角选择 | 进入多选 |
| 已进入但未选照片 | 显示未选圆圈,AI修图和删除置灰 | 选择照片;再次点击右上角选择退出 | 已选择或退出 |
| 已选择照片 | 选中照片显示勾选,AI修图和删除可用 | 发起批量修图或批量删除 | 弹窗、处理中或删除结果 |
| 选中项含修图中的照片 | 保留当前选择,AI修图置灰 | 等待任务完成;删除仍按现有删除规则处理 | 任务结束后恢复 |
| 退出多选 | 清空本次选择,恢复上传照片 | 上传或进入照片预览 | 普通状态 |
批量删除必须二次确认;用户取消时不删除任何照片并保留当前选择。批量 AI 修图复用与单张预览相同的预设、任务状态和结果覆盖逻辑。
### 状态流转图
![相册管理多选操作栏流程](./assets/travel-album-ai/02-multi-select-flow.svg)
### 本功能验收
- [ ] 进入多选后上传按钮消失,AI修图和删除出现。
- [ ] 未选择照片时两个批量按钮不可点击。
- [ ] 选择至少一张后两个批量按钮恢复可用。
- [ ] 包含修图中照片时不能再次发起 AI 修图。
- [ ] 退出多选后选择状态清空并恢复上传按钮。
## 4. AI 修图弹窗、原图精修、氛围感与按张收费模式
### 功能说明
用户从单张预览或相册多选操作栏点击“AI修图”后,以同一个底部弹窗展示 AI 修图配置。标题与弹窗顶部保留足够间距,标题下方展示“已选择 N 张照片”。弹窗内容可纵向滚动,取消和确定按钮固定在底部,照片较多并出现封面模块时也不会遮挡选择项。
“原图精修”是基础修图模块,使用横向滚动单选列表,每个预设同时展示名称和对应效果图:
| 预设 | 当前效果用途 |
| --- | --- |
| 还原为原图 | 清除该照片已有修图结果和封面类型 |
| 写真-简约肖像 | 低饱和、提亮的人像效果 |
| 写真-清冷古风 | 清冷复古效果 |
| 旅拍-锦绣 | 提升饱和度和对比度 |
| 旅拍-远山 | 低饱和、柔和远景效果 |
| 旅拍-薄雾 | 提亮并降低对比度 |
| 油画-夏日 | 偏暖夏日效果 |
| 油画-浓郁 | 高饱和、高对比效果 |
原图精修默认选中“还原为原图”,用户点击其他预设后仅保留一个选中项。没有可用原图时,效果图位置显示稳定占位图,不影响弹窗选择。每张样式效果图右下角都有“预览”按钮,点击后全屏查看当前照片应用该样式的效果,不改变当前选中项。
“氛围感修图”位于原图精修下方,并明确标记为“选填”。该模块与原图精修保持一致,使用横向滚动效果图单选,当前本地演示包含日落暖阳、复古胶片、清透蓝调、森系薄雾、浓郁故事五种样式。默认不选中;点击一种样式后仅保留该项,再次点击当前样式即可取消。每个样式同样提供“预览”按钮。选中后不是把氛围效果叠加并合并到精修图中,而是为每张照片额外生成一张可独立查看的“氛围感”结果。
原图精修、氛围感、封面模板和“查看高清图”统一进入同一个沉浸式大图预览页。大图页不再套用系统导航栏,只显示一个返回按钮、当前样式名称和“双指缩放查看细节”提示;关闭后回到原弹窗或照片页,并保留关闭前的选择状态。
弹窗内的 Tips 会随当前组合实时变化:
| 当前选择 | 确认后的结果 |
| --- | --- |
| 仅原图精修 | 每张照片生成 1 个“精修后”结果 |
| 原图精修 + 氛围感修图 | 每张照片生成“精修后 + 氛围感”2 个独立结果 |
| 有封面、未选氛围感 | 每张照片生成精修结果,第一张选中照片额外生成封面 |
| 有封面、已选氛围感 | 每张照片生成精修和氛围感 2 个结果,第一张选中照片再额外生成封面 |
“修图模式”区域只读展示“AI精修 · 按张收费”和“剩余 9 张”,不再显示勾选圆圈,也不能点击选择。当前剩余数量为本地演示数据,不会真实扣减额度;未来接入服务端时由接口返回剩余额度,并以任务创建成功或服务端明确返回的扣减结果为准。
点击“确定”提交当前原图精修、氛围感和封面组合并关闭弹窗;点击“取消”或关闭弹窗时不创建任务、不改变旧修图结果,也不改变额度。
### 状态流转图
![AI 修图弹窗流程](./assets/travel-album-ai/03-ai-edit-sheet-flow.svg)
### 本功能验收
- [ ] 标题与弹窗顶部间距自然,不贴近顶部。
- [ ] 已选照片数量准确。
- [ ] 原图精修预设同时展示文字和效果图,并可横向滚动、保持单选。
- [ ] 原图精修、氛围感和封面模板效果图均可通过小按钮全屏预览,预览不改变选择。
- [ ] 所有大图预览使用同一黑色头部、单一返回入口,并支持双指缩放。
- [ ] 氛围感修图横向展示多个效果图,默认不选中、保持单选,可再次点击当前样式取消。
- [ ] 同时选择原图精修和氛围感时,每张照片生成两个独立结果。
- [ ] Tips 能根据氛围感和封面选择动态说明实际输出结果。
- [ ] 修图模式只展示按张收费与剩余张数,不提供选择操作。
- [ ] 取消弹窗不会创建任务或覆盖旧结果。
## 5. AI 修图任务状态、失败重试与结果覆盖
### 功能说明
用户确认修图后,所选照片立即进入“修图中”。任务未结束前,相关 AI 修图入口置灰;即使用户离开照片预览页,本地演示任务仍会继续完成,并通过共享结果状态刷新相册缩略图。
| 任务状态 | 进入条件 | 页面表现 | 可用操作 | 后续状态 |
| --- | --- | --- | --- | --- |
| 可发起 | 未修图、已有成功结果或上次任务已结束 | AI修图按钮可用 | 选择预设并确认 | 修图中 |
| 修图中 | 已提交预设,等待结果 | 按钮置灰;缩略图显示“修图中” | 查看其他照片、离开页面 | 成功或失败 |
| 成功 | 获取原图并完成本地效果处理 | 分别保存精修、氛围感和封面结果;默认展示“精修后”;按钮恢复 | 在已有结果间切换、再次修图、还原原图 | 新任务或已上传 |
| 失败 | 原图无法获取或处理失败 | 缩略图显示“修图失败”;按钮恢复 | 再次修图 | 修图中 |
成功后,照片预览页至少出现“原图 / 精修后”切换;本次选择了氛围感时增加“氛围感”,存在封面结果时增加“封面”。各结果互相独立,不把两个效果合成同一张图;有修图结果时还可以点击“查看高清图”。再次修图时,旧结果在新任务处理期间继续保留,新任务成功后整组替换为本次最新组合:本次未选择氛围感时会清除旧氛围感结果;新任务失败时保留旧成功图片,但状态优先显示“修图失败”,提醒用户本次任务未成功。
选择“还原为原图”会同时清除修图结果、失败状态和封面类型,缩略图恢复为“已上传”。删除照片时同步清理该照片的本地修图记录,避免已删除照片的结果残留。
### 状态流转图
![AI 修图任务状态流转](./assets/travel-album-ai/04-ai-task-state-flow.svg)
### 本功能验收
- [ ] 修图中不能重复点击 AI修图。
- [ ] 离开预览页后任务仍能结束并刷新缩略图。
- [ ] 成功后可切换查看原图、精修后以及本次实际生成的氛围感或封面。
- [ ] 同选原图精修和氛围感时两个结果均可独立查看。
- [ ] 再次修图成功后以本次结果组合覆盖旧结果。
- [ ] 失败后按钮恢复并允许重试。
- [ ] 还原原图后所有本地修图类型和结果被清除。
## 6. 选中 4 张及以上时的封面模板选择
### 功能说明
从相册管理多选照片并点击“AI修图”时,系统根据选中照片数量决定是否展示“封面风格模板”:
| 选中数量 | 弹窗表现 |
| --- | --- |
| 1~3 张 | 展示“原图精修 + 氛围感修图(选填)”两个模块,不展示封面模板 |
| ≥ 4 张 | 展示“原图精修 + 氛围感修图(选填)+ 封面模板”三个模块,并提高弹窗高度 |
封面模板为横向滚动单选,默认选中“旅行纪念册”。每个模板效果图右下角提供“预览”按钮,点击后全屏查看完整排版,不改变当前模板选择。当前提供:
- 旅行纪念册
- 简约留白
- 景区故事
- 电影胶片
每个模板展示名称和以第一张选中照片生成的排版效果图。用户确认后,所有选中照片先生成原图精修结果;选择了氛围感时,所有照片再各自生成一个氛围感结果;第一张选中照片另外生成独立封面并作为封面承载图。封面不会替代精修或氛围感结果,用户进入预览后可分别切换查看。
当前所有封面模板 `quotaCost = 0`,弹窗显示“赠送 · 不占精修张数”,不会减少剩余修图额度。后续收费时保留同一选择流程,由服务端为模板返回大于 0 的 `quotaCost`,客户端在确认前显示实际消耗,并以服务端成功结果为准扣减;收费能力目前尚未实现。
再次对不少于 4 张照片选择新封面模板并修图时,新封面效果覆盖旧封面效果。封面承载图处理失败时不标记为封面成功,缩略图显示“修图失败”并允许重试。选择“还原为原图”时清除封面效果和封面类型。
### 状态流转图
![多图封面模板选择流程](./assets/travel-album-ai/05-cover-template-flow.svg)
### 本功能验收
- [ ] 选中 3 张时不显示封面模板。
- [ ] 选中 4 张或更多时显示封面模板。
- [ ] 有封面模块时,弹窗同时完整显示原图精修、氛围感和封面三个风格模块。
- [ ] 封面模板可横向滚动且始终单选。
- [ ] 每个封面模板均可独立预览,关闭预览后保留原选择。
- [ ] 默认选中旅行纪念册。
- [ ] 当前显示赠送并且不消耗精修额度。
- [ ] 成功后第一张选中照片成为封面承载图。
- [ ] 再次生成封面时以新结果覆盖旧结果。
## 7. 相册缩略图结果类型与显示优先级
### 功能说明
相册管理缩略图左上角复用同一状态位,根据照片当前结果显示不同类型,让用户无需进入预览即可区分未修、处理中、已修、封面和失败照片。
| 缩略图状态 | 角标文案与颜色 | 缩略图内容 | 进入条件 | 可用操作 |
| --- | --- | --- | --- | --- |
| 普通原图 | 绿色“已上传” | 服务端原图或缩略图 | 没有本地修图结果 | 预览、选择、AI修图、删除 |
| 处理中 | 蓝色“修图中” | 有旧成功结果时继续显示旧结果,否则显示原图 | AI 任务未结束 | 预览、删除;不能再次修图 |
| 普通修图结果 | 紫色“AI已修” | 最新精修图片 | 原图精修成功,可同时存在氛围感结果 | 预览原图/精修后/氛围感、再次修图、还原 |
| 封面结果 | 橙色“AI封面” | 所选封面模板排版图 | 封面承载图生成成功 | 预览、再次修图、重新生成封面、还原 |
| 修图失败 | 红色“修图失败” | 有旧成功结果时保留旧图,否则显示原图 | 最近一次任务失败 | 再次修图、删除 |
状态显示优先级为:
`修图中 > 修图失败 > AI封面 > AI已修 > 已上传`
该优先级保证处理中或失败状态不会被历史成功结果遮盖。普通修图成功后显示“AI已修”;封面模板成功后,第一张选中照片显示封面排版图和“AI封面”,其他照片继续显示“AI已修”。再次修图成功后缩略图切换为最新结果;还原原图后回到“已上传”;删除照片后对应本地状态一起删除。
当前状态和结果由内存数据源在相册管理与照片预览之间共享,尚不支持 App 重启后的恢复。服务端接入时至少需要返回素材 ID、任务状态、结果类型、结果图地址、原图精修预设 ID、是否生成氛围感、封面模板 ID、失败原因和额度消耗,客户端才能恢复完全一致的缩略图状态。
### 状态流转图
![相册缩略图结果状态](./assets/travel-album-ai/06-thumbnail-state-flow.svg)
### 本功能验收
- [ ] 未修图照片显示“已上传”。
- [ ] 修图中照片显示“修图中”且不能重复提交。
- [ ] 普通成功结果显示“AI已修”和最新效果图。
- [ ] 封面成功结果显示“AI封面”和封面排版图。
- [ ] 最近一次任务失败时优先显示“修图失败”。
- [ ] 还原、覆盖和删除后缩略图状态同步更新。
@@ -1,43 +0,0 @@
# 账号注销后端接口需求(简洁版)
> **本方案已被替代:** 2026-08-28 确认改用旧接口实现当前门店身份注销,不再实施主账号注销。参见 [门店身份注销接入说明](门店身份注销接入说明.md)。下文仅为历史记录。
当前 iOS 已有注销页面和本地 Mock 流程,需要后端提供真实能力。以下路径为建议,可复用已有等价接口。
## 1. 需要的接口
统一前缀:`/api/app/account-deletion`
| 接口 | 入参 | 需要返回 |
| --- | --- | --- |
| `GET /precheck` 注销核验 | 无,以 Token 识别主账号 | 钱包、作品与相册、项目、云盘资产;脱敏手机号;注销影响说明;是否可注销及阻断原因;核验标识和有效期 |
| `POST /send-sms-code` 发送验证码 | 无,发送至主账号绑定手机号 | 验证码会话标识、有效秒数、重发间隔;不返回验证码 |
| `POST /submit` 提交申请 | 核验标识、验证码会话及验证码、资产确认项、说明版本、幂等请求 ID | 申请 ID、状态、提交时间、计划注销时间 |
| `GET /status` 查询状态 | 无,以有效身份凭证识别主账号 | 当前状态、申请信息、能否取消、服务端时间 |
| `POST /cancel` 取消申请 | 申请 ID,使用恢复专用凭证 | 取消结果、新登录临时 Token、当前可选景区/门店身份 |
沿用现有 `token` 请求头和 `code/msg/data` 响应结构,成功码为 `100000`。错误需区分:注销条件不满足、核验过期、验证码错误/过期/限流、已有申请、超过取消期限、凭证失效。
## 2. 现有登录与鉴权需要配合
- 修改 `POST /api/app/v9/login`:身份验证通过后,正常账号按原流程登录;冷静期账号返回注销信息及**短时恢复专用 Token**,等待用户确认;已到期或已注销账号禁止登录。
- 恢复专用 Token 只能查询状态和取消绑定申请,不能访问业务接口。用户点击“恢复账号并登录”才调用取消接口,重新登录本身不能自动取消注销。
- `/api/app/v9/set-user`、刷新凭证及统一鉴权必须检查主账号状态,防止旧 Token、旧版本或其他设备绕过限制。取消成功后签发新凭证,不恢复旧 Token。
## 3. 必须保证的业务规则
1. **注销范围:** 手机号登录对应的主账号及其关联身份,由后端从凭证识别;不接受客户端指定任意手机号或用户 ID,不删除景区、门店实体或他人的共享资产。
2. **提交校验:** 后端再次检查验证码、资产确认及未完成业务;重复提交不能生成多个申请或延长冷静期。
3. **七天冷静期:** 截止时间由后端返回和判断,截止前可取消,恰好到期即不可取消;取消与到期任务必须互斥。
4. **会话限制:** 提交成功立即禁止该主账号全部设备的业务访问;客户端清理登录态。提交超时不能直接视为失败,应重新验证身份后查询结果。
5. **到期处理:** 后端自动执行,不依赖 App 在线;冷静期内不做不可逆删除,处理失败可重试,实际处理完成后才标记已注销。
建议状态:`none` 无申请、`pending` 冷静期、`canceled` 已取消、`processing` 到期处理中、`completed` 已完成。
## 4. 请后端与产品确认
- 余额、冻结款、提现中、未完成订单、线下未补缴款和负责人身份是否阻断注销,如何处理。
- 个人资产、共享资产、客户已购内容和交易记录分别删除、保留还是移交;注销后同手机号能否重新注册。
- 最终接口字段和错误码、短信频控、七天是否按 168 小时计算,以及测试账号、到期测试方式和可联调时间。
> 当前 Mock 的固定验证码和“放弃资产”文案仅用于演示,不能直接作为真实业务规则。
-431
View File
@@ -1,431 +0,0 @@
# 账号注销后端接口需求
> **2026-08-28 已被新范围替代:** 本次迭代改为使用旧 `account-deregister` 接口,仅注销当前 `store_user` 身份,不实施本文的主账号注销、新接口或恢复专用 Token 方案。当前接入情况见 [门店身份注销接入说明](门店身份注销接入说明.md)。下文保留为历史讨论记录。
更新日期:2026-08-27
适用端:随心瞰商家版 iOS;后端账号状态应同时约束 Android、旧版本客户端及其他登录入口。
文档性质:**接口需求建议稿,以下新增路径和字段尚未与后端确认,不代表线上已有接口。**
## 1. 需要后端提供什么
需要 **5 个注销接口、现有登录与鉴权流程改造,以及服务端到期处理任务**。
| 类型 | 建议接口 / 能力 | 用途 |
| --- | --- | --- |
| 新增 | `GET /api/app/account-deletion/precheck` | 返回真实资产、注销影响说明及阻断原因 |
| 新增 | `POST /api/app/account-deletion/send-sms-code` | 向主账号绑定手机号发送注销专用验证码 |
| 新增 | `POST /api/app/account-deletion/submit` | 校验验证码和用户确认,提交注销申请 |
| 新增 | `GET /api/app/account-deletion/status` | 查询当前账号的服务端注销状态及截止时间 |
| 新增 | `POST /api/app/account-deletion/cancel` | 冷静期内由用户明确确认后取消注销 |
| 修改 | `POST /api/app/v9/login` | 身份验证通过后区分正常登录、待注销恢复和不可恢复状态 |
| 修改 | `POST /api/app/v9/set-user` 与统一鉴权 | 禁止待注销或已注销账号取得、使用业务 Token |
| 服务端任务 | 到期注销、失败重试、会话失效 | 不依赖客户端在线或再次打开 App |
如果已有等价能力,可以复用后端现有路径,但需覆盖本文的数据和行为要求,不必重复建设。
### 当前客户端状态
- 已有设置入口、资产核验页、短信验证页、成功页、密码登录时的恢复确认和冷启动检查。
- 当前为 `AccountDeletionMockService`:资产是固定示例,验证码固定为 `123456`,注销记录保存在本机 `UserDefaults`。
- **目前不会发送真实短信,也不会注销后端账号或删除后端数据。**
- 接入真实接口后,服务端状态为唯一依据;本机时间、手机号输入值及本地记录不能作为注销结果或操作权限的依据。
## 2. 账号范围与统一约定
### 2.1 注销对象
当前功能意图是注销**手机号登录对应的主账号及其关联业务身份**,不是仅退出当前登录,也不是只停用当前选中的景区或门店身份。
- 后端根据 Token 解析稳定的主账号 ID,并据此聚合所有关联景区、门店身份的数据。
- 请求不接受客户端指定待注销的 `user_id`、`username`、`phone`、`scenic_id` 或 `store_id`;短信收件人也由后端确定。
- 当前业务身份资料里的手机号可能与主账号绑定手机号不同,不能直接用业务身份手机号发送注销短信。
- “解除景区、门店账号”指解除该用户的关联身份;**不应删除景区、门店实体,也不能误删其他用户、门店或客户共同拥有的数据**。
- 若存在管理员、负责人或共享资产,需明确移交、保留或阻断策略,不能因为客户端勾选“放弃资产”就直接删除。
### 2.2 请求和响应
沿用当前 App 网络层约定:
```http
Content-Type: application/json
Accept: application/json
token: <当前请求所需的凭证>
X-APP-VERSION: <客户端版本>
X-OS-TYPE: <客户端现有平台标识>
```
当前工程使用 `token` 请求头,**不是** `Authorization: Bearer ...`。恢复专用凭证也建议放在同一请求头,由服务端识别凭证类型和权限。
```json
{
"code": 100000,
"msg": "success",
"data": {}
}
```
- 成功业务码沿用 `100000`;新增失败码由后端统一分配,见第 6 节。
- JSON 字段使用 `snake_case`;布尔值使用 `true/false`,空列表使用 `[]`。
- ID 建议统一返回字符串,客户端不依赖数据库自增 ID 或 UUID 的内部格式。
- 时间统一使用带时区的 ISO 8601 字符串,例如 `2026-08-27T08:00:00Z`;客户端负责本地化显示。
- 时间相关响应返回 `server_time`;冷静期截止、验证码到期和取消资格全部由服务端判断。
### 2.3 状态及七天冷静期
建议在真实服务中区分“不可再取消”和“数据已处理完成”,避免定时任务尚未完成就展示为永久删除成功。
| `state` | 含义 | 允许恢复 | 允许进入业务 |
| --- | --- | --- | --- |
| `none` | 没有注销申请 | 不适用 | 是 |
| `pending` | 在冷静期内,待用户取消或到期 | 是,且服务端当前时间必须早于截止时间 | 否 |
| `canceled` | 最近一次申请已取消 | 不适用;可以重新申请 | 是,需重新取得有效业务 Token |
| `processing` | 已到截止时间,正在执行最终处理 | 否 | 否 |
| `completed` | 服务端已完成约定的注销处理 | 否 | 否 |
流转:`none/canceled → pending → processing → completed`;仅 `pending` 且未到期时允许转为 `canceled`。
- 建议默认冷静期为提交成功后 `7 × 24` 小时,后端返回准确的 `scheduled_deletion_at`,客户端不自行推算。
- 恰好到达截止时刻即不可取消;即使定时任务尚未运行,接口也必须立即按不可恢复处理。
- 任务失败保持不可恢复状态,记录原因并重试,不能重新开放登录或重置冷静期。
- 当前 Mock 没有 `processing` 状态;接入真实后端时客户端需同步扩展。
## 3. 五个接口的详细需求
### 3.1 注销前置核验
```http
GET /api/app/account-deletion/precheck
```
使用有效业务 Token,无查询参数。返回整个主账号范围内的资产快照、当前绑定手机号的脱敏值、注销后果,以及是否允许提交。
成功响应示例(资产数值仅为示例):
```json
{
"code": 100000,
"msg": "success",
"data": {
"precheck_id": "precheck_example_001",
"expires_at": "2026-08-27T08:10:00Z",
"server_time": "2026-08-27T08:00:00Z",
"masked_phone": "138****0000",
"can_submit": true,
"blocking_reasons": [],
"assets": [
{"kind": "wallet", "title": "钱包余额", "value": "0.00", "unit": "CNY", "value_text": "¥0.00"},
{"kind": "works", "title": "作品与相册", "value": "36", "unit": "item", "value_text": "36个"},
{"kind": "projects", "title": "项目", "value": "4", "unit": "item", "value_text": "4个"},
{"kind": "cloud_files", "title": "云盘文件", "value": "8589934592", "unit": "byte", "value_text": "8 GB"}
],
"consequences": [
"本人关联的景区与门店身份将解除",
"个人作品和云盘文件将按已确认的规则处理",
"提交后7天内再次登录并确认恢复,可取消注销"
],
"acknowledgement_version": "account-deletion-v1"
}
}
```
| 字段 | 要求 |
| --- | --- |
| `precheck_id` / `expires_at` | 后端生成的核验快照标识及有效期,绑定当前主账号,用于确认用户看到的资产和后果 |
| `masked_phone` | 主账号实际绑定手机号的脱敏值;不返回短信验证码 |
| `can_submit` | 是否满足注销条件;客户端据此禁止或允许继续 |
| `blocking_reasons` | 不可提交时返回 `[{"reason_code":"WALLET_NOT_SETTLED","message":"请先处理钱包余额或结算中的款项"}]`,可有多项 |
| `assets` | 当前页面需要 `wallet`、`works`、`projects`、`cloud_files` 四类;零资产也返回对应项 |
| `value` / `unit` / `value_text` | 原始值统一为字符串;金额为元且保留两位小数,计数、字节为整数字符串;`value_text` 供页面直接展示 |
| `consequences` | 由后端按最终业务规则返回,不能宣称会删除实际需要保留的数据 |
| `acknowledgement_version` | 本次注销说明版本,随提交保存确认记录 |
要求:
- 核验不得触发删除、资金扣除、身份解绑或短信发送。
- 作品、相册、项目及云盘文件的统计口径和去重方式由后端明确,不能把共享资产全部算成该用户可删除的资产。
- 余额、冻结款、提现中、未完成订单、未补缴收款等是否阻断,由产品和后端确认。**在规则确认前,建议未结清资金类问题阻断注销,不直接照搬 Mock 的“放弃余额”。**
- 提交时必须再次校验条件。资产或影响范围发生需要重新确认的变化时,返回“请重新核验”,不得默默沿用旧快照。
### 3.2 发送注销短信验证码
```http
POST /api/app/account-deletion/send-sms-code
```
使用有效业务 Token,请求体为 `{}`。只发送给当前主账号绑定手机号,不接受任意手机号参数。
```json
{
"code": 100000,
"msg": "success",
"data": {
"verification_id": "verification_example_001",
"masked_phone": "138****0000",
"expires_in": 300,
"retry_after": 60,
"server_time": "2026-08-27T08:01:00Z"
}
}
```
- `verification_id` 是验证码会话标识,绑定主账号、当时的绑定手机号及“账号注销”用途,提交时一并携带。
- `expires_in`、`retry_after` 单位为秒;示例为 5 分钟有效、60 秒后可重发,实际值由后端配置并返回。
- 使用 6 位数字验证码;不在响应、日志或埋点中返回明文验证码。
- 设置账号、手机号、IP 等维度的频率限制及错误次数限制;重发后旧验证码失效。
- 与登录、提现、实名认证等验证码用途隔离。可复用短信基础设施,不可混用验证码。
- 主账号换绑手机号后,旧手机号对应的验证码会话及核验快照失效,要求重新开始。
- 缺少绑定手机号、发送失败或触发限流时返回明确错误;前端不能在失败时显示“已发送”。
### 3.3 提交注销申请
```http
POST /api/app/account-deletion/submit
```
使用有效业务 Token。请求示例:
```json
{
"client_request_id": "709277cb-2085-49c1-b80f-042476c7c36b",
"precheck_id": "precheck_example_001",
"verification_id": "verification_example_001",
"sms_code": "482951",
"acknowledged_asset_kinds": ["wallet", "works", "projects", "cloud_files"],
"acknowledgement_version": "account-deletion-v1"
}
```
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `client_request_id` | 是 | 客户端为本次提交生成的 UUID 字符串;网络重试沿用同一个值 |
| `precheck_id` | 是 | 当前主账号的有效核验快照,不接受其他账号的快照 |
| `verification_id` / `sms_code` | 是 | 验证码会话及用户输入的真实短信码;示例不是固定验证码 |
| `acknowledged_asset_kinds` | 是 | 用户已确认的资产类别,须与快照中要求确认的集合一致 |
| `acknowledgement_version` | 是 | 用户确认的注销说明版本 |
成功响应示例:
```json
{
"code": 100000,
"msg": "success",
"data": {
"state": "pending",
"can_cancel": true,
"server_time": "2026-08-27T08:02:00Z",
"request": {
"id": "deletion_example_001",
"client_request_id": "709277cb-2085-49c1-b80f-042476c7c36b",
"status": "pending",
"submitted_at": "2026-08-27T08:02:00Z",
"scheduled_deletion_at": "2026-09-03T08:02:00Z",
"canceled_at": null,
"completed_at": null,
"acknowledged_asset_kinds": ["wallet", "works", "projects", "cloud_files"],
"acknowledgement_version": "account-deletion-v1"
}
}
}
```
处理要求:
1. 服务端重新校验身份、验证码、快照、确认版本以及资金和未完成业务条件。
2. 原子保存申请及用户确认记录、消耗验证码,并使该主账号所有设备上的业务 Token、旧登录临时 Token 和刷新凭证失去业务访问权限。
3. 冷静期内保留可恢复的身份及数据,不提前执行不可逆删除;状态必须限制新建订单、上传、提现等业务操作。
4. 同一主账号同一时刻最多有一条有效申请。已成功的同一 `client_request_id` 重试应返回原申请,不因验证码已消费而重复报错;同一幂等键不能承载不同请求内容。
5. 账号已有待处理申请时,不创建第二条,也不延长原截止时间;返回原状态或可识别的“已有申请”错误,客户端转查询状态。
6. 成功页使用服务端返回的截止时间。客户端收到成功响应后应立即清理业务登录态;即使用户尚未点击成功页“退出”,后端也已禁止业务访问。
**响应丢失的处理:** 提交可能已成功且原 Token 已失效。此时客户端重新完成身份验证,通过登录接口取得恢复专用凭证,再查 `status`;不得把网络超时直接当成提交失败,也不得为了查询结果自动取消注销。
### 3.4 查询注销状态
```http
GET /api/app/account-deletion/status
```
- 无查询参数,以凭证定位主账号。
- 接受有效业务 Token、正常登录临时 Token,或第 4 节定义的恢复专用 Token;不能提供匿名按手机号查询。
- 原业务 Token 已被注销操作撤销时,不重新赋予其查询权限,应先重新验证身份取得恢复专用 Token。
- 用于启动检查、回到前台、多设备状态校准,以及提交/取消请求超时后的结果确认。
响应 `data` 与提交接口一致,包含 `state`、`can_cancel`、`server_time`、`request`。无申请时:
```json
{
"code": 100000,
"msg": "success",
"data": {
"state": "none",
"can_cancel": false,
"server_time": "2026-08-27T08:00:00Z",
"request": null
}
}
```
- `canceled` 返回最近取消的申请和 `canceled_at`;`completed` 返回实际完成时间 `completed_at`。
- `can_cancel` 由后端计算,只能在状态为 `pending` 且未到截止时刻时为 `true`。
- 查询不得取消注销或延长冷静期。到期任务尚未完成时返回 `processing`,不能仅凭时间到了就返回 `completed`。
- 状态查询失败时,不应默认账号正常;客户端提示重试或重新登录,业务接口仍由后端鉴权保护。
### 3.5 取消注销申请
```http
POST /api/app/account-deletion/cancel
token: <恢复专用 Token>
```
只有重新验证身份且用户点击“恢复账号并登录”后调用:
```json
{
"request_id": "deletion_example_001"
}
```
成功响应示例:
```json
{
"code": 100000,
"msg": "success",
"data": {
"request_id": "deletion_example_001",
"state": "canceled",
"canceled_at": "2026-08-28T01:00:00Z",
"server_time": "2026-08-28T01:00:00Z",
"login": {
"token": "new-account-selection-token",
"scenic_users": [],
"store_users": []
}
}
}
```
- `login` 结构复用现有 v9 登录响应;示例数组省略业务内容,实际需返回恢复后当前可选的景区、门店身份。
- 返回的 `login.token` 为新生成的正常账号选择临时 Token;客户端继续走原有单账号自动选择、多账号选择及 `/v9/set-user` 流程,不复用注销前的旧凭证。
- `request_id` 必须与凭证绑定的主账号和注销申请一致,不能取消别人的申请或该账号下一次新申请。
- 与到期任务通过事务或等效并发控制互斥;按服务端时间决定取消是否成功,不能出现既恢复又删除的结果。
- 对同一已取消申请的重试,不重复改变状态或产生副作用。若凭证仍有效,可返回原取消结果和有效登录上下文;若凭证已撤销或过期,重新登录确认状态,不要求用户再次取消。
- 到期、`processing`、`completed` 均拒绝恢复,返回可识别错误;不得重新启用旧 Token。
## 4. 登录、会话与多端配合
### 4.1 调整现有 `/api/app/v9/login`
保留现有手机号密码登录参数。必须先完成密码/验证码等身份验证,再返回该主账号的注销状态,避免泄露任意手机号是否注册或注销。
| 状态 | 登录接口行为 |
| --- | --- |
| `none` / `canceled` | 沿用当前 `token`、`scenic_users`、`store_users`;可附带最新注销状态 |
| 未到期的 `pending` | 不签发正常账号选择或业务 Token;返回申请信息和恢复专用 Token,等待用户决定 |
| `processing` / `completed` | 返回明确的不可登录、不可恢复业务错误,不签发可登录凭证 |
待注销登录建议使用成功 Envelope 承载“验证身份成功但尚未登录”的结果:
```json
{
"code": 100000,
"msg": "success",
"data": {
"token": "",
"scenic_users": [],
"store_users": [],
"account_deletion": {
"state": "pending",
"request_id": "deletion_example_001",
"submitted_at": "2026-08-27T08:02:00Z",
"scheduled_deletion_at": "2026-09-03T08:02:00Z",
"server_time": "2026-08-28T01:00:00Z",
"can_cancel": true
},
"recovery_token": "opaque-recovery-token",
"recovery_token_expires_at": "2026-08-28T01:10:00Z"
}
}
```
恢复专用 Token 要求:
- 短时有效,并绑定主账号、当前注销申请和允许的操作;示例有效期为 10 分钟,实际由后端配置。
- **仅可查询注销状态和取消该申请**,不能调用 `/v9/set-user`、订单、钱包、文件下载等业务接口,也不能兑换或刷新成业务 Token。
- 不能通过改请求参数变成其他用户的凭证;不记录到普通日志、埋点或 URL。
- 用户选择“暂不登录”只结束本次登录,不调用取消接口;重新登录或查询本身不得自动恢复账号。
- 取消完成后撤销该申请的恢复操作权限;新登录凭证按取消接口约定返回。
### 4.2 修改 `/api/app/v9/set-user` 和统一鉴权
- 无论客户端是否接入新功能,待注销及不可恢复账号都不能通过旧临时 Token、刷新 Token、账号切换或其他登录入口继续使用业务。
- 校验“凭证属于谁、凭证可做什么、账号当前是什么状态”,不能只检查签名或过期时间。
- 提交注销需要覆盖同一主账号的全部设备和全部关联业务身份,不仅让提交申请的 iPhone 退出。
- Android 当前有 `/v9/login` 的密码和短信两种登录方式;所有方式执行相同的状态检查。iOS 短信登录入口目前仍待接入,不能因此遗漏后端约束。
- 冷静期取消后签发新凭证,不恢复旧凭证的有效性;业务身份列表以取消时的最新数据为准。
### 4.3 与客户端接入的关系
当前本地 `loginState`、`cancelDeletion` 是同步方法,接入网络后需改为异步;不能只替换 Mock 类名。
客户端还需扩展登录返回模型、恢复专用 Token 上下文、`processing` 状态、短信倒计时、阻断原因和核验快照处理。当前 Mock 使用登录输入框中的手机号取消注销,真实接口必须改用受验证的主账号/申请上下文,不能继续依赖可编辑输入值。
当前 Mock 申请 ID 使用 Swift `UUID`,资产枚举原始值包含 `cloudFiles`;本文建议接口使用字符串 ID 和 `cloud_files`。客户端需通过网络 DTO 显式映射,不能把本地 Mock 模型直接序列化后当作请求契约。
## 5. 服务端必须负责的后台处理
- 提交成功后持久保存申请,App 卸载、退出或长期离线都不影响到期处理。
- 冷静期内不执行不可逆清理;到期后先禁止恢复,再按最终确认的规则解除身份、处理个人资产和第三方关联。
- 共享资产、资金账务、交易记录、客户已购买内容等分别制定处理方案;需要保留的记录与可删除的个人数据应区分,保留范围及周期由相关负责人确认。
- 处理任务可重复执行且有重试机制;只有约定的必需处理步骤完成后才标记 `completed`,部分失败不能假报成功。
- 保留可审计的申请、确认说明版本、时间、取消记录、处理进度及失败原因;审计中不保留明文验证码或 Token。
- 到期删除与取消操作必须有统一的并发保护;业务鉴权和后台任务读取一致的主账号状态。
- 需要测试环境的可控时间或缩短冷静期能力,以便验证到期边界及失败重试;不得在生产开放客户端任意修改截止时间的接口。
## 6. 错误返回要求
沿用整数 `code` 和可直接展示的中文 `msg`。以下名称是**待后端分配业务码的语义清单**,不是现有错误码。
| 错误语义 | 典型场景 | 客户端处理 |
| --- | --- | --- |
| `ACCOUNT_IDENTITY_REQUIRED` | 主账号没有可用于验证的绑定手机号 | 提示先处理账号信息 |
| `DELETION_BLOCKED` | 资金未结清、订单未完成、负责人未移交等 | 展示阻断原因,禁止提交 |
| `PRECHECK_EXPIRED` / `PRECHECK_CHANGED` | 快照过期、资产或说明版本发生变化 | 重新核验并要求再次确认 |
| `ACKNOWLEDGEMENT_REQUIRED` | 缺少必需资产确认或确认版本不匹配 | 返回资产核验页 |
| `SMS_SEND_FAILED` / `SMS_RATE_LIMITED` | 短信发送失败或限流 | 提示重试;限流返回重试秒数 |
| `SMS_CODE_INVALID` / `SMS_CODE_EXPIRED` / `SMS_ATTEMPTS_EXCEEDED` | 验证码错误、过期、尝试超限 | 提示重输或重新发送 |
| `DELETION_ALREADY_PENDING` | 已有有效申请 | 查询原申请,不重复创建 |
| `NO_PENDING_DELETION` | 申请不存在或并非当前可取消申请 | 查询最新状态 |
| `DELETION_NOT_CANCELABLE` | 已到截止时间或正在最终处理 | 不再提供恢复入口 |
| `ACCOUNT_DELETION_COMPLETED` | 已完成注销 | 禁止登录和恢复 |
| `RECOVERY_TOKEN_INVALID` / `RECOVERY_TOKEN_EXPIRED` | 恢复凭证失效 | 重新验证身份,不自动取消 |
| `IDEMPOTENCY_CONFLICT` | 同一幂等键用于不同请求内容 | 停止自动重试,提示重新操作 |
错误响应继续采用现有 Envelope。后端如需在 `data` 中返回 `blocking_reasons`、`retry_after` 等结构化详情,需同时给出字段契约;当前 iOS `APIClient` 对失败响应只暴露 `code/msg`,接入时需要补充详情解析。
**与现有全局登录失效处理区分:** 当前 iOS 会将 HTTP `401/403` 及业务码 `180024`、`100091`、`100090`、`100060` 视为登录失效。资产阻断、验证码错误、快照过期等业务问题不要复用这些码,避免误触发全局退出。真实凭证失效仍沿用项目鉴权规则。
## 7. 联调验收清单
1. 主账号关联多个景区、门店时,核验范围完整;切换业务身份不会变成另一个注销对象;不能查询、提交或取消其他主账号的申请。
2. 无手机号、无资产、存在余额/冻结款/未完成业务时,分别返回约定的正常或阻断结果。
3. 正确验证码可提交;错误、过期、重发前旧码、其他用途码、其他账号验证码都不可提交。
4. 资产确认不完整、核验过期、资产发生变化、说明版本不一致时,后端拒绝并要求重新确认。
5. 成功申请返回准确截止时间;重复请求不产生新申请、不重置冷静期;请求超时后可重新验证身份并查到真实结果。
6. 提交成功后所有设备及旧版本业务访问受限;旧 Token、账号选择、短信登录等不能绕过注销状态。
7. 冷静期登录只显示恢复确认;点“暂不登录”保持待注销,点“恢复账号并登录”后取消并取得新的账号选择凭证。
8. 恢复专用 Token 只能查询状态和取消绑定申请;不能访问业务或取消其他申请。
9. 截止前可取消、恰好截止不可取消;取消与到期任务并发时只产生一个一致结果。
10. 未运行 App 也会按期处理;任务部分失败会重试,处理期间保持不可恢复,未完成时不返回 `completed`。
11. 已取消账号可再次发起新申请;旧申请和旧恢复凭证不能影响新申请。
12. 完成注销后不能登录或恢复;共享数据、客户内容和需保留记录按确认的方案处理,没有越权删除。
## 8. 请后端与产品确认并回传
- **账号及数据边界:** 主账号映射、关联身份范围;作品与相册/项目的统计口径;共享资产和已售内容的处理方式。
- **准入规则:** 余额是否允许放弃;冻结资金、提现中、未完成订单、线下未补缴款及负责人身份是否阻断,如何解除阻断。
- **时限和最终处理:** 七天是否按 168 小时计算;到期处理内容、记录保留范围与周期;注销后同手机号能否重新注册,且不得恢复旧账号数据。
- **接口契约:** 最终路径、字段类型、完整正常/异常响应、业务码、短信频控、恢复凭证权限及有效期。
- **联调交付:** 测试环境地址、测试账号及各状态样例、到期和失败重试验证方式、预计可联调时间。
上述规则确认前,客户端中的“永久删除”“放弃资产”等 Mock 文案不能直接作为最终业务承诺,应随实际后端处理规则调整。
-160
View File
@@ -1,160 +0,0 @@
# 门店身份注销接入说明
更新时间:2026-08-28。当前分支 `dev_9_7`,未提交或推送。
**最新实测:15:27:24的申请已于15:55:36因重新登录自动撤销(9 / CANCELLED_BY_LOGIN)。最新交互改为申请成功立即退出登录、下次登录取得身份凭证后核验;普通启动和普通页面返回前台不主动查询。本轮不重新申请、不等待终态。**
## 范围
采用用户提供的《260821-App 门店用户账号注销.md》中的旧接口,不需要新增手机号主账号接口。仅注销当前 `store_user` 对应的 `ss_store_user.id`;同手机号其他身份不受影响,不支持景区身份注销。移除了原手机号级 Mock、固定验证码和本地七天完成判定。
统一前缀:`/api/yf-handset-app/account-deregister`。
| 请求 | 请求体 |
| --- | --- |
| GET `/eligibility`、GET `/status` | 无 |
| POST `/waivers/wallet`、POST `/waivers/points` | `{"accepted":true}` |
| POST `/send-sms`、POST `/cancel` | 无业务字段 |
| POST `/apply` | `sms_code`、`reason` |
请求层冻结当前门店用户 ID 和 Token,请求前后核对身份;使用 `session.userId`,不是门店实体 ID。注销请求与响应正文不写入调试日志。完整脱敏数据见[旧接口返回数据说明](门店身份注销旧接口返回数据说明.md),操作经过见[接口实测](门店身份注销接口实测.md)。
## 已实现
- 设置页仅门店身份显示注销入口;页面采用“确认资产 → 手机验证”两步流程,以身份卡、资产卡、须知卡和底部主按钮明确注销范围。
- 注销页、只读条件页和状态页均使用下拉刷新,删除右上角刷新入口;操作过程统一使用全局通用 Loading,不保留页面内的小转圈。“其他身份不受影响”等提示不再区分其他身份类别。
- 查询资产、业务风险等待时间和全部阻断项;现金与积分合并为一个入口和一次弹窗确认,分别列明两项自愿放弃说明,零资产也不省略。底层顺序调用两个旧接口,逐次核对身份、余额及财务归属;部分成功保留真实标记,不自动重发修改请求。
- 条件满足后进入真实短信验证和申请确认。验证码为用户输入,收件手机号由后端决定。`/apply` 明确成功后立即复用已有退出通知清理会话并返回登录页,不再等一次状态查询,也不自动重新登录;响应丢失则保留核验保护,不能冒充成功或自动重发。
- 区分风险等待期 `eligible_at` 与冷静期 `cooling_until`;按服务端原文展示时间,不猜测时区。
- 严格识别实测状态:无记录、`0` 待确认草稿、`1` 冷静期、`9` 已撤销。字段缺失、状态矛盾和未知枚举都不默认放行。
- 普通冷启动使用已有会话直接进入首页;登录流程取得门店身份凭证后才核验状态,期间保留登录页面背景并显示全局 Loading,不显示“正在查询注销状态”的独立页面。正常结果直接进入业务页,冷静期、未知状态或失败才展示结果页。登录中的多身份选择仍属于此核验入口;已登录时切换身份不额外主动查询。完整草稿不影响第二次资产确认或正常使用。若核验发现冷静期,仍以独立卡片显示截止时间并提供下拉刷新、只读条件、主动撤销和退出入口。
- 用户确认撤销后先重查同一申请,POST 成功后再次 GET 核实。确认已撤销才恢复业务;响应丢失不自动重发,旧身份或旧限制版本的响应不能放行当前会话。旧 null/草稿不能作为撤销成功依据。
- 提交前按环境与门店用户 ID 保存意图,不保存 Token、手机号、验证码或资产。明确业务拒绝或新撤销记录可清除;重新申请前的旧撤销记录不能清除新意图。UserDefaults 不是后端幂等或断电事务保障。
- 冷静期 `eligibility` 虽然返回两项确认 false,也禁止再次确认;已撤销可重新准备申请,但必须按最新条件重新确认资产。
- 网络层将 `150015` 作为当前门店凭证的业务限制,不当作全局登录失效。核验期间暂停推送绑定和业务通知跳转;核验通过后恢复。
- 用15:29实测的150015与状态JSON补充集成回归,覆盖 `ProfileAPI.userInfo → APIClient → 身份限制通知 → RootCoordinator → 冷静期页面`;确认原Token和身份保留、业务暂停、无登录或撤销请求。通知转发在测试中使用独立NotificationCenter,不等同于真实SceneDelegate端到端操作。
- 普通页面从后台返回不主动查询,不替换导航栈。仅当前已显示的受限/核验页返回前台时刷新,并废弃后台前的旧查询;不会重新发送短信、申请或撤销。业务接口明确返回当前 Token 的 `150015` 时仍立即限制业务并核验。
- 普通登录、选择及切换身份不再无条件弹出注销提示;自动撤销规则保留在注销提交确认和申请状态页。登录后的服务端状态核验保持不变。身份列表为空时不进入首页,但“全部身份注销后后端究竟返回什么”尚未实测。
## 真实接口和页面验证
2026-08-28 在测试环境、配对 iPhone 11 上分次授权验证:
1. 两项零资产确认成功,第一次确认产生草稿0;第二次确认后允许申请。
2. 真机发送短信并收到验证码,14:23:40 提交测试申请,GET 确认冷静期1。
3. 14:24:04 主动撤销成功,14:24:05 两个 GET 均确认已撤销9。未留下待注销申请,未最终删除身份。
4. 安装新增状态处理后,14:39 重新打开 App,原身份直接恢复首页,没有重新登录;再次进入设置中的注销入口成功。读取详情页的镜像操作随后超时,因此未把撤销后详情页的完整展示记为通过。
短信和申请 POST 的原始响应体未保存,只有界面和后续 GET 的成功证据;不能将 GET 结构冒充 POST 响应。撤销响应体已保存脱敏样例。上述14:24立即撤销流程已结束。
随后用户明确要求在已登录的 iPhone Air 模拟器操作,并确认保留新申请至到期复核:15:27:24提交一次,界面自动进入冷静期限制页。15:28 GET `/status` 和 `/eligibility` 均确认冷静期1;15:29 GET `/userinfo` 返回真实150015,前后状态查询仍为1。截至15:50未撤销或重新登录;15:56再次只读复核时,服务端已返回15:55:36因重新登录自动撤销(9 / CANCELLED_BY_LOGIN)。该申请不再等待到期。模拟器仅用于用户指定的人工操作,不用于替代单元测试的真机要求。
## 自动化验证
- iPhone 11(`00008030-001E48E21139802E`),未使用模拟器。
- 冷静期/撤销首轮:61 项注销测试全部通过(API 8、进入核验21、响应8、流程24)。随后补充一项取消前后旧 null/草稿的防御测试,纳入最后全量回归。
- 冷静期页面真机 Mock 渲染截图已导出并检查,文案、截止时间和撤销入口完整可见。
- 测试中的短信、资产确认、申请与撤销只使用 Mock;宿主 App 仍可能产生既有后台请求,不能称整台设备离线。
- 前台恢复改动之前的全量:781项中771通过、10项失败(15条失败记录,2条 unexpected);与原始基线逐项对比,失败用例集合完全一致,无新增失败。注销相关62项全部通过(API 8、进入核验22、响应8、流程24)。全量不是全部通过。
- 前台路由版全量:791项中781通过、10项失败(15条失败记录,2条 unexpected),失败用例集合与基线完全一致;注销相关72项全部通过。
- UI改版前全量(15:45):792项中782通过、10项失败,无跳过项;失败用例集合仍与基线完全一致,无新增失败。注销相关73项全部通过(API 8、进入核验23、响应8、根路由10、流程24),均在iPhone 11运行;日志与xcresult摘要已交叉核对。
结果文件:
- 本轮61项:`/private/tmp/suixinkan-deregister-cooling-20260828.xcresult`
- 本轮冷静期截图:`/private/tmp/suixinkan-deregister-cooling-20260828-attachments/DB6A885A-C10C-49E0-A5F4-D3EB5D0AA3B1.png`
- 前台恢复改动之前的全量:`/private/tmp/suixinkan-deregister-cooling-full-20260828.xcresult`
- 前台恢复测试尝试(手机锁定,未执行,中断):`/private/tmp/suixinkan-deregister-foreground-20260828.xcresult`
- 当前最终构建(成功,仅编译):`/private/tmp/suixinkan-deregister-final-build-20260828.xcresult`
- 路由修正后32项回归:`/private/tmp/suixinkan-deregister-foreground-r2-20260828.xcresult`
- 前台路由版全量:`/private/tmp/suixinkan-deregister-foreground-final-20260828.xcresult`
- 当前最终全量:`/private/tmp/suixinkan-deregister-real-contract-20260828.xcresult`
- 原始基线:`/private/tmp/suixinkan-account-deletion-baseline-20260827-r2.xcresult`
## 尚未完成,不能视为完整上线验收
1. 旧文档没有给出终审“阻断”“正式完成”的数字枚举和完整响应;当前只能保守展示待核验页,不能伪造终态或自动清理账号。
2. 重新登录/选择身份自动撤销发生在哪个接口、受限 Token 能否切换其他身份、最终注销后鉴权及所有身份注销后的登录响应,尚未实测。
3. 真实150015接口响应已采样,客户端限制通知到核验页已用真实数据Mock验证;实际SceneDelegate端到端操作和真实多端验证仍缺。最新流程不再主动轮询其他设备的申请变化,依赖登录核验、用户进入注销功能查询或业务接口明确限制。
4. 新撤销按钮交互使用 Mock 验证与截图检查;真实撤销通过受控 API 完成,没有再次创建申请来验证新按钮。完整终态页面验收仍未完成。
5. 非零资产、确认过期、错误验证码、重复申请/撤销及完成后的错误响应还缺真实样例。
不要求后端新增接口;补充现有 Controller/Resource/DTO 源码或脱敏响应即可继续。严禁通过本机时间、`remaining_seconds: 0`、`can_apply` 或仍存在 `cooling_until` 推断已注销。
## 此前完整目标核对(15:50历史记录)
下表保留当时的验证范围。本轮最新结果见文末UI改版小节,不继续等待已撤销申请的终态。
| 要求 | 当前证据 | 结论 |
| --- | --- | --- |
| 仅当前门店身份,保留同手机号其他身份 | 请求冻结 `session.userId`/Token,8项请求层测试;无批量接口 | 已实现,路由回归已通过 |
| 展示所有条件、分别确认资产及快照失效 | 真实两项零资产确认与条件响应;模型/流程测试 | 已验证零资产正常流程,未真实操作非零资产 |
| 实际短信、申请确认、7天冷静期 | 真机收到短信,申请后 GET 返回1及7天截止时间 | 已验证,未捕获两个 POST 的原始响应体 |
| 主动撤销并恢复使用 | 真实 cancel 返回9,随后 GET 复核,冷启动恢复首页;撤销逻辑测试 | 主动接口已验证,新弹窗及前台路由Mock测试已通过 |
| 不将150015当作手机号登录失效 | 原请求Token隔离测试、推送暂停测试、真实userinfo响应;真实JSON重放至冷静期UI | 接口及客户端Mock集成已验证,实际SceneDelegate端到端操作待验证 |
| 冷启动、前台、多端及过期响应 | 冷启动已真机检查;RootTests 10项和状态回退测试1项 | 已通过iPhone真机Mock测试;不代替真实多端验证 |
| 冷静期到期复核、阻断、正式完成页面及清理 | 旧文档描述规则,但无终态枚举/响应 | 未完成,需要现有接口样例或源码 |
| 登录/选择同一身份自动撤销、全部身份注销后登录 | 旧文档规则、登录提示和空身份列表Mock测试 | 后端真实行为未验证 |
| 所有相关测试、全量回归及最终页面验收 | 当前73项注销相关测试通过;全量792项中10项基线失败 | 当前代码回归已执行;完整终态及真实多端验收未完成 |
| 不改变其他业务配置,不切分支、提交或推送 | 当前 `dev_9_7`;配置/依赖差异检查为空 | 保持约定 |
若后续扩展终态,仍需:`/status` 在“终审阻断”和“正式完成”时的脱敏完整响应或对应资源模型源码,以及终态 Token 查询和撤销规则。此前获准保留的测试申请已经自动撤销,本轮不重新申请。不得跳过7天冷静期、修改数据库或制造订单/资产变化来取得样例。
### 15:50完成度复核:仍未满足完整目标
直接核对当前状态模型和AccessViewModel:只映射草稿0、冷静期1和已撤销9,未知终态仍进入待核验页;尚无终审阻断、正式完成页面及清理依据。因此73项相关测试通过不能证明完整注销终态已经接入。
15:48与15:50的真实GET都返回同一冷静期申请,`completed_at`和`cancel_time`仍为null。期间只结束并重启iPhone Air的App进程,未清空数据或重新登录;重启后原Token仍能查询同一申请。窗口工具停留在另一模拟器窗口且菜单操作失败,未取得iPhone Air冷启动页面,故不将该项UI验证记为通过。
已查本机、原文档、公开文档入口、现有权限可见代码站及项目列表;仍无终态源码或响应。该缺口在提交后、真实响应集成回归后及本次复核中持续存在。当前需要服务端到期产生新状态,或取得现有后端源码才能推进终态接入;暂停重复测试与状态轮询。没有创建定时任务或自动监控,后续复核需恢复本任务。
## 本轮UI改版(2026-08-28)
- 两步页面沿用App蓝白色、16pt边距和白色圆角卡片;底部固定唯一主按钮,验证码与获取按钮同行;输入完整后才允许提交。
- 资产确认类阻断集中在资产卡,其他条件完整展示为待处理事项;订单等条件未满足时不能开始合并确认。
- 新增confirmAssetsAndContinue(snapshot:api:),跳过已确认项;任何一项失败或资产变化均停留在条件页,核对后由用户主动重试。
- 状态页用图标、标题、身份和截止时间分层展示。未知和失败状态只提供简短说明与重试,不伪造注销完成;删除草稿等调试文案。
- 本轮不扩展终态,不改变登录和状态核验架构,不操作真实短信、确认、申请或撤销。上文15:27—15:50记录为历史过程,当前以15:56已撤销结果为准。
- 最终真机回归:90项注销相关测试全部通过,其中本轮新增17项(13项合并确认、4项UIKit交互/布局);全量809项中799通过、10项失败、0跳过。失败用例集合与改版前基线完全一致,没有新增失败;全量不宣称全部通过。
- 已检查真机Mock截图:375pt资产页、非零资产及业务阻断、手机验证、冷静期、查询失败、未知状态;验证码与原因输入时,输入框可滚动至可见区域,底部按钮位于键盘上方。截图只包含测试窗口,未合成系统独立键盘窗口。所有短信及注销修改请求均为Mock,未再次操作真实注销。
- 现有键盘库的局部兼容处理已验证:只禁用本页两个输入框的重复位移,保持导航栏位置稳定;验证码和原因均可滚动至键盘上方,不改变其他页面的键盘设置。最终回归包含该处理。
本轮验证文件:
- 全量结果:`/private/tmp/suixinkan-deregister-redesign-r5-20260828.xcresult`
- 结构化摘要:`/private/tmp/suixinkan-deregister-redesign-r5-summary.json`
- 截图目录:`/private/tmp/suixinkan-deregister-redesign-r5-images/`
- 中间一次构建成功但因iPhone锁屏未能启动测试;解锁后使用同一构建完成上述真机回归,没有改用模拟器。
## 登录入口提示修复(2026-08-28)
- 移除登录、选择门店身份和切换门店身份时无条件出现的注销提示,删除无用的提示组件。
- 保留手机号/密码/协议校验和加载期间的防重复操作;不改变登录后的注销状态核验。自动撤销规则继续在注销提交确认和申请状态页说明。
- 登录页面支持注入现有AuthAPI用于Mock回归,不改登录协议、不访问真实登录接口,也不写入测试登录会话。
- iPhone 11回归:登录12项、账号切换2项、注销90项全部通过。全量812项中802通过、10项失败、0跳过;失败集合与修复前一致,没有新增失败。
- 结果:`/private/tmp/suixinkan-login-prompt-fix-20260828.xcresult`;摘要:`/private/tmp/suixinkan-login-prompt-fix-20260828-summary.json`。
## 申请成功退出与查询时机简化(2026-08-28)
- 提交前仍核对状态和资产;`/apply` 明确成功即发送现有退出通知,复用推送解绑、会话清理及返回登录页流程。不再追加成功后的 GET,也不先显示申请状态页。
- 仅填验证码、点击提交或打开最终确认弹窗都不会自动申请。最终弹窗新增“提交成功后将退出登录”;连点和成功后重复回调均不能重复申请或重复退出。
- 验证码等明确业务拒绝不退出;申请响应丢失保留提交意图并进入核验保护,不自动重发。已接受的提交意图仍按环境、门店身份保存,供下次登录核实。
- 普通启动和普通页面返回前台不主动查询注销状态;登录得到身份 Token 后核验。旧 `/status` 需要身份凭证,不能在未登录时只凭手机号查询;旧后端登录/选择该身份可能已自动撤销申请,因此此时可能返回已撤销。
- 保留业务 `150015` 限制和受限页面的刷新;不能因为简化正常启动就忽略服务端明确限制。进入注销功能时仍需查询条件与状态。
- 本轮只使用 Mock 验证提交和退出回调,不操作真实短信、资产确认、申请、撤销或登录。
- iPhone 11 真机全量回归:817项中807通过、10项失败、0跳过;失败用例集合与原始基线及上一轮完全一致,无新增失败。注销相关94项全部通过,包含本轮新增的登录核验时机、成功一次退出、失败不退出和最终确认弹窗测试;普通启动/前台不查询的路由断言同步更新。
- 结果:`/private/tmp/suixinkan-deregister-logout-20260828.xcresult`;摘要:`/private/tmp/suixinkan-deregister-logout-20260828-summary.json`。退出通过注入回调验证,没有为验收再次提交真实注销;现有退出通知到会话清理沿用原实现。
## 下拉刷新与统一 Loading(2026-08-28)
- 删除注销流程右上角刷新按钮,资产/验证页、只读条件页和申请状态页均支持下拉刷新;只执行查询,不自动确认资产、发短信、申请或撤销。
- 去掉注销页“景区”相关固定文案,统一描述其他身份不受影响。真实身份名称和服务端待处理事项仍如实展示,不改业务身份或接口字段。
- 初次查询、下拉刷新、确认资产、发送短信、提交申请及撤销操作统一复用 `GlobalLoadingManager` 的全屏遮罩与动画。下拉只作为触发手势,隐藏其系统转圈,避免叠加两套 Loading。
- 登录核验不再先创建查询根页面:保留当前登录页面背景,核验成功直接进入首页;异常结果才创建状态页,复用已查询的结果,不追加重复 GET。
- 保留请求前后的身份校验、限制信号优先级和后台旧响应作废。退出/切换会话会结束本次核验持有的 Loading,迟到响应不能关闭新请求的 Loading 或替换新账号页面。
- 测试使用独立会话、Mock 网络和测试窗口;本轮不发出真实短信、资产确认、申请、撤销或登录请求。
- 最终 iPhone 11 全量回归:821项中811通过、10项既有失败、0跳过,失败集合与上一轮完全一致。98项注销相关测试全部通过,覆盖下拉刷新、失败后结束加载、输入保留、登录期间不换根、重复核验及旧响应隔离。
- 已检查真机 Mock 截图:资产页和冷静期页右上角无刷新按钮;提交中显示现有白色圆角动画卡片与全屏灰色遮罩,没有页面内转圈。登录不换根通过独立窗口路由测试验证,未再次操作真实账号登录。
- 首次增量构建存在旧初始化签名缓存,清理构建产物后解决;连续 UI 回归需要等待测试窗口显示稳定后再模拟登录,修正仅在测试夹具中,业务代码没有新增延时,也没有放宽断言。
- 结果:`/private/tmp/suixinkan-deregister-loading-r3-20260828.xcresult`;摘要:`/private/tmp/suixinkan-deregister-loading-r3-20260828-summary.json`;截图:`/private/tmp/suixinkan-deregister-loading-r3-20260828-images/`。
-342
View File
@@ -1,342 +0,0 @@
# 门店身份注销接口实测
实测时间:2026-08-28 11:39;追加 13:45–13:46 零资产确认(服务端响应时间)。
环境:`https://api-test.zhifly.cn`。首次 11:39 使用用户明确授权的 iPhone 当前 `store_user` 登录 Token,仅执行 `GET /eligibility` 与 `GET /status`,当时未调用修改接口。后续额外授权的两项零资产确认见第 5 节。请求头与当前 Debug App 一致:`token`、`X-APP-VERSION: 1.3.1`、`X-OS-TYPE: iOS`、JSON Accept/Content-Type。
以下为真实响应;仅 `store_user_id`、`finance_identity_id` 统一替换为数值 `0` 脱敏,不能把该值用作有效身份。没有保存 Token。
## 1. 注销条件
`GET /api/yf-handset-app/account-deregister/eligibility`
HTTP 200,业务码 `100000`:
```json
{
"code": 100000,
"msg": "success",
"data": {
"can_apply": false,
"store_user_id": 0,
"finance_identity_id": 0,
"wallet_balance_fen": 0,
"wallet_balance": "0.00",
"points_balance": 0,
"wallet_waived": false,
"points_waived": false,
"unfulfilled_count": 37,
"fulfillment_in_progress_count": 0,
"risk_end_at": "2026-08-27 15:47:32",
"eligible_at": "2026-09-03 15:47:32",
"risk_window_hours": 168,
"blockers": [
{
"code": "ORDER_UNFULFILLED",
"message": "账户仍有未履约订单或带单",
"action": "complete_orders"
},
{
"code": "RISK_WINDOW_NOT_EXPIRED",
"message": "最后一笔业务的风险结束时间尚未超过7天",
"action": "wait_risk_window"
},
{
"code": "WALLET_WAIVER_MISSING",
"message": "请先确认放弃现金余额",
"action": "confirm_wallet_waiver"
},
{
"code": "POINTS_WAIVER_MISSING",
"message": "请先确认放弃积分",
"action": "confirm_points_waiver"
}
],
"deregister": null
},
"time": "2026-08-28 11:39:51"
}
```
本次观测到的类型:
| 字段 | JSON 类型 / 含义 |
| --- | --- |
| `can_apply` | 布尔值,服务端是否允许申请 |
| `store_user_id`、`finance_identity_id` | 整数 |
| `wallet_balance_fen` | 整数,现金余额(分) |
| `wallet_balance` | 字符串,金额展示值;本次为 `"0.00"` |
| `points_balance` | 整数,积分余额 |
| `wallet_waived`、`points_waived` | 独立布尔值,是否已确认放弃 |
| `unfulfilled_count`、`fulfillment_in_progress_count` | 整数 |
| `risk_end_at`、`eligible_at` | 时间字符串,格式 `yyyy-MM-dd HH:mm:ss`;时区及无历史业务时的空值形式尚未确认 |
| `risk_window_hours` | 整数,本次为 `168` |
| `blockers` | 对象数组,每项有字符串 `code`、`message`、`action` |
| `deregister` | 本次为 `null`,尚未观测非空结构 |
本次余额与积分均为零,服务端仍返回两项放弃确认缺失。客户端不能因为资产为零就省略确认。当前还有 37 项未履约订单或带单,且风险等待期未结束,不能提交注销。`eligible_at` 是业务风险期截止时间,不是提交申请后的冷静期截止时间;达到该时间也不代表其他条件自动满足。
## 2. 注销状态
`GET /api/yf-handset-app/account-deregister/status`
HTTP 200,业务码 `100000`:
```json
{
"code": 100000,
"msg": "success",
"data": {
"deregister": null
},
"time": "2026-08-28 11:39:51"
}
```
当前查询没有返回注销申请。不能据此推断非空申请的字段名称、状态枚举或是否可撤销。
## 3. 首次只读探测后尚缺的信息(后续进展见第 5、6 节)
- 草稿、冷静期、阻断、撤销、完成时 `data.deregister` 的非空结构、状态字段和值。
- `cooling_until`、`remaining_seconds` 的实际位置及类型。
- 无历史业务时风险时间的空值形式、时间字符串所用时区。
- 重新登录或选择身份自动撤销的具体接口时机,以及受限 Token 是否允许切换其他身份。
这些情况需要现有后端响应样例/源码,或在获得明确授权的专用测试身份上验证。本次授权仅限只读查询,不为采样创建、撤销注销申请或确认资产放弃。
## 4. 后续零资产确认授权(2026-08-28 13:42)
用户随后明确允许:在测试环境重新核实现金余额与积分均为 0 后,分别调用两项资产放弃确认接口,并查询是否产生草稿。此授权不包含发送短信、提交或撤销注销申请,也不允许非零资产确认。
已确认 iPhone 11 连接,并读取本 App 的当前会话:没有登录 Token,保存的账号类型为 `photog`,不是旧注销接口要求的 `store_user`。因此此次没有发出任何接口请求或资产确认,设备偏好未修改,本地临时偏好副本已删除。需在测试版 App 登录门店身份后继续;不要为采样登录已有未完成注销申请的身份,以免触发自动撤销。
## 5. 已完成两项真实零资产确认(2026-08-28 13:45–13:46)
用户重新登录后,核实当前为有效 `store_user`。每项确认前均重新查询:身份与设备会话一致、现金余额(分和展示金额)与积分均为 0;只访问测试环境,禁止重定向,不自动重试修改请求。仅执行了已授权的 `POST /waivers/wallet`、`POST /waivers/points` 各一次,没有发送短信、提交或撤销注销申请。
本次身份初始没有未履约/交付处理中记录,`risk_end_at`、`eligible_at` 均为 JSON `null`,阻断仅为两项确认缺失。与第 1 节较早查询的身份情况不同,不能沿用之前的 37 项未履约记录。
| 阶段 | `wallet_waived` | `points_waived` | `can_apply` | `deregister` |
| --- | --- | --- | --- | --- |
| 确认前 | false | false | false | null |
| 现金确认后 | true | false | false | `status: 0` 草稿 |
| 积分确认后 | true | true | true | 同一未提交草稿 |
两个确认请求均 HTTP 200、`code: 100000`,`data` 返回 `asset_type`(分别为 `"wallet"`、`"points"`)、`wallet_balance_fen: 0`、`wallet_balance: "0.00"`、`points_balance: 0` 及两项确认时间。现金确认后 `wallet_waived_at: "2026-08-28 13:45:57"`、`points_waived_at: null`;积分确认后后者变为 `"2026-08-28 13:46:42"`。
两个 GET 中的 `data.deregister` 都返回以下结构;仅 `id` 脱敏为 0,实际是正整数:
```json
{
"id": 0,
"status": 0,
"status_label": "待确认",
"reason": "",
"apply_time": null,
"cooling_until": null,
"remaining_seconds": 0,
"cancel_time": null,
"blocked_code": "",
"blocked_reason": "",
"completed_at": null
}
```
**确认结论:第一次资产确认即产生非空草稿;`status` 为数字,0 对应“待确认”。非空记录不等于已提交,不能一律限制普通业务或后续资产确认。两项确认后即使仍有草稿,`can_apply` 也会变为 true。** `remaining_seconds: 0` 在草稿中存在,不能单独据此认定冷静期已结束或注销已完成。
已据此修正客户端草稿判断、继续确认/验证和启动核验,并补充 Mock 测试。只将完整且无提交、撤销、阻断或完成字段冲突的 `status: 0` 识别为草稿;未知状态仍不推断。所有本地会话临时副本已删除,未写回设备偏好,未保存 Token。
仍缺:冷静期、撤销、阻断、完成的实际状态值及对应时间数据;确认过期响应;登录/切换自动撤销的具体时机。继续真实验证需要另行授权短信、提交与撤销操作,当前授权不包含这些操作。
## 6. 短信、申请与立即撤销(2026-08-28 14:16–14:24)
用户另外授权在切换后的专用测试身份上发送短信、提交申请,获取状态后立即撤销。真机页面核实:现金/积分为0、两项确认已完成、无未履约及风险等待阻断。通过 iPhone 镜像发送短信,用户提供验证码后提交;未将验证码写入源码或文档。短信和申请的原始 POST 响应体没有保存,不能把后续 GET 当作 POST 原文。
- 申请原因:`API test; cancel immediately`。
- 申请时间:`2026-08-28 14:23:40`。
- 查询确认:`status: 1`、`status_label: "冷静期中"`,冷静期截止 `2026-09-04 14:23:40`。
- 核对当前身份、同一申请及原因后只调用一次 `/cancel`,不自动重试。
- 撤销成功时间:`2026-08-28 14:24:04`,返回 `status: 9`、`status_label: "已撤销"`。
- `14:24:05` 再次 GET `/status` 与 `/eligibility` 确认已撤销;未留下待注销申请,未执行最终注销。
- 本地临时会话副本已删除,未编辑设备偏好,未在文档中保存 Token、手机号或真实身份 ID。
### 冷静期状态
```json
{
"code": 100000,
"msg": "success",
"data": {
"deregister": {
"id": 0,
"status": 1,
"status_label": "冷静期中",
"reason": "API test; cancel immediately",
"apply_time": "2026-08-28 14:23:40",
"cooling_until": "2026-09-04 14:23:40",
"remaining_seconds": 604776,
"cancel_time": null,
"blocked_code": "",
"blocked_reason": "",
"completed_at": null
}
},
"time": "2026-08-28 14:24:03"
}
```
### 撤销响应
```json
{
"code": 100000,
"msg": "注销申请已撤销",
"data": {
"deregister": {
"id": 0,
"status": 9,
"status_label": "已撤销",
"reason": "API test; cancel immediately",
"apply_time": "2026-08-28 14:23:40",
"cooling_until": "2026-09-04 14:23:40",
"remaining_seconds": 0,
"cancel_time": "2026-08-28 14:24:04",
"blocked_code": "CANCELLED_BY_USER",
"blocked_reason": "用户主动撤销注销",
"completed_at": null
}
},
"time": "2026-08-28 14:24:04"
}
```
### 撤销后的条件
```json
{
"code": 100000,
"msg": "success",
"data": {
"can_apply": false,
"store_user_id": 0,
"finance_identity_id": 0,
"wallet_balance_fen": 0,
"wallet_balance": "0.00",
"points_balance": 0,
"wallet_waived": false,
"points_waived": false,
"unfulfilled_count": 0,
"fulfillment_in_progress_count": 0,
"risk_end_at": null,
"eligible_at": null,
"risk_window_hours": 168,
"blockers": [
{
"code": "WALLET_WAIVER_MISSING",
"message": "请先确认放弃现金余额",
"action": "confirm_wallet_waiver"
},
{
"code": "POINTS_WAIVER_MISSING",
"message": "请先确认放弃积分",
"action": "confirm_points_waiver"
}
],
"deregister": {
"id": 0,
"status": 9,
"status_label": "已撤销",
"reason": "API test; cancel immediately",
"apply_time": "2026-08-28 14:23:40",
"cooling_until": "2026-09-04 14:23:40",
"remaining_seconds": 0,
"cancel_time": "2026-08-28 14:24:04",
"blocked_code": "CANCELLED_BY_USER",
"blocked_reason": "用户主动撤销注销",
"completed_at": null
}
},
"time": "2026-08-28 14:24:05"
}
```
冷静期 GET `/eligibility` 已返回两项确认 false 和缺少确认的两个阻断,撤销后也是如此;冷静期不能因此重新确认资产。撤销记录保留旧冷静期时间;remaining_seconds 为0不等于注销完成。正式完成与终审阻断的状态值、自动撤销和所有身份注销后的登录响应仍未验证。此次授权操作已结束,不继续发送短信或创建申请。
## 2026-08-28:补查终态契约及新申请准备
用户随后允许继续申请注销,以尝试获取缺失的终态响应。本节是新的调查记录,不代表已经创建新的申请。
| 查找范围 | 结果 |
| --- | --- |
| 本机 iOS、Android 参考工程及桌面相关文件 | 未找到旧注销后端的 Controller、Resource、状态枚举或 migration 源码 |
| 测试 API 的 `/docs`、`/api/documentation`、`/openapi.json`、`/swagger.json` | HTTP 200,但业务体均为路由不存在,不能当作可用接口文档 |
| Gitea 匿名仓库搜索 | 没有返回匹配仓库;不代表私有仓库不存在 |
| Chrome 中已有登录的 Gitea 会话 | 可访问当前用户的仓库列表;代码搜索 `StoreUserDeregister`、`deregister` 均未匹配,未取得注销后端源码 |
| 已打开的飞书原始文档 | 页面显示最近修改为8月21日;相关规则与本地文档一致,未给出终态数字枚举、完整 JSON 或完成后的鉴权响应 |
| 真机当前 App 会话 | 无有效门店登录态,因此没有发起认证查询、短信、资产确认、申请或撤销 |
当前仍需先在真机登录一个允许最终注销的专用测试门店身份,再读取该身份的条件。若保留申请等待正式完成,需经过服务端7天冷静期;不能修改本机时间来替代服务端等待,也不能为制造终审阻断而擅自修改订单、资产或数据库。按旧文档,重新登录或选中申请中的同一身份会自动撤销申请,等待期间应避免这种操作。
“终审阻断”需要到期复核发现条件变化,单纯提交并等待不能保证得到该状态。此次只读查找未新增任何终态实测结论。
## 2026-08-28 15:23:按用户要求改在模拟器准备新申请
- 用户明确要求操作其已登录的 iPhone Air 模拟器。本次是人工界面操作,不是模拟器单元测试;不改变此前真机测试结论。
- 页面当前身份为北大科技园,显示现金0元、积分0;初始只有两项资产确认阻断,上次申请已撤销。
- 用户授权分别确认两项零资产并发送验证码。继续操作时,现金已显示确认完成,因此没有重复提交现金确认;随后单独确认零积分。
- 页面显示两项资产均已确认、当前条件满足;仍为未提交草稿。
- 15:23通过页面发送一次注销验证码,界面提示已发送至当前身份绑定手机号。未保存短信 POST 原始响应,不能把界面提示当作完整接口 JSON。
- 停留在验证码输入页,等待用户提供本次验证码;此时尚未提交新申请,也未进入新的冷静期。
## 2026-08-28 15:27—15:29:新申请已提交并保留
- 用户提供本次验证码,并在最终弹窗前明确确认:只注销北大科技园身份,保留申请至服务端7天后复核,正式完成不可恢复,不像上次立即撤销。
- 15:27:24在模拟器点击一次“确认提交申请”。原因为 `Test account deregistration`;验证码不写入文档或代码。
- 页面进入冷静期核验页,显示截止时间 `2026-09-04 15:27:24`(服务端时间)。
- 15:28:27只读 GET `/status`、`/eligibility` 均确认 `deregister.status: 1`,`cancel_time`、`completed_at` 均为 null。
- 15:29:22以同一现有 Token GET `/api/yf-handset-app/userinfo`,返回 HTTP 200、业务码150015、`data.status: "cooling"`、相同冷静期截止时间;响应没有 `time` 字段。前后 GET `/status` 均成功且仍是同一冷静期记录。
- 未重复发送短信、未重复申请、未执行撤销、登录或身份选择。未保留 Token 副本或修改模拟器偏好设置。
**此时申请仍待注销,与14:24已撤销的旧申请不同;后续已于15:55:36因重新登录自动撤销,见文末15:56记录。** 终审阻断/正式完成未发生;不能把冷静期截止当作已经注销。查询样例已加入[旧接口返回数据说明](门店身份注销旧接口返回数据说明.md)。短信和申请 POST 原始响应体仍未捕获,不能用 GET 代替。
本轮脱敏证据:
- `/private/tmp/suixinkan-contract-discovery-20260828/152826-simulator-status-redacted.json`
- `/private/tmp/suixinkan-contract-discovery-20260828/152826-simulator-eligibility-redacted.json`
- `/private/tmp/suixinkan-contract-discovery-20260828/152921-simulator-userinfo-cooling-redacted.json`
- `/private/tmp/suixinkan-contract-discovery-20260828/152921-simulator-status-before-auth-redacted.json`
- `/private/tmp/suixinkan-contract-discovery-20260828/152921-simulator-status-after-auth-redacted.json`
## 2026-08-28 15:45:真实响应回归与源码补查
- 将本轮150015及对应status响应整理为Swift测试夹具;逐字段对比采样JSON,仅注销记录ID替换为测试值301。
- 在iPhone 11上完成全量792项测试:782通过、10项既有失败,无跳过;失败用例集合与原始基线一致。注销相关73项全部通过,包括真实userinfo限制响应经API、通知和路由进入冷静期页面的新增Mock集成测试。
- 本轮仅更新测试与文档,未操作模拟器中的真实申请;未变更工程配置、版本号或依赖。
- 补查已保存项目列表,未发现相关后端工程;Android工程现有Git主机的HTTP网页根返回空响应,未取得源码。未猜测后端仓库路径、修改服务器或扩展访问权限。
- 结果:`/private/tmp/suixinkan-deregister-real-contract-20260828.xcresult`。终态样例仍缺,不能将测试通过作为正式注销完成的证据。
## 2026-08-28 15:48—15:50:最终只读复核与冷启动尝试
- 15:48:04查询仍为申请时间15:27:24的冷静期1。
- 仅终止并重启iPhone Air中的App进程;未卸载、清空数据、登录或选择身份。工具未能切换至正确模拟器窗口,故冷启动UI没有验证完成。
- 15:50:51用原会话再次GET `/status`、`/eligibility`,均返回同一申请的冷静期1,截止时间不变,`remaining_seconds: 603392`,撤销时间与完成时间均为null。
- 证据:`/private/tmp/suixinkan-contract-discovery-20260828/155050-simulator-status-redacted.json`及同前缀的`eligibility-redacted.json`。
- 未产生新的终态依据,不继续重复轮询或制造业务变化;待服务端到期状态或已有源码可用后再推进。
## 2026-08-28 15:56:重新登录自动撤销
用户告知已进入后,只读查询发现15:27:24申请已于15:55:36自动撤销。两次GET均返回status9、CANCELLED_BY_LOGIN、用户重新登录自动撤销的说明,completed_at仍为null。没有调用cancel或重新申请;未采集触发撤销的具体登录请求,因此不能断言具体接口时机。
证据:/private/tmp/suixinkan-contract-discovery-20260828/155633-simulator-status-redacted.json及同前缀eligibility文件。此前待注销记录的截止时间已失效,不再等待该申请于9月4日完成。用户随后要求UI简化,本轮仅使用Mock测试,不再操作真实注销。
## 2026-08-28 17:01:验证资产确认接口的false参数
用户单独授权尝试现金、积分接口传`accepted: false`。使用iPhone 11当前已登录门店身份和测试环境,先只读确认两项标记均为true、金额均为0、状态为未提交草稿0,再分别调用一次两个确认接口。
- 现金:17:01:14返回HTTP 200、业务码100099、`msg: 请明确确认自愿放弃对应资产`、`data: []`。
- 积分:17:01:32返回同样结果。
- 每次调用后重新GET查询:`wallet_waived`和`points_waived`仍为true,草稿仍为0,金额未变化,没有进入冷静期。
- 结论:当前接口不接受false作为撤回确认;本轮没有调用true、短信、申请、撤销或登录接口。未重试POST。
完整返回及前后状态见[旧接口返回数据说明4.4节](门店身份注销旧接口返回数据说明.md)。脱敏证据目录:`/private/tmp/suixinkan-waiver-false-20260828/`。
@@ -1,477 +0,0 @@
# 门店身份注销旧接口返回数据说明
整理日期:2026-08-28。依据旧接口文档及当天测试环境的真实响应。
**注销范围:当前 `store_user` 对应的门店用户身份,不是手机号主账号,不影响同手机号的其他身份。**
示例中的 `store_user_id`、`finance_identity_id` 和注销记录 `id` 均脱敏为 `0`,实际为正整数;脱敏值不能用于请求或业务判断。本文不包含 Token、手机号或姓名。
## 1. 请求信息与实测范围
- 测试环境:`https://api-test.zhifly.cn`
- 统一路径前缀:`/api/yf-handset-app/account-deregister`
- 请求头:`token: <当前身份Token>`、`X-APP-VERSION: 1.3.1`、`X-OS-TYPE: iOS`、JSON Accept/Content-Type。
| 方法 | 路径后缀 | 用途 | 真实响应覆盖情况 |
| --- | --- | --- | --- |
| GET | `/eligibility` | 查询注销条件 | 已实测:阻断、两项确认、允许申请、冷静期、撤销后 |
| GET | `/status` | 查询注销记录 | 已实测:无记录、待确认草稿、冷静期、已撤销 |
| POST | `/waivers/wallet` | 确认放弃现金 | 已实测:零余额确认成功 |
| POST | `/waivers/points` | 确认放弃积分 | 已实测:零积分确认成功 |
| POST | `/send-sms` | 发送注销验证码 | 真机发送并收到短信;未保存原始响应体 |
| POST | `/apply` | 提交注销申请 | 真机提交后 GET 确认冷静期;未保存 POST 原始响应体 |
| POST | `/cancel` | 撤销注销申请 | 已实测:撤销成功,并 GET 复核 |
已按用户分次授权执行两项零资产确认,以及一次“短信 → 提交 → 查询 → 立即撤销”。申请时间为 14:23:40,14:24:04 撤销成功,14:24:05 查询确认已撤销;未保留待注销申请,未执行最终注销。正文不记录验证码。
**此前申请状态(15:56复核):15:27:24创建的申请已于15:55:36因重新登录自动撤销(9 / CANCELLED_BY_LOGIN)。没有保留这次待注销申请,旧截止时间不再代表有效冷静期。**
17:01补充实测:当前真机身份查询为未提交草稿0,两项资产已确认,尚未进入冷静期。按用户授权分别尝试`accepted: false`,两条接口均拒绝,确认标记没有重置,见4.4节。
## 2. 公共响应与业务码
| 字段 | 已观测 JSON 类型 | 含义 |
| --- | --- | --- |
| `code` | number(整数) | 业务码,成功为 `100000` |
| `msg` | string | 响应说明,成功可为 `"success"` 或 `"注销申请已撤销"`,不能固定匹配文案 |
| `data` | object;未登录时为 array | 具体业务数据 |
| `time` | string | 成功样例中的服务端时间,格式 `yyyy-MM-dd HH:mm:ss`;时区待确认 |
**HTTP 200 不等于业务成功,必须检查 `code`。**
| 业务码 | 含义 | 验证情况 |
| --- | --- | --- |
| `100000` | 请求成功 | 两个 GET、两个资产确认及撤销响应均观察到 |
| `100090` | 未登录 | 不带 Token 查询两个 GET 时实测 |
| `100099` | 本次表示未明确同意放弃资产 | 两项确认接口传`accepted: false`时实测,见4.4 |
| `150015` | 冷静期身份访问普通业务接口受限 | 15:29使用申请中的身份 GET `/api/yf-handset-app/userinfo` 实测,见5.7 |
两个 GET 的未登录响应均为 HTTP 200,正文如下,没有 `time` 字段:
```json
{"code":100090,"msg":"未登录","data":[]}
```
## 3. 注销条件:GET `/eligibility`
### 3.1 存在阻断时的真实响应
HTTP 200,以下为服务端 `2026-08-28 11:39:51` 的响应:
```json
{
"code": 100000,
"msg": "success",
"data": {
"can_apply": false,
"store_user_id": 0,
"finance_identity_id": 0,
"wallet_balance_fen": 0,
"wallet_balance": "0.00",
"points_balance": 0,
"wallet_waived": false,
"points_waived": false,
"unfulfilled_count": 37,
"fulfillment_in_progress_count": 0,
"risk_end_at": "2026-08-27 15:47:32",
"eligible_at": "2026-09-03 15:47:32",
"risk_window_hours": 168,
"blockers": [
{
"code": "ORDER_UNFULFILLED",
"message": "账户仍有未履约订单或带单",
"action": "complete_orders"
},
{
"code": "RISK_WINDOW_NOT_EXPIRED",
"message": "最后一笔业务的风险结束时间尚未超过7天",
"action": "wait_risk_window"
},
{
"code": "WALLET_WAIVER_MISSING",
"message": "请先确认放弃现金余额",
"action": "confirm_wallet_waiver"
},
{
"code": "POINTS_WAIVER_MISSING",
"message": "请先确认放弃积分",
"action": "confirm_points_waiver"
}
],
"deregister": null
},
"time": "2026-08-28 11:39:51"
}
```
### 3.2 `data` 字段说明
| 字段 | 已观测 JSON 类型 | 数据信息 |
| --- | --- | --- |
| `can_apply` | boolean | 服务端是否允许提交申请 |
| `store_user_id` | number(整数) | 门店用户身份 ID,不是门店实体 ID |
| `finance_identity_id` | number(整数) | 财务身份 ID |
| `wallet_balance_fen` | number(整数) | 现金余额,单位分,适合金额计算 |
| `wallet_balance` | string | 现金金额展示值,如 `"0.00"`,不是 JSON 数字 |
| `points_balance` | number(整数) | 积分余额 |
| `wallet_waived` | boolean | 当前现金余额快照是否已确认放弃 |
| `points_waived` | boolean | 当前积分余额快照是否已确认放弃 |
| `unfulfilled_count` | number(整数) | 未履约记录数量;本次分别观察到 37 和 0 |
| `fulfillment_in_progress_count` | number(整数) | 处理中记录数量,本次为 0;具体统计范围待后端确认 |
| `risk_end_at` | string / null | 最后业务风险结束时间 |
| `eligible_at` | string / null | 业务风险等待截止时间,**不是注销冷静期截止** |
| `risk_window_hours` | number(整数) | 业务风险等待时长,本次为 168 小时 |
| `blockers` | array<object> | 全部阻断项;无阻断时为 `[]` |
| `deregister` | object / null | 注销记录;草稿结构见第 5 节 |
时间字符串本次采用 `yyyy-MM-dd HH:mm:ss`。另一个测试身份的两个风险时间均为 null,不能把 null 转成当前时间或自行追加 168 小时等待。
### 3.3 `blockers[]` 字段与取值
每项包含三个字符串:`code` 为业务标识,`message` 为展示提示,`action` 为建议动作。应展示全部阻断项。
| 已实测 `code` | 含义 | 已实测 `action` |
| --- | --- | --- |
| `ORDER_UNFULFILLED` | 存在未履约订单或带单 | `complete_orders` |
| `RISK_WINDOW_NOT_EXPIRED` | 风险等待期未结束 | `wait_risk_window` |
| `WALLET_WAIVER_MISSING` | 未确认放弃现金 | `confirm_wallet_waiver` |
| `POINTS_WAIVER_MISSING` | 未确认放弃积分 | `confirm_points_waiver` |
旧文档另列出以下代码,但未实测其完整对象和 `action`:
| 仅文档列出的代码 | 文档含义 |
| --- | --- |
| `SHARED_FINANCE_IDENTITY` | 与其他启用身份共用财务账本,需人工处理 |
| `NEGATIVE_ASSET` | 负余额或负积分 |
| `FINANCE_IN_FLIGHT` | 财务流程尚未收口 |
| `FULFILLMENT_IN_PROGRESS` | 退款、分账或交付任务处理中 |
| `WAIVER_STALE` | 确认后余额变化,需重新确认 |
### 3.4 另一个身份的连续数据变化
以下身份现金、积分均为 0,未履约及处理中数量为 0,两个风险时间均为 null。它与 3.1 中有 37 项未履约记录的身份不同。
| 阶段 | `wallet_waived` | `points_waived` | `can_apply` | 阻断 | `deregister` |
| --- | --- | --- | --- | --- | --- |
| 确认前 | false | false | false | 缺两项确认 | null |
| 现金确认后 | true | false | false | 仅缺积分确认 | `status: 0` 草稿 |
| 积分确认后 | true | true | true | `[]` | 仍为 `status: 0` 草稿 |
| 申请后的冷静期 | false | false | false | 缺两项确认 | `status: 1` 冷静期中 |
| 主动撤销后 | false | false | false | 缺两项确认 | `status: 9` 已撤销 |
**第一次资产确认就会产生非空草稿。两项确认后可以申请,但尚未提交申请,也未进入冷静期。**
冷静期中两项确认已变回 false,不应按阻断建议再次确认资产。撤销后重新申请需重新核验条件和确认资产;尚未真实测试再次申请。
## 4. 两项资产确认接口
现金和积分分别请求,均使用以下 JSON 请求体:
```json
{"accepted":true}
```
### 4.1 POST `/waivers/wallet`
HTTP 200,现金确认成功的真实响应:
```json
{
"code": 100000,
"msg": "success",
"data": {
"asset_type": "wallet",
"wallet_balance_fen": 0,
"wallet_balance": "0.00",
"points_balance": 0,
"wallet_waived_at": "2026-08-28 13:45:57",
"points_waived_at": null
},
"time": "2026-08-28 13:45:57"
}
```
### 4.2 POST `/waivers/points`
HTTP 200,积分确认成功的真实响应:
```json
{
"code": 100000,
"msg": "success",
"data": {
"asset_type": "points",
"wallet_balance_fen": 0,
"wallet_balance": "0.00",
"points_balance": 0,
"wallet_waived_at": "2026-08-28 13:45:57",
"points_waived_at": "2026-08-28 13:46:42"
},
"time": "2026-08-28 13:46:42"
}
```
### 4.3 确认响应字段
| 字段 | 已观测 JSON 类型 | 含义 |
| --- | --- | --- |
| `asset_type` | string | 本次确认的资产:`wallet` 或 `points` |
| `wallet_balance_fen` | number(整数) | 当前现金余额,分 |
| `wallet_balance` | string | 当前现金金额展示值 |
| `points_balance` | number(整数) | 当前积分余额 |
| `wallet_waived_at` | string(本次均非空) | 现金确认时间;其他场景是否可为 null 尚未实测 |
| `points_waived_at` | string / null | 积分确认时间;现金确认后仍为 null,积分确认后为时间字符串 |
零余额、零积分也需分别确认。确认成功后重新查询条件和状态,以最新标记为准。旧文档规定确认绑定余额快照;确认本身不等于提交注销或立即清零资产。
### 4.4 `accepted: false` 不支持撤回确认(真实测试)
2026-08-28 17:01,使用iPhone 11当前已登录门店身份,在测试环境分别发送一次:
```json
{"accepted":false}
```
前置状态:`status: 0`未提交草稿,`wallet_waived: true`、`points_waived: true`,现金与积分均为0;`apply_time`、`cooling_until`、`completed_at`均为null。
| 接口 | 服务端响应时间 | HTTP状态 | 业务码 | 结果 |
| --- | --- | --- | --- | --- |
| POST `/waivers/wallet` | 2026-08-28 17:01:14 | 200 | 100099 | 拒绝false,确认标记未变化 |
| POST `/waivers/points` | 2026-08-28 17:01:32 | 200 | 100099 | 拒绝false,确认标记未变化 |
现金接口完整响应如下;积分接口除`time`为`2026-08-28 17:01:32`外,其余字段相同:
```json
{
"code": 100099,
"msg": "请明确确认自愿放弃对应资产",
"data": [],
"time": "2026-08-28 17:01:14"
}
```
每次POST后均再次查询`/status`和`/eligibility`:两项确认仍为true,草稿仍为0,金额未变化,`can_apply`仍为true。**当前接口不能通过传false把已确认状态重置为未确认。** 本轮没有发送true、短信、申请或撤销请求,也没有重新登录。`/cancel`能否取消未提交草稿仍未验证,不能由本次结果推断。
脱敏证据:`/private/tmp/suixinkan-waiver-false-20260828/device-wallet-response.json`、`device-points-response.json`及同目录各自的`before/after-status`、`before/after-eligibility`文件。Token未输出或保存;读取真机会话时的临时副本已删除。
## 5. 注销状态:GET `/status`
### 5.1 无记录
HTTP 200,真实响应:
```json
{
"code": 100000,
"msg": "success",
"data": {
"deregister": null
},
"time": "2026-08-28 11:39:51"
}
```
### 5.2 已确认资产、尚未提交的草稿
HTTP 200,真实响应:
```json
{
"code": 100000,
"msg": "success",
"data": {
"deregister": {
"id": 0,
"status": 0,
"status_label": "待确认",
"reason": "",
"apply_time": null,
"cooling_until": null,
"remaining_seconds": 0,
"cancel_time": null,
"blocked_code": "",
"blocked_reason": "",
"completed_at": null
}
},
"time": "2026-08-28 13:46:42"
}
```
### 5.3 `data.deregister` 字段
| 字段 | 本次实际类型/值 | 含义与待确认事项 |
| --- | --- | --- |
| `id` | number(正整数,示例脱敏为 0) | 注销记录 ID,不是门店用户 ID |
| `status` | number(整数),`0` / `1` / `9` | 分别为待确认、冷静期中、已撤销;其他值未知 |
| `status_label` | string | 服务端状态文案,如 `"冷静期中"`、`"已撤销"` |
| `reason` | string | 草稿为空;提交后保留用户输入的原因 |
| `apply_time` | string / null | 申请时间,本次 `2026-08-28 14:23:40` |
| `cooling_until` | string / null | 冷静期截止,本次 `2026-09-04 14:23:40`;撤销后仍保留 |
| `remaining_seconds` | number(整数) | 查询时剩余秒数;草稿、已撤销都为 0,不能据此认定已完成 |
| `cancel_time` | string / null | 撤销时间,本次 `2026-08-28 14:24:04` |
| `blocked_code` | string | 草稿/冷静期为空;主动撤销为 `CANCELLED_BY_USER` |
| `blocked_reason` | string | 草稿/冷静期为空;主动撤销为 `用户主动撤销注销` |
| `completed_at` | null | 完成时间;非空类型和格式未实测 |
同一草稿结构也出现在 `eligibility` 的 `data.deregister` 内。两个 GET 的 `cooling_until`、`remaining_seconds` 位于该记录对象中;不能据此推断 `/apply` 响应的嵌套结构。
### 5.4 冷静期的真实响应
GET `/status`,HTTP 200。该结果确认申请已受理,但不是 `/apply` 的原始响应体。
```json
{
"code": 100000,
"msg": "success",
"data": {
"deregister": {
"id": 0,
"status": 1,
"status_label": "冷静期中",
"reason": "API test; cancel immediately",
"apply_time": "2026-08-28 14:23:40",
"cooling_until": "2026-09-04 14:23:40",
"remaining_seconds": 604776,
"cancel_time": null,
"blocked_code": "",
"blocked_reason": "",
"completed_at": null
}
},
"time": "2026-08-28 14:24:03"
}
```
### 5.5 主动撤销的真实响应
POST `/cancel`,HTTP 200,无业务请求字段。撤销后再次 GET `/status` 返回相同记录,`msg: "success"`、`time: "2026-08-28 14:24:05"`。
```json
{
"code": 100000,
"msg": "注销申请已撤销",
"data": {
"deregister": {
"id": 0,
"status": 9,
"status_label": "已撤销",
"reason": "API test; cancel immediately",
"apply_time": "2026-08-28 14:23:40",
"cooling_until": "2026-09-04 14:23:40",
"remaining_seconds": 0,
"cancel_time": "2026-08-28 14:24:04",
"blocked_code": "CANCELLED_BY_USER",
"blocked_reason": "用户主动撤销注销",
"completed_at": null
}
},
"time": "2026-08-28 14:24:04"
}
```
`status: 9` 表示已撤销;保留旧 `apply_time`、`cooling_until` 不代表仍在冷静期。这里的 `CANCELLED_BY_USER` 是撤销原因,不是终审阻断状态。两个 GET 均确认两项资产确认标记变为 false;冷静期时已经是 false,不能断言由撤销动作单独导致。
### 5.6 新申请保留在冷静期的真实响应
用户最终确认后,于模拟器点击一次提交;以下为后续 GET `/status` 的真实响应(HTTP 200),不是 POST `/apply` 的响应体:
```json
{
"code": 100000,
"msg": "success",
"data": {
"deregister": {
"id": 0,
"status": 1,
"status_label": "冷静期中",
"reason": "Test account deregistration",
"apply_time": "2026-08-28 15:27:24",
"cooling_until": "2026-09-04 15:27:24",
"remaining_seconds": 604736,
"cancel_time": null,
"blocked_code": "",
"blocked_reason": "",
"completed_at": null
}
},
"time": "2026-08-28 15:28:27"
}
```
同时 GET `/eligibility` 返回现金0、积分0、两项确认 false,以及缺少资产确认的两个阻断;其 `deregister` 仍为上述冷静期记录。不能因此重新确认资产或重复申请。
### 5.7 冷静期普通业务限制的真实响应
15:29以同一身份的现有 Token 只读请求 GET `/api/yf-handset-app/userinfo`,HTTP 200:
```json
{
"code": 150015,
"msg": "账号处于注销冷静期,请先撤销注销后再继续使用",
"data": {
"status": "cooling",
"cooling_until": "2026-09-04 15:27:24",
"remaining_seconds": 604681
}
}
```
该响应没有 `time`,且 `data.status` 是字符串 `"cooling"`;与 `/status` 中 `data.deregister.status` 的数字1不是同一层级或类型,不能共用数字状态 DTO。
请求前后 GET `/status` 均成功且保持同一冷静期申请,`cancel_time`、`completed_at` 均为 null。这证明本次普通信息查询受限,但当前 Token 仍可查询注销状态;不代表正式完成后的鉴权规则已验证,也不能外推为全部普通业务接口已逐一验证。
上述150015和随后status响应已整理为测试夹具(仅记录ID替换为301),并在iPhone 11重放验证业务API、限制通知、状态查询和冷静期页面之间的衔接。此项为Mock集成验证,不是再次请求真实账号或重新申请。
### 5.8 重新登录自动撤销的真实响应
用户重新进入后,15:56只读GET确认之前的申请已撤销,HTTP 200:
```json
{
"code": 100000,
"msg": "success",
"data": {
"deregister": {
"id": 0,
"status": 9,
"status_label": "已撤销",
"reason": "Test account deregistration",
"apply_time": "2026-08-28 15:27:24",
"cooling_until": "2026-09-04 15:27:24",
"remaining_seconds": 0,
"cancel_time": "2026-08-28 15:55:36",
"blocked_code": "CANCELLED_BY_LOGIN",
"blocked_reason": "用户重新登录,自动撤销注销",
"completed_at": null
}
},
"time": "2026-08-28 15:56:34"
}
```
CANCELLED_BY_LOGIN与主动撤销的CANCELLED_BY_USER不同,但同为已撤销9。本次未捕获触发撤销的登录请求,不能确定是哪个登录或身份选择接口触发。并未完成正式注销。
## 6. 尚缺的响应与验证
| 接口 | 旧文档已知信息 | 尚缺信息 |
| --- | --- | --- |
| POST `/send-sms` | 已通过真机发送并收到短信 | 原始成功/失败响应、频率限制及业务码 |
| POST `/apply` | 请求字段 `sms_code`、`reason`;提交后查到冷静期1 | POST 完整响应层级、验证码错误及重复申请响应 |
| POST `/cancel` | 成功体及撤销后9状态已实测 | 失败、重复撤销和终态撤销响应 |
尚未获得阻断、正式完成的 `/status` 响应,不能猜测数字枚举。时间字符串的时区仍待确认,`completed_at` 非空类型和格式仍未实测。
已获得登录自动撤销后的真实状态9和CANCELLED_BY_LOGIN原因,但具体触发接口仍未捕获。到期复核、正式完成后的鉴权响应及全部身份注销后的登录响应仍未验证。此前两次申请现均已撤销。
## 7. 接入注意
1. 按当前门店用户身份隔离状态,不能按手机号共用状态。
2. 展示全部阻断项,不能只使用 `can_apply` 或 HTTP 状态码判断流程。
3. 完整的未提交草稿不应阻止第二项资产确认或手机号验证,非空记录不等于已提交。
4. `eligible_at` 是提交前的业务风险等待截止;`cooling_until` 是提交后的冷静期截止,两者不能混用。
5. `remaining_seconds: 0`、本机时间到期、`can_apply: true` 都不能单独作为注销完成依据。
6. 未知状态、字段缺失或请求失败不能默认解释为“没有申请”。
7. 已撤销9仍含 `cooling_until`,且 `blocked_code` 非空;不能误显示为仍在冷静期或终审阻断。
历史探测过程见 [接口实测记录](门店身份注销接口实测.md),客户端进度见 [接入说明](门店身份注销接入说明.md)。
+6 -6
View File
@@ -436,7 +436,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements; CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2010101; CURRENT_PROJECT_VERSION = 1020101;
DEVELOPMENT_TEAM = 56GVN5RNVN; DEVELOPMENT_TEAM = 56GVN5RNVN;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
@@ -460,7 +460,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2.1.1; MARKETING_VERSION = 1.2.1;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@@ -502,7 +502,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements; CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2010101; CURRENT_PROJECT_VERSION = 1020101;
DEVELOPMENT_TEAM = 56GVN5RNVN; DEVELOPMENT_TEAM = 56GVN5RNVN;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
"FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = (
@@ -526,7 +526,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2.1.1; MARKETING_VERSION = 1.2.1;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
@@ -741,7 +741,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements; CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2010101; CURRENT_PROJECT_VERSION = 1020101;
DEVELOPMENT_TEAM = 56GVN5RNVN; DEVELOPMENT_TEAM = 56GVN5RNVN;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -761,7 +761,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 2.1.1; MARKETING_VERSION = 1.2.1;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
"-ObjC", "-ObjC",
+1 -3
View File
@@ -6,7 +6,6 @@
import UIKit import UIKit
/// 应用根页面路由。 /// 应用根页面路由。
@MainActor
enum AppRouter { enum AppRouter {
enum Root { enum Root {
@@ -38,8 +37,7 @@ enum AppRouter {
} }
} }
/// 切换到显式构造的根页面,用于业务进入前的注销状态核验。 private static func setRoot(_ viewController: UIViewController, on window: UIWindow?, animated: Bool) {
static func setRoot(_ viewController: UIViewController, on window: UIWindow?, animated: Bool = true) {
guard let window else { return } guard let window else { return }
guard animated, let snapshot = window.snapshotView(afterScreenUpdates: true) else { guard animated, let snapshot = window.snapshotView(afterScreenUpdates: true) else {
-3
View File
@@ -17,7 +17,6 @@ final class NetworkServices {
let orderAPI: OrderAPI let orderAPI: OrderAPI
let homeAPI: HomeAPI let homeAPI: HomeAPI
let paymentAPI: PaymentAPI let paymentAPI: PaymentAPI
let offlineCollectionAPI: OfflineCollectionAPI
let taskAPI: TaskAPI let taskAPI: TaskAPI
let inviteAPI: InviteAPI let inviteAPI: InviteAPI
let walletAPI: WalletAPI let walletAPI: WalletAPI
@@ -44,7 +43,6 @@ final class NetworkServices {
orderAPI = OrderAPI(client: client) orderAPI = OrderAPI(client: client)
homeAPI = HomeAPI(client: client) homeAPI = HomeAPI(client: client)
paymentAPI = PaymentAPI(client: client) paymentAPI = PaymentAPI(client: client)
offlineCollectionAPI = OfflineCollectionAPI(client: client)
taskAPI = TaskAPI(client: client) taskAPI = TaskAPI(client: client)
inviteAPI = InviteAPI(client: client) inviteAPI = InviteAPI(client: client)
walletAPI = WalletAPI(client: client) walletAPI = WalletAPI(client: client)
@@ -76,7 +74,6 @@ final class NetworkServices {
orderAPI = OrderAPI(client: apiClient) orderAPI = OrderAPI(client: apiClient)
homeAPI = HomeAPI(client: apiClient) homeAPI = HomeAPI(client: apiClient)
paymentAPI = PaymentAPI(client: apiClient) paymentAPI = PaymentAPI(client: apiClient)
offlineCollectionAPI = OfflineCollectionAPI(client: apiClient)
taskAPI = TaskAPI(client: apiClient) taskAPI = TaskAPI(client: apiClient)
inviteAPI = InviteAPI(client: apiClient) inviteAPI = InviteAPI(client: apiClient)
walletAPI = WalletAPI(client: apiClient) walletAPI = WalletAPI(client: apiClient)
+1 -2
View File
@@ -19,8 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
if AppStore.shared.session.privacyAgreementAccepted, !AppStore.shared.session.token.isEmpty { if AppStore.shared.session.privacyAgreementAccepted, !AppStore.shared.session.token.isEmpty {
AMapBootstrap.configureIfNeeded() AMapBootstrap.configureIfNeeded()
} }
let pushManager = PushNotificationManager.shared PushNotificationManager.shared.initializeIfPrivacyAccepted(launchOptions: launchOptions)
pushManager.initializeIfPrivacyAccepted(launchOptions: launchOptions)
return true return true
} }
@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "offline_security_shield_generated.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

@@ -1,12 +1,15 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "payment_method_alipay_official.png", "filename" : "payment_method_alipay.svg",
"idiom" : "universal" "idiom" : "universal"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"preserves-vector-representation" : true
} }
} }
@@ -0,0 +1,4 @@
<!-- 来源:阿里巴巴矢量图标库,支付宝支付,图标 ID 1486848 -->
<svg width="24" height="24" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M1024.0512 701.0304V196.864A196.9664 196.9664 0 0 0 827.136 0H196.864A196.9664 196.9664 0 0 0 0 196.864v630.272A196.9152 196.9152 0 0 0 196.864 1024h630.272a197.12 197.12 0 0 0 193.8432-162.0992c-52.224-22.6304-278.528-120.32-396.4416-176.64-89.7024 108.6976-183.7056 173.9264-325.3248 173.9264s-236.1856-87.2448-224.8192-194.048c7.4752-70.0416 55.552-184.576 264.2944-164.9664 110.08 10.3424 160.4096 30.8736 250.1632 60.5184 23.1936-42.5984 42.496-89.4464 57.1392-139.264H248.064v-39.424h196.9152V311.1424H204.8V267.776h240.128V165.632s2.1504-15.9744 19.8144-15.9744h98.4576V267.776h256v43.4176h-256V381.952h208.8448a805.9904 805.9904 0 0 1-84.8384 212.6848c60.672 22.016 336.7936 106.3936 336.7936 106.3936zM283.5456 791.6032c-149.6576 0-173.312-94.464-165.376-133.9392 7.8336-39.3216 51.2-90.624 134.4-90.624 95.5904 0 181.248 24.4736 284.0576 74.5472-72.192 94.0032-160.9216 150.016-253.0816 150.016z" fill="#009FE8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@@ -1,12 +1,15 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "payment_method_cash_generated.png", "filename" : "payment_method_cash.svg",
"idiom" : "universal" "idiom" : "universal"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"preserves-vector-representation" : true
} }
} }
@@ -0,0 +1,5 @@
<!-- 来源:阿里巴巴矢量图标库,现金,图标 ID 25537599 -->
<svg width="24" height="24" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M128 256v512h768V256H128z m853.333333-85.333333v682.666666H42.666667V170.666667h938.666666z" fill="#2E2F30"/>
<path d="M512 682.666667a170.666667 170.666667 0 1 1 0-341.333334 170.666667 170.666667 0 0 1 0 341.333334z m0-85.333334a85.333333 85.333333 0 1 0 0-170.666666 85.333333 85.333333 0 0 0 0 170.666666z" fill="#2E2F30"/>
</svg>

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

@@ -1,12 +1,15 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "payment_method_wechat_official.png", "filename" : "payment_method_wechat.svg",
"idiom" : "universal" "idiom" : "universal"
} }
], ],
"info" : { "info" : {
"author" : "xcode", "author" : "xcode",
"version" : 1 "version" : 1
},
"properties" : {
"preserves-vector-representation" : true
} }
} }
@@ -0,0 +1,4 @@
<!-- 来源:阿里巴巴矢量图标库,微信支付,图标 ID 781032 -->
<svg width="24" height="24" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M404.511405 600.865957c-4.042059 2.043542-8.602935 3.223415-13.447267 3.223415-11.197016 0-20.934798-6.169513-26.045189-15.278985l-1.959631-4.296863-81.56569-178.973184c-0.880043-1.954515-1.430582-4.14746-1.430582-6.285147 0-8.251941 6.686283-14.944364 14.938224-14.944364 3.351328 0 6.441713 1.108241 8.94165 2.966565l96.242971 68.521606c7.037277 4.609994 15.433504 7.305383 24.464181 7.305383 5.40101 0 10.533914-1.00284 15.328104-2.75167l452.645171-201.459315C811.496653 163.274644 677.866167 100.777241 526.648117 100.777241c-247.448742 0-448.035176 167.158091-448.035176 373.361453 0 112.511493 60.353576 213.775828 154.808832 282.214547 7.582699 5.405103 12.537548 14.292518 12.537548 24.325012 0 3.312442-0.712221 6.358825-1.569752 9.515724-7.544837 28.15013-19.62599 73.202209-20.188808 75.314313-0.940418 3.529383-2.416026 7.220449-2.416026 10.917654 0 8.245801 6.692423 14.933107 14.944364 14.933107 3.251044 0 5.89015-1.202385 8.629541-2.7793l98.085946-56.621579c7.377014-4.266164 15.188934-6.89913 23.790846-6.89913 4.577249 0 9.003048 0.703011 13.174044 1.978051 45.75509 13.159718 95.123474 20.476357 146.239666 20.476357 247.438509 0 448.042339-167.162184 448.042339-373.372709 0-62.451354-18.502399-121.275087-51.033303-173.009356L407.778822 598.977957 404.511405 600.865957z" fill="#00C800"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

@@ -1,21 +0,0 @@
{
"images": [
{
"filename": "payment_sailimu_background.png",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x"
},
{
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

@@ -1,21 +0,0 @@
{
"images": [
{
"filename": "payment_sailimu_logo.png",
"idiom": "universal",
"scale": "1x"
},
{
"idiom": "universal",
"scale": "2x"
},
{
"idiom": "universal",
"scale": "3x"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

@@ -1,8 +0,0 @@
{
"images" : [
{ "filename" : "travel_album_cover_photo_icon.png", "idiom" : "universal", "scale" : "1x" },
{ "filename" : "travel_album_cover_photo_icon@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "travel_album_cover_photo_icon@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

@@ -1,8 +0,0 @@
{
"images" : [
{ "filename" : "travel_album_header_background@1x.png", "idiom" : "universal", "scale" : "1x" },
{ "filename" : "travel_album_header_background@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "travel_album_header_background@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

@@ -1,8 +0,0 @@
{
"images" : [
{ "filename" : "travel_album_sort_icon.png", "idiom" : "universal", "scale" : "1x" },
{ "filename" : "travel_album_sort_icon@2x.png", "idiom" : "universal", "scale" : "2x" },
{ "filename" : "travel_album_sort_icon@3x.png", "idiom" : "universal", "scale" : "3x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 B

@@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "ai_retouch_template_placeholder.png", "filename" : "travel_album_preview_ai_edit.svg",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 -128 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path fill="#FFFFFF" d="M764 824q-40 40 -97 40t-97 -40l-544 -542v-320q0 -25 17.5 -43t42.5 -18h340l528 538q41 40 41 97t-41 97zM338 -29h-179q-26 0 -44.5 18.5t-18.5 45.5v196q12 -3 31 -9t65 -29.5t77 -53.5q29 -27 63.5 -86t38.5 -82h-33zM885 463l-435 -438q-73 119 -124 171q-15 15 -39.5 32t-48 31t-45.5 26t-35 19l-13 6l448 446q24 24 60.5 24t60.5 -25l171 -171q25 -26 25 -61.5t-25 -59.5zM611 315l-43 43l32 31l42 -42zM692 397l-42 42l126 127l43 -43z"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "travel_album_preview_delete.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 -128 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path fill="#FFFFFF" d="M368 440v-279q0 -13 8.5 -22t20.5 -9t20 8q9 10 9 23v279q0 13 -8.5 22.5t-20.5 9.5t-20.5 -9.5t-8.5 -22.5zM569 440v-279q0 -13 8.5 -22t20.5 -9t20.5 9t8.5 22v279q0 13 -8.5 22.5t-20.5 9.5t-20.5 -9.5t-8.5 -22.5zM283 -109h442q57 0 97 42t40 102v606h95q12 0 20.5 9.5t8.5 22.5t-8.5 22t-20.5 9h-259l-1 25q0 53 -35 101t-81 48h-154q-45 0 -80 -48t-36 -101v-25h-251q-12 0 -20.5 -9t-8.5 -22t8.5 -22.5t20.5 -9.5h87v-606q0 -60 40 -102t96 -42zM369 730q1 27 18.5 55t37.5 30h2h154q21 -2 39 -30t19 -55l1 -26h-272zM205 641h599v-606q0 -34 -23 -57.5t-56 -23.5h-442q-33 0 -55.5 23.5t-22.5 57.5v606v0z"/>
</svg>

After

Width:  |  Height:  |  Size: 700 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "travel_album_preview_refresh.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 -128 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path fill="#FFFFFF" d="M505.5 -127q-102.5 0 -196.5 39q-90 39 -159.5 108.5t-108.5 159.5q-39 94 -39 196.5t39 195.5q39 90 108.5 160t159.5 108q94 40 196.5 40t195.5 -40q90 -38 160 -108t108 -160q40 -93 40 -195.5t-40 -196.5q-38 -90 -108 -159.5t-160 -108.5q-93 -39 -195.5 -39zM505 819q-90 0 -172 -35q-80 -33 -141 -94.5t-95 -140.5q-35 -83 -35 -173t35 -172q34 -80 95 -141t141 -95q82 -35 172 -35t173 35q79 34 140.5 95t94.5 141q35 82 35 172t-35 173q-33 79 -94.5 140.5t-140.5 94.5q-83 35 -173 35zM595 472h-240l62 62q9 9 9 21.5t-9 21.5t-21.5 9t-21.5 -9l-113 -114q-9 -9 -9 -21.5t9 -21.5l113 -113q9 -9 21.5 -9t21.5 9t9 21.5t-9 21.5l-62 61h240q41 0 69.5 -28t28.5 -68t-28.5 -68.5t-69.5 -28.5h-221q-12 0 -21 -9t-9 -21.5t9 -21.5t21 -9h221q66 0 112 46.5t46 111.5t-46 111t-112 46z"/>
</svg>

After

Width:  |  Height:  |  Size: 863 B

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "purchased_alpine_lake.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "purchased_forest_waterfall.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "purchased_grassland_path.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Some files were not shown because too many files have changed in this diff Show More