打通 Client↔Admin 闭环的一环:录入 PDD 链接 → 创建采集任务 → 客户端领走去采 → 规格和价格显示在页面上。#16 写的 MarkCollecting 和 SoftDeletePddProduct 至此才有生产调用方。 链接解析严格、不做容错兜底:goods_id 上有 UNIQUE 约束,防重全靠它。 猜一个的话同一商品会存成好几行、采好几遍,规格映射还说不清指向哪一行。 短链一律拒绝,卡域名是为了拦"粘了淘宝链接"这种失误。 创建采集任务先占状态再建任务:MarkCollecting 只在 pending/failed 时成功, 同时充当"有没有人已经在采"的判断,与建任务在同一事务里, 所以并发点多次只会建出一个任务(实测 4 并发 → 1 条)。 submit.go 的 collectedData 增加 Dimensions —— 没有它就只能按 Go 的 map 遍历,而 map 无序,同一商品每次刷新"颜色/尺码"的先后都可能变。 顺带修 #16 一处缺陷:EnsurePddProduct 复活分支清空了 skus_json 却漏了 title,导致复活后状态显示"未采集"但标题还留着旧值。 审查打回一次:清理"PDD 链接唯一的录入口"这一过期说法(全库 5 处), 以及 shopee.go 里"空 → no_link"的过期 TODO —— no_link 已在 #16 从 CHECK 约束删除,照写会直接撞约束。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
387 lines
17 KiB
Markdown
387 lines
17 KiB
Markdown
# 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 链接
|
||
│
|
||
↓
|
||
PDD 商品页 ──────→ PDD 商品表
|
||
│ 点【创建采集任务】
|
||
↓
|
||
创建采集任务(不指定 Client)→ 谁领到算谁的
|
||
↓
|
||
Client 采回 PDD 的颜色尺码和价格
|
||
↓
|
||
存入 PDD 商品表.skus_json
|
||
│
|
||
顺运宝货运单 ─同步─→ 货运单表
|
||
│ 双击行
|
||
↓
|
||
匹配弹窗:蝦皮规格 ←→ PDD 规格
|
||
(匹配过的自动带出,只有新规格要手工做)
|
||
↓
|
||
创建采购任务 → 分配 Client → 被领取
|
||
↓
|
||
Client 下单 → 提交结果 → 人工付款
|
||
```
|
||
|
||
两个要点:
|
||
|
||
- **采集是商品级的**,一个 PDD 商品采一次,所有相关订单共用结果。
|
||
- **匹配是 SKU 级的且可复用**,同一个蝦皮 SKU 只需人工匹配一次。
|
||
|
||
## 4. 产品范围
|
||
|
||
顶级导航固定五个模块,顺序不变:
|
||
|
||
| # | 模块 | 职责 |
|
||
|---|---|---|
|
||
| 1 | 蝦皮数据 | 商品档案、PDD 链接、发起采集 |
|
||
| 2 | PDD 商品 | PDD 商品档案、发起采集、查看采回来的规格价格 |
|
||
| 3 | 顺运宝数据 | 货运单、规格匹配、生成采购任务 |
|
||
| 4 | 采购任务 | 执行进度跟踪 |
|
||
| 5 | 客户端列表 | 客户端注册与状态 |
|
||
|
||
五个模块**统一使用三段式页面布局**:顶部工具条 / 中间带勾选的表格 / 底部状态条。
|
||
详见 [05 界面规范](05-ui-specification.md)。
|
||
|
||
PDD 商品之所以单独一个模块,是因为它在数据上就是**独立实体**
|
||
(自己的表、自己的采集状态,可以被多个蝦皮商品共用),
|
||
见 [03 数据模型 §4](03-data-model.md)。挂在蝦皮模块下面的话,
|
||
同一个 PDD 商品被两个蝦皮商品引用时就说不清该显示在谁名下。
|
||
|
||
### 4.1 蝦皮数据模块
|
||
|
||
**顶部工具条:** 导入按钮、商品 ID 搜索框、搜索按钮、删除按钮、批量采集按钮。
|
||
|
||
**中间表格**(按 SKU 展开显示,数据来自商品表和 SKU 表联查):
|
||
|
||
| 列 | 说明 |
|
||
|---|---|
|
||
| 勾选 | 支持批量删除、批量采集 |
|
||
| 商品 ID | 蝦皮商品编号 |
|
||
| 商品名称 | |
|
||
| 颜色 / 尺码 / 建议 | 从规格原文解析,解析不出来留空并标记 |
|
||
| PDD 链接 | 人工填写,空的要显眼 |
|
||
| 采集状态 | 见 §6 |
|
||
| 更新时间 | |
|
||
|
||
**双击行打开编辑弹窗**,这是**从蝦皮商品出发**填 PDD 链接、发起采集的入口:
|
||
|
||
- 可编辑:PDD 链接、颜色、尺码、建议;
|
||
- 弹窗内提供【采集】按钮,紧挨 PDD 链接输入框;
|
||
- `[必须]` 采集按钮**在本页只出现在弹窗里**,不要放在每个表格行——
|
||
一个商品有 N 个 SKU 行,放行上就是 N 个按钮干同一件事,还会建出 N 个重复任务。
|
||
|
||
`[注意]` PDD 商品模块(§4.2)也能录入链接和发起采集,两处并存。
|
||
本页的入口目前**还是骨架**(点了返回 501),
|
||
两者要不要合并、以哪个为准,属于「蝦皮↔PDD 关联入口」的范围,
|
||
由那张工单统一决定,不要在本模块单独改。
|
||
|
||
**其他要求:**
|
||
|
||
- `[必须]` 支持**手动新增**一条 SKU。报表只含有销售成绩的 SKU(样本里平均每商品仅 1.17 个),
|
||
订单来了查无此 SKU 是常态,必须能补。
|
||
- `[必须]` 导入是 **upsert**,绝不允许先清空再导入,否则人工填的 PDD 链接会被洗掉。
|
||
- `[必须]` 规格原文永远保留,解析失败留空,不要猜。
|
||
|
||
**底部状态条:** 最近一次导入的时间、条数、失败行数。
|
||
|
||
### 4.2 PDD 商品模块
|
||
|
||
维护拼多多商品档案,并发起采集。这条链路**不依赖蝦皮和顺运宝的任何数据**,
|
||
可以单独跑通:建商品 → 建采集任务 → Client 领走执行 → 提交结果 → 页面显示已采集。
|
||
|
||
**顶部工具条:** 创建按钮、创建采集任务按钮、采集状态筛选、商品 ID/链接搜索框、搜索按钮、删除按钮。
|
||
|
||
**中间表格:**
|
||
|
||
| 列 | 说明 |
|
||
|---|---|
|
||
| 勾选 | 支持批量删除、批量建采集任务 |
|
||
| 商品 ID | 从链接解析出来的 `goods_id` |
|
||
| 标题 | 采集回来的,未采集时显示占位文案 |
|
||
| PDD 链接 | 截断显示,可点开 |
|
||
| 采集状态 | 见 §6.1,用中文文字,不能只靠颜色 |
|
||
| 规格数 | 从 `skus_json` 算 |
|
||
| 采集时间 | 未采集显示 `—` |
|
||
| 更新时间 | |
|
||
|
||
`[必须]` **规格数必须显示。** 采到 1 个和采到 20 个差别很大——
|
||
只采到 1 个通常意味着客户端没点开规格面板,是采集有问题。
|
||
不显示这一列的话,要点进每个商品才能发现。
|
||
|
||
**创建:** `[必须]` 只填 PDD 链接,其余字段全靠采集回填。
|
||
`[必须]` **链接必须能解析出 `goods_id`,解析不出来直接报错并且不写库**。
|
||
不接受短链接——`goods_id` 上有 UNIQUE 约束,防重全靠它,
|
||
拿不到就没法查重,同一个商品会存成好几行。
|
||
|
||
**双击行打开弹窗:** 查看商品信息,可编辑链接,并显示采回来的规格和价格。
|
||
|
||
- `[必须]` **规格和价格必须显示**,这是采集结果的全部价值所在——
|
||
不显示的话操作员没法确认"采得对不对、是不是我要的那个商品"。
|
||
- `[必须]` 维度顺序按 `skus_json` 里的 `dimensions`(Go 的 map 无序,必须靠它定顺序)。
|
||
- `[必须]` 价格显示成 `¥12.56`,底层存整数分;`price_cent` 为 `null` 时显示"未采到",
|
||
**不得显示成 ¥0.00**。
|
||
- `[必须]` 编辑链接时新链接必须还是**同一个商品**。换成别的商品要新建一条:
|
||
这一行上挂着采集结果和 SKU 映射,`goods_id` 一换那些数据就全指到错的商品上了。
|
||
|
||
**创建采集任务:**
|
||
|
||
- `[必须]` **不指定客户端**,谁领到就在领取时标记谁。采集是纯读取操作,
|
||
哪台机器跑都一样,指定了反而会在那台机器关着的时候干等。
|
||
- `[必须]` 按 `goods_id` 去重;`collecting` 状态的跳过,并在结果里说明跳过了几个。
|
||
- `[必须]` 按钮文案用「**创建采集任务**」,不要用「采集」。它做的是建一个任务,
|
||
不是立刻去采——真正的采集要等 Client 来领、去手机上跑,可能几秒也可能几分钟。
|
||
|
||
**删除:** 软删除。二次确认要写明"重新创建同一链接可恢复,**但采集结果会清空**"。
|
||
不硬删是因为 SKU 映射指向它,硬删会把人工攒了很久的匹配成果一起带走。
|
||
|
||
**底部状态条:** 各采集状态的条数统计。
|
||
|
||
### 4.3 顺运宝数据模块
|
||
|
||
**顶部工具条:** 同步按钮、订单号搜索框、搜索按钮、创建采购任务按钮、删除按钮。
|
||
|
||
- `[待定]` 同步按钮 MVP 阶段只做占位:点击后提示"同步功能待接入",不发请求。
|
||
|
||
**中间表格:**
|
||
|
||
| 列 | 说明 |
|
||
|---|---|
|
||
| 勾选 | 支持批量删除、批量建任务 |
|
||
| 货运单 ID | |
|
||
| 订单号 | |
|
||
| 商品标题 | |
|
||
| 蝦皮商品 ID | 关联蝦皮数据模块 |
|
||
| 规格 SKU | 蝦皮的规格编号 |
|
||
| 数量 | |
|
||
| 价格 | 蝦皮售价,**台币分**,见 §7 |
|
||
| 图片 | 存 URL,表格里显示缩略图 |
|
||
| 匹配状态 | 已匹配 / 待匹配 |
|
||
| 更新时间 | |
|
||
|
||
原始的完整货运单 JSON 存 `syb_data` 字段,**不作为表格列显示**,在详情里看。
|
||
|
||
**双击行打开匹配弹窗:**
|
||
|
||
- 左边显示蝦皮的颜色尺码,右边显示该商品 `pdd_data` 里的 PDD 颜色尺码;
|
||
- 操作员选好对应关系,点保存;
|
||
- `[必须]` 保存的是**可复用的 SKU 映射**,不是这一张订单的临时数据。
|
||
下次遇到同一个蝦皮 SKU 自动带出,操作员只需确认。
|
||
- `[必须]` 该商品尚未采集(`pdd_data` 为空)时,弹窗要明确提示"请先到蝦皮数据模块采集",
|
||
而不是显示一个空列表让人困惑。
|
||
|
||
**创建采购任务:** 勾选若干行 → 点按钮 → 校验通过后生成任务。校验规则见 §5。
|
||
|
||
**底部状态条:** 最近同步时间、待匹配条数。
|
||
|
||
### 4.4 采购任务模块
|
||
|
||
**顶部工具条:** 订单号搜索框、搜索按钮、删除按钮。
|
||
|
||
**中间表格:**
|
||
|
||
| 列 | 说明 |
|
||
|---|---|
|
||
| 勾选 | 支持批量删除 |
|
||
| 订单号 | |
|
||
| 商品标题 | |
|
||
| 颜色 / 尺码 | **PDD 侧**的规格,不是蝦皮的 |
|
||
| 数量 | |
|
||
| 价格上限 | 人民币分,见 §7 |
|
||
| 蝦皮 ID | |
|
||
| 分配客户端 | 可改派 |
|
||
| 状态 | 见 §6 |
|
||
| 更新时间 | |
|
||
|
||
**底部状态条:** 各状态的任务条数统计。
|
||
|
||
### 4.5 客户端列表模块
|
||
|
||
**顶部工具条:** 客户端名称搜索框、搜索按钮、删除按钮。
|
||
|
||
**中间表格:** 勾选、名称、序列号、状态、最近活动时间、更新时间。
|
||
|
||
**注册方式:**
|
||
|
||
- `[必须]` 设置页通过独立登记接口幂等新增或更新 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 链接**人工填写**,不自动抓取。
|
||
- PDD 商品是**独立模块**,创建商品和创建采集任务都在那里;
|
||
蝦皮数据模块的编辑弹窗负责的是「这个蝦皮商品对应哪个 PDD 商品」。
|
||
- **采集任务不指定客户端**,谁领到算谁的;采购任务仍可分配,也允许留空。
|
||
- 任务**分配给指定客户端**,Client 只领分给自己的。
|
||
- **不加心跳接口**;设置页显式登记,领取接口保留兼容登记,在线状态由最近活动时间派生。
|
||
- SKU 映射**独立成表且可复用**,同一蝦皮 SKU 只人工匹配一次。
|
||
- 货运单的"规格 SKU"**直接对应蝦皮 `商品規格ID`**,不需要转换。
|
||
但**不加外键**——本地查不到该 SKU 是常态,见 [03 数据模型](03-data-model.md) §4。
|
||
- Go 版本固定 **1.23.0**。
|