Files
cmbuyer/docs/04-architecture.md
T

483 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 架构设计
> 本文讲「怎么把技术栈搭起来」:系统结构、职责划分、数据模型、技术难点、开发顺序。
> 具体用了哪些框架 / 库 / 数据库 / 部署方式,见 [技术栈](03-tech-stack.md)。
## 一、系统结构
```text
第三方 ERP(顺运宝) Excel 表格 人工填链接
│ │ │
└────────────┬───────────┴────────────────────┘
v
┌─────────────────────────────────────────┐
│ 采购服务(admin/,Go 单二进制) │
│ · 建单、批量开始试选与任务生命周期 │
│ · 候选确认与下单授权(唯一决策权威) │
│ · 证据存储与审计 │
│ · 管理 Web(服务端渲染) │
└─────────────────────────────────────────┘
^ HTTP / JSON
│ Bearer Token + 设备绑定
v
┌─────────────────────────────────────────┐
│ 采购工具(client/,Python + PySide6) │
│ · 领任务、跑流程、回传结果 │
│ · 本地执行轨迹与证据落盘 │
│ · AI 辅助调用(P1) │
└─────────────────────────────────────────┘
│ ADB(USB / WiFi)
v
┌─────────────────────────────────────────┐
│ Android 手机(拼多多 App) │
└─────────────────────────────────────────┘
```
组件落位:
- 采购服务:Go + gin,入口 `admin/cmd/server/main.go`,模板
`admin/internal/transport/webui/templates/`
- 采购工具:Python,入口 `client/src/main.py`,真机流程 `client/src/android/pdd_flow.py`
- 数据库:SQLite,迁移由 goose 管理
- 证据存储:采购服务本地文件系统,SHA-256 寻址
- 外部服务:顺运宝 ERP(只读)、AI provider(P1,仅采购工具调用)
## 二、职责划分
### 采购服务(网页端,`admin/`)
**独占**:
- 任务的创建、状态流转和终态判定
- 候选商品的接收与展示
- **下单授权的签发与作废**——这是唯一的资金决策权威
- 金额上限的判定
- 证据资产的存储与访问控制
- 管理员会话与设备凭据
**不做**:
- 不连接手机、不发 ADB 命令
- 不保存、代理或下发任何 AI provider 凭据
- 不解析拼多多页面
### 采购工具(桌面端,`client/`)
**独占**:
- ADB 连接与设备健康检查
- 拼多多页面识别、点击、选规格、设数量
- 页面截图与节点树采集
- AI 调用(P1)
- 完整执行轨迹的本地留档
**不做**:
- **不自行决定买哪个候选**——必须等采购服务的授权
- **不自行放宽金额上限**——本地校验只能更严,不能更松
- 不直接读 Excel 或访问 ERP
- 不在没有授权的情况下执行任何创建订单的动作
### 权威冲突规则
两端都会校验规格、数量、金额。**判定不一致时一律转人工,不取任一方结论。**
这条是硬规则:双闸门的价值在于分歧能被发现,自动选一边等于把双闸门降级成单闸门。
## 三、两趟执行
这是本项目最核心的结构决策。**MVP 只做 A 路径(任务自带商品链接),分两趟跑完。**
```text
┌────────── 采购服务开始第一趟 ───────────┐
│ 新任务先保存为 DRAFT │
│ 管理员在任务表格勾选一条或多条 │
│ 原子转为 PENDING,只进入试选队列 │
└────────────────────┬─────────────────────┘
v
┌──────────────── 第一趟:试选 ────────────────┐
│ 采购工具轮询领取 PENDING 任务 │
│ 1. open_product(url) │
│ 2. 经版本绑定、精确唯一的受控入口打开规格面板 │
│ 3. 按维度精确勾选颜色分类、尺码 │
│ 4. 【闸门一】读该 SKU 单价,算合计 │
│ 5. 截图 │
│ 6. 退出商品,释放手机 │
│ 7. 回传标题 / 选中规格 / 单价 / 合计 / 截图 │
└────────────────────┬─────────────────────────┘
v
任务转 WAITING_CONFIRMATION
│
┌────────────────────┴─────────────────────────┐
│ 人在采购服务确认:机器选对了吗 │
│ 看:需求 vs 选中规格、单价、合计、截图 │
│ 点「确认下单(不付款)」→ 签发授权,锁定授权价 │
│ 或「退回,不买」→ 任务终止 │
└────────────────────┬─────────────────────────┘
v
┌──────────────── 第二趟:下单 ────────────────┐
│ 采购工具轮询拿到授权 │
│ 1. 重新 open_product(url) │
│ 2. 重新按维度精确勾选同一规格 │
│ 3. 【闸门二】重读单价,必须与授权价一致 │
│ 4. 设数量并复核 │
│ 5. 进订单确认页 │
│ 6. 【闸门三】读「实付款」,不得超授权上限 │
│ 7. 三个闸门全过 → 点一次「提交订单」 │
│ 8. 回传订单截图 │
└────────────────────┬─────────────────────────┘
v
任务转 WAITING_PAYMENT
│
人工在拼多多核对后付款
```
### 为什么分两趟而不是停在面板上等人
一台手机是瓶颈。若第一趟停在规格面板等人确认,手机被占住跑不了别的任务,面板还可能
超时或被拼多多重置。**第一趟必须退出并释放手机**,第二趟重新进入。
代价是同一商品走两遍,但第二趟很快,而且换来两个好处:手机可以在人思考时继续跑别的
任务的试选;价格变动能被第二趟抓住。
### 第一趟受控规格面板入口
T-103 的真机证据推翻了“商品详情页存在独立规格入口”的假设:拼多多 8.17.0、goods_id
`937122477375` 只能从“快要抢光”打开规格面板。项目所有者于 2026-08-04 批准把这一点击定义为
**可逆且能力受限的规格面板导航**,不把它当作下单授权,也不把购买语义文案整体加入白名单。
第一趟执行器只能持有以下 capability:
1. 打开 canonical 商品链接。
2. 点击与证据哈希、拼多多版本和页面状态绑定的精确唯一 `快要抢光` 入口。
3. 在已确认的维度容器中精确选择规格并读回选中态。
4. 从规格面板读取单价、保存原始截图、关闭面板并退出商品页。T-103 真机 spike 先在本机验证;
T-204 再把原始截图接入采购服务供管理员查看。
第一趟 capability **不得包含**通用 `click`、数量增减、进入订单确认页、提交订单或付款能力。
“免拼购买 / 单独购买 / 直接拼成”等其他文案即使人工认为行为相同,也必须各自重新取证后才能评审;
入口缺失、重复、版本失配、打开后面板判据不唯一,或出现“提交订单”以外的未知终态按钮时立即停止。
第一趟的静态依赖检查必须证明 `set_quantity()`、`go_to_order_confirm()`、`submit_order()` 和任何
支付函数不可达。
### 三道价格闸门
| 闸门 | 位置 | 作用 | 不通过时 |
| --- | --- | --- | --- |
| 一 | 第一趟规格面板 | 读该 SKU 单价,算合计,回传给人看 | 读不到即停,转人工 |
| 二 | 第二趟规格面板 | 重读单价,**必须与授权时锁定的价格一致** | 不一致即停,转人工 |
| 三 | 订单确认页 | 读「实付款」,不得超授权总额上限 | 超出即停,转人工 |
**闸门二不可省略。** 人确认的是「32.50 元这一单」,不是「这个商品」。拼多多价格波动
常见,不能因为「人已经确认过」就照下不误。
**价格只在规格面板和订单确认页读。** 商品详情页正文和搜索结果卡片上的价格文本在真机上
被拆成多个节点(`¥` 与数字分离)、带 `券后` 一类前缀、实付价 / 原价 / 促销价难以区分
——前序项目在这里耗掉大量时间且无可靠结论。
### B 路径(图片搜索)——V2,MVP 不做
任务只有参考图、没有链接时,需要先搜图找出商品。**该路径推迟到 V2**,MVP 阶段建单必须
提供商品链接。
V2 实现时仍遵守:**图搜的唯一产出是 goods_id**,不在搜索结果卡片上读价格或据价筛选,
拿到 goods_id 后汇入本节的两趟流程。
## 四、安全边界(硬约束)
以下每一条都必须有单元测试证明,且不得在任务中「顺手放宽」。
| 边界 | 规则 | 违反后果 |
| --- | --- | --- |
| 不付款 | 任何路径都不点击支付、免密支付、先用后付或扣款控件 | 真实资金损失 |
| **提交订单四条件** | 见下方专节。四者缺一不可,且**只允许点击一次** | 误下单 / 重复下单 |
| 订单确认页其余零点击 | 除「提交订单」与返回外,不点击确认页上任何控件 | 误触发未知动作 |
| 规格精确匹配 | 按维度等值匹配,防前缀碰撞(`红`/`粉红`、`1`/`10`);找不到即停 | 买错货 |
| 提交订单控件唯一 | 文本精确等于「提交订单」且可点击祖先唯一,否则停 | 点到未知控件 |
| 数量必须复核 | 设置后读回确认精确等于要求值,否则停 | 买错数量 |
| 价格三道闸门 | 见第三节。任一道读不到或不通过即停,**不用其他位置的数字凑合** | 超预算采购 |
| 第一趟不下单 | 只允许点击证据/版本绑定的精确唯一受控入口打开规格面板,当前仅为 `快要抢光`;随后只选规格、读价、截图和返回。数量、确认页、`提交订单`、付款与通用点击能力均不可达 | 无授权下单 |
| 外部支付页 | 检测到微信等外部支付交接立即停止、转人工、保留证据 | 凭据泄露 |
| 安全校验 | 检测到验证码、风控、人脸、短信校验立即停止,不尝试绕过 | 封号 / 违规 |
| 页面个人信息 | 内部原始截图可包含并上传页面已显示的地址、手机号,供已登录管理员查看;不得把这些内容解析成业务字段或写入普通日志、Git、Vikunja。支付凭据仍不得读取或上传 | 非必要扩散 / 凭据泄露 |
| 授权一次性 | 一笔授权只能产生一笔订单,重复提交幂等 | 重复采购 |
| 服务端提交围栏 | 真机点击前必须由采购服务原子冻结授权并创建唯一提交记录;失败或响应不明不得点击 | 并发 / 断网导致重复下单 |
| App 版本失配即停 | 运行版本与本项目已取证版本不一致时停止领取真机任务,先重新取证 | 旧判据误点新页面 |
### 提交订单的四个前置条件
这是本项目唯一会创建真实待付款订单的动作。**四者同时满足才允许点击,且只点一次:**
1. **授权存在且未消费,并已建立服务端提交围栏**——采购服务已签发、采购工具已 ack;
真机点击前,采购服务在一个原子事务中把授权从可执行态冻结为本次唯一
`order_submission`。围栏接口失败或响应不明时不得点击。
2. **闸门二通过**——第二趟重读的单价与授权时锁定的价格一致。
3. **闸门三通过**——订单确认页「实付款」不超过授权总额上限。
4. **控件唯一**——文本精确等于「提交订单」且可点击祖先唯一。
点击之后,**无论发生什么都不重试**:
| 点击后观察到 | 处置 |
| --- | --- |
| 正常进入订单结果页 | 回传订单截图,任务转 `WAITING_PAYMENT` |
| 跳转微信等外部支付 | 立即停止,转人工,提示「订单可能已创建、支付未完成」 |
| 安全校验 | 立即停止,转人工,保留证据 |
| 超时或页面无法判定 | 转人工,**预留金额额度**,提示订单状态不明 |
后三种情况一律**禁止自动重试点击**。授权保持永久围栏,结果明确后再记为已消费;在此之前
也绝不能重新开放——宁可人工核实一遍,不可能重复下单。
### dry-run、提交围栏与结果调和
下单被拆成三个不可逆程度不同的阶段,任何客户端本地判断都不能替代服务端围栏:
1. **dry-run(只读演练)**:进入订单确认页,读取规格、数量和「实付款」,确认提交控件
唯一,上传证据后退出。该阶段绝不点击「提交订单」,也不消费授权。
2. **提交围栏**:真实第二趟再次读取并通过三道闸门后,采购工具向采购服务申请围栏。采购服务
原子校验任务版本、命令、未消费授权和唯一性,创建 `order_submissions` 记录并冻结授权。
只有明确收到成功响应,采购工具才可点击一次。
3. **结果调和**:点击后只上报观察结果。明确创建则转 `WAITING_PAYMENT`;超时、外部支付、
安全校验或断连均转 `RECONCILIATION_REQUIRED`,保留额度并由人核查。**不得释放围栏、
重新签发授权或自动重试点击。**
授权超时和主动放弃只允许发生在提交围栏建立之前。围栏之后即使租约过期,也只能恢复
同一提交记录并进入调和,不能把任务重新放回可领取队列。
## 五、数据模型
### 5.1 核心实体
```sql
-- 采购任务:创建后业务约束不可变
CREATE TABLE tasks (
id TEXT PRIMARY KEY, -- UUID
source TEXT NOT NULL, -- MANUAL | EXCEL | ERP
source_ref TEXT, -- 外部单号(如虾皮订单号),非内部 id
title TEXT NOT NULL,
goods_id TEXT NOT NULL, -- MVP 必填;B 路径(可为空)推迟到 V2
sku_color TEXT NOT NULL,
sku_size TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
max_total_price TEXT NOT NULL, -- 十进制字符串,资金边界不得为空
reference_asset_id TEXT, -- 参考图,MVP 可选;B 路径(V2)必填
status TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,-- 乐观锁
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- 第一趟试选结果:人做确认决策的依据
CREATE TABLE spec_trials (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
attempt INTEGER NOT NULL,
product_title TEXT NOT NULL, -- 商品页读到的标题
selected_color TEXT NOT NULL, -- 实际勾选到的颜色分类
selected_size TEXT NOT NULL, -- 实际勾选到的尺码
unit_price TEXT NOT NULL, -- 闸门一读到的单价
total_price TEXT NOT NULL, -- unit_price × quantity
evidence_sha256 TEXT NOT NULL, -- 规格面板截图
created_at TEXT NOT NULL,
UNIQUE (task_id, attempt)
);
-- 下单授权:唯一的资金决策记录
CREATE TABLE order_authorizations (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
spec_trial_id TEXT NOT NULL REFERENCES spec_trials(id),
version INTEGER NOT NULL,
goods_id TEXT NOT NULL,
sku_color TEXT NOT NULL,
sku_size TEXT NOT NULL,
quantity INTEGER NOT NULL,
authorized_unit_price TEXT NOT NULL, -- 锁定价:闸门二据此比对
total_price_cap TEXT NOT NULL, -- 授权总额上限:闸门三据此比对
note TEXT, -- 可选备注
status TEXT NOT NULL,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL, -- 围栏前超时自动作废;围栏后不再释放
UNIQUE (task_id, version)
);
-- 真实点击前的服务端一次性围栏;一笔授权最多一条
CREATE TABLE order_submissions (
id TEXT PRIMARY KEY,
task_id TEXT NOT NULL REFERENCES tasks(id),
authorization_id TEXT NOT NULL REFERENCES order_authorizations(id),
command_id TEXT NOT NULL,
dry_run_id TEXT NOT NULL,
status TEXT NOT NULL, -- FENCED | SUBMITTED | RECONCILIATION_REQUIRED | MANUAL_RESOLVED
verified_unit_price TEXT NOT NULL,
quantity_read INTEGER NOT NULL,
confirm_page_amount TEXT NOT NULL,
created_at TEXT NOT NULL,
resolved_at TEXT,
UNIQUE (authorization_id),
UNIQUE (command_id)
);
```
`authorized_unit_price` 是第二趟闸门二的比对基准,**必须来自人确认时看到的那个试选
结果**,不能在签发时重新取值。`expires_at` 见 5.3 节。
MVP 的授权没有「选择理由 / 拒绝理由」——那是从多个候选里挑一个时的留档需求。这里人
只回答「机器选对了吗」,保留一个可选 `note` 即可。
金额一律用**十进制字符串**存储和传输,不用浮点数。
### 5.2 状态机
任务状态(采购服务权威)。创建与开始试选分离;两趟执行对应两次 `CLAIMED → RUNNING`:
```text
DRAFT ─start trial→ PENDING ─┐
PENDING_RETRIAL ─────────────┴─claim→ CLAIMED ─start→ RUNNING(TRIAL)
↑ │ ├→ NEEDS_MANUAL
└────────release──────┘ └→ WAITING_CONFIRMATION
├→ CANCELED
└→ AUTHORIZED
└claim→ ORDERING
├→ NEEDS_MANUAL(围栏前失败)
└→ [submission FENCED]
├→ WAITING_PAYMENT
│ └→ SUCCEEDED
└→ RECONCILIATION_REQUIRED
└→ 人工核查 / 调和
```
| 状态 | 含义 |
| --- | --- |
| `DRAFT` | 已保存、等待管理员开始试选;设备不可领取,也不存在下单授权 |
| `RUNNING(TRIAL)` | 第一趟试选中:正在勾选规格、读价、截图 |
| `WAITING_CONFIRMATION` | 试选已回传,**等人确认机器选对了没** |
| `PENDING_RETRIAL` | 旧授权已过期或在围栏前被放弃,必须重新跑第一趟取得新价格 |
| `AUTHORIZED` | 已签发授权,等采购工具下一轮轮询领走 |
| `ORDERING` | 第二趟下单中:重新选规格、过闸门二三、提交订单 |
| `WAITING_PAYMENT` | 订单已创建,等人在拼多多付款。**这不是成功** |
| `RECONCILIATION_REQUIRED` | 已建立提交围栏,但点击结果不明确;可能已创建订单,只能核查,不能重试 |
| `NEEDS_MANUAL` | 围栏前的转人工情形(规格不匹配、价格不符、页面识别失败等) |
| `SUCCEEDED` | 订单已付款且核对通过 |
授权状态:`PENDING_DELIVERY → DELIVERED → ACKNOWLEDGED → EXECUTING → FENCED → CONSUMED`。
人退回或重新确认时旧授权转 `SUPERSEDED`;超时转 `EXPIRED`。
`DRAFT → PENDING` 只能由管理端“开始试选”动作触发。批量开始在一个事务中校验全部任务仍为
`DRAFT` 且版本一致后统一流转;任一冲突时整批不变,避免用户误以为选中的任务都已开始。
这个动作只开放第一趟领取资格,不创建 `order_authorizations` 或 `order_submissions`。
### 5.3 授权超时(MVP 必做,不得推后)
> **前序项目的教训**:曾出现 `EXECUTING` 授权永不推进,导致确认表单被永久隐藏、任务
> 锁死,只能新建任务绕过。
规则:
- 每笔授权带 `expires_at`。**仅在尚未建立提交围栏时**,超时自动转 `EXPIRED`。
- 授权 `EXPIRED` 后任务转 `PENDING_RETRIAL`,先重新跑第一趟取得新价格,再回到人工确认;
不允许在旧 `spec_trials` 上直接重新确认。
- 采购服务在围栏建立前提供「放弃当前授权」入口;围栏建立后改为「进入人工核查」,不得
作废或释放授权。
- **任何时候都不允许出现「任务停在某状态且界面上没有任何可用动作」的组合。**
这是验收项,不是实现细节。
授权过期后重新确认时,必须重新走第一趟试选取得新的 `spec_trials` 记录——不能复用旧的
锁定价,因为价格可能已经变了。已建立围栏的授权不参与本超时流程。
### 5.4 证据分层
| 数据 | 位置 | 理由 |
| --- | --- | --- |
| 原始商品页 / 规格页 screenshot | 采购工具本机 + 采购服务内部证据存储(T-204) | 内部系统允许保留页面中已显示的地址和手机号;设备鉴权后上传,只有已登录管理员可查看,不做遮罩或裁剪 |
| 原始商品页 / 规格页完整 XML | **仅采购工具本机隔离目录** | 自动化运行时在内存中读取规格/价格,完整 XML 不上传、不进入日志、Git、Vikunja 或 fixture |
| T-103 真机 spike 证据 | 先在采购工具本机;T-204 接入上传 | T-103 只验证选择与读价,不实现 HTTP 证据链;这是一项任务拆分,不是禁止原始截图上传 |
| 最小脱敏 XML fixture | 采购工具测试 / 可提交 Git | 只保留页面判据所需结构;自动复检无地址、手机号、支付凭据后才可发布 |
| 原始订单确认页截图 | 采购服务内部证据存储 | 授权后核对与审计必须留;允许页面已显示的地址/手机号,禁止外部支付凭据 |
| 原始订单核对截图 | 采购服务内部证据存储 | 资金核对证据;只有已认证设备上传、已登录管理员查看 |
| AI 调用记录(P1) | **仅采购工具本地** | 含 prompt / 响应全文,脱敏成本高 |
| 失败现场快照 | 原始物仅采购工具本机;只能手工导出脱敏派生物 | 同上 |
截图上传器只能接收调用方显式传入的截图文件,不能枚举整个原始目录,也不能上传 XML、manifest 中的
本机路径或其他文件。采购工具可以在真机流程内存中读取当前页面树,但只返回规格、选中态、价格和页面
状态摘要;不得把地址或手机号解析成结构化业务字段。原始 XML 不进入 HTTP、日志、Vikunja、Git 或
fixture;仓库只允许进入与页面判据有关的最小 XML fixture。
T-204 负责原始截图上传、SHA-256 校验、内部访问控制和保留策略,不再实现遮罩或裁剪。证据响应使用
`Cache-Control: no-store`,不得暴露为无需登录的静态目录。外部支付页及任何支付凭据不属于“内部原图
可上传”的范围,遇到支付交接仍立即停止。
## 六、关键技术难点
| 难点 | 说明 | 应对 |
| --- | --- | --- |
| 拼多多页面结构随版本变化 | 前序项目已观察到详情页无独立规格入口、价格节点拆分等变化 | **每条判据先做真机 spike 取证再写代码**;判据与 App 版本一并记录 |
| 规格面板安全入口 | T-103 已在拼多多 8.17.0 真机确认衣服商品只能通过购买语义入口打开规格面板 | T-110 已批准仅使用当前证据证明的精确唯一 `快要抢光` 作为受控导航;其他文案不泛化,第一趟下单能力保持不可达 |
| 规格面板上的价格位置 | 选中 SKU 后价格显示在哪、是否含券后前缀,未取证 | **T-103 必须一并取证**,闸门一依赖它;读不到就转人工,不用详情页数字凑合 |
| 同一商品两趟结果不一致 | 第二趟价格变了、规格选项变了或商品下架 | 闸门二拦截;一律转人工,不自动放弃也不自动继续 |
| 图搜结果含跨类目商品(V2) | 搜服装出现纸巾 | B 路径只产 goods_id 且限 5 个;后续用 VLM 看截图筛同款 |
| WiFi ADB 稳定性 | 息屏、换网、DHCP 续租会断连 | 超时可配置;断连视为技术失败并保留现场,不重试点击 |
| 同一手机 USB + WiFi 同时在线 | `adb devices` 列出两条,自动选设备会失败 | serial 必填;多在线通道必须读到 `ro.serialno` 或 `ro.boot.serialno` 才能比对。身份一致或任一身份读取失败时都 fail closed,不能以相同 model/product 猜测后继续 |
| 不可逆动作的重试 | 点击「现在买」后超时,无法判断订单是否已创建 | 一律转人工并预留金额额度,**禁止自动重试点击** |
| 双端契约漂移 | 两端独立演进会静默不兼容 | 契约改动必跑完整门禁;[api.md](api.md) 是唯一权威 |
**高风险功能先做最小原型。** Phase 1 的真机 spike 必须先于 Phase 2 的界面开发完成。
## 七、推荐开发顺序
1. **Phase 0 地基**:两端骨架、测试命令、`init` 脚本可运行。
2. **Phase 1 真机取证**:WiFi ADB 连通;先验证第一趟的打开商品 → 受控打开规格面板 → 按维度
精确勾选颜色分类和尺码 → **读到该 SKU 单价** → 原始截图取证 → 退出;再以独立的第二趟 spike
验证设数量 → 进订单确认页 → 读「实付款」。第一趟 capability 不含后三项。
**结论写入文档,判据带拼多多 App 版本。**
3. **Phase 2 采购服务核心**:数据模型与状态机、手工建单、任务查询、试选结果接收、
确认页与授权签发、**授权超时与放弃**。
4. **Phase 3 双端打通**:设备侧 API、采购工具 `HttpTaskSource`/`HttpResultSink`、
定时轮询、第一趟试选端到端。
5. **Phase 4 闭环收尾**:第二趟下单(含三道闸门与提交)、失败分类、完整验收、打包。
6. **V2 及以后**:图片搜索路径、候选对照台、Excel 导入、ERP 建单、订单自动核对、AI 辅助。
**不要在 Phase 1 结论出来之前写 Phase 2 的页面**——确认页要显示什么,取决于真机上
究竟能读到什么。尤其是闸门一的单价,如果规格面板上读不可靠,整个确认页的设计要改。
> 2026-08-04:Phase 1 证明已验证衣服商品没有独立规格入口。项目所有者随后批准 T-110 的受控入口
> 方案:当前只允许证据绑定的精确唯一 `快要抢光` 打开规格面板,并以 capability 隔离保证数量、确认页、
> 提交订单和付款在第一趟不可达。项目所有者随后决定加速 MVP:停止截图遮罩器开发,T-103 使用现有
> 安全最小 XML 继续实现选择与读价;T-204 直接把内部原始截图上传采购服务,不遮罩地址或手机号。
## 八、项目结构
```text
cmbuyer/
├── docs/
├── admin/ # 采购服务(Go)
│ ├── cmd/server/
│ ├── internal/
│ │ ├── domain/ # 实体与状态机,无外部依赖
│ │ ├── usecase/ # 业务用例
│ │ ├── transport/
│ │ │ ├── httpapi/ # 设备侧 API
│ │ │ └── webui/ # 管理页面 + 模板 + 静态资源
│ │ └── storage/ # SQLite 与证据资产
│ └── migrations/
├── client/ # 采购工具(Python)
│ ├── src/
│ │ ├── android/ # adb / device / pdd_flow
│ │ ├── core/ # models / task_runner / sources 抽象
│ │ ├── remote/ # HttpTaskSource / HttpResultSink
│ │ └── app/ # PySide6 GUI
│ └── tests/
└── scripts/
```
`client/src/core/sources.py` 必须保留 `TaskSource` / `ResultSink` 抽象,执行器只依赖抽象。
这样离线 Excel 模式可作为降级路径存在,且执行器不因来源变化而改动。
## 九、架构纪律
- 业务事实和 schema 变化必须同步更新本文与 [api.md](api.md)。
- 不在代码里发明文档没有的接口、字段和状态。
- 第四节的安全边界不得在任务中放宽;确需变更时先改本文并说明理由。
- 高风险模块先单独真机验证,再接入完整流程。
- 前序项目 `cmroubao` / `cmpdd` 是设计依据,**不是事实来源**;引用其结论时必须在本项目
重新验证。