fix: 恢复并发 SKU 规格唯一键冲突 (#283)
This commit is contained in:
@@ -336,10 +336,23 @@ func findCatalogSKUBySpec(q Execer, goodsID, specKey string) (*catalogSKUStored,
|
||||
return scanCatalogSKU(q.QueryRow(`SELECT sku_id,shopee_sku_id,goods_id,spec_raw,spec_key,color,size,advice,parse_ok,sku_code,is_manual,source,source_observed_at,field_sources,field_observed_at FROM shopee_skus WHERE goods_id=? AND spec_key=?`, goodsID, specKey))
|
||||
}
|
||||
|
||||
func findCatalogSKUBySpecForUpdate(q Execer, goodsID, specKey string) (*catalogSKUStored, error) {
|
||||
// 规格唯一键冲突后必须使用当前读。普通 SELECT 在 MySQL 默认的
|
||||
// REPEATABLE READ 事务中会继续使用第一次查询的快照,看不到竞争方
|
||||
// 刚刚提交的 SKU,导致无法恢复本来可幂等的并发写入。
|
||||
return scanCatalogSKU(q.QueryRow(`SELECT sku_id,shopee_sku_id,goods_id,spec_raw,spec_key,color,size,advice,parse_ok,sku_code,is_manual,source,source_observed_at,field_sources,field_observed_at FROM shopee_skus WHERE goods_id=? AND spec_key=? FOR UPDATE`, goodsID, specKey))
|
||||
}
|
||||
|
||||
func findCatalogSKUByExternalID(q Execer, skuID string) (*catalogSKUStored, error) {
|
||||
return scanCatalogSKU(q.QueryRow(`SELECT sku_id,shopee_sku_id,goods_id,spec_raw,spec_key,color,size,advice,parse_ok,sku_code,is_manual,source,source_observed_at,field_sources,field_observed_at FROM shopee_skus WHERE shopee_sku_id=?`, skuID))
|
||||
}
|
||||
|
||||
func isShopeeSKUSpecUniqueConflict(err error) bool {
|
||||
var mysqlErr *mysql.MySQLError
|
||||
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 &&
|
||||
strings.Contains(mysqlErr.Message, "uq_shopee_skus_spec")
|
||||
}
|
||||
|
||||
// UpsertCatalogShopeeSKU 用真实 ID 或商品规格身份定位同一内部记录,人工行永不覆盖。
|
||||
func UpsertCatalogShopeeSKU(q Execer, in CatalogShopeeSKUInput) (CatalogSKUOutcome, error) {
|
||||
bySpec, err := findCatalogSKUBySpec(q, in.GoodsID, in.SpecKey)
|
||||
@@ -372,10 +385,23 @@ func UpsertCatalogShopeeSKU(q Execer, in CatalogShopeeSKUInput) (CatalogSKUOutco
|
||||
sourcesJSON, _ := json.Marshal(fieldSources)
|
||||
timesJSON, _ := json.Marshal(fieldTimes)
|
||||
_, err = q.Exec(`INSERT INTO shopee_skus(sku_id,shopee_sku_id,goods_id,spec_raw,spec_key,color,size,advice,parse_ok,sku_code,is_manual,source,field_sources,field_observed_at,source_observed_at,created_at,updated_at) VALUES(?,NULLIF(?,''),?,?,?,NULLIF(?,''),NULLIF(?,''),NULLIF(?,''),?,NULLIF(?,''),0,?,?,?,?,?,?)`, in.RecordID, in.ShopeeSKUID, in.GoodsID, in.SpecRaw, in.SpecKey, in.Color, in.Size, in.Advice, parse, in.SKUCode, in.Source, string(sourcesJSON), string(timesJSON), in.ObservedAt, in.Now, in.Now)
|
||||
if err != nil {
|
||||
if err == nil {
|
||||
return CatalogSKUCreated, nil
|
||||
}
|
||||
if !isShopeeSKUSpecUniqueConflict(err) {
|
||||
return "", err
|
||||
}
|
||||
return CatalogSKUCreated, nil
|
||||
|
||||
// 目录导入和 SYB 同步可以同时写同一个规格。唯一键已经证明竞争方
|
||||
// 已提交该业务身份;重新读取后沿用下面原有的来源优先级规则,不能
|
||||
// 把它当成同步失败,也不能用 INSERT IGNORE 静默跳过字段补全。
|
||||
current, err = findCatalogSKUBySpecForUpdate(q, in.GoodsID, in.SpecKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("规格唯一键冲突后重读商品 %s 的 SKU 失败: %w", in.GoodsID, err)
|
||||
}
|
||||
if current == nil {
|
||||
return "", fmt.Errorf("规格唯一键冲突后未找到商品 %s 的规格 %s", in.GoodsID, in.SpecKey)
|
||||
}
|
||||
}
|
||||
if current.ShopeeSKUID.Valid && in.ShopeeSKUID != "" && current.ShopeeSKUID.String != in.ShopeeSKUID {
|
||||
return "", fmt.Errorf("商品 %s 的规格 %s 已对应真实蝦皮 SKU %s", in.GoodsID, in.SpecRaw, current.ShopeeSKUID.String)
|
||||
|
||||
Reference in New Issue
Block a user