docs: 建立 Admin 子项目的文档基线
仓库从单子项目变成两个:client(Python + PyQt5 桌面端)和 admin(Go + Gin + HTML 模板 Web 管理端)。两者技术栈完全不同, 规则各自独立,只通过三个 HTTP 接口交互。 新增 admin/AGENTS.md 和 docs/admin/ 共 9 份文档,结构与 docs/client/ 对齐。 关键设计决策(均已与用户确认) - 四模块:蝦皮数据 / 顺运宝数据 / 采购任务 / 客户端列表, 统一三段式布局(工具条 / 带勾选的表格 / 状态条) - 蝦皮数据拆成商品表 + SKU 表:报表本身就是两层结构, 且 PDD 链接是商品级的,放 SKU 级会重复维护 - pdd_data 存商品级,一个 PDD 商品采一次,所有订单共用 - SKU 映射独立成表且可复用,同一蝦皮 SKU 只需人工匹配一次 - PDD 链接人工填写,采集按钮就放在填链接的编辑弹窗里 - 任务分配给指定客户端;不加心跳,注册在领取时完成, 在线状态由 last_seen_at 派生 - SQLite 驱动固定 modernc.org/sqlite(纯 Go 免 cgo,go build 直接出 exe) - 不引入前端框架和 npm 构建,只允许原生 JS 或 htmx 基于样本文件 raw_data/蝦皮数据样本.xlsx 实测得出的约束 - 11287 行 = 5195 商品汇总行 + 6092 SKU 行,导入必须分开处理 - 商品規格ID 零重复,是天然主键 - 规格原文两种格式各占 53.4% / 46.6%,只解析【】会漏掉一半 - 平均每商品仅 1.17 个 SKU,报表不是全量目录, 因此必须支持手动新增,且货运单外键不能加硬约束 同步更新 - 根 AGENTS.md 开头的指针改为两个子项目对照表 - docs/README.md 重构为双子项目索引 - .gitignore 加 admin/data/、admin.exe、Excel 锁文件 说明:Gitea 尚未配置,本次无对应工单号。 raw_data/ 未提交,含台币销售额等商业数据,待用户决定。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+9
-2
@@ -1,12 +1,19 @@
|
|||||||
# 本地数据:数据库、日志、截图、控件树 XML
|
# 本地数据:数据库、日志、截图、控件树 XML、上传的 Excel
|
||||||
# 位置和用途见 docs/client/03-data-model.md §2.1
|
# 位置和用途见 docs/client/03-data-model.md §2.1 和 docs/admin/02-architecture.md §6
|
||||||
client/data/
|
client/data/
|
||||||
|
admin/data/
|
||||||
data/
|
data/
|
||||||
|
|
||||||
# 打包产物
|
# 打包产物
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.spec
|
*.spec
|
||||||
|
admin/admin.exe
|
||||||
|
admin/admin
|
||||||
|
|
||||||
|
# Excel 打开时生成的锁文件
|
||||||
|
~$*.xlsx
|
||||||
|
~$*.xls
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -13,13 +13,17 @@
|
|||||||
|
|
||||||
改动看起来再合理,只要碰到上面任意一条,先停下来告诉用户。
|
改动看起来再合理,只要碰到上面任意一条,先停下来告诉用户。
|
||||||
|
|
||||||
> **动 `client/` 下任何东西之前,先读 [client/AGENTS.md](client/AGENTS.md)。**
|
> **动代码之前,先读对应子项目的 AGENTS.md**,那里有技术栈、分层和硬性规则,本文件不重复:
|
||||||
> 那里有技术栈、代码分层、线程、采购安全和界面的硬性规则,本文件不重复。
|
|
||||||
> **本项目的代码目前全部在 `client/` 下**,所以基本上只要是写代码,就必须先读它。
|
|
||||||
>
|
>
|
||||||
> 第一次接手本项目,还要先看:
|
> | 你要改的东西在 | 先读 | 技术栈 |
|
||||||
> [上手指南](docs/client/00-getting-started.md)(装环境、跑起来)和
|
> |---|---|---|
|
||||||
> [术语表](docs/client/00-glossary.md)(Outbox、幂等、不可逆阶段等)。
|
> | `client/` 桌面客户端 | [client/AGENTS.md](client/AGENTS.md) | Python + PyQt5 |
|
||||||
|
> | `admin/` 管理端 | [admin/AGENTS.md](admin/AGENTS.md) | Go + Gin + HTML 模板 |
|
||||||
|
>
|
||||||
|
> **两个子项目技术栈完全不同,规则不通用,不要拿一边的经验套另一边。**
|
||||||
|
>
|
||||||
|
> 第一次接手,先看对应的上手指南和术语表:
|
||||||
|
> [Client](docs/client/00-getting-started.md) · [Admin](docs/admin/00-getting-started.md)
|
||||||
|
|
||||||
## 需求、缺陷与任务工作流
|
## 需求、缺陷与任务工作流
|
||||||
|
|
||||||
@@ -235,9 +239,23 @@ Gitea 使用约定(**首次使用前需由项目负责人补全**):
|
|||||||
- 报错分析应指出具体位置、直接原因、修复方法和验证方式,不只给出修改后的完整代码。
|
- 报错分析应指出具体位置、直接原因、修复方法和验证方式,不只给出修改后的完整代码。
|
||||||
- 不假设维护者熟悉 Qt 线程、SQLite 事务、Admin 幂等或 uiautomator2 控件树;涉及这些概念时给出一句话背景。
|
- 不假设维护者熟悉 Qt 线程、SQLite 事务、Admin 幂等或 uiautomator2 控件树;涉及这些概念时给出一句话背景。
|
||||||
|
|
||||||
## Client 子项目
|
## 子项目
|
||||||
|
|
||||||
- 处理 `client/` 下的需求、代码、测试或文档前,必须读取并遵循 [client/AGENTS.md](client/AGENTS.md)。
|
本仓库有两个子项目,**技术栈完全不同,规则各自独立**:
|
||||||
- 从仓库根目录启动时也不能跳过该文件(本文件开头已重复提醒一次)。
|
|
||||||
- Client 的详细需求和技术基线位于 [docs/client/](docs/client/);按当前任务只读取相关文档,清单见 [文档索引](docs/README.md)。
|
| 子项目 | 是什么 | 规则文件 | 基线文档 |
|
||||||
- Client 专用技术栈、线程、自动化安全、界面和验证规则不在根文件重复维护。
|
|---|---|---|---|
|
||||||
|
| `client/` | Windows 桌面客户端,控制安卓设备执行采集和采购 | [client/AGENTS.md](client/AGENTS.md) | [docs/client/](docs/client/) |
|
||||||
|
| `admin/` | 本地 Web 管理端,管理商品、订单、任务和客户端 | [admin/AGENTS.md](admin/AGENTS.md) | [docs/admin/](docs/admin/) |
|
||||||
|
|
||||||
|
- 处理某个子项目下的需求、代码、测试或文档前,必须读取并遵循它的 `AGENTS.md`。
|
||||||
|
- 从仓库根目录启动时也不能跳过(本文件开头已重复提醒一次)。
|
||||||
|
- 按当前任务只读取相关文档,清单见 [文档索引](docs/README.md)。
|
||||||
|
- 子项目专用的技术栈、分层、安全和验证规则不在根文件重复维护。
|
||||||
|
|
||||||
|
### 两者的接口边界
|
||||||
|
|
||||||
|
Admin 和 Client 通过三个 HTTP 接口交互,**契约以 Client 侧文档为准**:
|
||||||
|
[docs/client/04-admin-api-contract.md](docs/client/04-admin-api-contract.md)。
|
||||||
|
|
||||||
|
改动接口时,两边的文档必须在同一个工单里同步更新,不允许只改一边。
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Admin 子项目规则
|
||||||
|
|
||||||
|
本文件适用于 `admin/` 下的全部代码、模板和测试,并继承仓库根目录 `AGENTS.md` 的
|
||||||
|
**红线**、工作流、Git、安全、文档和初级程序员维护规则。
|
||||||
|
|
||||||
|
## 开始工作前
|
||||||
|
|
||||||
|
- 从仓库根目录执行 Admin 任务时,也必须先读取本文件。
|
||||||
|
- 第一次接手,先看这两份:
|
||||||
|
- 环境和运行:[00 上手指南](../docs/admin/00-getting-started.md)
|
||||||
|
- 看不懂的词:[00 术语表](../docs/admin/00-glossary.md)
|
||||||
|
- 只读取与当前任务有关的长期基线,不必每次加载全部文档:
|
||||||
|
- 需求边界:[01 产品需求基线](../docs/admin/01-requirements.md)
|
||||||
|
- 分层和目录:[02 系统架构](../docs/admin/02-architecture.md)
|
||||||
|
- 数据库表:[03 数据模型](../docs/admin/03-data-model.md)
|
||||||
|
- 给 Client 的接口:[04 Client 接口实现](../docs/admin/04-client-api.md)
|
||||||
|
- 页面和交互:[05 界面规范](../docs/admin/05-ui-specification.md)
|
||||||
|
- 测试和安全:[06 质量与安全](../docs/admin/06-quality-security.md)
|
||||||
|
- Admin 提供的接口必须满足 Client 侧契约
|
||||||
|
[docs/client/04-admin-api-contract.md](../docs/client/04-admin-api-contract.md),
|
||||||
|
两边不一致时以 Client 契约为准,要改先走工单。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- 固定使用 **Go + Gin + Go 标准库 `html/template`**。
|
||||||
|
- 数据库固定 **SQLite**,驱动固定 `modernc.org/sqlite`。
|
||||||
|
**不得改用 `mattn/go-sqlite3`** —— 那个要 cgo,Windows 上得装 gcc,
|
||||||
|
交叉编译和打包 exe 都会变得很麻烦。
|
||||||
|
- Excel 读取固定 `github.com/xuri/excelize/v2`。
|
||||||
|
- Go 版本固定 **1.23.0**,依赖版本以 `admin/go.mod` / `go.sum` 为准。
|
||||||
|
- 新增或升级依赖前先过工单;确认后把 `go.mod` 和 `go.sum` 一起提交。
|
||||||
|
|
||||||
|
### 前端约束
|
||||||
|
|
||||||
|
- **不得引入 React、Vue、Angular 等前端框架,不得引入 npm 构建流程。**
|
||||||
|
页面一律服务端渲染。
|
||||||
|
- 搜索、删除、导入这类操作用普通表单提交,**零 JavaScript**。
|
||||||
|
- 勾选、弹窗、导入进度确实需要 JS,只允许两种做法:
|
||||||
|
- 原生 JavaScript,直接写在模板里或放 `static/js/` 下的独立文件;
|
||||||
|
- htmx(单个 js 文件,无构建步骤)。
|
||||||
|
- **不得引入打包器**(webpack/vite/esbuild)。CSS 手写,放 `static/css/`。
|
||||||
|
|
||||||
|
## 代码分层
|
||||||
|
|
||||||
|
- `handler` 只负责解析请求、调用 service、渲染模板或返回 JSON,**不写业务逻辑,不拼 SQL**。
|
||||||
|
- `service` 放业务逻辑(导入解析、创建任务、匹配复用),不认识 Gin 的 `*gin.Context`。
|
||||||
|
- `repository` 封装 SQLite,**只有这一层能写 SQL**。
|
||||||
|
- `model` 只放数据结构,不导入 Gin 和数据库驱动。
|
||||||
|
- 给 Client 的接口和给浏览器的页面**分开放**(`handler/api/` 和 `handler/web/`),
|
||||||
|
两者的错误格式、认证方式都不一样,混在一起迟早出事。
|
||||||
|
|
||||||
|
## 数据与接口
|
||||||
|
|
||||||
|
- 表结构以 [03 数据模型](../docs/admin/03-data-model.md) 为准。
|
||||||
|
- 给 Client 的接口以 [docs/client/04-admin-api-contract.md](../docs/client/04-admin-api-contract.md) 为准。
|
||||||
|
- `[必须]` **金额一律用整数存**,人民币用分、台币用分,字段名带单位后缀(`_cent`)。
|
||||||
|
**禁止用 float 存金额。**
|
||||||
|
- `[必须]` 时间一律带时区 ISO 8601,库里存 UTC,页面上转本地时区显示。
|
||||||
|
- `[必须]` Excel 导入只做 **upsert(更新或新增)**,
|
||||||
|
**绝不允许先清空再导入** —— 会把人工填的 PDD 链接全洗掉。
|
||||||
|
- `[必须]` 蝦皮规格原文(`spec_raw`)永远保留,解析不出来就留空,不要瞎猜。
|
||||||
|
- `[必须]` Client 提交结果时,**不管任务是否已取消、是否已重派,一律接受**,
|
||||||
|
理由见 Client 契约 §6.1。这条最容易被顺手违反。
|
||||||
|
|
||||||
|
## 界面规则
|
||||||
|
|
||||||
|
- 四个模块统一使用三段式布局:**顶部工具条 / 中间带勾选的表格 / 底部状态条**。
|
||||||
|
第一个页面写完,其余三个照抄结构改字段,不要给某个模块搞特殊。
|
||||||
|
- 表格行的身份用业务主键,**不得用行号**。
|
||||||
|
- 批量删除必须二次确认,并显示"将删除 N 条"。
|
||||||
|
- 破坏性操作(删除、导入覆盖)用 POST,**不得用 GET**。
|
||||||
|
- 页面必须能在 1366×768 上正常使用,表格横向滚动而不是压缩列宽。
|
||||||
|
|
||||||
|
## 安全
|
||||||
|
|
||||||
|
- 根目录 `AGENTS.md` 的五条红线同样适用。
|
||||||
|
- `[必须]` 所有写操作(新增/编辑/删除/导入/建任务)加 CSRF 防护。
|
||||||
|
- `[必须]` SQL 一律用参数化查询,**禁止字符串拼接 SQL**。
|
||||||
|
- `[必须]` 模板输出走 `html/template` 的自动转义,
|
||||||
|
**禁止用 `template.HTML` 包裹用户可控内容**。
|
||||||
|
- `[必须]` 上传的 Excel 限制大小和扩展名,解析失败要有明确报错,不能让整个进程崩掉。
|
||||||
|
- `[必须]` 日志、页面、导出里不得出现 token、密码、Cookie。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
全部命令从 `admin/` 目录执行。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go vet ./...
|
||||||
|
go build ./...
|
||||||
|
go test ./...
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
- `go run .` 启动后访问 `http://localhost:8080`,**不会连手机、不会下单**,可随时运行。
|
||||||
|
- 修改数据库时测试首次建库和从上一版本迁移。
|
||||||
|
- 修改给 Client 的接口时,跑契约测试,确认仍满足 Client 侧 §6.1 的无条件接受。
|
||||||
|
- 修改 Excel 导入时用 `raw_data/` 下的样本跑一遍,核对导入条数。
|
||||||
|
- 交付时说明已运行的命令、结果和未验证的部分。
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
module cmautobuy/admin
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
// 依赖用 go get 添加后会自动写到下面,连同 go.sum 一起提交。
|
||||||
|
// 本项目固定使用(理由见 admin/AGENTS.md 技术栈一节):
|
||||||
|
// github.com/gin-gonic/gin Web 框架
|
||||||
|
// modernc.org/sqlite SQLite 驱动,纯 Go,不需要 cgo
|
||||||
|
// github.com/xuri/excelize/v2 读蝦皮 Excel 报表
|
||||||
|
//
|
||||||
|
// 不得改用 github.com/mattn/go-sqlite3(需要 cgo,Windows 上要装 gcc,
|
||||||
|
// 交叉编译和打包 exe 都会变麻烦)。
|
||||||
+70
-16
@@ -1,18 +1,34 @@
|
|||||||
# 项目文档索引
|
# 项目文档索引
|
||||||
|
|
||||||
本目录保存项目级文档。长期稳定的 Client 基线文档放在 `docs/client`,实施过程和完成记录放在 `docs/task`,两者不得混用。
|
本目录保存项目级文档。长期稳定的基线文档按子项目放在 `docs/client` 和 `docs/admin`,
|
||||||
|
实施过程和完成记录放在 `docs/task`,两者不得混用。
|
||||||
|
|
||||||
|
## 项目由两部分组成
|
||||||
|
|
||||||
|
| 子项目 | 是什么 | 技术栈 | 规则文件 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Client** | Windows 桌面客户端,控制安卓设备在拼多多采集和下单 | Python 3.10 + PyQt5 | [client/AGENTS.md](../client/AGENTS.md) |
|
||||||
|
| **Admin** | 本地 Web 管理端,管理商品、货运单、采购任务和客户端 | Go + Gin + HTML 模板 | [admin/AGENTS.md](../admin/AGENTS.md) |
|
||||||
|
|
||||||
|
两者通过三个 HTTP 接口交互,契约以
|
||||||
|
[Client 侧的接口契约](client/04-admin-api-contract.md) 为准。
|
||||||
|
|
||||||
|
> **两个子项目技术栈完全不同,规则不通用。** 别拿 Client 的经验套 Admin,反之亦然。
|
||||||
|
|
||||||
## 新人从这里开始
|
## 新人从这里开始
|
||||||
|
|
||||||
| 文档 | 用途 |
|
| 我要做 | 先读 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [00 上手指南](client/00-getting-started.md) | 装环境、装依赖、连手机、把程序跑起来、常见报错 |
|
| 上手 Client | [Client 上手指南](client/00-getting-started.md) + [Client 术语表](client/00-glossary.md) |
|
||||||
| [00 术语表](client/00-glossary.md) | Outbox、幂等、SKU 等专业词的一句话解释 |
|
| 上手 Admin | [Admin 上手指南](admin/00-getting-started.md) + [Admin 术语表](admin/00-glossary.md) |
|
||||||
|
| 搞清楚整条业务链路 | [Admin 需求](admin/01-requirements.md) §3 |
|
||||||
|
|
||||||
## 按任务找文档
|
## 按任务找文档
|
||||||
|
|
||||||
**不用通读全部文档**,按你要做的事挑:
|
**不用通读全部文档**,按你要做的事挑:
|
||||||
|
|
||||||
|
### Client(桌面客户端)
|
||||||
|
|
||||||
| 我要做的事 | 主要看 | 顺带看 |
|
| 我要做的事 | 主要看 | 顺带看 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 第一次把项目跑起来 | [00 上手指南](client/00-getting-started.md) | — |
|
| 第一次把项目跑起来 | [00 上手指南](client/00-getting-started.md) | — |
|
||||||
@@ -24,20 +40,52 @@
|
|||||||
| 写测试 | [06 质量与安全](client/06-quality-security.md) §2 | — |
|
| 写测试 | [06 质量与安全](client/06-quality-security.md) §2 | — |
|
||||||
| 搞不清这功能到底要不要做 | [01 产品需求基线](client/01-requirements.md) | — |
|
| 搞不清这功能到底要不要做 | [01 产品需求基线](client/01-requirements.md) | — |
|
||||||
| 打包成 exe、改文件路径 | [01 需求](client/01-requirements.md) §8.1 | [03 数据模型](client/03-data-model.md) §2 |
|
| 打包成 exe、改文件路径 | [01 需求](client/01-requirements.md) §8.1 | [03 数据模型](client/03-data-model.md) §2 |
|
||||||
| 建工单、写归档 | [模板](templates/task.md) | 根目录 [AGENTS.md](../AGENTS.md) |
|
|
||||||
|
|
||||||
无论做哪一样,都必须先看一遍 [client/AGENTS.md](../client/AGENTS.md)(技术栈和红线)。
|
### Admin(Web 管理端)
|
||||||
|
|
||||||
|
| 我要做的事 | 主要看 | 顺带看 |
|
||||||
|
|---|---|---|
|
||||||
|
| 第一次把项目跑起来 | [00 上手指南](admin/00-getting-started.md) | — |
|
||||||
|
| 改页面、加表格列 | [05 界面规范](admin/05-ui-specification.md) | [02 架构](admin/02-architecture.md) §4 模板 |
|
||||||
|
| 加字段、改表、写 SQL | [03 数据模型](admin/03-data-model.md) | [02 架构](admin/02-architecture.md) §2 分层 |
|
||||||
|
| 改 Excel 导入 | [03 数据模型](admin/03-data-model.md) §3.3 | [00 术语表](admin/00-glossary.md) §3 upsert |
|
||||||
|
| 改给 Client 的接口 | [04 Client 接口实现](admin/04-client-api.md) | [Client 侧契约](client/04-admin-api-contract.md) |
|
||||||
|
| 写测试 | [06 质量与安全](admin/06-quality-security.md) §2 | — |
|
||||||
|
| 搞不清这功能到底要不要做 | [01 产品需求基线](admin/01-requirements.md) | — |
|
||||||
|
|
||||||
|
### 通用
|
||||||
|
|
||||||
|
| 我要做的事 | 看这里 |
|
||||||
|
|---|---|
|
||||||
|
| 建工单、写归档 | [模板](templates/task.md) + 根目录 [AGENTS.md](../AGENTS.md) |
|
||||||
|
|
||||||
|
无论做哪一样,都必须先看一遍对应子项目的 `AGENTS.md`(技术栈和红线)。
|
||||||
|
|
||||||
## Client 基线文档
|
## Client 基线文档
|
||||||
|
|
||||||
| 文档 | 用途 |
|
| 文档 | 用途 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [01 产品需求基线](client/01-requirements.md) | 定义目标、范围、业务流程和验收边界 |
|
| [00 上手指南](client/00-getting-started.md) | 装环境、连手机、跑起来、常见报错 |
|
||||||
| [02 系统架构](client/02-architecture.md) | 定义模块、依赖、线程模型和故障恢复原则 |
|
| [00 术语表](client/00-glossary.md) | Outbox、幂等、不可逆阶段等 |
|
||||||
| [03 数据模型](client/03-data-model.md) | 定义 SQLite 表、状态和 `pdd_data` JSON 结构 |
|
| [01 产品需求基线](client/01-requirements.md) | 目标、范围、任务类型、打包策略 |
|
||||||
| [04 Admin 接口契约](client/04-admin-api-contract.md) | 定义 Client 与 Admin 的版本化接口边界 |
|
| [02 系统架构](client/02-architecture.md) | 分层、线程模型、Worker 模板 |
|
||||||
| [05 界面交互规范](client/05-ui-specification.md) | 定义 PDD 任务页、设置页和表格交互 |
|
| [03 数据模型](client/03-data-model.md) | SQLite 表、状态机、`pdd_data` 结构 |
|
||||||
| [06 质量、安全与测试](client/06-quality-security.md) | 定义测试策略、采购安全、日志和发布门禁 |
|
| [04 Admin 接口契约](client/04-admin-api-contract.md) | **两个子项目的接口边界,改动需同步 Admin** |
|
||||||
|
| [05 界面交互规范](client/05-ui-specification.md) | PDD 任务页、设置页、表格交互 |
|
||||||
|
| [06 质量、安全与测试](client/06-quality-security.md) | 测试策略、采购安全、发布门禁 |
|
||||||
|
|
||||||
|
## Admin 基线文档
|
||||||
|
|
||||||
|
| 文档 | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| [00 上手指南](admin/00-getting-started.md) | 装 Go、跑起来、常见报错 |
|
||||||
|
| [00 术语表](admin/00-glossary.md) | 货运单、upsert、SKU 映射等 |
|
||||||
|
| [01 产品需求基线](admin/01-requirements.md) | 业务链路、四个模块、状态定义 |
|
||||||
|
| [02 系统架构](admin/02-architecture.md) | 分层、目录、模板组织、前端约束 |
|
||||||
|
| [03 数据模型](admin/03-data-model.md) | SQLite 表、Excel 导入规则 |
|
||||||
|
| [04 Client 接口实现](admin/04-client-api.md) | 服务端怎么实现那三个接口 |
|
||||||
|
| [05 界面规范](admin/05-ui-specification.md) | 三段式布局、四个页面、弹窗 |
|
||||||
|
| [06 质量、安全与测试](admin/06-quality-security.md) | 测试、Web 安全、发布门禁 |
|
||||||
|
|
||||||
## 文档标注说明
|
## 文档标注说明
|
||||||
|
|
||||||
@@ -51,20 +99,23 @@
|
|||||||
|
|
||||||
## 文档生命周期
|
## 文档生命周期
|
||||||
|
|
||||||
- `docs/client` 只记录不随单个任务频繁变化的产品和技术基线。
|
- `docs/client` 和 `docs/admin` 只记录不随单个任务频繁变化的产品和技术基线。
|
||||||
- 日常需求、缺陷、进度、阻塞和方案变更以 Gitea 工单为事实来源。
|
- 日常需求、缺陷、进度、阻塞和方案变更以 Gitea 工单为事实来源。
|
||||||
- 单元任务完成后,按 `AGENTS.md` 归档到 `docs/task/<工单号>-<简短名称>.md`。
|
- 单元任务完成后,按 `AGENTS.md` 归档到 `docs/task/<工单号>-<简短名称>.md`。
|
||||||
- 基线发生实质变化时,必须先更新对应 Gitea 工单并完成评审,再在同一任务中更新这里的相关文档。
|
- 基线发生实质变化时,必须先更新对应 Gitea 工单并完成评审,再在同一任务中更新这里的相关文档。
|
||||||
|
- **接口契约改动必须两边同步**:`docs/client/04-*` 和 `docs/admin/04-*` 在同一个工单里一起改。
|
||||||
|
|
||||||
## 文档和代码对不上怎么办
|
## 文档和代码对不上怎么办
|
||||||
|
|
||||||
现在的代码还没做到文档描述的目标状态,**对不上是正常的**,已知差异列在 [02 架构](client/02-architecture.md) §3.1。
|
现在的代码还没做到文档描述的目标状态,**对不上是正常的**。
|
||||||
|
Client 的已知差异列在 [02 架构](client/02-architecture.md) §3.1;
|
||||||
|
Admin 目前尚未开始编码。
|
||||||
|
|
||||||
按下面处理,不要一发现不一致就停工:
|
按下面处理,不要一发现不一致就停工:
|
||||||
|
|
||||||
| 情况 | 怎么办 |
|
| 情况 | 怎么办 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 差异已经列在 §3.1 差异清单里 | 按代码现状继续做,不用停 |
|
| 差异已经列在差异清单里 | 按代码现状继续做,不用停 |
|
||||||
| 差异不在清单里,但只影响写法、不影响业务结果 | 按文档做,并在工单里记一句 |
|
| 差异不在清单里,但只影响写法、不影响业务结果 | 按文档做,并在工单里记一句 |
|
||||||
| 差异会影响业务结果(金额、数量、状态、下单与否、数据结构) | **停下来**,在工单里说明,等用户确认哪边是对的 |
|
| 差异会影响业务结果(金额、数量、状态、下单与否、数据结构) | **停下来**,在工单里说明,等用户确认哪边是对的 |
|
||||||
|
|
||||||
@@ -72,4 +123,7 @@
|
|||||||
|
|
||||||
## 文档状态
|
## 文档状态
|
||||||
|
|
||||||
当前 Client 文档为 **基线草案**。Admin 尚未完成,真实采购的最终下单开关、认证方式和部分字段仍需评审确认;这些事项已在对应文档中标注为 `[待定]`,并给出了临时默认值。
|
Client 和 Admin 文档均为 **基线草案**。
|
||||||
|
|
||||||
|
Admin 尚未开始编码;顺运宝同步方式、认证方式和部分字段仍需确认,
|
||||||
|
这些事项已在对应文档中标注为 `[待定]`,并给出了临时默认值。
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# 00 Admin 上手指南
|
||||||
|
|
||||||
|
- 文档状态:基线草案
|
||||||
|
- 读者:第一次接手 Admin 的开发者
|
||||||
|
- 目标:照着做完,能在自己电脑上把 Admin 跑起来,并知道下一步读哪份文档
|
||||||
|
|
||||||
|
这份文档只讲"怎么把环境搭好、怎么跑起来"。业务规则不在这里,见 [文档索引](../README.md)。
|
||||||
|
遇到不认识的词(货运单、upsert、SKU 映射……),先查 [术语表](00-glossary.md)。
|
||||||
|
|
||||||
|
## 1. 准备电脑环境
|
||||||
|
|
||||||
|
| 需要什么 | 版本 | 怎么确认装好了 |
|
||||||
|
|---|---|---|
|
||||||
|
| Go | **1.23.0** | `go version` 输出 `go version go1.23.0 windows/amd64` |
|
||||||
|
| Git | 任意较新版本 | `git --version` 有输出 |
|
||||||
|
| DB Browser for SQLite | 任意 | 用来看数据库,可选但强烈建议装 |
|
||||||
|
|
||||||
|
版本已定为 **go1.23.0**,写在 `admin/go.mod` 里。低于这个版本可能编译不过。
|
||||||
|
|
||||||
|
**不需要装 gcc。** 本项目的 SQLite 驱动是纯 Go 实现的
|
||||||
|
(`modernc.org/sqlite`),不依赖 cgo,这也是选它的原因——
|
||||||
|
`go build` 直接出单个 exe,不用配 C 编译器。
|
||||||
|
|
||||||
|
## 2. 拉依赖
|
||||||
|
|
||||||
|
先配国内代理,否则大概率拉不动:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go env -w GOPROXY=https://goproxy.cn,direct
|
||||||
|
```
|
||||||
|
|
||||||
|
**第一次**需要把三个依赖装上(`go.mod` 里只有模块名和 Go 版本,依赖靠 `go get` 添加):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd D:\chengma\cmautobuy\admin
|
||||||
|
go get github.com/gin-gonic/gin
|
||||||
|
go get modernc.org/sqlite
|
||||||
|
go get github.com/xuri/excelize/v2
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
跑完 `go.mod` 会多出 `require` 段,还会生成 `go.sum`。
|
||||||
|
`[必须]` **这两个文件都要提交 Git**,否则别人拉下来版本对不上。
|
||||||
|
|
||||||
|
**之后**只需要:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
预期没有输出——Go 的惯例,成功时不打印东西。
|
||||||
|
|
||||||
|
> 三个依赖是定死的,见 [admin/AGENTS.md](../../admin/AGENTS.md) 技术栈一节。
|
||||||
|
> 特别注意 SQLite 驱动**不要**换成 `mattn/go-sqlite3`,那个需要 cgo。
|
||||||
|
|
||||||
|
## 3. 跑起来
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd D:\chengma\cmautobuy\admin
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
然后浏览器打开 <http://localhost:8080>。
|
||||||
|
|
||||||
|
**这个命令是安全的**:Admin 只管理数据,不会连手机、不会下单。放心随便跑。
|
||||||
|
|
||||||
|
改了代码要重启才生效(Go 不像 Python 那样改完就生效)。按 `Ctrl+C` 停掉再 `go run .`。
|
||||||
|
|
||||||
|
## 4. 你应该看到什么
|
||||||
|
|
||||||
|
左边(或顶部)是四个模块的导航:
|
||||||
|
|
||||||
|
| 模块 | 干什么的 |
|
||||||
|
|---|---|
|
||||||
|
| 蝦皮数据 | 导入蝦皮商品报表,填 PDD 链接,发起采集 |
|
||||||
|
| 顺运宝数据 | 同步货运单,匹配规格,生成采购任务 |
|
||||||
|
| 采购任务 | 看任务执行到哪一步了 |
|
||||||
|
| 客户端列表 | 看哪些客户端在干活 |
|
||||||
|
|
||||||
|
每个页面都是同一个结构:**上面工具条 / 中间表格 / 下面状态条**。
|
||||||
|
四个页面长得像是故意的,见 [05 界面规范](05-ui-specification.md)。
|
||||||
|
|
||||||
|
## 5. 常见报错
|
||||||
|
|
||||||
|
| 报错 | 原因 | 怎么办 |
|
||||||
|
|---|---|---|
|
||||||
|
| `go: command not found` | Go 没装或不在 PATH | 装 Go,重开 PowerShell |
|
||||||
|
| `dial tcp ... i/o timeout` | 拉不到模块 | 配 GOPROXY,见第 2 步 |
|
||||||
|
| `no required module provides package` | 依赖没装 | 回第 2 步跑那三条 `go get` |
|
||||||
|
| `listen tcp :8080: bind: ...` | 8080 端口被占了 | 换端口:`go run . -port 8081`,或关掉占用的程序 |
|
||||||
|
| 页面全是没样式的白底黑字 | 静态文件没加载到 | 确认是从 `admin/` 目录启动的,静态文件路径是相对的 |
|
||||||
|
| `database is locked` | 有别的程序占着数据库 | 关掉 DB Browser 再试;程序运行时只读打开是可以的 |
|
||||||
|
| 导入 Excel 报错但没说哪一行 | 解析器没带行号 | 这是 bug,报错必须带行号,见 [06](06-quality-security.md) §2 |
|
||||||
|
|
||||||
|
## 6. 数据库在哪、怎么看
|
||||||
|
|
||||||
|
跟 Client 一样是**便携模式**——数据都在程序旁边的 `data/` 里:
|
||||||
|
|
||||||
|
```text
|
||||||
|
admin/data/
|
||||||
|
├── admin.db 数据库
|
||||||
|
├── logs/ 运行日志
|
||||||
|
└── uploads/ 上传的 Excel 原件
|
||||||
|
```
|
||||||
|
|
||||||
|
跑源码时在 `admin\data\`;将来打包成 exe 后,在 exe 旁边。
|
||||||
|
|
||||||
|
用 **DB Browser for SQLite** 打开 `admin.db` 就能看数据。
|
||||||
|
每张表什么意思见 [03 数据模型](03-data-model.md)。
|
||||||
|
|
||||||
|
> 写代码时**不要自己拼这个路径**,用统一的 `DataDir()` 函数,
|
||||||
|
> 见 [02 架构](02-architecture.md) §6。
|
||||||
|
|
||||||
|
## 7. 拿样本数据试一下导入
|
||||||
|
|
||||||
|
仓库里有一份真实的蝦皮导出样本:
|
||||||
|
|
||||||
|
```text
|
||||||
|
raw_data/蝦皮数据样本.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
11287 行,其中 5195 行是商品汇总行、6092 行是 SKU 行。
|
||||||
|
导入后应该得到 **5195 条商品 + 6092 条 SKU**。数字对不上就是解析有问题。
|
||||||
|
|
||||||
|
解析规则见 [03 数据模型](03-data-model.md) §3.3,那里说明了为什么一个文件要拆成两张表。
|
||||||
|
|
||||||
|
## 8. 提交代码前的自检
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd D:\chengma\cmautobuy\admin
|
||||||
|
|
||||||
|
go vet ./... # 静态检查,有问题会打印出来
|
||||||
|
go build ./... # 能编译
|
||||||
|
go test ./... # 测试能过
|
||||||
|
```
|
||||||
|
|
||||||
|
三条都没报错就算通过。完整验证要求见 [admin/AGENTS.md](../../admin/AGENTS.md) §验证。
|
||||||
|
|
||||||
|
## 9. 接下来读什么
|
||||||
|
|
||||||
|
| 我要做的事 | 读这份 |
|
||||||
|
|---|---|
|
||||||
|
| 改页面、加表格列 | [05 界面规范](05-ui-specification.md) |
|
||||||
|
| 加字段、改表、写 SQL | [03 数据模型](03-data-model.md) |
|
||||||
|
| 改给 Client 的接口 | [04 Client 接口实现](04-client-api.md) + [Client 侧契约](../client/04-admin-api-contract.md) |
|
||||||
|
| 改 Excel 导入 | [03 数据模型](03-data-model.md) §3.3 |
|
||||||
|
| 搞不清这功能要不要做 | [01 产品需求基线](01-requirements.md) |
|
||||||
|
|
||||||
|
无论做哪一样,都必须先看一遍 [admin/AGENTS.md](../../admin/AGENTS.md)(技术栈和红线)。
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# 00 Admin 术语表
|
||||||
|
|
||||||
|
- 文档状态:基线草案
|
||||||
|
- 用途:Admin 文档里出现的专业词,在这里查一句话解释
|
||||||
|
|
||||||
|
跨 Client 和 Admin 共用的词(幂等、SKU、采集任务、采购任务、便携模式……)
|
||||||
|
在 [Client 术语表](../client/00-glossary.md) 里,本文不重复。
|
||||||
|
|
||||||
|
## 1. 业务名词
|
||||||
|
|
||||||
|
| 词 | 一句话解释 | 在本项目里指 |
|
||||||
|
|---|---|---|
|
||||||
|
| 蝦皮 / Shopee | 台湾的电商平台,**我们在上面卖货** | 订单的来源。导出的报表是繁体中文、金额是台币 |
|
||||||
|
| 顺运宝 / SYB | 物流服务商,提供**货运单** | 告诉我们"这单要发什么货",是采购的触发源 |
|
||||||
|
| 货运单 | 顺运宝那边的一条发货记录 | `syb_orders` 表的一行 |
|
||||||
|
| 拼多多 / PDD | 大陆的电商平台,**我们在上面进货** | 采购的目标平台 |
|
||||||
|
| 商品規格ID | 蝦皮给每个 SKU 的唯一编号 | `shopee_skus.sku_id`。实测 6092 条零重复,是天然主键 |
|
||||||
|
| 规格原文 | 蝦皮报表里没拆开的那一列,例如 `黑色,M【建議40-50公斤】` | `shopee_skus.spec_raw`,**永远原样保留** |
|
||||||
|
| 建议 | 蝦皮规格里的建议体重,例如 `40-50公斤` | 不是"建议采购链接",别理解错 |
|
||||||
|
| SKU 映射 | "蝦皮的这个规格 = 拼多多的那个规格"的对应关系 | `sku_mappings` 表。**匹配一次,以后同商品自动带出** |
|
||||||
|
| 采集状态 | 某个蝦皮商品对应的 PDD 商品数据采到没有 | `shopee_products.collect_status` |
|
||||||
|
|
||||||
|
## 2. 技术名词
|
||||||
|
|
||||||
|
| 词 | 一句话解释 | 在本项目里指 |
|
||||||
|
|---|---|---|
|
||||||
|
| Gin | Go 的一个 Web 框架,负责把 URL 路由到你的函数 | 唯一允许使用的 Web 框架 |
|
||||||
|
| `html/template` | Go 自带的 HTML 模板引擎,**会自动转义**,防 XSS | 所有页面都用它渲染,不引入前端框架 |
|
||||||
|
| 服务端渲染 | 页面的 HTML 在服务器上拼好再发给浏览器 | 与之相对的是前端框架在浏览器里拼,本项目**不用** |
|
||||||
|
| htmx | 一个单文件 JS 库,让 HTML 标签直接发请求换局部内容 | 需要局部刷新时可用,**没有构建步骤** |
|
||||||
|
| upsert | "有就更新、没有就新增",一次操作搞定 | Excel 导入的唯一正确做法,见 §3 |
|
||||||
|
| cgo | Go 调用 C 代码的机制。**用了就需要装 C 编译器** | 本项目**避开它**,所以 SQLite 驱动选纯 Go 的 |
|
||||||
|
| `modernc.org/sqlite` | 纯 Go 实现的 SQLite,不需要 cgo | 固定用它,`go build` 直接出 exe |
|
||||||
|
| excelize | Go 读写 Excel 的库 | 固定用它读蝦皮报表 |
|
||||||
|
| CSRF | 攻击者诱导你在已登录状态下发出非本意的请求 | 所有写操作都要防,见 [06](06-quality-security.md) §4 |
|
||||||
|
| 参数化查询 | SQL 里用 `?` 占位、值单独传,而不是拼字符串 | 防 SQL 注入的唯一正确做法 |
|
||||||
|
|
||||||
|
## 3. 为什么导入必须是 upsert
|
||||||
|
|
||||||
|
这条单独说,因为**做错了会丢数据**。
|
||||||
|
|
||||||
|
蝦皮报表可以反复导入(每次导出的都是"最近有成绩的商品",不是全量)。
|
||||||
|
而 **PDD 链接是人工一条条填上去的**,只存在我们自己的库里,报表里没有。
|
||||||
|
|
||||||
|
所以导入时:
|
||||||
|
|
||||||
|
| 做法 | 后果 |
|
||||||
|
|---|---|
|
||||||
|
| 先 `DELETE` 再 `INSERT` | **人工填的 PDD 链接、SKU 映射全没了** ✗ |
|
||||||
|
| 按主键 upsert | 报表里有的字段更新,人工填的字段原样保留 ✓ |
|
||||||
|
|
||||||
|
主键:商品用 `商品ID`,SKU 用 `商品規格ID`。
|
||||||
|
|
||||||
|
## 4. 蝦皮报表的两层结构
|
||||||
|
|
||||||
|
一个文件里混了两种行,别当成一种:
|
||||||
|
|
||||||
|
| 行类型 | 判断方法 | 条数(样本) | 导入到 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 商品汇总行 | `商品規格ID` 是 `-` | 5195 | `shopee_products` |
|
||||||
|
| SKU 行 | `商品規格ID` 是数字 | 6092 | `shopee_skus` |
|
||||||
|
|
||||||
|
平均每个商品只有 1.17 个 SKU —— 因为这份报表**只包含有销售成绩的 SKU,不是完整目录**。
|
||||||
|
所以订单来了查不到 SKU 是正常现象,界面必须支持**手动新增**,见 [01 需求](01-requirements.md) §4.1。
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
# 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 客户端列表模块
|
||||||
|
|
||||||
|
**顶部工具条:** 客户端名称搜索框、搜索按钮、删除按钮。
|
||||||
|
|
||||||
|
**中间表格:** 勾选、名称、序列号、状态、最近活动时间、更新时间。
|
||||||
|
|
||||||
|
**注册方式:**
|
||||||
|
|
||||||
|
- `[必须]` 客户端**调用领取接口时自动注册**:新序列号就新增,已有就更新。
|
||||||
|
**不设单独的注册或心跳接口**,理由见 [04 Client 接口实现](04-client-api.md) §3。
|
||||||
|
- `[必须]` `状态` 是**派生字段,不存库**:
|
||||||
|
最近活动时间在 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 采集状态(蝦皮商品)
|
||||||
|
|
||||||
|
| 值 | 中文 | 含义 |
|
||||||
|
|---|---|---|
|
||||||
|
| `no_link` | 未填链接 | 还没填 PDD 链接 |
|
||||||
|
| `pending` | 未采集 | 已填链接,还没发起采集 |
|
||||||
|
| `collecting` | 采集中 | 采集任务已创建,尚未回结果 |
|
||||||
|
| `collected` | 已采集 | `pdd_data` 已就绪 |
|
||||||
|
| `failed` | 采集失败 | Client 报告失败,可重新采集 |
|
||||||
|
|
||||||
|
`[必须]` `collecting` 状态的商品不允许再建采集任务,按钮置灰并提示"已在采集中"。
|
||||||
|
|
||||||
|
### 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**。
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
# 02 Admin 系统架构
|
||||||
|
|
||||||
|
- 文档状态:基线草案,待架构评审
|
||||||
|
- 适用范围:`admin/`
|
||||||
|
|
||||||
|
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。
|
||||||
|
没有标注的默认是 `[必须]`。看不懂的词查 [术语表](00-glossary.md)。
|
||||||
|
|
||||||
|
## 1. 架构目标
|
||||||
|
|
||||||
|
- 结构简单到初级程序员能独立加一个模块;
|
||||||
|
- 页面服务端渲染,不引入前端构建链;
|
||||||
|
- 业务逻辑和 Web 框架解耦,方便单测;
|
||||||
|
- 给 Client 的接口和给浏览器的页面互不干扰;
|
||||||
|
- `go build` 出单个 exe,不依赖 C 编译器。
|
||||||
|
|
||||||
|
## 2. 分层
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ handler/web 页面:解析表单 → 调 service │
|
||||||
|
│ → 渲染模板 │
|
||||||
|
│ handler/api 给 Client 的 JSON 接口 │
|
||||||
|
└───────────────────────┬──────────────────────┘
|
||||||
|
│ 只传普通结构体
|
||||||
|
┌───────────────────────▼──────────────────────┐
|
||||||
|
│ service 业务逻辑 │
|
||||||
|
│ 导入解析 / 建任务 / 匹配复用 │
|
||||||
|
└───────────────────────┬──────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────────▼──────────────────────┐
|
||||||
|
│ repository SQLite 读写 │
|
||||||
|
│ 只有这一层能写 SQL │
|
||||||
|
└───────────────────────┬──────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────────────────────▼──────────────────────┐
|
||||||
|
│ model 纯数据结构,不导入 Gin/驱动 │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- `[必须]` `handler` 不写业务逻辑、不拼 SQL。它只做三件事:取参数、调 service、渲染。
|
||||||
|
- `[必须]` `service` 不认识 `*gin.Context`。这样才能不起服务器就单测。
|
||||||
|
- `[必须]` 只有 `repository` 能写 SQL,且**一律参数化查询**。
|
||||||
|
- `[必须]` `handler/web` 和 `handler/api` **分开**:
|
||||||
|
页面出错要渲染错误页,接口出错要返回 JSON;页面要 CSRF,接口要认证。
|
||||||
|
混在一起迟早写错。
|
||||||
|
|
||||||
|
## 3. 目录
|
||||||
|
|
||||||
|
```text
|
||||||
|
admin/
|
||||||
|
├── main.go 启动、路由注册
|
||||||
|
├── go.mod
|
||||||
|
├── config/ 端口、路径、超时
|
||||||
|
├── handler/
|
||||||
|
│ ├── web/ 四个模块的页面
|
||||||
|
│ │ ├── shopee.go
|
||||||
|
│ │ ├── syb.go
|
||||||
|
│ │ ├── task.go
|
||||||
|
│ │ └── client.go
|
||||||
|
│ └── api/ 给 Client 的接口
|
||||||
|
│ └── client_api.go
|
||||||
|
├── service/
|
||||||
|
│ ├── shopee_import.go Excel 解析与 upsert
|
||||||
|
│ ├── collect.go 创建采集任务
|
||||||
|
│ ├── mapping.go SKU 映射复用
|
||||||
|
│ ├── purchase.go 创建采购任务与校验
|
||||||
|
│ └── clientreg.go 客户端注册与在线判断
|
||||||
|
├── repository/
|
||||||
|
│ ├── db.go 连接、迁移
|
||||||
|
│ ├── shopee.go
|
||||||
|
│ ├── syb.go
|
||||||
|
│ ├── task.go
|
||||||
|
│ └── client.go
|
||||||
|
├── model/ 纯结构体
|
||||||
|
├── templates/ 见 §4
|
||||||
|
├── static/
|
||||||
|
│ ├── css/
|
||||||
|
│ └── js/
|
||||||
|
├── testdata/ 测试用的小样本
|
||||||
|
└── data/ 运行时数据,见 §6(不进 Git)
|
||||||
|
```
|
||||||
|
|
||||||
|
`[建议]` 一个模块一个文件,文件名和模块对应。初级程序员找代码时不用猜。
|
||||||
|
|
||||||
|
## 4. 模板组织
|
||||||
|
|
||||||
|
```text
|
||||||
|
templates/
|
||||||
|
├── layout.html 整体骨架:导航 + 内容占位 + 状态条
|
||||||
|
├── shopee/
|
||||||
|
│ ├── list.html 表格页
|
||||||
|
│ └── edit_modal.html 编辑弹窗(含 PDD 链接和采集按钮)
|
||||||
|
├── syb/
|
||||||
|
│ ├── list.html
|
||||||
|
│ └── match_modal.html 规格匹配弹窗
|
||||||
|
├── task/list.html
|
||||||
|
├── client/list.html
|
||||||
|
└── partials/
|
||||||
|
├── toolbar.html 顶部工具条
|
||||||
|
├── table.html 带勾选的表格
|
||||||
|
└── statusbar.html 底部状态条
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` 三段式布局做成 `partials/` 里的公共片段,四个模块复用。
|
||||||
|
**不要每个页面复制一份**——改一次样式要改四个地方,必然改漏。
|
||||||
|
|
||||||
|
`[必须]` 模板输出走 `html/template` 的自动转义。
|
||||||
|
**禁止用 `template.HTML` 包裹用户可控的内容**(商品名、订单号都是外部来的)。
|
||||||
|
|
||||||
|
## 5. 前端约束
|
||||||
|
|
||||||
|
`[必须]` **不引入 React / Vue / npm / 打包器。**
|
||||||
|
|
||||||
|
| 功能 | 怎么实现 |
|
||||||
|
|---|---|
|
||||||
|
| 搜索、删除、导入 | 普通表单 POST,服务端渲染,**零 JS** |
|
||||||
|
| 全选 / 反选 | 原生 JS,十几行 |
|
||||||
|
| 双击行开弹窗 | 原生 JS 或 htmx |
|
||||||
|
| 导入进度 | MVP 先做成"提交后显示结果统计",不做实时进度条 |
|
||||||
|
|
||||||
|
`[必须]` 破坏性操作(删除、导入)用 **POST**,不得用 GET。
|
||||||
|
浏览器和爬虫会预取 GET 链接,用 GET 删数据迟早出事故。
|
||||||
|
|
||||||
|
## 6. 文件位置
|
||||||
|
|
||||||
|
跟 Client 一样是**便携模式**:程序自己产生的东西全在 `data/` 下。
|
||||||
|
|
||||||
|
```text
|
||||||
|
data/
|
||||||
|
├── admin.db SQLite 数据库
|
||||||
|
├── logs/ 运行日志
|
||||||
|
└── uploads/ 上传的 Excel 原件
|
||||||
|
```
|
||||||
|
|
||||||
|
| 情况 | `data/` 在哪 |
|
||||||
|
|---|---|
|
||||||
|
| 打包成 exe 后 | exe 旁边 |
|
||||||
|
| 直接 `go run .` | `admin/data/` |
|
||||||
|
|
||||||
|
`[必须]` 路径统一走一个函数,**代码里不许出现第二处拼路径的地方**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataDir 返回可写数据目录,不存在就创建。
|
||||||
|
// 打包后 = exe 旁边的 data/;go run 时 = 当前目录下的 data/。
|
||||||
|
func DataDir() (string, error) {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dir := filepath.Join(filepath.Dir(exe), "data")
|
||||||
|
|
||||||
|
// go run 会把程序编译到临时目录,那种情况下用当前工作目录
|
||||||
|
if isTempBuild(exe) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dir = filepath.Join(wd, "data")
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 别这么做 | 为什么 |
|
||||||
|
|---|---|
|
||||||
|
| 硬编码 `D:\...\data` | 换台机器就废了 |
|
||||||
|
| 直接用 `os.Getwd()` | 双击 exe 和命令行启动,当前目录不一样 |
|
||||||
|
| 往 exe 所在目录写日志但不建 `data/` | 打包后和程序文件混在一起,升级时会被删 |
|
||||||
|
|
||||||
|
`[必须]` 升级方式:换掉 exe,**`data/` 原样保留**。所以数据库必须支持迁移,见 [03](03-data-model.md) §2。
|
||||||
|
|
||||||
|
## 7. 请求流程示例
|
||||||
|
|
||||||
|
以"导入蝦皮 Excel"为例,看清各层职责:
|
||||||
|
|
||||||
|
```text
|
||||||
|
浏览器 POST /shopee/import (multipart 文件)
|
||||||
|
↓
|
||||||
|
handler/web/shopee.go
|
||||||
|
- 校验文件大小和扩展名
|
||||||
|
- 存到 data/uploads/
|
||||||
|
- 调 service.ImportShopeeExcel(path)
|
||||||
|
↓
|
||||||
|
service/shopee_import.go
|
||||||
|
- 用 excelize 逐行读
|
||||||
|
- 按 商品規格ID 是否为 "-" 分成商品行/SKU 行
|
||||||
|
- 解析规格原文 → 颜色/尺码/建议(失败留空)
|
||||||
|
- 调 repository 做 upsert
|
||||||
|
- 返回 {商品数, SKU数, 失败行号列表}
|
||||||
|
↓
|
||||||
|
repository/shopee.go
|
||||||
|
- INSERT ... ON CONFLICT(...) DO UPDATE
|
||||||
|
- 只更新报表来的字段,人工填的 pdd_goods_url 不动
|
||||||
|
↓
|
||||||
|
handler 渲染结果页:导入 5195 商品 / 6092 SKU,失败 0 行
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` 注意最后一步:**upsert 时不得覆盖人工维护的字段**。
|
||||||
|
这是导入逻辑里最容易写错、后果最严重的一处,见 [03](03-data-model.md) §3.3。
|
||||||
|
|
||||||
|
## 8. 与 Client 的边界
|
||||||
|
|
||||||
|
- Admin 是任务的创建者和分配者,Client 是执行者。
|
||||||
|
- Admin **不知道** Client 执行到哪一步(没有心跳,是有意的)。
|
||||||
|
- Client 提交结果时,Admin **无条件接受**,哪怕任务已取消或已重派。
|
||||||
|
- 详见 [04 Client 接口实现](04-client-api.md)。
|
||||||
|
|
||||||
|
## 9. 相关文档
|
||||||
|
|
||||||
|
- [上手指南](00-getting-started.md)
|
||||||
|
- [术语表](00-glossary.md)
|
||||||
|
- [产品需求基线](01-requirements.md)
|
||||||
|
- [数据模型](03-data-model.md)
|
||||||
|
- [Client 接口实现](04-client-api.md)
|
||||||
|
- [界面规范](05-ui-specification.md)
|
||||||
|
- [质量与安全](06-quality-security.md)
|
||||||
|
- [Client 侧接口契约](../client/04-admin-api-contract.md)(对接时以它为准)
|
||||||
@@ -0,0 +1,332 @@
|
|||||||
|
# 03 Admin 数据模型
|
||||||
|
|
||||||
|
- 文档状态:基线草案,待数据评审
|
||||||
|
- 数据库:SQLite(驱动 `modernc.org/sqlite`,纯 Go 免 cgo)
|
||||||
|
- 位置:`data/admin.db`,见 [02 架构](02-architecture.md) §6
|
||||||
|
|
||||||
|
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。
|
||||||
|
没有标注的默认是 `[必须]`。看不懂的词查 [术语表](00-glossary.md)。
|
||||||
|
|
||||||
|
## 1. 设计原则
|
||||||
|
|
||||||
|
- `[必须]` 金额一律整数,字段名带单位后缀(`_cent`),**禁止 float**。
|
||||||
|
- `[必须]` 台币和人民币**分开存、不互相换算覆盖**。
|
||||||
|
- `[必须]` 时间存带时区 ISO 8601 的 UTC 字符串,页面上转本地时区显示。
|
||||||
|
- `[必须]` 外部来的原始数据(规格原文、货运单 JSON、采集结果 JSON)**原样保留**,
|
||||||
|
规范化字段用于查询和显示。解析失败留空,不要猜。
|
||||||
|
- `[必须]` **人工维护的字段不得被导入覆盖**(详见 §3.3)。
|
||||||
|
- `[必须]` SQL 一律参数化查询。
|
||||||
|
|
||||||
|
## 2. 建库与迁移
|
||||||
|
|
||||||
|
每次打开连接至少设置:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
PRAGMA journal_mode = WAL;
|
||||||
|
PRAGMA busy_timeout = 5000;
|
||||||
|
```
|
||||||
|
|
||||||
|
用 `PRAGMA user_version` 管理顺序迁移。
|
||||||
|
|
||||||
|
`[必须]` 升级必须支持从所有已发布版本迁移,**不得在启动时删库重建**。
|
||||||
|
理由:`data/` 在升级时是保留的(见 [02](02-architecture.md) §6),
|
||||||
|
里面有人工填了几个月的 PDD 链接和 SKU 映射,删掉就没了。
|
||||||
|
|
||||||
|
## 3. 蝦皮数据
|
||||||
|
|
||||||
|
蝦皮报表**一个文件里混了两层数据**,所以拆成两张表。
|
||||||
|
|
||||||
|
### 3.1 `shopee_products` 商品级
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE shopee_products (
|
||||||
|
goods_id TEXT PRIMARY KEY, -- 蝦皮「商品ID」
|
||||||
|
title TEXT NOT NULL, -- 蝦皮「商品名稱」
|
||||||
|
shopee_status TEXT, -- 蝦皮「商品當前狀態」
|
||||||
|
main_sku_code TEXT, -- 蝦皮「主商品貨號」
|
||||||
|
|
||||||
|
-- 下面三个是我们自己维护的,报表里没有,导入时绝不能覆盖
|
||||||
|
pdd_goods_url TEXT, -- ★ 人工填写
|
||||||
|
pdd_goods_id TEXT, -- 从 url 解析出来
|
||||||
|
pdd_data TEXT, -- ★ 采集结果 JSON
|
||||||
|
|
||||||
|
collect_status TEXT NOT NULL DEFAULT 'no_link'
|
||||||
|
CHECK (collect_status IN (
|
||||||
|
'no_link', 'pending', 'collecting',
|
||||||
|
'collected', 'failed'
|
||||||
|
)),
|
||||||
|
collect_error TEXT,
|
||||||
|
collected_at TEXT,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shopee_products_status
|
||||||
|
ON shopee_products(collect_status);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `pdd_data` 存 Client 采回来的 PDD 商品数据,结构和
|
||||||
|
[Client 侧 `pdd_data`](../client/03-data-model.md) §8.1 一致,
|
||||||
|
里面有 `dimensions` 和 `skus`,匹配弹窗右侧就是从它渲染的。
|
||||||
|
- `[必须]` `pdd_data` 放在**商品级**,不是订单级。
|
||||||
|
一个 PDD 商品采一次,所有相关订单共用。
|
||||||
|
|
||||||
|
### 3.2 `shopee_skus` SKU 级
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE shopee_skus (
|
||||||
|
sku_id TEXT PRIMARY KEY, -- 蝦皮「商品規格ID」,实测零重复
|
||||||
|
goods_id TEXT NOT NULL,
|
||||||
|
spec_raw TEXT NOT NULL, -- ★ 规格原文,永远保留
|
||||||
|
color TEXT, -- 解析结果,失败留空
|
||||||
|
size TEXT,
|
||||||
|
advice TEXT, -- 建议体重,如「40-50公斤」
|
||||||
|
parse_ok INTEGER NOT NULL DEFAULT 0, -- 0=解析失败,界面上要标出来
|
||||||
|
sku_code TEXT, -- 蝦皮「商品選項貨號」
|
||||||
|
is_manual INTEGER NOT NULL DEFAULT 0, -- 1=人工新增的,导入不得删
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (goods_id) REFERENCES shopee_products(goods_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_shopee_skus_goods ON shopee_skus(goods_id);
|
||||||
|
CREATE INDEX idx_shopee_skus_parse ON shopee_skus(parse_ok);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Excel 导入规则
|
||||||
|
|
||||||
|
`[必须]` 这一节的每一条都要照做,写错会丢数据。
|
||||||
|
|
||||||
|
**第一步:分行**
|
||||||
|
|
||||||
|
| 行类型 | 判断方法 | 样本条数 | 导入到 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 商品汇总行 | `商品規格ID` 是 `-` 或空 | 5195 | `shopee_products` |
|
||||||
|
| SKU 行 | `商品規格ID` 是数字 | 6092 | `shopee_skus` |
|
||||||
|
|
||||||
|
导入 `raw_data/蝦皮数据样本.xlsx` 应得到 **5195 商品 + 6092 SKU**,数字对不上就是解析有问题。
|
||||||
|
|
||||||
|
**第二步:按列名找索引,不要写死列号**
|
||||||
|
|
||||||
|
报表有 40 列,蝦皮改一次导出格式列号就变。启动时按表头文字定位:
|
||||||
|
|
||||||
|
```go
|
||||||
|
idx := map[string]int{}
|
||||||
|
for i, name := range header {
|
||||||
|
idx[strings.TrimSpace(name)] = i
|
||||||
|
}
|
||||||
|
goodsID := row[idx["商品ID"]]
|
||||||
|
```
|
||||||
|
|
||||||
|
找不到必需列时**直接报错停止**,不要用默认值蒙混过去。
|
||||||
|
|
||||||
|
**第三步:解析规格原文**
|
||||||
|
|
||||||
|
`商品規格` 这一列格式**不统一**,实测两种各占一半:
|
||||||
|
|
||||||
|
| 格式 | 占比 | 样例 |
|
||||||
|
|---|---|---|
|
||||||
|
| 有【】 | 53.4% | `黑色,M【建議40-50公斤】` |
|
||||||
|
| 无括号、空格分隔 | 46.6% | `卡其色拼黑色,L 建議50-57.5kg` |
|
||||||
|
|
||||||
|
好消息:**逗号数恒为 1**(6092 条无例外),所以"颜色,尺码"这个二分结构是稳的。
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
1. 按第一个逗号切开 → 左边是颜色,右边是"尺码 + 可能的建议";
|
||||||
|
2. 右边尝试提取建议:先找 `【建議...】`,再找 ` 建議...`;
|
||||||
|
3. 剩下的就是尺码;
|
||||||
|
4. `[必须]` 任何一步失败都**不要猜**,把 `parse_ok` 置 0,颜色尺码留空,
|
||||||
|
`spec_raw` 照常保存。界面上把这些行标出来让人工补。
|
||||||
|
|
||||||
|
**第四步:upsert,绝不清空**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
INSERT INTO shopee_products (goods_id, title, shopee_status, main_sku_code,
|
||||||
|
created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(goods_id) DO UPDATE SET
|
||||||
|
title = excluded.title,
|
||||||
|
shopee_status = excluded.shopee_status,
|
||||||
|
main_sku_code = excluded.main_sku_code,
|
||||||
|
updated_at = excluded.updated_at;
|
||||||
|
-- 注意:pdd_goods_url / pdd_data / collect_status 一个都不在这里
|
||||||
|
```
|
||||||
|
|
||||||
|
| 别这么做 | 后果 |
|
||||||
|
|---|---|
|
||||||
|
| `DELETE FROM shopee_products` 再导入 | **人工填的 PDD 链接、采集结果全没了** |
|
||||||
|
| `DO UPDATE SET` 里写 `pdd_goods_url = excluded.pdd_goods_url` | 报表里没这列,会被更新成空 |
|
||||||
|
| 删掉报表里没出现的 SKU | 人工新增的(`is_manual=1`)会被误删 |
|
||||||
|
|
||||||
|
**第五步:返回统计**
|
||||||
|
|
||||||
|
导入结束返回 `{商品数, SKU数, 解析失败行号列表}`,页面上显示出来。
|
||||||
|
**不要静默跳过失败行。**
|
||||||
|
|
||||||
|
## 4. `syb_orders` 顺运宝货运单
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE syb_orders (
|
||||||
|
syb_id TEXT PRIMARY KEY, -- 货运单 ID
|
||||||
|
order_no TEXT NOT NULL, -- 订单号
|
||||||
|
title TEXT, -- 商品标题
|
||||||
|
shopee_goods_id TEXT, -- 蝦皮商品 ID
|
||||||
|
shopee_sku_id TEXT, -- 蝦皮规格 ID
|
||||||
|
quantity INTEGER NOT NULL CHECK (quantity > 0),
|
||||||
|
price_twd_cent INTEGER CHECK (price_twd_cent IS NULL OR price_twd_cent >= 0),
|
||||||
|
image_url TEXT, -- 存 URL,不存图片本身
|
||||||
|
syb_data TEXT NOT NULL DEFAULT '{}',-- 完整货运单 JSON,原样保留
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_syb_orders_order ON syb_orders(order_no);
|
||||||
|
CREATE INDEX idx_syb_orders_goods ON syb_orders(shopee_goods_id);
|
||||||
|
CREATE INDEX idx_syb_orders_list ON syb_orders(updated_at DESC, syb_id DESC);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `price_twd_cent` 是**台币分**,是蝦皮那边的售价,
|
||||||
|
和采购任务的人民币价格上限**没有换算关系**,不要互相赋值。
|
||||||
|
- `image_url` 存 URL。`[必须]` 不要把图片二进制存进 SQLite。
|
||||||
|
- `shopee_sku_id` **直接对应 `shopee_skus.sku_id`**,编号格式一致,不需要额外转换。
|
||||||
|
|
||||||
|
`[必须]` 但**不要加外键约束**。理由:蝦皮报表不是全量目录(样本里平均每商品仅 1.17 个 SKU),
|
||||||
|
货运单来了而本地查不到这个 SKU 是**常态**。加了外键,同步就会直接失败。
|
||||||
|
|
||||||
|
正确做法是软关联:查不到时照常保存货运单,界面上标出"SKU 未收录",
|
||||||
|
并引导操作员到蝦皮数据模块手动新增(见 [05 界面规范](05-ui-specification.md) §4.4)。
|
||||||
|
|
||||||
|
**"编号能对上"和"本地一定查得到"是两回事,别混。**
|
||||||
|
|
||||||
|
**"匹配状态"是派生的,不存字段**:`sku_mappings` 里有对应记录就是"已匹配"。
|
||||||
|
|
||||||
|
## 5. `sku_mappings` 规格映射
|
||||||
|
|
||||||
|
这张表是"蝦皮的这个规格 = 拼多多的那个规格",**匹配一次,以后复用**。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE sku_mappings (
|
||||||
|
shopee_sku_id TEXT PRIMARY KEY,
|
||||||
|
goods_id TEXT NOT NULL,
|
||||||
|
pdd_options TEXT NOT NULL, -- JSON: {"color":"黑色","size":"M码"}
|
||||||
|
mapped_at TEXT NOT NULL,
|
||||||
|
mapped_by TEXT,
|
||||||
|
FOREIGN KEY (shopee_sku_id) REFERENCES shopee_skus(sku_id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_sku_mappings_goods ON sku_mappings(goods_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `pdd_options` 用 JSON 而不是固定的"颜色/尺码"两列,
|
||||||
|
因为 PDD 商品可能有第三个规格维度
|
||||||
|
(Client 侧 [01](../client/01-requirements.md) §11 已明确要求按任意维度设计)。
|
||||||
|
- `[必须]` 打开匹配弹窗时**先查这张表**,有记录就自动带出,操作员只需确认。
|
||||||
|
这是省人工的关键,不要做成每张订单都从头匹配。
|
||||||
|
|
||||||
|
## 6. `tasks` 任务
|
||||||
|
|
||||||
|
采集任务和采购任务共用一张表,用 `task_type` 区分。
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE tasks (
|
||||||
|
task_id TEXT PRIMARY KEY, -- 给 Client 的稳定编号,如 PDD-20260806-0001
|
||||||
|
task_type TEXT NOT NULL CHECK (task_type IN ('collect', 'purchase')),
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending'
|
||||||
|
CHECK (status IN ('pending', 'assigned', 'claimed',
|
||||||
|
'succeeded', 'manual_review',
|
||||||
|
'failed', 'cancelled')),
|
||||||
|
version INTEGER NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||||
|
priority INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
assigned_client TEXT, -- 分配给哪个客户端
|
||||||
|
claimed_at TEXT,
|
||||||
|
|
||||||
|
-- 采购任务才有
|
||||||
|
syb_id TEXT,
|
||||||
|
order_no TEXT,
|
||||||
|
goods_id TEXT, -- 蝦皮商品 ID
|
||||||
|
shopee_sku_id TEXT,
|
||||||
|
|
||||||
|
-- 发给 Client 的执行参数
|
||||||
|
pdd_goods_url TEXT NOT NULL, -- ★ Client 契约要求必填
|
||||||
|
pdd_goods_id TEXT,
|
||||||
|
pdd_options TEXT, -- JSON,采购任务的目标规格
|
||||||
|
quantity INTEGER CHECK (quantity IS NULL OR quantity > 0),
|
||||||
|
max_price_cent INTEGER CHECK (max_price_cent IS NULL OR max_price_cent > 0),
|
||||||
|
|
||||||
|
-- 结果
|
||||||
|
result_data TEXT, -- Client 提交的 pdd_data
|
||||||
|
error_code TEXT,
|
||||||
|
error_message TEXT,
|
||||||
|
finished_at TEXT,
|
||||||
|
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_tasks_claim ON tasks(assigned_client, status, priority DESC, created_at);
|
||||||
|
CREATE INDEX idx_tasks_list ON tasks(updated_at DESC, task_id DESC);
|
||||||
|
CREATE INDEX idx_tasks_order ON tasks(order_no);
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` 两条硬约束,来自 [Client 契约](../client/04-admin-api-contract.md) §4:
|
||||||
|
|
||||||
|
1. **`pdd_goods_url` 不能为空**,否则 Client 无法执行(它那边是 `NOT NULL`)。
|
||||||
|
2. **采购任务的 `quantity` 和 `max_price_cent` 都必须有值**,
|
||||||
|
这是价格保护,没有它 Client 会拒绝执行。
|
||||||
|
|
||||||
|
`max_price_cent` 是**人民币分**,默认从 `pdd_data` 里对应 SKU 的价格带出,操作员可改但不能清空。
|
||||||
|
|
||||||
|
状态含义见 [01 需求](01-requirements.md) §6.2。
|
||||||
|
|
||||||
|
## 7. `clients` 客户端
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE clients (
|
||||||
|
client_id TEXT PRIMARY KEY, -- 序列号,来自 X-Client-Id
|
||||||
|
name TEXT, -- 客户端上报,可人工改
|
||||||
|
device_address TEXT,
|
||||||
|
platform TEXT,
|
||||||
|
pdd_package TEXT,
|
||||||
|
capabilities TEXT, -- claim 请求里的 capabilities 原文
|
||||||
|
last_seen_at TEXT NOT NULL, -- 每次调接口都刷新
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `[必须]` **没有 `status` 字段。** 在线状态是**算出来的**:
|
||||||
|
`last_seen_at` 在 N 分钟内算在线,否则离线。`[建议]` N 默认 10 分钟。
|
||||||
|
存成字段会和真实情况不同步。
|
||||||
|
- `[必须]` 注册发生在**领取任务时**,新序列号新增、已有的更新,
|
||||||
|
**不设单独的注册或心跳接口**,见 [04](04-client-api.md) §3。
|
||||||
|
|
||||||
|
## 8. 数据关系总览
|
||||||
|
|
||||||
|
```text
|
||||||
|
shopee_products ──1:N──→ shopee_skus
|
||||||
|
│ │
|
||||||
|
│ pdd_data │ 1:1
|
||||||
|
│ (采集结果) ↓
|
||||||
|
│ sku_mappings
|
||||||
|
│ ↑
|
||||||
|
│ │ 查映射
|
||||||
|
syb_orders ────────────────────┘
|
||||||
|
│
|
||||||
|
└──创建──→ tasks ──分配──→ clients
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. 与 Client 数据模型的关系
|
||||||
|
|
||||||
|
Admin 和 Client **各有一个 SQLite,互不相通**,只通过接口交换数据。
|
||||||
|
|
||||||
|
| 概念 | Admin 这边 | Client 那边 |
|
||||||
|
|---|---|---|
|
||||||
|
| 任务编号 | `tasks.task_id` | `pdd_tasks.remote_task_id` |
|
||||||
|
| 任务状态 | 7 个(§6) | 8 个,是本机执行状态 |
|
||||||
|
| 采集结果 | `shopee_products.pdd_data` | `pdd_tasks.pdd_data` |
|
||||||
|
|
||||||
|
`[必须]` **两边的状态是两套,不要试图同步。**
|
||||||
|
Admin 只知道"发出去了 / 收到结果了",中间过程看不到,这是有意的设计。
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# 04 Admin 侧的 Client 接口实现
|
||||||
|
|
||||||
|
- 文档状态:基线草案,待联合评审
|
||||||
|
- 适用范围:`admin/handler/api/`
|
||||||
|
|
||||||
|
> **权威契约在 Client 那边:**[docs/client/04-admin-api-contract.md](../client/04-admin-api-contract.md)。
|
||||||
|
> 本文只讲**Admin 这边怎么实现**。两边说法不一致时**以 Client 契约为准**,要改先走工单。
|
||||||
|
|
||||||
|
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。
|
||||||
|
|
||||||
|
## 1. 一共只有三个接口
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/client/tasks/claim 领一个任务
|
||||||
|
POST /api/v1/client/tasks/{task_id}/result 提交成功结果
|
||||||
|
POST /api/v1/client/tasks/{task_id}/failure 提交失败/需人工
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` **不得新增"让 Client 查询状态"类接口**,也不得加回租约和心跳。
|
||||||
|
理由见 Client 契约 §1.1:本项目人工付款,重复下单只产生重复的**未付款**订单,
|
||||||
|
不值得为它引入一整套中断逻辑。
|
||||||
|
|
||||||
|
## 2. 领取任务
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/client/tasks/claim
|
||||||
|
X-Client-Id: client-001
|
||||||
|
```
|
||||||
|
|
||||||
|
请求体里有设备信息和能力声明,还有客户端名称(见 §3)。
|
||||||
|
|
||||||
|
**处理步骤:**
|
||||||
|
|
||||||
|
1. 先做客户端注册/更新(§3);
|
||||||
|
2. 查 `tasks`:`assigned_client = X-Client-Id` 且 `status = 'assigned'`,
|
||||||
|
按 `priority DESC, created_at` 取**一条**;
|
||||||
|
3. 没有就返回 `204 No Content`;
|
||||||
|
4. 有就把 `status` 改成 `claimed`、记 `claimed_at`,返回任务。
|
||||||
|
|
||||||
|
`[必须]` 第 2 步只返回**分配给这个客户端**的任务。这是已定案的分配模型
|
||||||
|
(Client 契约 §5.1),不要做成谁都能抢。
|
||||||
|
|
||||||
|
`[必须]` 第 4 步的查询和更新要在**同一个事务**里,用条件更新防并发:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE tasks SET status = 'claimed', claimed_at = ?, updated_at = ?
|
||||||
|
WHERE task_id = ? AND status = 'assigned';
|
||||||
|
-- 检查影响行数,为 0 说明被别人抢先了,重新取下一条
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应体:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"task": {
|
||||||
|
"id": "PDD-20260806-0001",
|
||||||
|
"type": "purchase",
|
||||||
|
"version": 1,
|
||||||
|
"priority": 10,
|
||||||
|
"payload": {
|
||||||
|
"goods_id": "737116531267",
|
||||||
|
"goods_url": "https://mobile.yangkeduo.com/goods.html?goods_id=737116531267",
|
||||||
|
"options": { "color": "黑色", "size": "M码" },
|
||||||
|
"quantity": 2,
|
||||||
|
"max_price_cent": 4200
|
||||||
|
},
|
||||||
|
"created_at": "2026-08-06T07:00:00Z",
|
||||||
|
"updated_at": "2026-08-06T07:05:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` **响应里不含租约**,也不含 Admin 侧状态。Client 不关心这些。
|
||||||
|
|
||||||
|
`[必须]` `payload.goods_url` 必须有值;采购任务的 `quantity` 和 `max_price_cent` 必须有值。
|
||||||
|
这些在建任务时就该校验住(见 [01 需求](01-requirements.md) §5),
|
||||||
|
不要等到这里才发现发不出去。
|
||||||
|
|
||||||
|
## 3. 客户端注册就在领取里做
|
||||||
|
|
||||||
|
`[必须]` **不设单独的注册接口,也不设心跳接口。**
|
||||||
|
|
||||||
|
`claim` 请求里已经带齐了初始化需要的东西:
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-Client-Id: client-001 ← 序列号,主键
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client": { "name": "办公室-01" },
|
||||||
|
"supported_types": ["collect", "purchase"],
|
||||||
|
"device": { "address": "192.168.0.173:5555",
|
||||||
|
"platform": "android",
|
||||||
|
"pdd_package": "com.xunmeng.pinduoduo" },
|
||||||
|
"capabilities": { "purchase_mode": "dry_run", "schema_versions": [1] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> `[待定]` `client.name` 是对 Client 契约 §5 的一处小扩展,需在联合评审时确认。
|
||||||
|
> 未确认前 Admin 要容忍它缺失:没有就用 `X-Client-Id` 当显示名。
|
||||||
|
|
||||||
|
处理:
|
||||||
|
|
||||||
|
```text
|
||||||
|
upsert clients:
|
||||||
|
没有该 client_id → 新增,name 用上报的(没有就用 client_id)
|
||||||
|
已有 → 更新 device/capabilities/last_seen_at
|
||||||
|
name 若已被人工改过,不要覆盖
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` `result` 和 `failure` 接口**也要刷新 `last_seen_at`**。
|
||||||
|
否则客户端执行长任务期间不调 claim,会被误判成离线。
|
||||||
|
|
||||||
|
**新客户端第一次来必然拿不到任务**(还没人给它分配),返回 `204` 是正常的。
|
||||||
|
它这时已经登记进列表了,操作员就能给它分配任务。
|
||||||
|
|
||||||
|
## 4. 提交结果
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/client/tasks/{task_id}/result
|
||||||
|
Idempotency-Key: <task_id>:<attempt_id>:result-v1
|
||||||
|
```
|
||||||
|
|
||||||
|
处理:
|
||||||
|
|
||||||
|
1. 用 `Idempotency-Key` 查是否处理过 → 处理过就返回上次的结果,**不重复落库**;
|
||||||
|
2. 把 `pdd_data` 写进 `tasks.result_data`;
|
||||||
|
3. 采集任务:同时写进 `shopee_products.pdd_data`,`collect_status` 置 `collected`;
|
||||||
|
4. `tasks.status` 置 `succeeded`;
|
||||||
|
5. 刷新客户端 `last_seen_at`;
|
||||||
|
6. 返回 `{"accepted": true, "result_id": "...", "accepted_at": "..."}`。
|
||||||
|
|
||||||
|
第 2~4 步 `[必须]` 在**同一个事务**里。
|
||||||
|
|
||||||
|
### 4.1 无条件接受 —— 最容易写错的一条
|
||||||
|
|
||||||
|
`[必须]` 下面每一条都不允许违反:
|
||||||
|
|
||||||
|
- **不得**因为任务已取消而拒绝结果;
|
||||||
|
- **不得**因为任务已重派给别的客户端而拒绝结果;
|
||||||
|
- **必须**能接受同一任务来自**多个客户端**的多份结果;
|
||||||
|
- 只有**从未分配给该客户端**的任务才返回 `403`。
|
||||||
|
|
||||||
|
原因:Client 中途不查任务状态(契约 §1),所以它**必然**会提交一些
|
||||||
|
"Admin 这边已经不要了"的结果。而它可能真的已经下单了,这些数据必须能交上来留痕。
|
||||||
|
|
||||||
|
`accepted: true` 的意思是"**我收到并存下了**",不代表这个任务还算数。
|
||||||
|
任务算不算数由人工审核决定。
|
||||||
|
|
||||||
|
`[建议]` 已取消任务收到的结果,落库时标记出来,在界面上让操作员能看到"这单已取消但客户端还是买了"。
|
||||||
|
|
||||||
|
## 5. 提交失败
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/v1/client/tasks/{task_id}/failure
|
||||||
|
Idempotency-Key: <task_id>:<attempt_id>:failure-v1
|
||||||
|
```
|
||||||
|
|
||||||
|
`status` 只会是 `retry_wait`、`manual_review`、`failed`、`cancelled` 之一,按下表落地:
|
||||||
|
|
||||||
|
| Client 报告 | Admin 置为 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `retry_wait` | `assigned` | 放回去,等它再来领 |
|
||||||
|
| `manual_review` | `manual_review` | 等人处理 |
|
||||||
|
| `failed` | `failed` | |
|
||||||
|
| `cancelled` | `cancelled` | |
|
||||||
|
|
||||||
|
采集任务失败时,同步把 `shopee_products.collect_status` 置 `failed`,
|
||||||
|
并把错误信息写进 `collect_error`,界面上要看得见。
|
||||||
|
|
||||||
|
`[必须]` §4.1 的无条件接受**同样适用于本接口**。
|
||||||
|
|
||||||
|
## 6. 幂等怎么做
|
||||||
|
|
||||||
|
`[必须]` 建一张表记录处理过的键:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE idempotency_keys (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
request_hash TEXT NOT NULL, -- 请求体的哈希
|
||||||
|
response_body TEXT NOT NULL, -- 上次返回了什么
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- 键相同、哈希相同 → 直接返回 `response_body`,不再处理;
|
||||||
|
- 键相同、哈希不同 → 返回 `409 IDEMPOTENCY_CONFLICT`;
|
||||||
|
- 键不存在 → 正常处理,然后连同响应一起写入,**和业务写入在同一事务**。
|
||||||
|
|
||||||
|
不这么做的话,Client 网络超时重发就会产生两条结果。
|
||||||
|
|
||||||
|
## 7. 错误格式
|
||||||
|
|
||||||
|
页面出错渲染错误页,**接口出错返回 JSON**,两者不要混:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "IDEMPOTENCY_CONFLICT",
|
||||||
|
"message": "相同幂等键提交了不同内容",
|
||||||
|
"retryable": false,
|
||||||
|
"request_id": "7a5d...",
|
||||||
|
"details": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
状态码用法见 Client 契约 §3。注意 `403` 的含义:**只有从未分配给该客户端的任务**才返回它。
|
||||||
|
|
||||||
|
## 8. 认证
|
||||||
|
|
||||||
|
`[待定]` 认证方式尚未定案(Client 契约 §10 待确认 #1)。
|
||||||
|
|
||||||
|
临时默认:接口读 `Authorization: Bearer <token>` 请求头但**不校验**,
|
||||||
|
MVP 阶段只在内网/本机运行。
|
||||||
|
|
||||||
|
`[必须]` 无论最终怎么做,**token 不得写进日志**。
|
||||||
|
|
||||||
|
## 9. Admin 侧实现清单
|
||||||
|
|
||||||
|
Client 契约里散落的 Admin 侧硬要求,汇总在这里,**可以直接当验收标准用**:
|
||||||
|
|
||||||
|
- [ ] `claim` 一次只返回一个任务,且只返回分配给该客户端的
|
||||||
|
- [ ] `claim` 原子完成,并发下不会把同一任务发给两个客户端
|
||||||
|
- [ ] `claim` 无任务时返回 `204`,不是 `200` 加空对象
|
||||||
|
- [ ] 响应不含租约、不含 Admin 侧状态
|
||||||
|
- [ ] `payload.goods_url` 必有值
|
||||||
|
- [ ] 采购任务的 `quantity`、`max_price_cent` 必有值,且是人民币分整数
|
||||||
|
- [ ] 不向只声明 `dry_run` 的客户端分配需要真实下单的任务
|
||||||
|
- [ ] `result` / `failure` 幂等:同键同内容返回同结果
|
||||||
|
- [ ] 同键不同内容返回 `409`
|
||||||
|
- [ ] **任务已取消,仍接受结果**
|
||||||
|
- [ ] **任务已重派,仍接受结果**
|
||||||
|
- [ ] **同一任务接受多个客户端的多份结果**
|
||||||
|
- [ ] 只有从未分配过的任务才返回 `403`
|
||||||
|
- [ ] `claim` / `result` / `failure` 都刷新 `last_seen_at`
|
||||||
|
- [ ] token 不进日志
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# 05 Admin 界面规范
|
||||||
|
|
||||||
|
- 文档状态:基线草案,待界面评审
|
||||||
|
- 技术栈:Go `html/template` 服务端渲染 + 手写 CSS + 原生 JS / htmx
|
||||||
|
|
||||||
|
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。
|
||||||
|
没有标注的默认是 `[必须]`。看不懂的词查 [术语表](00-glossary.md)。
|
||||||
|
|
||||||
|
## 1. 设计目标
|
||||||
|
|
||||||
|
- 四个模块**长得一样**,写完第一个,其余三个照抄结构改字段;
|
||||||
|
- 操作员一眼看出"这条数据卡在哪一步、下一步该点什么";
|
||||||
|
- 不引入前端框架和构建流程;
|
||||||
|
- 破坏性操作(删除、导入)必须有确认,不能手滑。
|
||||||
|
|
||||||
|
## 2. 统一的三段式布局
|
||||||
|
|
||||||
|
`[必须]` 四个模块全部用这个结构,**不要给某个模块搞特殊**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ [导航] 蝦皮数据 │ 顺运宝数据 │ 采购任务 │ 客户端列表 │
|
||||||
|
├────────────────────────────────────────────────────────────┤
|
||||||
|
│ 顶部工具条:[操作按钮…] [搜索框] [搜索] [删除] │
|
||||||
|
├────────────────────────────────────────────────────────────┤
|
||||||
|
│ ☐ │ 列1 │ 列2 │ 列3 │ … │
|
||||||
|
│ ☐ │ │ │ │ │
|
||||||
|
│ 数据表格(占满剩余高度) │
|
||||||
|
│ │
|
||||||
|
├────────────────────────────────────────────────────────────┤
|
||||||
|
│ 底部状态条:最近一次操作的结果和关键统计 │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` 三段做成 `templates/partials/` 里的公共片段复用,
|
||||||
|
**不要每个页面复制一份**——改一次样式要改四处,必然改漏。
|
||||||
|
|
||||||
|
`[必须]` 页面在 1366×768 上可用。列多了让表格**横向滚动**,不要压缩列宽把字挤成两行。
|
||||||
|
|
||||||
|
## 3. 表格通用规则
|
||||||
|
|
||||||
|
- `[必须]` 第一列是勾选框,表头有全选。
|
||||||
|
- `[必须]` 行的身份用**业务主键**(商品規格ID、货运单ID、任务编号、序列号),
|
||||||
|
**不得用行号**——排序和筛选一变行号就错位。
|
||||||
|
- `[必须]` 批量删除要二次确认,弹窗里写清"**将删除 N 条,不可恢复**"。
|
||||||
|
- `[必须]` 删除、导入用 **POST**,不得用 GET。浏览器和插件会预取 GET 链接。
|
||||||
|
- `[建议]` 默认按 `更新时间 DESC` 排序。
|
||||||
|
- `[建议]` 一页 50 条,超过分页。搜索走数据库,不要一次查出来在内存里过滤。
|
||||||
|
- `[必须]` 长文本(商品标题)截断显示,鼠标悬停给完整内容。
|
||||||
|
- `[必须]` 空状态要分情况,文案不能都是"暂无数据":
|
||||||
|
- 从没导入过 → "还没有数据,点左上角『导入』开始"
|
||||||
|
- 搜索无结果 → "没有匹配的记录" + 清除条件入口
|
||||||
|
|
||||||
|
## 4. 蝦皮数据页
|
||||||
|
|
||||||
|
### 4.1 工具条
|
||||||
|
|
||||||
|
```text
|
||||||
|
[导入 Excel] [批量采集] 商品ID [________] [搜索] [删除]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **导入 Excel**:选文件 → POST 上传 → 显示结果统计
|
||||||
|
(导入 5195 商品 / 6092 SKU,失败 N 行)。
|
||||||
|
`[必须]` 失败行要列出行号和原因,不能静默跳过。
|
||||||
|
- **批量采集**:勾选若干行 → 服务端**按商品去重**后建采集任务。
|
||||||
|
`[必须]` 已是"采集中"的商品跳过,并在结果里说明跳过了几个。
|
||||||
|
|
||||||
|
### 4.2 表格列
|
||||||
|
|
||||||
|
| 列 | 显示规则 |
|
||||||
|
|---|---|
|
||||||
|
| ☐ | 勾选 |
|
||||||
|
| 商品 ID | |
|
||||||
|
| 商品名称 | 截断 + 悬停完整 |
|
||||||
|
| 颜色 / 尺码 / 建议 | 解析失败的显示为 `—` 并**整行标黄**,提示需人工补 |
|
||||||
|
| PDD 链接 | 空的显示"**未填写**"并标红,这是最需要操作员注意的状态 |
|
||||||
|
| 采集状态 | 未填链接 / 未采集 / 采集中 / 已采集 / 采集失败 |
|
||||||
|
| 更新时间 | 本地时区 |
|
||||||
|
|
||||||
|
`[必须]` 状态不能只靠颜色区分,必须有文字。
|
||||||
|
|
||||||
|
### 4.3 编辑弹窗(双击行打开)
|
||||||
|
|
||||||
|
这是 **PDD 链接唯一的录入口**,也是采集的唯一发起点。
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 编辑商品 40653075144 │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ 商品名称 【台灣現貨隔日達】西裝外套… │ ← 只读
|
||||||
|
│ 规格原文 黑色,M【建議40-50公斤】 │ ← 只读,永远显示
|
||||||
|
│ │
|
||||||
|
│ 颜色 [黑色___________] │
|
||||||
|
│ 尺码 [M_______________] │
|
||||||
|
│ 建议 [40-50公斤_______] │
|
||||||
|
│ │
|
||||||
|
│ PDD 链接 [___________________] [采集] │ ← ★
|
||||||
|
│ 采集状态 已采集(2026-08-06 15:20) │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ [取消] [保存] │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` 几条硬规则:
|
||||||
|
|
||||||
|
- **采集按钮只在这里出现**,不要放到表格每一行。一个商品有 N 个 SKU 行,
|
||||||
|
放行上就是 N 个按钮干同一件事,还会建出 N 个重复任务。
|
||||||
|
- **规格原文只读且永远显示**。操作员靠它判断颜色尺码该怎么填。
|
||||||
|
- PDD 链接为空时**采集按钮置灰**,旁边提示"请先填写 PDD 链接"。
|
||||||
|
- 采集状态是"采集中"时按钮置灰,提示"已在采集中"。
|
||||||
|
- 采集失败时显示错误原因,按钮变成"重新采集"。
|
||||||
|
|
||||||
|
### 4.4 手动新增
|
||||||
|
|
||||||
|
`[必须]` 必须提供"新增 SKU"入口。
|
||||||
|
|
||||||
|
原因:蝦皮报表只包含**有销售成绩的** SKU(样本里平均每个商品仅 1.17 个),
|
||||||
|
订单来了查无此 SKU 是常态。没有这个入口整条链路就断了。
|
||||||
|
|
||||||
|
新增的行 `is_manual = 1`,`[必须]` 后续导入**不得删除**它们。
|
||||||
|
|
||||||
|
## 5. 顺运宝数据页
|
||||||
|
|
||||||
|
### 5.1 工具条
|
||||||
|
|
||||||
|
```text
|
||||||
|
[同步] [创建采购任务] 订单号 [________] [搜索] [删除]
|
||||||
|
```
|
||||||
|
|
||||||
|
`[待定]` MVP 阶段**同步按钮只做占位**:点击提示"同步功能待接入",不发请求。
|
||||||
|
|
||||||
|
### 5.2 表格列
|
||||||
|
|
||||||
|
☐ / 货运单ID / 订单号 / 商品标题 / 蝦皮商品ID / 规格SKU / 数量 /
|
||||||
|
价格(台币)/ 图片 / **匹配状态** / 更新时间
|
||||||
|
|
||||||
|
- 图片显示小缩略图,点击看大图。`[必须]` 存 URL,不要把图片塞进数据库。
|
||||||
|
- 匹配状态是**算出来的**(`sku_mappings` 里有没有记录),不是存的字段。
|
||||||
|
- 完整货运单 JSON 不作为列显示,在详情里看。
|
||||||
|
|
||||||
|
### 5.3 规格匹配弹窗(双击行打开)
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ 规格匹配 · 订单 SO-20260806-001 │
|
||||||
|
├────────────────────────┬─────────────────────────┤
|
||||||
|
│ 蝦皮 │ 拼多多 │
|
||||||
|
│ │ │
|
||||||
|
│ 颜色 黑色 │ 颜色 [黑色 ▾] │
|
||||||
|
│ 尺码 M │ 尺码 [M码 ▾] │
|
||||||
|
│ 建议 40-50公斤 │ │
|
||||||
|
│ 原文 黑色,M【建議40…】 │ 单价 ¥39.90 │
|
||||||
|
├────────────────────────┴─────────────────────────┤
|
||||||
|
│ ⓘ 已有匹配记录,已自动带出,确认无误后保存 │
|
||||||
|
│ [取消] [保存] │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` 几条硬规则:
|
||||||
|
|
||||||
|
- 右侧下拉的选项来自该商品 `pdd_data` 里的 `dimensions`,
|
||||||
|
**不要写死"颜色/尺码"两个维度**——PDD 商品可能有第三个维度。
|
||||||
|
- **打开时先查 `sku_mappings`**,有记录就自动带出并提示"已自动带出"。
|
||||||
|
这是省人工的关键:同一个蝦皮 SKU 只需人工匹配一次。
|
||||||
|
- 该商品**还没采集**(`pdd_data` 为空)时,不要显示一个空下拉让人困惑,
|
||||||
|
直接提示:"该商品尚未采集 PDD 数据,请先到『蝦皮数据』填写链接并采集",
|
||||||
|
并给一个跳转链接。
|
||||||
|
- 保存写的是 `sku_mappings`(可复用),**不是这一张订单的临时数据**。
|
||||||
|
|
||||||
|
### 5.4 创建采购任务
|
||||||
|
|
||||||
|
勾选若干行 → 点按钮 → 逐条校验(见 [01 需求](01-requirements.md) §5)。
|
||||||
|
|
||||||
|
`[必须]` 校验不过的**不要静默跳过**,要列出来告诉操作员缺什么:
|
||||||
|
|
||||||
|
```text
|
||||||
|
成功创建 3 个任务。以下 2 条未创建:
|
||||||
|
· SO-...-007 该商品未填写 PDD 链接
|
||||||
|
· SO-...-009 规格未匹配
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` 创建前弹出确认框,让操作员选**分配给哪个客户端**,并确认价格上限。
|
||||||
|
价格上限默认从 `pdd_data` 带出,可改,**不允许为空**。
|
||||||
|
|
||||||
|
## 6. 采购任务页
|
||||||
|
|
||||||
|
工具条:`订单号 [____] [搜索] [删除]`
|
||||||
|
|
||||||
|
表格列:☐ / 订单号 / 商品标题 / 颜色 / 尺码 / 数量 / 价格上限 /
|
||||||
|
蝦皮ID / **分配客户端** / 状态 / 更新时间
|
||||||
|
|
||||||
|
- `[必须]` 颜色尺码显示的是 **PDD 侧**的规格(实际要买的),不是蝦皮的。
|
||||||
|
- `[必须]` 价格上限显示成 `¥42.00`(分转元),底层存的是整数分。
|
||||||
|
- `[建议]` 提供"改派"操作,把任务分给别的客户端——
|
||||||
|
没有心跳,客户端挂了要靠人工改派。
|
||||||
|
- 状态含义见 [01 需求](01-requirements.md) §6.2。
|
||||||
|
|
||||||
|
底部状态条显示各状态的条数统计。
|
||||||
|
|
||||||
|
## 7. 客户端列表页
|
||||||
|
|
||||||
|
工具条:`名称 [____] [搜索] [删除]`
|
||||||
|
|
||||||
|
表格列:☐ / 名称 / 序列号 / 状态 / 最近活动 / 更新时间
|
||||||
|
|
||||||
|
- `[必须]` **状态是算出来的**:`最近活动` 在 N 分钟内为"在线",否则"离线"。
|
||||||
|
`[建议]` N 默认 10 分钟。
|
||||||
|
- `[必须]` 名称可以人工改成好记的("办公室-01")。改过之后
|
||||||
|
**客户端上报的名称不再覆盖它**。
|
||||||
|
- `[建议]` 界面上说明一句:"客户端执行长任务期间可能显示为离线,属正常现象。"
|
||||||
|
因为没有心跳,这是已知且接受的取舍(见 [04](04-client-api.md) §3)。
|
||||||
|
|
||||||
|
## 8. 反馈方式
|
||||||
|
|
||||||
|
| 场景 | 怎么反馈 |
|
||||||
|
|---|---|
|
||||||
|
| 搜索、翻页 | 直接刷新表格,不弹任何东西 |
|
||||||
|
| 导入完成 | 底部状态条 + 结果统计(成功 N / 失败 M + 失败行号) |
|
||||||
|
| 保存成功 | 关闭弹窗,刷新行,状态条提示一句 |
|
||||||
|
| 校验不通过 | 保留用户输入,**焦点移到第一个错误字段**,就近显示错误 |
|
||||||
|
| 批量操作部分失败 | 列出失败项和原因,不要只说"部分失败" |
|
||||||
|
| 删除 | 二次确认框,写明"将删除 N 条,不可恢复" |
|
||||||
|
|
||||||
|
`[必须]` 报错要说清**哪一步失败、下一步做什么**,不要把 Go 的错误堆栈贴到页面上。
|
||||||
|
堆栈写日志。
|
||||||
|
|
||||||
|
## 9. 可访问性与细节
|
||||||
|
|
||||||
|
- `[必须]` 表单控件有可见 `<label>`,占位符不能当标签用。
|
||||||
|
- `[必须]` 状态不能只靠颜色,必须有文字。
|
||||||
|
- `[建议]` 搜索框支持回车提交。
|
||||||
|
- `[建议]` 主要操作支持键盘 Tab 到达。
|
||||||
|
- `[必须]` 金额显示带币种符号,台币和人民币要能一眼分清(`NT$` / `¥`)。
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
# 06 Admin 质量、安全与测试基线
|
||||||
|
|
||||||
|
- 文档状态:基线草案,待质量评审
|
||||||
|
- 适用范围:Admin 源码、模板、数据库和发布产物
|
||||||
|
|
||||||
|
本文档中 `[必须]` / `[建议]` / `[待定]` 的含义见 [文档索引](../README.md#文档标注说明)。
|
||||||
|
没有标注的默认是 `[必须]`。看不懂的词查 [术语表](00-glossary.md)。
|
||||||
|
|
||||||
|
## 1. 质量目标
|
||||||
|
|
||||||
|
- 导入不丢人工维护的数据(PDD 链接、SKU 映射、手动新增的行)。
|
||||||
|
- 不把校验不过的任务发给 Client——发出去 Client 也执行不了。
|
||||||
|
- 同一任务不会被两个客户端同时领走。
|
||||||
|
- Client 提交的结果一定收得下,哪怕任务已取消。
|
||||||
|
- 页面和日志不泄露 token、密码、Cookie。
|
||||||
|
|
||||||
|
## 2. 测试分层
|
||||||
|
|
||||||
|
### 2.1 单元测试
|
||||||
|
|
||||||
|
`[必须]` 覆盖:
|
||||||
|
|
||||||
|
- **规格原文解析**:两种格式各占一半,两种都要有用例,
|
||||||
|
再加解析失败的用例(确认 `parse_ok=0` 且不瞎猜);
|
||||||
|
- **Excel 导入分行**:商品汇总行 vs SKU 行的判断;
|
||||||
|
- **upsert 不覆盖人工字段**:先填 PDD 链接,再导入一次,断言链接还在;
|
||||||
|
- 任务创建校验(六条规则逐条);
|
||||||
|
- 金额换算显示(分 ↔ 元),边界值 0 和大额;
|
||||||
|
- SKU 映射复用:第二次匹配同一 SKU 应自动带出;
|
||||||
|
- 在线状态派生:`last_seen_at` 刚好在边界前后。
|
||||||
|
|
||||||
|
`[必须]` 导入相关的测试用 `testdata/` 下的**小样本**(几十行),
|
||||||
|
不要每次跑测试都读 1.9MB 的完整报表。
|
||||||
|
|
||||||
|
### 2.2 集成测试
|
||||||
|
|
||||||
|
用临时 SQLite 覆盖:
|
||||||
|
|
||||||
|
- 完整导入 → 建采集任务 → 模拟 Client 提交结果 → `collect_status` 变 `collected`;
|
||||||
|
- 并发 `claim`:两个客户端同时领,只有一个拿到;
|
||||||
|
- 幂等:同键同内容重复提交,只落库一次;
|
||||||
|
- 同键不同内容 → `409`;
|
||||||
|
- **任务已取消,Client 提交结果仍被接受**;
|
||||||
|
- **任务已重派给别人,原客户端提交仍被接受**;
|
||||||
|
- 同一任务收到两个客户端的两份结果,都存下来;
|
||||||
|
- 数据库从上一版本迁移,人工数据不丢。
|
||||||
|
|
||||||
|
中间三条是 [04](04-client-api.md) §4.1 那条最容易写错的规则,**必须有测试盯着**。
|
||||||
|
|
||||||
|
### 2.3 页面测试
|
||||||
|
|
||||||
|
- `go vet ./...` 无告警;
|
||||||
|
- 所有模板能正常渲染(起服务跑一遍四个页面,断言 200);
|
||||||
|
- 空数据、少量数据、大量数据三种情况;
|
||||||
|
- 批量删除的二次确认存在;
|
||||||
|
- 校验失败时输入不丢。
|
||||||
|
|
||||||
|
### 2.4 契约测试
|
||||||
|
|
||||||
|
`[必须]` 拿 [04 §9 的实现清单](04-client-api.md) 逐条写测试。
|
||||||
|
那张清单本身就是验收标准,可以直接照着做。
|
||||||
|
|
||||||
|
## 3. 数据安全
|
||||||
|
|
||||||
|
`[必须]` 下面几条错一条就会丢数据:
|
||||||
|
|
||||||
|
| 规则 | 为什么 |
|
||||||
|
|---|---|
|
||||||
|
| 导入只能 upsert,**禁止先清空再导入** | 人工填了几个月的 PDD 链接会被洗掉 |
|
||||||
|
| upsert 的 `DO UPDATE SET` 里**不得出现** `pdd_goods_url` / `pdd_data` / `collect_status` | 报表里没这些列,写进去会被更新成空 |
|
||||||
|
| **不得删除报表里没出现的行** | 手动新增的(`is_manual=1`)会被误删;报表本身就不是全量 |
|
||||||
|
| 迁移前备份或用可回滚步骤 | `data/` 在升级时保留,迁移失败会毁掉全部历史 |
|
||||||
|
| **不得在启动时删库重建** | 同上 |
|
||||||
|
|
||||||
|
`[建议]` 导入前自动把 `admin.db` 复制一份到 `data/backup/`,保留最近几份。
|
||||||
|
|
||||||
|
## 4. Web 安全
|
||||||
|
|
||||||
|
- `[必须]` 所有写操作(新增/编辑/删除/导入/建任务)加 **CSRF 防护**。
|
||||||
|
- `[必须]` 破坏性操作用 **POST**,不得用 GET。
|
||||||
|
- `[必须]` SQL **一律参数化查询**,禁止字符串拼接。
|
||||||
|
搜索框的内容是用户可控的,拼进 SQL 就是注入。
|
||||||
|
- `[必须]` 模板输出走 `html/template` 的自动转义。
|
||||||
|
**禁止用 `template.HTML` 包裹用户可控内容**——商品名、订单号都来自外部。
|
||||||
|
- `[必须]` 上传的 Excel 限制**大小和扩展名**,解析失败要返回明确错误,
|
||||||
|
不能 panic 把整个进程带崩。
|
||||||
|
- `[必须]` 文件名不得直接用于拼路径(路径穿越),落盘时用自己生成的名字。
|
||||||
|
- `[建议]` MVP 只监听 `127.0.0.1`,不对外暴露。要给内网用再单独评估。
|
||||||
|
|
||||||
|
## 5. 错误处理
|
||||||
|
|
||||||
|
`[必须]` 页面出错渲染错误页,接口出错返回 JSON,**两者不要混**。
|
||||||
|
|
||||||
|
`[必须]` 错误信息要说清三件事:**发生了什么、保住了什么、下一步做什么**。
|
||||||
|
|
||||||
|
```text
|
||||||
|
好:导入失败:第 128 行「商品規格ID」为空且不是汇总行。
|
||||||
|
前 127 行已成功导入,请修正后重新导入。
|
||||||
|
差:导入失败
|
||||||
|
差:runtime error: index out of range [40] with length 39
|
||||||
|
```
|
||||||
|
|
||||||
|
`[必须]` Go 的错误堆栈只写日志,不上页面。
|
||||||
|
|
||||||
|
`[建议]` 用稳定错误码,方便排查:
|
||||||
|
|
||||||
|
| 前缀 | 场景 |
|
||||||
|
|---|---|
|
||||||
|
| `IMPORT_*` | Excel 格式、列缺失、行解析失败 |
|
||||||
|
| `TASK_*` | 建任务校验不通过 |
|
||||||
|
| `CLIENT_*` | 客户端注册、领取冲突 |
|
||||||
|
| `DB_*` | 迁移、写入、锁超时 |
|
||||||
|
|
||||||
|
## 6. 日志
|
||||||
|
|
||||||
|
`[必须]` 日志写 `data/logs/`,至少包含时间、级别、模块、事件名、关键 ID。
|
||||||
|
|
||||||
|
`[建议]` 记录这些事件:
|
||||||
|
|
||||||
|
- `shopee_import_started/completed/failed`(带条数统计)
|
||||||
|
- `collect_task_created`
|
||||||
|
- `purchase_task_created`
|
||||||
|
- `task_claimed`(带 client_id、task_id)
|
||||||
|
- `task_result_received`
|
||||||
|
- `task_result_received_but_cancelled` ← 这条要单独记,方便事后对账
|
||||||
|
- `client_registered`
|
||||||
|
|
||||||
|
`[必须]` **不得记录** token、密码、Cookie,也不要把完整请求体无脑打进日志。
|
||||||
|
|
||||||
|
## 7. 性能
|
||||||
|
|
||||||
|
规模很小(个位数操作员、单机运行),不要提前优化。但下面几条是基本功:
|
||||||
|
|
||||||
|
- `[必须]` 搜索、排序、分页走**数据库查询**,不要一次查全量再在内存里过滤。
|
||||||
|
- `[必须]` 常用查询有索引(见 [03 数据模型](03-data-model.md) 里的 `CREATE INDEX`)。
|
||||||
|
- `[必须]` 导入 1 万行要在一个事务里批量写,不要一行一个事务。
|
||||||
|
- `[建议]` 导入时不要把整个文件读进内存,用 excelize 的流式行读取。
|
||||||
|
|
||||||
|
## 8. 发布门禁
|
||||||
|
|
||||||
|
| # | 门禁项 | 怎么验证 | 谁负责 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | 依赖版本已固定 | `go.mod` / `go.sum` 已提交,`go mod verify` 通过 | 开发者 |
|
||||||
|
| 2 | `go vet ./...` 无告警 | | 开发者 |
|
||||||
|
| 3 | `go test ./...` 全绿 | 不允许有跳过而未说明的用例 | 开发者 |
|
||||||
|
| 4 | 四个页面能正常打开 | 起服务跑一遍 | 开发者 |
|
||||||
|
| 5 | 样本导入条数正确 | 导 `raw_data/蝦皮数据样本.xlsx`,应得 5195 商品 + 6092 SKU | 开发者 |
|
||||||
|
| 6 | 数据库从上一版本迁移成功 | 拿旧 `admin.db` 副本启动新版本,人工数据不丢 | 开发者 |
|
||||||
|
| 7 | 契约测试通过 | [04 §9 清单](04-client-api.md)逐条 | 开发者 |
|
||||||
|
| 8 | 干净环境启动 | 没装过本项目的机器上 `go run .` 或跑 exe | 开发者 |
|
||||||
|
| 9 | 日志和页面无敏感信息 | 翻一遍 `data/logs/` | 开发者 |
|
||||||
|
| 10 | 开源许可证已确认 | 新增依赖的许可证 | **项目负责人** |
|
||||||
|
|
||||||
|
第 10 项开发者**不要自己判断放行**,把包名和许可证类型报给项目负责人。
|
||||||
|
|
||||||
|
## 9. 打包
|
||||||
|
|
||||||
|
`[待定]` 打包动作属于 MVP 之后,但策略现在定下来:
|
||||||
|
|
||||||
|
- `go build` 直接出**单个 exe**,不需要 C 编译器
|
||||||
|
(这就是选 `modernc.org/sqlite` 的原因);
|
||||||
|
- 模板和静态文件用 `//go:embed` 打进 exe,**不要散在外面**,
|
||||||
|
否则用户可能改坏或删掉;
|
||||||
|
- `data/` 留在 exe 旁边,**升级时保留**,见 [02 架构](02-architecture.md) §6;
|
||||||
|
- 升级方式:换掉 exe,`data/` 不动,靠 `PRAGMA user_version` 自动迁移。
|
||||||
|
|
||||||
|
产出目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CMAutoBuyAdmin/
|
||||||
|
├── admin.exe 单文件,模板和静态资源都在里面
|
||||||
|
└── data/ 数据,升级时保留
|
||||||
|
├── admin.db
|
||||||
|
├── logs/
|
||||||
|
└── uploads/
|
||||||
|
```
|
||||||
|
|
||||||
|
比 Client 简单——Go 没有 Python 那种依赖收集问题,不需要 `app/` 目录。
|
||||||
|
|
||||||
|
## 10. 任务完成定义
|
||||||
|
|
||||||
|
单元任务同时满足下面几条才算完成:
|
||||||
|
|
||||||
|
1. 工单验收标准逐项通过;
|
||||||
|
2. 代码、迁移、测试和必要文档同步完成;
|
||||||
|
3. 没有静默跳过的测试;
|
||||||
|
4. 变更不泄露敏感信息;
|
||||||
|
5. Gitea 工单更新最终结果和提交哈希;
|
||||||
|
6. 完成记录归档到 `docs/task`。
|
||||||
Reference in New Issue
Block a user