chengma
|
ad1ccc2c60
|
feat: 完善 SYB 蝦皮主数据与软删除 (#203 #204 #205)
|
2026-08-13 10:39:21 +08:00 |
|
chengma
|
07ca1b0afb
|
feat: 支持蝦皮批量创建 PDD 采集任务 (#185)
|
2026-08-12 16:12:35 +08:00 |
|
chengma
|
889f568554
|
feat: 增加蝦皮店铺名称搜索 (#182)
|
2026-08-12 15:09:35 +08:00 |
|
chengma
|
c39f5a7912
|
feat: 增加蝦皮店铺图片筛选 (#150)
|
2026-08-11 12:04:38 +08:00 |
|
chengma
|
38d6a0a04b
|
feat: 展示蝦皮主图和店铺 (#142)
|
2026-08-11 11:18:51 +08:00 |
|
chengma
|
4259be0e55
|
feat: 支持无SKU目录导入策略 (#141)
|
2026-08-11 11:03:18 +08:00 |
|
chengma
|
d1165b9e17
|
feat: 重构顺运宝规格主链路 (#88)
|
2026-08-10 12:24:23 +08:00 |
|
chengma
|
13cfced80d
|
feat: 迁移 Admin Repository 到 MySQL 8 (#79)
|
2026-08-10 02:21:31 +08:00 |
|
chengma
|
518e6df349
|
feat: 统一蝦皮与顺运宝 PDD 关联入口 (#68)
|
2026-08-09 22:40:58 +08:00 |
|
chengma
|
c6ef54dce1
|
feat: 建立顺运宝采购处理状态 (#67)
|
2026-08-09 22:32:19 +08:00 |
|
 chengmaandClaude Opus 5
|
edfe39cc35
|
feat: 蝦皮数据页分页与状态筛选 (#43)
导入真实样本后 /shopee 一次吐 3.4MB / 5195 行。但只加分页会把问题从
「5195 行糊在一起」变成「260 页里藏着 6 个」——那 6 个待补规格的商品
仍然找不到。所以分页和状态筛选一起做。
HTML 3.4MB → 16.7KB。
状态条显示全量而不是本页:「共 5195 个商品 · 第 1/260 页」。
显示「共 20 个商品」会让操作员以为总共就 20 个。筛选后显示筛选结果
总数:「待补规格:6 个商品」。
列表查询和 COUNT 共用同一套筛选条件拼装。分开写两份 WHERE,迟早
有天忘了给 COUNT 也加条件,页码算错而且没人发现(#19 踩过一次)。
page 越界兜到最后一页而不是显示空表格——空表格会让操作员以为数据没了。
总数为 0 时显示「第 1/1 页」,不出现「第 1/0 页」。
「待补规格」用 EXISTS 不用 JOIN+DISTINCT:一个商品有多个失败 SKU 时
JOIN 会出重复行,DISTINCT 又让 LIMIT/OFFSET 的行为难推理。
分页控件是 <a href> 纯 GET,浏览器前进后退和书签都正常。首末页用
<span class="disabled"> 禁用,语义上不再是链接,不只靠颜色区分。
这是全项目第一个分页页面,通用逻辑单独放 service/pagination.go 供
后面四页复用,规则写进 05 §3.2 而不是蝦皮页那一节(#34 踩过这个错)。
05 §3 的每页条数从「建议 50」改为「统一 20」并写明理由。
实现踩到 html/template 的 URL 上下文转义:夹在字面量 & 中间的动态内容
会被整体当成一个参数值转义,?/= 变成 %3F/%3D 让链接失效。改为在 Go 里
把整段 URL 拼好,模板作为单个 pipeline 输出,并加了回归测试。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-08 11:40:07 +08:00 |
|
 chengmaandClaude Opus 5
|
64d5e74d0d
|
feat: 蝦皮数据页改为商品级列表与规格弹窗 (#41)
原来是 SKU 级一行,问题不在行数(6092→5195 只少 15%),在语义错了:
pdd_goods_url / pdd_goods_id 在 shopee_products 上,是商品级字段。
商品 24420648774 有 62 个 SKU,列表里 62 行显示同一个 PDD 链接,
将来双击任意一行编辑的也是同一个字段,操作员会以为改的是这一行。
加了两列,都是防止聚合后丢信息:
「SKU 数」—— 只显示颜色数和尺码数会严重误导。实测 602 个商品(11.6%)
的颜色数×尺码数大于实际 SKU 数,最悬殊的 26886533818 是 15×5=75 但
实际只有 28 个。蝦皮报表只含有销售成绩的 SKU,数据本来就不全。
「待补」—— 原来解析失败的行整行标黄,聚合成数量后这个信号会丢。
23 条失败分布在 6 个商品里,其中 4 个是「部分失败」:数字看着正常,
坏数据被吃掉,永远没人去补。现在待补>0 整行标黄。
颜色数/尺码数只统计 parse_ok=1,否则空值会被算进 DISTINCT 多出
一个「空颜色」。
弹窗只读,待补的行显示 spec_raw 原文——不显示的话人工不知道该填什么,
保留原文这个设计就白做了。复用 #18 的弹窗机制,app.js 未改动。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-08 11:12:25 +08:00 |
|
 chengmaandClaude Opus 5
|
e29d6683ad
|
feat: 蝦皮 Excel 报表导入 (#38)
蝦皮数据模块此前整个是骨架,四个入口全返回 501,excelize 连依赖都没装。
本工单做导入和列表,Save/Delete/Collect 保持 501。
真实样本实测:5195 个商品 / 6092 个 SKU,23 行解析失败。
upsert 白名单式,pdd_goods_url / pdd_goods_id 既不在 INSERT 列清单里
也不在 DO UPDATE SET 里。报表没有这两列,写进去就是写空值——操作员
攒几周的 PDD 链接会被一次导入洗光,而且不报错,等到建采购任务才发现。
有独立测试守着:往 DO UPDATE SET 里加回这一行,测试立刻变红。
按 sheet 名字取「最佳表現商品」,不用第 0 个。工作簿有 5 个 sheet,
另外 4 个是广告报表,连 商品規格ID 列都没有;蝦皮调顺序时按下标
会静默导入一张完全不相干的表。
按列名找索引。40 列里 32 列是统计指标,蝦皮加一列所有列号就错位,
而且不报错——会把「點擊率」当成「商品規格」存进去。
规格解析三种格式:含【】53.4%、逗号+空格 26.4%、只有逗号 20.2%,
只认【】会漏掉 46.6%。括号不配对(21 行)和右半整个被【】包住(2 行)
一律判整体失败,不逐段硬凑——错法不统一,针对每种错法写规则等于在猜,
而且没有人工核对过的正确答案可验证。失败时 color/size/advice 必须全空,
原文存进 spec_raw 交人工补。
不写 DELETE + INSERT 的全量替换,将来手动新增的 SKU 会被删掉。
excelize v2.9.1 的 go 指令正好是 1.23.0,与项目固定版本一致。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-08 09:57:12 +08:00 |
|
 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
|
7ad82b7795
|
feat: 实现结果提交接口与幂等处理
补齐 submit_result / submit_failure,Admin 侧的三个接口全部可用,
Client 的完整一圈(领取 → 执行 → 提交)现在能走通了。
实现
- 幂等:idempotency_keys 表。同键同内容返回上次的响应且不重复落库,
同键不同内容返回 409。幂等记录与业务写入在**同一事务**,
分开写的话业务成功但幂等没记上,重试会被重复处理
- 无条件接受(契约 §4.1,最容易写错的一条):
任务已取消、已重派给别人,都照样接受结果——客户端中途不查任务状态,
必然会提交"Admin 这边已经不要了"的结果,而它可能真的已经下过单,
这些数据必须留痕
- 采集任务的结果落到商品级 shopee_products.pdd_data 并置 collected;
失败则置 failed 并把原因写进 collect_error,操作员才看得见
- 失败状态映射:retry_wait→assigned,其余同名
- 三个接口都刷新 last_seen_at
新增 task_claims 表(migrations v2)
契约要求"只有从未分配给该客户端的任务才返回 403",但 assigned_client
只记当前归属,重派后就查不出原来那台领过——而契约又要求那种情况必须接受。
没有这张表这条规则根本没法判断。顺带得到一份审计记录。
修复第二个并发 bug:事务必须 BEGIN IMMEDIATE
并发提交报 SQLITE_BUSY。根因是 Go 的 db.Begin() 默认发 BEGIN DEFERRED,
事务开始时不拿写锁,多个事务各自先读再想升级成写就互相卡死,
这种情况 busy_timeout 救不了。DSN 加 _txlock=immediate 后事务一开始
就排队拿锁。实测 6 个并发事务:默认失败 5/6,加参数后 0/6。
已写进 docs/admin/03-data-model.md §2.1。
已验证(Go 1.23.0)
- 30 个单元测试全过,并发用例重复 20 次稳定通过
- 端到端:claim 200 → 提交 200 → 重复提交返回完全相同的响应 →
同键不同内容 409 → 没领过的客户端 403 → 任务不存在 404 →
缺 Idempotency-Key 400;库里 task=succeeded、幂等 1 条、领取历史 1 条
说明:Gitea 尚未配置,本次无对应工单号。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-06 17:04:49 +08:00 |
|