Files
cmautobuy/docs/admin/01-requirements.md
T
chengmaandClaude Opus 5 998c06a2bf feat: PDD 商品数据独立成表 (#16)
原来 pdd_data 是 shopee_products 上的一个 JSON 字段,两个蝦皮商品指向
同一个 PDD 链接时会各存一份、各采一次;collect_status 描述的是 PDD 商品的
状态,却挂在蝦皮商品上,两份可能不一致。

更要紧的是 PDD 商品变动频繁(A 下架就得换 B),而 sku_mappings 只按
shopee_sku_id 做键——换商品后旧映射还在,B 恰好有同名规格但完全是另一件货
时会静默买错,事后查不出来。

改动
- 新增 pdd_products 表:id 主键 + goods_id UNIQUE + 4 个状态值(去掉
  no_link,「未填链接」改由 shopee_products.pdd_goods_id 为空表达)+
  软删除可复活
- shopee_products 去掉 pdd_data / collect_status / collect_error /
  collected_at,pdd_goods_id 改为引用
- sku_mappings 主键改为 (shopee_sku_id, pdd_goods_id),新增 pdd_option_key。
  查映射永远带上当前 PDD 商品,换商品后天然查不到旧映射,不需要删数据;
  换回原商品时旧映射直接复用
- 新增 OptionKey():用 json.Marshal 实现(Go 序列化 map 按键名排序,
  天然规范化),不自己拼字符串——规格文字里可能含 = 或 ;。
  存映射和查 SKU 必须用同一个函数,各写一遍会静默算出不同结果
- 采集结果改落 pdd_products,新增两条校验:
  返回的 goods_id 与请求不符 → 整体回滚拒绝(422),不静默存下;
  skus 为空数组 → 置 failed 而非 collected,否则界面显示"已采集"
  但数据毫无用处

实施时超出工单但必要的三处
- TaskExists 重构为 GetTaskInfo:原函数只返回蝦皮 goods_id,
  而采集结果要按 PDD goods_id 落库,不改取不到正确的键
- 复活时一并清空旧采集结果(skus_json / collect_msg / collected_at),
  否则复活后会显示"已采集"但数据是删除前的
- 删除 repository/shopee.go:两个函数签名全变且已迁到 pdd.go,留着是死代码

已验证(Go 1.23.0)
- go vet / gofmt / go test 全过,55 个测试
- 端到端补验了工单未覆盖的 HTTP 层:goods_id 不符返回 422
  COLLECT_GOODS_MISMATCH 且整体回滚(skus_json 空、任务仍 claimed、
  幂等记录 0 条);skus 为空返回 200 但状态 failed

遗留
- MarkCollecting / SoftDeletePddProduct 暂无调用方,等界面工单接上
- artifact_ref 存 diagnostics 原始 JSON,未按 client-001:artifacts/... 规范化,
  因 Client 侧尚未定义 diagnostics 结构
- 界面未实现(工单明确排除),四个页面仍为骨架

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 10:27:39 +08:00

318 lines
13 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.
# 01 Admin 产品需求基线
- 文档状态:基线草案,待需求评审
- 适用范围:`admin/`
- 产品类型:本地运行的 Web 管理端(Go + Gin + HTML 模板)
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。
没有标注的默认是 `[必须]`。看不懂的词查 [术语表](00-glossary.md)。
## 1. 背景与目标
我们在**蝦皮**(台湾)卖货,在**拼多多**(大陆)进货,中间由**顺运宝**提供货运单。
Admin 的职责是把这三方的数据串起来,最终产出 Client 能执行的采购任务。
产品目标:
1. 导入并维护蝦皮商品档案,为每个商品登记对应的拼多多链接。
2. 同步顺运宝货运单,知道"这单该买什么"。
3. 把蝦皮的颜色尺码,对应到拼多多的颜色尺码,**且这个对应关系可复用**。
4. 生成采购任务并分配给指定客户端,跟踪执行结果。
5. 管理客户端清单,知道谁在干活。
## 2. 用户与外部系统
- **操作人员:**导入报表、填 PDD 链接、发起采集、做规格匹配、创建并分配任务、处理异常。
- **Client:**领取任务、在安卓设备上执行、提交结果。契约见 [Client 侧文档](../client/04-admin-api-contract.md)。
- **蝦皮:**只提供 Excel 报表导出,**没有接口对接**。
- **顺运宝:**提供货运单,`[待定]` 同步方式待确认,MVP 先做占位按钮。
- **拼多多:**由 Client 操作,Admin 不直接接触。
## 3. 完整业务链路
这条链路是理解全部四个模块的关键,先看懂它:
```text
蝦皮报表 ──导入──→ 商品表 + SKU 表
│
编辑弹窗:人工填 PDD 链接
│ 点【采集】
↓
创建采集任务 → 分配 Client → 被领取
↓
Client 采回 PDD 的颜色尺码和价格
↓
存入 商品表.pdd_data
│
顺运宝货运单 ─同步─→ 货运单表
│ 双击行
↓
匹配弹窗:蝦皮规格 ←→ PDD 规格
(匹配过的自动带出,只有新规格要手工做)
↓
创建采购任务 → 分配 Client → 被领取
↓
Client 下单 → 提交结果 → 人工付款
```
两个要点:
- **采集是商品级的**,一个 PDD 商品采一次,所有相关订单共用结果。
- **匹配是 SKU 级的且可复用**,同一个蝦皮 SKU 只需人工匹配一次。
## 4. 产品范围
顶级导航固定四个模块,顺序不变:
| # | 模块 | 职责 |
|---|---|---|
| 1 | 蝦皮数据 | 商品档案、PDD 链接、发起采集 |
| 2 | 顺运宝数据 | 货运单、规格匹配、生成采购任务 |
| 3 | 采购任务 | 执行进度跟踪 |
| 4 | 客户端列表 | 客户端注册与状态 |
四个模块**统一使用三段式页面布局**:顶部工具条 / 中间带勾选的表格 / 底部状态条。
详见 [05 界面规范](05-ui-specification.md)。
### 4.1 蝦皮数据模块
**顶部工具条:** 导入按钮、商品 ID 搜索框、搜索按钮、删除按钮、批量采集按钮。
**中间表格**(按 SKU 展开显示,数据来自商品表和 SKU 表联查):
| 列 | 说明 |
|---|---|
| 勾选 | 支持批量删除、批量采集 |
| 商品 ID | 蝦皮商品编号 |
| 商品名称 | |
| 颜色 / 尺码 / 建议 | 从规格原文解析,解析不出来留空并标记 |
| PDD 链接 | 人工填写,空的要显眼 |
| 采集状态 | 见 §6 |
| 更新时间 | |
**双击行打开编辑弹窗**,这是 PDD 链接唯一的录入口:
- 可编辑:PDD 链接、颜色、尺码、建议;
- 弹窗内提供【采集】按钮,紧挨 PDD 链接输入框;
- `[必须]` 采集按钮**只出现在弹窗里**,不要放在每个表格行——
一个商品有 N 个 SKU 行,放行上就是 N 个按钮干同一件事,还会建出 N 个重复任务。
**其他要求:**
- `[必须]` 支持**手动新增**一条 SKU。报表只含有销售成绩的 SKU(样本里平均每商品仅 1.17 个),
订单来了查无此 SKU 是常态,必须能补。
- `[必须]` 导入是 **upsert**,绝不允许先清空再导入,否则人工填的 PDD 链接会被洗掉。
- `[必须]` 规格原文永远保留,解析失败留空,不要猜。
**底部状态条:** 最近一次导入的时间、条数、失败行数。
### 4.2 顺运宝数据模块
**顶部工具条:** 同步按钮、订单号搜索框、搜索按钮、创建采购任务按钮、删除按钮。
- `[待定]` 同步按钮 MVP 阶段只做占位:点击后提示"同步功能待接入",不发请求。
**中间表格:**
| 列 | 说明 |
|---|---|
| 勾选 | 支持批量删除、批量建任务 |
| 货运单 ID | |
| 订单号 | |
| 商品标题 | |
| 蝦皮商品 ID | 关联蝦皮数据模块 |
| 规格 SKU | 蝦皮的规格编号 |
| 数量 | |
| 价格 | 蝦皮售价,**台币分**,见 §7 |
| 图片 | 存 URL,表格里显示缩略图 |
| 匹配状态 | 已匹配 / 待匹配 |
| 更新时间 | |
原始的完整货运单 JSON 存 `syb_data` 字段,**不作为表格列显示**,在详情里看。
**双击行打开匹配弹窗:**
- 左边显示蝦皮的颜色尺码,右边显示该商品 `pdd_data` 里的 PDD 颜色尺码;
- 操作员选好对应关系,点保存;
- `[必须]` 保存的是**可复用的 SKU 映射**,不是这一张订单的临时数据。
下次遇到同一个蝦皮 SKU 自动带出,操作员只需确认。
- `[必须]` 该商品尚未采集(`pdd_data` 为空)时,弹窗要明确提示"请先到蝦皮数据模块采集",
而不是显示一个空列表让人困惑。
**创建采购任务:** 勾选若干行 → 点按钮 → 校验通过后生成任务。校验规则见 §5。
**底部状态条:** 最近同步时间、待匹配条数。
### 4.3 采购任务模块
**顶部工具条:** 订单号搜索框、搜索按钮、删除按钮。
**中间表格:**
| 列 | 说明 |
|---|---|
| 勾选 | 支持批量删除 |
| 订单号 | |
| 商品标题 | |
| 颜色 / 尺码 | **PDD 侧**的规格,不是蝦皮的 |
| 数量 | |
| 价格上限 | 人民币分,见 §7 |
| 蝦皮 ID | |
| 分配客户端 | 可改派 |
| 状态 | 见 §6 |
| 更新时间 | |
**底部状态条:** 各状态的任务条数统计。
### 4.4 客户端列表模块
**顶部工具条:** 客户端名称搜索框、搜索按钮、删除按钮。
**中间表格:** 勾选、名称、序列号、状态、最近活动时间、更新时间。
**注册方式:**
- `[必须]` 设置页通过独立登记接口幂等新增或更新 Client;该接口不得领取或修改任务。
- `[必须]` 领取接口保留隐式登记作为旧 Client 的兼容兜底。
- `[必须]` 不设心跳接口,在线状态由登记、领取和提交产生的最近活动时间派生。
- `[必须]` `状态` 是**派生字段,不存库**:
最近活动时间在 N 分钟内算"在线",否则"离线"。`[建议]` N 默认 10 分钟。
- 名称由客户端上报,操作员可以在 Admin 这边改成好记的名字。
**底部状态条:** 在线 / 离线数量统计。
## 5. 创建采购任务的校验
`[必须]` 下面任何一条不满足就不允许创建,并明确告诉操作员缺什么:
1. 该蝦皮商品已填 PDD 链接;
2. 该蝦皮商品已采集成功(`pdd_data` 非空);
3. 该蝦皮 SKU 已有 PDD 规格映射;
4. 数量大于 0;
5. **价格上限已填且大于 0** —— 默认从 `pdd_data` 里该 SKU 的价格带出,操作员可改,但不允许为空;
6. 已选择分配的客户端。
第 5 条是硬要求:Client 契约规定采购任务必须带明确的价格保护
(见 [Client 契约](../client/04-admin-api-contract.md) §4),没有它 Client 会拒绝执行。
## 6. 状态定义
### 6.1 采集状态
界面上显示 5 种,但**数据来自两张表**——采集的对象是 PDD 商品,
所以状态存在 `pdd_products` 上,不在蝦皮商品上。
| 界面显示 | 怎么判断 |
|---|---|
| 未填链接 | `shopee_products.pdd_goods_id` 为空 |
| 未采集 | `pdd_products.collect_status = 'pending'` |
| 采集中 | `= 'collecting'` |
| 已采集 | `= 'collected'` |
| 采集失败 | `= 'failed'`,原因在 `collect_msg` |
`[必须]` `collecting` 状态的商品不允许再建采集任务,按钮置灰并提示"已在采集中"。
`[必须]` 状态挂在 PDD 商品上,好处是**两个蝦皮商品指向同一个 PDD 链接时,
状态只有一份**——不会各记一份还可能不一致,也不会把同一个商品采两遍。
### 6.2 任务状态(采集任务和采购任务通用)
这是 **Admin 侧的状态**,和 Client 本地的 8 个状态是两套,不要混
(Client 侧见 [03 数据模型](../client/03-data-model.md) §7)。
| 值 | 中文 | 什么时候 |
|---|---|---|
| `pending` | 待分配 | 刚创建,还没指定客户端 |
| `assigned` | 待领取 | 已分配给某个客户端 |
| `claimed` | 已领取 | 客户端领走了,正在执行 |
| `succeeded` | 成功 | 收到结果 |
| `manual_review` | 需人工 | 客户端报告需要人处理 |
| `failed` | 失败 | 客户端报告失败 |
| `cancelled` | 已取消 | 人工取消 |
`[必须]` **Admin 看不到客户端执行到哪一步**(没有心跳,是有意的)。
`claimed` 之后就只能等结果。想知道细节看 Client 那边的界面。
客户端提交失败时带的 `status`,按下表落地:
| 客户端报告 | Admin 置为 |
|---|---|
| `retry_wait` | `assigned`(等它再来领) |
| `manual_review` | `manual_review` |
| `failed` | `failed` |
| `cancelled` | `cancelled` |
## 7. 金额与币种
`[必须]` **一律用整数存,禁止浮点。** 字段名带单位后缀。
| 场景 | 币种 | 字段示例 |
|---|---|---|
| 蝦皮售价 | 台币 | `price_twd_cent` |
| PDD 采购价、价格上限 | 人民币 | `max_price_cent` |
`[必须]` **两种币种不得混用,不得互相换算后覆盖原值。**
采购任务里的价格上限是**人民币**,来源是采集回来的 PDD 价格,
和蝦皮的台币售价没有换算关系。
`[待定]` 是否需要展示汇率或利润,待业务确认。MVP 不做。
## 8. MVP 范围
MVP 包含:
- 四模块页面框架和统一的三段式布局;
- SQLite 建库与迁移;
- 蝦皮 Excel 导入(upsert)、搜索、批量删除、手动新增、编辑弹窗;
- PDD 链接录入与发起采集;
- 顺运宝数据的**手工录入或造数**(同步按钮占位);
- 规格匹配弹窗与可复用映射;
- 创建采购任务并分配客户端;
- 给 Client 的四个接口:登记、领取、提交结果、提交失败;
- 客户端自动注册与在线状态。
MVP 之后:
- 接入顺运宝真实同步;
- 打包成 exe;
- 权限与多用户;
- 统计报表。
## 9. 非目标
- 不做面向外部的公网服务,只在内网/本机运行。
- 不直接对接蝦皮和拼多多的接口。
- 不自动决定买哪个商品——PDD 链接和规格匹配都由人确认。
- 不自动付款。
- MVP 不做用户登录和权限体系。
## 10. 非功能需求
- 单机运行,操作人员规模是个位数,不追求高并发。
- 页面在 1366×768 上可正常使用。
- 导入 1 万行 Excel 应在可接受时间内完成,并显示进度或结果统计。
- 所有写操作有 CSRF 防护,所有 SQL 参数化。
- 日志、页面、导出不含 token、密码、Cookie。
- 数据放程序旁边的 `data/`,便携模式,见 [02 架构](02-architecture.md) §6。
## 11. 待确认事项
不许因此停工。先按临时默认值做,定了再按工单改。
| # | 待确认什么 | 临时默认值 |
|---|---|---|
| 1 | 顺运宝同步方式(接口?导出文件?) | 按钮占位,数据靠手工录入或造数 |
| 2 | 蝦皮有无全量规格导出 | 没有。靠反复导入累积 + 手动新增补齐 |
| 3 | Admin 是否需要登录 | MVP 不做,仅本机/内网访问 |
| 4 | 是否需要利润和汇率展示 | 不做 |
**已定案:**
- PDD 链接**人工填写**,入口在蝦皮数据模块的编辑弹窗,采集按钮也在那里。
- 任务**分配给指定客户端**,Client 只领分给自己的。
- **不加心跳接口**;设置页显式登记,领取接口保留兼容登记,在线状态由最近活动时间派生。
- SKU 映射**独立成表且可复用**,同一蝦皮 SKU 只人工匹配一次。
- 货运单的"规格 SKU"**直接对应蝦皮 `商品規格ID`**,不需要转换。
但**不加外键**——本地查不到该 SKU 是常态,见 [03 数据模型](03-data-model.md) §4。
- Go 版本固定 **1.23.0**。