user_version = 2
go run .
实测该库的表清单里没有 pdd_products。
pdd_products
#16 原地改写了 migration v1,没有新增迁移。而 Migrate 是:
Migrate
for v := current; v < len(migrations); v++ { ... }
老库 user_version 已经是 2,len(migrations) 也是 2,循环一次都不进, 改写后的 v1 永远不会执行。
user_version
len(migrations)
docs/task/16-*.md 写的理由是「migrations 直接改 v1(当时无业务代码依赖), 不加新迁移」——这个前提当时就不成立,8 月 6 日已经存在数据库了。
docs/task/16-*.md
程序静默启动,拿着一个和代码对不上的库。错误要等操作员点到那个页面才暴露。 换成写操作的页面,就不是报错而是写坏数据。
做:
admin/AGENTS.md
不做:
给 v3 套一堆 CREATE TABLE IF NOT EXISTS 去兼容当前这个被改坏的 v1 也能跑通, 但那样新库和老库会走两条不同的路径,必须分别测试。
CREATE TABLE IF NOT EXISTS
这次的缺陷根源恰恰就是只测了一条路径。 恢复 v1 之后两条路径收敛到 同一个终点,测一次就够,而且「迁移只追加」这条不变量也被恢复了。
[必须] v1 恢复后的内容要与 git show 7ad82b7:admin/repository/db.go 完全一致, 逐字对照,不要凭记忆重写。
[必须]
git show 7ad82b7:admin/repository/db.go
① 建 pdd_products 和索引(内容照搬当前 v1 里的定义,含全部注释)
② 把老 shopee_products 上的 PDD 数据搬过去
shopee_products
[必须] 必须去重。多个蝦皮商品可以填同一个 PDD 链接—— 这正是 #16 要解决的问题之一。直接 INSERT ... SELECT 会撞 goods_id 的 UNIQUE 约束。
INSERT ... SELECT
goods_id
INSERT INTO pdd_products (goods_id, url, title, skus_json, collect_status, collect_msg, collected_at, created_at, updated_at) SELECT pdd_goods_id, MIN(COALESCE(pdd_goods_url, '')), NULL, MAX(pdd_data), CASE MAX(collect_status) WHEN 'collected' THEN 'collected' WHEN 'failed' THEN 'failed' ELSE 'pending' END, MAX(collect_error), MAX(collected_at), MIN(created_at), MAX(updated_at) FROM shopee_products WHERE pdd_goods_id IS NOT NULL AND pdd_goods_id <> '' GROUP BY pdd_goods_id;
[必须] collecting 映射成 pending 而不是原样保留。 原来正在采的那次,客户端早就不在了;保留 collecting 会让这个商品 永远建不了新的采集任务(MarkCollecting 只在 pending/failed 时成功), 在界面上表现为「一直转圈、按钮永远置灰」,而且没有任何东西能把它解开。
collecting
pending
MarkCollecting
failed
[必须] no_link 映射成 pending。新 CHECK 里没有 no_link,原样写会撞约束。
no_link
[建议] title 可以用 json_extract(pdd_data, '$.title') 带出来, 但要包 json_valid(pdd_data) 判断——老数据不保证是合法 JSON。取不到就留 NULL。
[建议]
title
json_extract(pdd_data, '$.title')
json_valid(pdd_data)
③ 重建 shopee_products
去掉 pdd_data / collect_status / collect_error / collected_at, 删掉 idx_shopee_products_status,加上 idx_shopee_products_pdd。
pdd_data
collect_status
collect_error
collected_at
idx_shopee_products_status
idx_shopee_products_pdd
按 SQLite 的建表-搬数据-删旧表-改名流程做。
④ 重建 sku_mappings
sku_mappings
[必须] 旧数据全部丢弃,不要试图搬过去。 理由:
新主键需要 pdd_goods_id 和 pdd_option_key 两个旧表没有的列。 pdd_option_key 是 Go 的 service.OptionKey() 用 json.Marshal 算出来的, SQL 复现不了。硬凑一个键出来会有两种后果: 键不对 → 映射静默失效,人工白匹配一遍; 键碰巧撞上别的规格 → 静默买错东西,事后查不出来。 后者正是 #16 存在的全部意义,不能在迁移里重新引入。
pdd_goods_id
pdd_option_key
service.OptionKey()
json.Marshal
规格匹配的界面还没做(属于后续工单),所以此刻不可能存在真实映射数据。
[必须] 丢弃的行数 要打日志,不能静默丢:
迁移 v3:丢弃了 N 条旧规格映射(缺少 pdd_goods_id / pdd_option_key,无法安全迁移,请重新匹配)
N 为 0 时不打,避免每次启动都刷一行没用的日志。
[必须] 重建前后都要 PRAGMA foreign_keys 的处理符合 SQLite 官方的 12 步表重建流程,避免外键把 shopee_skus 的数据带走。
PRAGMA foreign_keys
shopee_skus
[必须] 这是本工单最重要的交付物——它才是能防住下次的东西。
func TestMigrate_不同起点最终schema一致(t *testing.T) { // A: 全新库,一路迁到最新 // B: 只跑 migrations[0:2] 造出一个 v2 老库,再 Migrate 到最新 // 断言:两者的 sqlite_master 内容完全一致(表、索引、列、约束) }
[必须] 比对要覆盖表、索引、列定义和 CHECK 约束,不能只比表名。 只比表名的话,sku_mappings 主键不同这种问题照样漏过去。
[必须] 再加一条带数据的用例:老库里塞两个蝦皮商品指向同一个 PDD 链接,迁移后断言 pdd_products 里只有 1 行,且 collect_status 合法。
Migrate 成功后,检查代码依赖的表是否都在。缺了就返回错误、拒绝启动:
数据库结构与本程序不匹配:缺少表 pdd_products。 这通常是数据库比程序旧、而迁移没有覆盖到。 请备份 data/admin.db 后删除它让程序重建,或联系维护者。
[必须] 是拒绝启动,不是打个警告继续跑。 静默启动正是这次要等到点页面才发现问题的原因。
[建议] 只查表名即可,不必逐列校验——够抓住这一类问题,代价也低。
在 admin/AGENTS.md 的数据库章节加一条 [必须]:
`[必须]` migrations 只追加,**不得修改已经发布过的条目**。 改了的话,已经建过库的机器版本号已经越过它,永远不会重跑, 程序会拿着对不上的库静默启动(见 #20)。 需要改结构就加新的一条。
admin/repository/db.go
admin/repository/migrate_test.go
docs/admin/03-data-model.md
collect_status = 'collecting'
collect_status = 'no_link'
go vet
gofmt -l .
go test ./...
cd D:\chengma\cmautobuy\admin go vet ./... gofmt -l . go test ./... -count=1 go test ./repository/ -run Migrate -v -count=1
老库实测(仓库里有一份 8 月 6 日的备份):
copy admin\data\backup-20260807-113030\admin.db admin\data\admin.db del admin\data\admin.db-wal, admin\data\admin.db-shm go run . # 期望:启动日志出现 v3 迁移;打开 /pdd 返回 200
git show
回退:git revert 即可。已建的库若已迁到 v3,回退程序会因 「数据库版本高于本程序支持的」而拒绝启动——这是 Migrate 已有的正确行为。
git revert
用户确认验收通过。归档状态已更新(3fdab7b)。
3fdab7b
d2de733
docs/task/20-admin-修复迁移被原地改写.md
遗留:Windows 环境未实测;真实业务数据(非空表)的迁移未验证。
No dependencies set.
The note is not visible to the blocked user.
基本信息
要解决什么
复现步骤
user_version = 2go run .实测该库的表清单里没有
pdd_products。根因
#16 原地改写了 migration v1,没有新增迁移。而
Migrate是:老库
user_version已经是 2,len(migrations)也是 2,循环一次都不进,改写后的 v1 永远不会执行。
docs/task/16-*.md写的理由是「migrations 直接改 v1(当时无业务代码依赖),不加新迁移」——这个前提当时就不成立,8 月 6 日已经存在数据库了。
比 500 更危险的一点
程序静默启动,拿着一个和代码对不上的库。错误要等操作员点到那个页面才暴露。
换成写操作的页面,就不是报错而是写坏数据。
做什么 / 不做什么
做:
admin/AGENTS.md写死「迁移只追加、不改已发布的」不做:
怎么做
为什么是「恢复 v1 + 新增 v3」,而不是给 v3 加兼容判断
给 v3 套一堆
CREATE TABLE IF NOT EXISTS去兼容当前这个被改坏的 v1 也能跑通,但那样新库和老库会走两条不同的路径,必须分别测试。
这次的缺陷根源恰恰就是只测了一条路径。 恢复 v1 之后两条路径收敛到
同一个终点,测一次就够,而且「迁移只追加」这条不变量也被恢复了。
[必须]v1 恢复后的内容要与git show 7ad82b7:admin/repository/db.go完全一致,逐字对照,不要凭记忆重写。
v3 要做的事
① 建
pdd_products和索引(内容照搬当前 v1 里的定义,含全部注释)② 把老
shopee_products上的 PDD 数据搬过去[必须]必须去重。多个蝦皮商品可以填同一个 PDD 链接——这正是 #16 要解决的问题之一。直接
INSERT ... SELECT会撞goods_id的 UNIQUE 约束。[必须]collecting映射成pending而不是原样保留。原来正在采的那次,客户端早就不在了;保留
collecting会让这个商品永远建不了新的采集任务(
MarkCollecting只在pending/failed时成功),在界面上表现为「一直转圈、按钮永远置灰」,而且没有任何东西能把它解开。
[必须]no_link映射成pending。新 CHECK 里没有no_link,原样写会撞约束。[建议]title可以用json_extract(pdd_data, '$.title')带出来,但要包
json_valid(pdd_data)判断——老数据不保证是合法 JSON。取不到就留 NULL。③ 重建
shopee_products去掉
pdd_data/collect_status/collect_error/collected_at,删掉
idx_shopee_products_status,加上idx_shopee_products_pdd。按 SQLite 的建表-搬数据-删旧表-改名流程做。
④ 重建
sku_mappings[必须]旧数据全部丢弃,不要试图搬过去。 理由:新主键需要
pdd_goods_id和pdd_option_key两个旧表没有的列。pdd_option_key是 Go 的service.OptionKey()用json.Marshal算出来的,SQL 复现不了。硬凑一个键出来会有两种后果:
键不对 → 映射静默失效,人工白匹配一遍;
键碰巧撞上别的规格 → 静默买错东西,事后查不出来。
后者正是 #16 存在的全部意义,不能在迁移里重新引入。
规格匹配的界面还没做(属于后续工单),所以此刻不可能存在真实映射数据。
[必须]丢弃的行数 要打日志,不能静默丢:N 为 0 时不打,避免每次启动都刷一行没用的日志。
[必须]重建前后都要PRAGMA foreign_keys的处理符合 SQLite 官方的12 步表重建流程,避免外键把
shopee_skus的数据带走。迁移收敛测试
[必须]这是本工单最重要的交付物——它才是能防住下次的东西。[必须]比对要覆盖表、索引、列定义和 CHECK 约束,不能只比表名。只比表名的话,
sku_mappings主键不同这种问题照样漏过去。[必须]再加一条带数据的用例:老库里塞两个蝦皮商品指向同一个PDD 链接,迁移后断言
pdd_products里只有 1 行,且collect_status合法。启动时 schema 自检
Migrate成功后,检查代码依赖的表是否都在。缺了就返回错误、拒绝启动:[必须]是拒绝启动,不是打个警告继续跑。静默启动正是这次要等到点页面才发现问题的原因。
[建议]只查表名即可,不必逐列校验——够抓住这一类问题,代价也低。写进 AGENTS.md
在
admin/AGENTS.md的数据库章节加一条[必须]:预计修改文件
admin/repository/db.goMigrate后加 schema 自检admin/repository/migrate_test.goadmin/AGENTS.mddocs/admin/03-data-model.md验收标准
git show 7ad82b7:admin/repository/db.go逐字一致pdd_products与当前 v1 里的定义一致(含 UNIQUE、CHECK、软删除列)pdd_products只有 1 行collect_status = 'collecting'→ 迁移后是pendingcollect_status = 'no_link'→ 迁移后是pending,不撞 CHECKsku_mappings有数据时,丢弃行数被打进日志shopee_products迁移后不再有那四个字段,shopee_skus数据未被外键带走admin/AGENTS.md有「迁移只追加」的[必须]条目go vet/gofmt -l ./go test ./...全过怎么验证
老库实测(仓库里有一份 8 月 6 日的备份):
风险和回退
shopee_skus带走git show,不许凭记忆collecting卡死pending并写明理由回退:
git revert即可。已建的库若已迁到 v3,回退程序会因「数据库版本高于本程序支持的」而拒绝启动——这是
Migrate已有的正确行为。验收通过,关闭
用户确认验收通过。归档状态已更新(
3fdab7b)。d2de733docs/task/20-admin-修复迁移被原地改写.md遗留:Windows 环境未实测;真实业务数据(非空表)的迁移未验证。
ila referenced this issue2026-08-10 10:32:21 +08:00
ila referenced this issue2026-08-10 10:40:56 +08:00