Files
cmautobuy/docs/admin/06-quality-security.md
T
chengmaandClaude Opus 5 f0d7da37cf feat: 采集采购页,采集与采购统一展示 (#19)
原来的采购任务页是骨架:var rows []gin.H 从不查库,页面永远为空;
TODO 写的还是只查 task_type = 'purchase'。所以 #18 建出来的采集任务
在界面上哪儿都看不到——用户点完「创建采集任务」只能盯着
collect_status 猜。

模块名定为「采集采购」(用户指定),直接点出这页装的是哪两类任务,
比泛称「任务」更能让人一眼知道点进去看什么。路由 /tasks 不变,
改路由会让已有书签和文档链接全失效,没有收益。

列不按类型并列——两种任务字段完全不同,并列会让采集任务行一半是空列。
改成固定列 + 一列「目标」把业务信息概括成一句话:
  采集  PDD 737116531267
  采购  SO-001 · M/黑色 · 2件 · ≤¥42.00
拼接逻辑在 service 层,模板只负责显示。

统计和列表共用同一个筛选条件拼装函数。分开写的话总有一天会忘了
给统计也加条件,数字和表格对不上,操作员会以为页面坏了。

无主任务的客户端列显示「—」。#17 之后采集任务默认无主,这列会大量为空。

详情弹窗只读,采购专有字段(数量、价格上限、目标规格)在采集任务里
整段不出现,不显示空行。复用 #18 的弹窗机制,app.js 无需改动。

顺带清掉 PDD 页加进来之后一直没跟上的模块计数:多处「四个模块/四个页面」
改成五个。其中 06-quality-security.md 那两处是验证清单,
照着做的人只会测四个页面,PDD 页永远不在回归范围里。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:16:05 +08:00

197 lines
8.5 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.
# 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` 刚好在边界前后。
`[必须]` 导入相关的测试用 `admin/testdata/` 下的**小样本**(几十行),
不要读完整报表。
`[必须]` 这份小样本**必须提交进 Git**,否则别人拉下来测试跑不了。
制作方法:从真实报表里挑几十行,**删掉全部销售额、曝光、转化率等指标列**,
只保留导入用得到的字段(商品ID、商品名稱、商品規格ID、商品規格、貨號等),
并把商品名称改成无意义的占位文字。
完整报表含商业数据,**不进 Git**(见 `.gitignore`)。
### 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/`),应得 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`。