补上 #20 和 #24 的归档(此前只提交了代码),并把 #16 #17 #19 的状态改为验收通过。 #20 归档记录了工单中途被修改的经过:原工单写死「user_version=2 ⇒ 老结构」, 用户实测炸出 table already exists 后才发现 #16 的原地改写让这个版本号 对应两种结构,路径本来就是两条。按 CLAUDE.md §7.5 改工单而不是让实现硬凑。 也记录了变异测试第一轮漏掉两个的原因——架构角色的设计让收敛测试变成了 近乎同义反复。 #24 归档记录了四个变异全部被抓到,其中「破坏原子性判断」那个最关键: 加超时分支最容易顺手把原子抢占破坏掉。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
- 类型:重构(数据模型)
|
||||
- 父级大工单:#14
|
||||
- 所属 MVP / 版本:#15 / MVP
|
||||
- 状态:待验收
|
||||
- 状态:验收通过
|
||||
- 日期:2026-08-07
|
||||
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/16
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
- 类型:需求(含接口契约变更)
|
||||
- 父级大工单:#14
|
||||
- 所属 MVP / 版本:#15 / MVP
|
||||
- 状态:待验收
|
||||
- 状态:验收通过
|
||||
- 日期:2026-08-07
|
||||
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/17
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- 父级大工单:#14
|
||||
- 所属 MVP / 版本:#15 / MVP
|
||||
- 关联:#18(缺陷暴露于该工单交付后)
|
||||
- 状态:待验收
|
||||
- 状态:验收通过
|
||||
- 日期:2026-08-07
|
||||
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/19
|
||||
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# 20 Admin 修复迁移被原地改写导致老库缺表
|
||||
|
||||
- 类型:缺陷(数据库迁移设计)
|
||||
- 父级大工单:#14
|
||||
- 所属 MVP / 版本:#15 / MVP
|
||||
- 关联:#16(缺陷来源)
|
||||
- 状态:验收通过
|
||||
- 日期:2026-08-07
|
||||
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/20
|
||||
|
||||
## 背景与目标
|
||||
|
||||
用户点 PDD 商品页报 **HTTP 500「读取 PDD 商品列表失败」**。
|
||||
|
||||
根因:**#16 原地改写了 migration v1,没有新增迁移。** 而 `Migrate` 是
|
||||
|
||||
```go
|
||||
for v := current; v < len(migrations); v++ { ... }
|
||||
```
|
||||
|
||||
老库 `user_version` 已经是 2、`len(migrations)` 也是 2,**循环一次都不进**,
|
||||
改写后的 v1 永远不会执行,`pdd_products` 表根本没被建出来。
|
||||
|
||||
`docs/task/16-*.md` 当时写的理由是「migrations 直接改 v1(当时无业务代码依赖),
|
||||
不加新迁移」——**这个前提当时就不成立**,8 月 6 日已经存在数据库了。
|
||||
|
||||
比 500 更危险的是:程序**静默启动**,拿着一个和代码对不上的库。
|
||||
错误要等操作员点到那个页面才暴露;换成写操作的页面就不是报错而是写坏数据。
|
||||
|
||||
## 最终方案
|
||||
|
||||
### v1 恢复原样,#16 的改动全部挪进 v3
|
||||
|
||||
**不是**给 v3 加一堆 `IF NOT EXISTS` 去兼容被改坏的 v1。那样新库和老库会走
|
||||
**两条不同路径**,必须分别测试——而这次的缺陷根源恰恰就是只测了一条
|
||||
(架构角色审查 #16 时全在全新库上跑)。
|
||||
|
||||
恢复 v1 之后两条路径收敛到同一个终点,测一次就够,
|
||||
「迁移只追加」这条不变量也被恢复了。
|
||||
|
||||
### v3 必须认两种 `user_version = 2`
|
||||
|
||||
**实施过程中用户实测炸出的第二个问题:**
|
||||
|
||||
```
|
||||
迁移 v3 建 pdd_products 第 1 条语句失败: table pdd_products already exists
|
||||
```
|
||||
|
||||
因为 #16 的原地改写,`user_version = 2` 对应**两种不同结构**:
|
||||
|
||||
| 建库时用的代码 | user_version | pdd_products |
|
||||
|---|---|---|
|
||||
| 原始 v1(#16 之前) | 2 | 无 |
|
||||
| #16 改写后的 v1 | 2 | **有,且已是最终结构** |
|
||||
|
||||
原工单写死了「v2 ⇒ 老结构」这个前提,**前提是错的**。任何在 998c06a 之后、
|
||||
本工单之前建过库的机器都是第二种——用户就是,而且是架构角色让他删库重建
|
||||
才变成这样的。
|
||||
|
||||
改法:`migrateV3` 每一步**先查实际结构再决定做不做**
|
||||
(`PRAGMA table_info` / `sqlite_master`),只有版本号推进是无条件的:
|
||||
|
||||
| 步骤 | 执行条件 |
|
||||
|---|---|
|
||||
| 建 `pdd_products` + 索引 | 该表不存在时 |
|
||||
| 从 `shopee_products` 搬 PDD 数据 | 它还有 `pdd_data` 列时 |
|
||||
| 重建 `shopee_products` | 它还有那四个旧列中任意一个时 |
|
||||
| 重建 `sku_mappings` | 它还没有 `pdd_goods_id` 列时 |
|
||||
| `PRAGMA user_version = 3` | 无条件 |
|
||||
|
||||
已是最终结构的库只推版本号,日志也照实说
|
||||
(`数据库迁移 v3:结构已是最新,仅更新版本号`),不谎称「新增 pdd_products」。
|
||||
|
||||
### 数据迁移的三条硬规则
|
||||
|
||||
**① 必须按 `pdd_goods_id` 去重。** 多个蝦皮商品可以填同一个 PDD 链接——
|
||||
这正是 #16 要解决的问题之一。直接 `INSERT ... SELECT` 会撞 UNIQUE。
|
||||
|
||||
**② `collecting` 映射成 `pending`。** 原样保留会让 `MarkCollecting` 永远不成功,
|
||||
那个商品**再也建不了采集任务**,界面上表现为按钮永远置灰且无法解开。
|
||||
|
||||
**③ 旧 `sku_mappings` 全部丢弃,但要打日志。** 新主键需要 `pdd_option_key`,
|
||||
那是 Go 的 `OptionKey()` 用 `json.Marshal` 算的,**SQL 复现不了**。
|
||||
硬凑一个键出来:键不对 → 映射静默失效;键碰巧撞上别的规格 →
|
||||
**静默买错东西**。后者正是 #16 存在的全部意义,不能在迁移里重新引入。
|
||||
|
||||
### 另加两道闸
|
||||
|
||||
- **启动时 `CheckSchema`**:缺表即**拒绝启动**,不是警告后继续。
|
||||
- **`admin/AGENTS.md` 写死**「migrations 只追加、不得修改已发布条目」。
|
||||
|
||||
## 与建单方案的差异
|
||||
|
||||
1. **v3 没有放进 `migrations [][]string`**,而是单独写成 `migrateV3()` 函数 +
|
||||
`schemaVersion` 常量。原因:v3 需要在事务外切 `PRAGMA foreign_keys`、
|
||||
需要在丢数据前用 Go 统计行数打日志,纯 SQL 顺序执行表达不了。
|
||||
2. **未采纳 `[建议]` 的 `title` 用 `json_extract` 提取**(可选增强,未做以减少风险面)。
|
||||
3. **改动了 `admin/main.go`**(不在预计文件表里):`CheckSchema` 必须有人调用,
|
||||
否则验收项无法达成。只加 3 行。
|
||||
4. **加了一行无条件的 v3 日志**。原设计只在丢弃旧映射时才打日志,
|
||||
用真实的空数据库测试时启动日志完全看不出发生过迁移,
|
||||
验证步骤「期望启动日志出现 v3 迁移」没法核对。
|
||||
5. **v3 改为查实际结构**(见上,工单中途修正)。
|
||||
|
||||
## 改了哪些
|
||||
|
||||
- `admin/repository/db.go`:v1 逐字恢复成 `7ad82b7` 的原样;新增 `schemaVersion`
|
||||
和 `migrateV3()`;新增 `CheckSchema()`。
|
||||
- `admin/repository/migrate_test.go`:新建。三起点收敛测试 + 数据场景 + 幂等 + `CheckSchema`。
|
||||
- `admin/main.go`:`Migrate` 之后调 `CheckSchema`,失败 `log.Fatalf`。
|
||||
- `admin/AGENTS.md`:「migrations 只追加」的 `[必须]` 条目。
|
||||
- `docs/admin/03-data-model.md`:§2.3 迁移版本历史、§2.4 启动自检。
|
||||
|
||||
## 验收结果
|
||||
|
||||
| 验收标准 | 结果 |
|
||||
|---|---|
|
||||
| v1 与 `7ad82b7` **逐字一致** | 通过(两边均 4380 字节,程序化 diff EQUAL) |
|
||||
| v3 建出的 `pdd_products` 与原 v1 定义一致 | 通过 |
|
||||
| **全新库 / v2 老结构库 / v2 新结构库,迁移后 schema 完全一致** | 通过 |
|
||||
| schema 比对覆盖表、索引、列定义、CHECK 约束 | 通过 |
|
||||
| 两个蝦皮商品指向同一 PDD 链接 → 只有 1 行 | 通过 |
|
||||
| `collecting` → `pending` | 通过 |
|
||||
| `no_link` → `pending`,不撞 CHECK | 通过 |
|
||||
| 丢弃旧映射时打日志,为 0 时不打 | 通过 |
|
||||
| `shopee_skus` 数据不被外键带走 | 通过(`PRAGMA foreign_key_check` 干净) |
|
||||
| 缺表时**拒绝启动**并给出提示 | 通过 |
|
||||
| **v2 新结构库连续调用两次 `Migrate` 不报错** | 通过 |
|
||||
| `admin/AGENTS.md` 有对应 `[必须]` 条目 | 通过 |
|
||||
| `go vet` / `gofmt` / `go test` 全过 | 通过 |
|
||||
|
||||
## 测试
|
||||
|
||||
架构角色亲自执行(Go 1.23.0,`admin/` 目录):
|
||||
|
||||
```
|
||||
go vet ./... 无输出
|
||||
gofmt -l . 无输出
|
||||
go test ./... -count=1 ok repository / ok service,15 个迁移与自检测试全过
|
||||
```
|
||||
|
||||
### 变异测试(架构角色补做,工单未要求)
|
||||
|
||||
第一轮实现交付后,对 `migrateV3` 里的定义故意改坏,看测试抓不抓得到:
|
||||
|
||||
| 变异 | 第一轮 | 返工后 |
|
||||
|---|---|---|
|
||||
| `collect_status` 的 CHECK 放回 `no_link` | **全绿,没抓到** | 变红 |
|
||||
| 去掉 `goods_id` 的 `UNIQUE` | **全绿,没抓到** | 变红 |
|
||||
| `sku_mappings` 主键退回单列 | 靠 service 层用例才抓到 | 变红 |
|
||||
| 改掉一个列名(探针) | — | 变红 |
|
||||
|
||||
第一轮漏掉两个,是因为**架构角色的设计让收敛测试变成了近乎同义反复**——
|
||||
全新库也走 v3,两条路径共用同一份 v3 代码,v3 自己写错时两边会一起错。
|
||||
补了「最终 schema 符合设计要求」的行为断言测试后,四个变异全部被抓到。
|
||||
|
||||
第四个变异是专门探那个新加的「引号归一化」正则会不会把比对弄瞎的。
|
||||
正则是锚定的、只吃 `CREATE TABLE/INDEX` 后面那个标识符的外层引号,
|
||||
碰不到列定义和 CHECK——探针变红印证了。
|
||||
|
||||
### 真实库端到端
|
||||
|
||||
用 `git show 998c06a` 的代码原样造出与用户库结构完全一致的库
|
||||
(`user_version=2` + `pdd_products` 已存在),跑修复后的二进制:
|
||||
|
||||
```
|
||||
第一次启动 数据库迁移 v3:结构已是最新,仅更新版本号
|
||||
/pdd /shopee /syb /tasks /clients 全部 200
|
||||
第二次启动 (无迁移日志,完全跳过)
|
||||
/pdd 200
|
||||
最终 user_version = 3,PRAGMA foreign_key_check OK
|
||||
```
|
||||
|
||||
另用真正的老结构库(`backup-20260807-113030/admin.db`,`user_version=2`、
|
||||
无 `pdd_products`)跑一遍:迁移日志三步全做,五页全 200,`sku_mappings`
|
||||
主键已是 `(shopee_sku_id, pdd_goods_id)`,`shopee_products` 那四列消失。
|
||||
|
||||
缺表拒绝启动:删掉 `pdd_products` 后启动,打印
|
||||
「数据库结构与本程序不匹配:缺少表 pdd_products…」并退出,服务未起。
|
||||
|
||||
**未验证到的部分:**
|
||||
|
||||
- **Windows 环境未实测**(本机是 WSL/Linux)。改动的代码(结构探测、
|
||||
FK 开关、RENAME 顺序)与平台无关,依赖是纯 Go 的 `modernc.org/sqlite`。
|
||||
- 备份库里所有表都是 0 行,「两个蝦皮商品同链接」和「旧 `sku_mappings`
|
||||
有数据」这两类场景**只在自动化测试的临时库里验证过**,没有用真实业务数据验证。
|
||||
- 未验证并发调用 `Migrate`(两个进程同时首次启动争抢迁移),工单未要求。
|
||||
|
||||
## 实现自己发现并修掉的两个问题
|
||||
|
||||
这两个都是**新测试逼出来的**,不是读代码看出来的:
|
||||
|
||||
1. **`ALTER TABLE ... RENAME TO` 会自动重写别的表里指向它的外键子句。**
|
||||
最初为绕开引号问题改成「先把旧表 RENAME 让位」,结果 `shopee_skus`
|
||||
的外键被悄悄改成指向一张马上被删掉的 `shopee_products_old`,
|
||||
`PRAGMA foreign_key_check` 直接报错。改回 SQLite 官方 12 步顺序解决。
|
||||
2. **RENAME 会给表名套一层双引号。** `sqlite_master.sql` 变成
|
||||
`CREATE TABLE "foo" (...)`,而从未 RENAME 过的表没有这层引号,
|
||||
导致收敛比对被判定成不一致。用只针对 CREATE 语句开头标识符的正则归一化解决。
|
||||
|
||||
## 审查过程
|
||||
|
||||
工单中途修改一次(**架构角色的设计前提错了,不是实现问题**)。
|
||||
|
||||
原工单写死「`user_version = 2` ⇒ 老结构」,并明确否掉了 `IF NOT EXISTS`
|
||||
这类判断,理由是「会造成两条路径」。用户实测炸出 `table pdd_products already exists`
|
||||
之后才发现:路径本来就是两条,是 #16 造成的既成事实,v3 必须两种都认。
|
||||
|
||||
按 `CLAUDE.md` §7.5「实现暴露了设计问题,架构要认」,改工单而不是让实现硬凑。
|
||||
|
||||
## 遗留问题
|
||||
|
||||
1. Windows 环境未实测。
|
||||
2. 真实业务数据(非空表)的迁移未验证。
|
||||
|
||||
## 教训
|
||||
|
||||
同一个根因(#16 原地改写 v1)咬了三次:用户点页面 500、审查 #16 时没抓到、
|
||||
修复时又漏了第三种状态。前两次是「只在全新库上测」,第三次是
|
||||
**基于对现状的假设**而不是去查实际存在哪些库。
|
||||
|
||||
`admin/AGENTS.md` 已加「迁移只追加」。建议再补一条:
|
||||
**改迁移前先列出现实中存在哪些 schema 状态,一个个验。**
|
||||
|
||||
## 相关提交
|
||||
|
||||
- `d2de733` fix: 修复迁移被原地改写导致老库缺表 (#20)
|
||||
@@ -0,0 +1,195 @@
|
||||
# 24 Admin 商品卡在「采集中」无法恢复
|
||||
|
||||
- 类型:缺陷(状态机死锁)
|
||||
- 父级大工单:#14
|
||||
- 所属 MVP / 版本:#15 / MVP
|
||||
- 关联:#18(缺陷所在页面)、#16(`collect_status` 的引入)
|
||||
- 状态:验收通过
|
||||
- 日期:2026-08-07
|
||||
- Gitea 工单:http://ilaer.eicp.net:8418/chengma/cmautobuy/issues/24
|
||||
|
||||
## 背景与目标
|
||||
|
||||
在 PDD 商品页点「创建采集任务」之后,商品进入 `collecting`,
|
||||
**再也出不来**:
|
||||
|
||||
```text
|
||||
第 1 次点 已创建 1 个采集任务,等待客户端领取
|
||||
第 2 次点 已创建 0 个采集任务(跳过 1 个采集中或已删除的)
|
||||
弹窗「重新采集」 同上
|
||||
商品状态: collecting ← 永远卡在这里
|
||||
```
|
||||
|
||||
根因:`MarkCollecting` 只在 `pending` / `failed` 时成功,而全库只有四个地方
|
||||
会改 `collect_status`,其中把 `collecting` 改走的两个
|
||||
(`SetCollectResult` / `SetCollectFailed`)**只有客户端提交结果才会触发**。
|
||||
|
||||
客户端离线、崩溃、任务被删都是常态,不是异常。每发生一次就**永久废掉一个商品**,
|
||||
只能改数据库救。而当时 Client 的 `claim_next` 还没实现,等于每点一次就废一个。
|
||||
|
||||
提示「跳过 1 个采集中或已删除的」对操作员**毫无信息量**:
|
||||
到底是在采、被删了、还是卡死了,看不出来,也不知道该等还是该做点什么。
|
||||
|
||||
## 最终方案
|
||||
|
||||
### 超时在读取那一刻现算,不开后台协程
|
||||
|
||||
```sql
|
||||
UPDATE pdd_products
|
||||
SET collect_status = 'collecting', updated_at = ?
|
||||
WHERE goods_id = ? AND deleted_at IS NULL
|
||||
AND ( collect_status IN ('pending', 'failed')
|
||||
OR (collect_status = 'collecting' AND updated_at < ?) )
|
||||
```
|
||||
|
||||
`[必须]` **仍是同一条原子 `UPDATE`**,靠影响行数判断是否抢到。
|
||||
加超时分支最容易顺手把原子抢占破坏掉。
|
||||
|
||||
`[必须]` **不写后台协程定期把超时的改回 `pending`。** 后台扫要处理
|
||||
「扫到一半客户端正好提交了」的竞态;现算没有调度、没有并发窗口。
|
||||
本项目已经在并发上栽过两次(PRAGMA 没作用到连接池、`BEGIN DEFERRED` 死锁),
|
||||
能不引入并发就不引入。
|
||||
|
||||
### 阈值 15 分钟,写成有名字的常量
|
||||
|
||||
```go
|
||||
// CollectStaleAfter 是采集任务多久没动静就当它死了。
|
||||
//
|
||||
// 采集本身几十秒到两分钟;加上排队等客户端来领,15 分钟足够宽裕。
|
||||
// 宁可短也不要长:采集是**只读**操作,多采一次没有任何副作用,
|
||||
// 而卡死的代价是这个商品永久报废、只能改数据库救。
|
||||
const CollectStaleAfter = 15 * time.Minute
|
||||
```
|
||||
|
||||
### 字符串比时间依赖 `NowISO` 的格式
|
||||
|
||||
`[必须]` `updated_at < ?` 用的是字符串比较,只有在**定宽 + 统一 UTC + 补零**
|
||||
(`2026-08-07T06:10:19Z`)时字典序才等于时间序。
|
||||
|
||||
已在 `NowISO` 上加注释说明有代码依赖这个性质——改成带时区偏移的本地时间
|
||||
会让这个判断**静默失效**:不报错,但判断全错。
|
||||
|
||||
### 已知取舍:可能采两次
|
||||
|
||||
超时后允许重新建任务,但**老任务还在队列里**,客户端上线后可能把两个都领走。
|
||||
可接受:采集是只读的,后一次结果覆盖前一次,数据仍然正确。
|
||||
|
||||
`[必须]` 这条取舍写进了代码注释和 `docs/admin/03-data-model.md` §4.1,
|
||||
并明写「**不要看到"可能采两次"就加锁或加租约去"修"它**——租约和心跳是
|
||||
被明确移除过的设计」。
|
||||
|
||||
### 为什么不加「手工取消采集」按钮
|
||||
|
||||
超时自动回退已经覆盖了这个场景,而且**不需要操作员判断**——
|
||||
他并不知道客户端是不是还活着、该不该取消。多一个按钮就多一处要维护、
|
||||
多一处可能被误点。
|
||||
|
||||
### 界面两处
|
||||
|
||||
**列表和弹窗**:超时的显示「**采集中(超时)**」,不是「采集中」。
|
||||
操作员盯着「采集中」不知道它已经死了,会一直等。
|
||||
|
||||
**筛选下拉不新增「已超时」**:它不是数据库里真实存在的 `collect_status` 取值,
|
||||
只是「采集中」在读取那一刻的一种呈现,加进去会让下拉取值和 `collect_status` 对不上。
|
||||
|
||||
**跳过提示按原因分类**:
|
||||
|
||||
```text
|
||||
改前 已创建 0 个采集任务(跳过 1 个采集中或已删除的)
|
||||
改后 没有创建任何任务:1 个正在采集中(还需等待约 15 分钟才可重试)
|
||||
```
|
||||
|
||||
## 与建单方案的差异
|
||||
|
||||
1. **常量必须导出**(`model.CollectStaleAfter`)——`repository` 和 `service`
|
||||
都要用,Go 不允许跨包访问未导出名。工单里的小写名是示意。
|
||||
2. **`CreatePddCollectTasks` 签名改成返回结构体**,用来携带分类计数。
|
||||
3. **多加一个「已采集的」跳过桶**。工单示例只列了「正在采集的」「已删除的」两类,
|
||||
原先已 `collected` 的行会被含糊地并进同一桶,单独标注更准确。
|
||||
4. **「采集中(超时)」也用到详情弹窗**。工单只要求列表页;不这样做会出现
|
||||
列表说超时、弹窗说采集中,自相矛盾。
|
||||
5. `admin/templates/pdd/list.html` **未改动**——文案通过已有的 `{{.StatusText}}`
|
||||
绑定自动生效,比工单预计的改动更小。
|
||||
|
||||
## 改了哪些
|
||||
|
||||
- `admin/model/model.go`:`CollectStaleAfter` 常量;`NowISO` 加注释。
|
||||
- `admin/repository/pdd.go`:`MarkCollecting` 增加超时分支(仍是单条原子 UPDATE)。
|
||||
- `admin/service/pdd.go`:`statusTextFor` / `isCollectingStale`(列表与详情共用);
|
||||
`CollectTaskResult` 分类计数;提示文案组装。
|
||||
- `admin/handler/web/pdd.go`:改调 `FormatCollectTaskMessage`。
|
||||
- `admin/service/pdd_page_test.go`:超时、边界、并发、分类计数、文案共 8 个新用例。
|
||||
- `docs/admin/03-data-model.md` §4.1、`05-ui-specification.md` §5.2/§5.5。
|
||||
|
||||
## 验收结果
|
||||
|
||||
| 验收标准 | 结果 |
|
||||
|---|---|
|
||||
| `collecting` 且超过 15 分钟 → 能重新创建 | 通过 |
|
||||
| `collecting` 但**没到** 15 分钟 → 仍然跳过 | 通过 |
|
||||
| `pending` / `failed` → 照常能建 | 通过 |
|
||||
| 已软删除的 → 仍然跳过 | 通过 |
|
||||
| 阈值是有名字的常量并写清理由 | 通过 |
|
||||
| **多个请求同时对同一「已超时」商品建任务,只产生 1 条** | 通过 |
|
||||
| 列表页超时显示「采集中(超时)」 | 通过 |
|
||||
| 状态有文字区分,不只靠颜色 | 通过 |
|
||||
| 筛选下拉未新增「已超时」 | 通过(仍是 5 项) |
|
||||
| 跳过原因分类计数 | 通过 |
|
||||
| 一个都没建成时告诉操作员还要等多久 | 通过 |
|
||||
| `NowISO` 有格式依赖的注释 | 通过 |
|
||||
| 「可能采两次」写进代码注释和文档 | 通过 |
|
||||
| **没有后台协程、没有租约或心跳** | 通过(生产代码零 `go func`) |
|
||||
| `go vet` / `gofmt` / `go test` 全过 | 通过 |
|
||||
|
||||
## 测试
|
||||
|
||||
架构角色亲自执行(Go 1.23.0,`admin/` 目录):
|
||||
|
||||
```
|
||||
go vet ./... 无输出
|
||||
gofmt -l . 无输出
|
||||
go test ./... -count=1 ok repository / ok service
|
||||
go test ./service/ -run '并发' -count=20 ok(稳定)
|
||||
```
|
||||
|
||||
### 变异测试(架构角色补做,工单未要求)
|
||||
|
||||
| 变异 | 结果 |
|
||||
|---|---|
|
||||
| 去掉超时限制(`collecting` 一律允许重建) | 4 个用例变红 |
|
||||
| 去掉超时分支(退回卡死行为) | 2 个变红 |
|
||||
| **破坏原子性判断**(`n == 1` → `n >= 0`) | **5 个变红** |
|
||||
| 比较方向反了(`<` → `>`) | 5 个变红 |
|
||||
|
||||
第三个是重点——加超时分支最容易把原子抢占破坏掉,测试能抓到说明有牙。
|
||||
|
||||
### 端到端
|
||||
|
||||
```
|
||||
① 第一次点 已创建 1 个采集任务,等待客户端领取 列表: 采集中
|
||||
② 立刻再点 没有创建任何任务:1 个正在采集中
|
||||
(还需等待约 15 分钟才可重试) 列表: 采集中
|
||||
③ updated_at 往前调 20 分钟 列表: 采集中(超时)
|
||||
弹窗: 采集中(超时)
|
||||
④ 再点 已创建 1 个采集任务,等待客户端领取
|
||||
⑤ 任务表 2 条(已知取舍:老任务还在队列里)
|
||||
```
|
||||
|
||||
筛选下拉仍是 5 项,页面里「已超时」出现 0 次。
|
||||
|
||||
**未验证到的部分:**
|
||||
|
||||
- **15 分钟的精确边界未单独测**。测的是 20 分钟(超时)和 5 分钟(未超时)。
|
||||
当前是严格 `<`,即「正好卡在 15 分钟那一刻」算未超时。
|
||||
- Windows 环境未实测(本机 WSL/Linux)。
|
||||
- 未与真实 Client 联调确认「超时后的第二个任务被领走、其结果正确覆盖第一个」——
|
||||
工单验证步骤明确说不需要真机和客户端。
|
||||
|
||||
## 遗留问题
|
||||
|
||||
1. 15 分钟边界的精确行为未单独测试。
|
||||
2. 「可能采两次」在真实客户端上线后的表现未验证。
|
||||
|
||||
## 相关提交
|
||||
|
||||
- `e40ce5a` fix: 商品卡在「采集中」无法恢复 (#24)
|
||||
Reference in New Issue
Block a user