 chengmaandClaude Opus 5
|
998c06a2bf
|
feat: PDD 商品数据独立成表 (#16)
原来 pdd_data 是 shopee_products 上的一个 JSON 字段,两个蝦皮商品指向
同一个 PDD 链接时会各存一份、各采一次;collect_status 描述的是 PDD 商品的
状态,却挂在蝦皮商品上,两份可能不一致。
更要紧的是 PDD 商品变动频繁(A 下架就得换 B),而 sku_mappings 只按
shopee_sku_id 做键——换商品后旧映射还在,B 恰好有同名规格但完全是另一件货
时会静默买错,事后查不出来。
改动
- 新增 pdd_products 表:id 主键 + goods_id UNIQUE + 4 个状态值(去掉
no_link,「未填链接」改由 shopee_products.pdd_goods_id 为空表达)+
软删除可复活
- shopee_products 去掉 pdd_data / collect_status / collect_error /
collected_at,pdd_goods_id 改为引用
- sku_mappings 主键改为 (shopee_sku_id, pdd_goods_id),新增 pdd_option_key。
查映射永远带上当前 PDD 商品,换商品后天然查不到旧映射,不需要删数据;
换回原商品时旧映射直接复用
- 新增 OptionKey():用 json.Marshal 实现(Go 序列化 map 按键名排序,
天然规范化),不自己拼字符串——规格文字里可能含 = 或 ;。
存映射和查 SKU 必须用同一个函数,各写一遍会静默算出不同结果
- 采集结果改落 pdd_products,新增两条校验:
返回的 goods_id 与请求不符 → 整体回滚拒绝(422),不静默存下;
skus 为空数组 → 置 failed 而非 collected,否则界面显示"已采集"
但数据毫无用处
实施时超出工单但必要的三处
- TaskExists 重构为 GetTaskInfo:原函数只返回蝦皮 goods_id,
而采集结果要按 PDD goods_id 落库,不改取不到正确的键
- 复活时一并清空旧采集结果(skus_json / collect_msg / collected_at),
否则复活后会显示"已采集"但数据是删除前的
- 删除 repository/shopee.go:两个函数签名全变且已迁到 pdd.go,留着是死代码
已验证(Go 1.23.0)
- go vet / gofmt / go test 全过,55 个测试
- 端到端补验了工单未覆盖的 HTTP 层:goods_id 不符返回 422
COLLECT_GOODS_MISMATCH 且整体回滚(skus_json 空、任务仍 claimed、
幂等记录 0 条);skus 为空返回 200 但状态 failed
遗留
- MarkCollecting / SoftDeletePddProduct 暂无调用方,等界面工单接上
- artifact_ref 存 diagnostics 原始 JSON,未按 client-001:artifacts/... 规范化,
因 Client 侧尚未定义 diagnostics 结构
- 界面未实现(工单明确排除),四个页面仍为骨架
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 10:27:39 +08:00 |
|
 chengmaandClaude Opus 5
|
6ae768463f
|
feat: 实现客户端注册与任务领取接口
Admin 的第一个业务功能。选它打头是因为它是穿透所有层的最薄一条竖切
(HTTP → handler/api → service → repository → SQLite → handler/web → 页面),
一个工单把分层模式立起来,后面四个模块照抄;同时它是与 Client 联调的接口,
能解锁另一条并行的工作线。
实现
- POST /tasks/claim:注册 + 领取。注册就在这里做,没有单独的注册接口,
也没有心跳(理由见 docs/admin/04-client-api.md §3)
- 领取用条件更新 + 检查影响行数防并发,SQLite 没有 SELECT FOR UPDATE
- 客户端列表页:查询、按名称搜索、批量删除
- 在线状态是**算出来的**(last_seen_at 在 10 分钟内),数据库里没有该字段
- CSRF 中间件:双提交 Cookie,手写 82 行不引依赖。
**只挂页面路由**,/api/v1/client/* 不能加——Client 不是浏览器、没有 Cookie
- 14 个单元测试
修复一个真 bug:PRAGMA 必须写进 DSN
并发领取测试报 database is locked (SQLITE_BUSY)。根因是
PRAGMA busy_timeout 每连接生效,而 database/sql 是连接池——
db.Exec("PRAGMA ...") 只作用于当时那条连接,池子新开的连接没执行过。
单线程正常、一并发就炸。改成 DSN 传参后并发测试跑 20 次全过。
这个坑已写进 docs/admin/03-data-model.md §2.1。
与工单的两处差异
- 去掉 name_is_custom 列后,"人工改的名字不被覆盖"改用更简单的做法:
ON CONFLICT DO UPDATE SET 里不含 name,即只在首次注册时写入。
效果相同,零额外字段、零迁移。已同步 04 §3
- 验收项"不向 dry_run 客户端分配真实下单任务"**未实现**:
tasks 表没有字段标记任务是否需要真实下单。当前真实下单开关默认关闭、
MVP 全是演练模式,暂不出问题,但开真实下单前必须补该字段,需另开工单
已验证(Go 1.23.0)
- go vet / gofmt / go test 全过,并发测试重复 20 次稳定通过
- 端到端:无任务 claim 204;插入任务后 claim 200 且 payload 含
goods_url/goods_id/options/quantity/max_price_cent、无租约无 Admin 状态;
重复 claim 204;缺 X-Client-Id 400;POST 无 CSRF token 403;
列表页两台客户端在线状态与统计正确
说明:Gitea 尚未配置,本次无对应工单号。
submit_result / submit_failure 及其幂等处理留给下一个工单。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-06 16:56:18 +08:00 |
|
 chengmaandClaude Opus 5
|
799ee33045
|
feat: 搭建 Admin 项目骨架(Go + Gin + html/template)
可运行的骨架:四个页面能打开、数据库自动建表、给 Client 的三个接口
按契约响应。业务逻辑留 35 处 TODO(骨架),每处标明要点和文档章节。
结构(对应 docs/admin/02-architecture.md §2/§3)
- handler/web + handler/api 分开:页面要 CSRF、出错渲染错误页;
接口要认证、出错返回 JSON。混在一起迟早写错
- service 不认识 *gin.Context,方便不起服务器直接单测
- 只有 repository 能写 SQL
- 模板用 {{define "目录/名字"}},一次全部 ParseFS 进来不冲突
- 模板和静态资源用 //go:embed 打进二进制
已实测(Go 1.23.0,本机验证)
- go build / go vet / gofmt 全部通过
- 单文件产物 34MB,无需 cgo
- 四个页面 + 静态资源均 200,/ 正确 302 到 /shopee
- 建表 7 张 + 索引 10 个,user_version=1,二次启动不重复建表
- claim 带 X-Client-Id 返回 204(无任务可领),
缺 X-Client-Id 返回 400 + 契约格式的 JSON 错误体
依赖版本被 Go 1.23.0 卡死,已钉住并写进文档
- gin v1.11.0 (v1.12.0 起要求 Go >= 1.25.0)
- modernc.org/sqlite v1.38.0(v1.40.0 起要求 >= 1.24.0,v1.48.0 起 >= 1.25.0)
- excelize v2.9.1 (尚未入 go.mod,写导入功能时再加)
直接 go get 不带版本会拉到最新版并报 requires go >= 1.25.0,
所以文档里的命令一律带版本号。要升依赖就得先升 Go。
一处主动调整:迁移语句从「一个字符串装多条 SQL」改成 [][]string 逐条执行。
database/sql 的 Exec 对一次多条语句的支持因驱动而异,拆开最稳妥,
报错还能精确到第几条。
说明:Gitea 尚未配置,本次无对应工单号。
admin/testdata/ 只有 README,脱敏小样本需人工制作。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-06 16:33:45 +08:00 |
|