Files
cmautobuy/admin/service/optionkey.go
T
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

56 lines
2.1 KiB
Go
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.
package service
import (
"encoding/json"
"fmt"
)
// OptionKey 把一个规格组合变成**稳定的字符串标识**。
//
// {"size":"M","color":"黑色"} -> {"color":"黑色","size":"M"}
// {"color":"黑色","size":"M"} -> {"color":"黑色","size":"M"} (同一个结果)
//
// # 为什么需要它
//
// 采集回来的 PDD 数据里**没有 SKU 编号**,一个规格只能靠它的
// options 组合来认。而 JSON 对象的键是无序的:
//
// 存映射时:{"color":"黑色","size":"M"}
// 采回来时:{"size":"M","color":"黑色"}
//
// 这两个是同一个规格,但字符串不相等。直接比原始 JSON 会匹配不上,
// 而且是**静默失效**——不报错,只是查不到,最后表现为"明明匹配过却说待匹配"。
//
// # 为什么只能有这一处实现
//
// 存映射用它算 key,查规格也用它算 key,两边必须**逐字节一致**。
// 如果 Client 那边也算一份、或者别处再写一个"差不多"的版本,
// 只要有一点点不同(空格、转义、键序),映射就会静默对不上。
// 所以:**Client 只上报 options 对象,key 一律由 Admin 这一个函数算。**
//
// 实现上直接用 json.Marshal —— Go 序列化 map 时会**按键名排序**,
// 这正好就是我们要的规范化,不用自己拼字符串(自己拼容易漏掉
// 值里含分隔符、含引号之类的边界情况)。
func OptionKey(options map[string]string) (string, error) {
if len(options) == 0 {
return "", fmt.Errorf("规格组合不能为空")
}
buf, err := json.Marshal(options)
if err != nil {
return "", fmt.Errorf("规格组合无法序列化: %w", err)
}
return string(buf), nil
}
// OptionKeyFromJSON 从原始 options JSON 算出 key。
//
// 用于处理采集回来的数据:先解析成 map,再走 OptionKey,
// 这样键序、空格、缩进的差异都会被抹平。
func OptionKeyFromJSON(raw string) (string, error) {
var options map[string]string
if err := json.Unmarshal([]byte(raw), &options); err != nil {
return "", fmt.Errorf("options 不是合法的字符串对象: %w", err)
}
return OptionKey(options)
}