fix: 恢复并发 SKU 规格唯一键冲突 (#283)

This commit is contained in:
chengma
2026-08-21 09:16:22 +08:00
parent 8652f36b2a
commit 05b04e0349
2 changed files with 111 additions and 2 deletions
+83
View File
@@ -4,13 +4,96 @@ import (
"database/sql"
"errors"
"fmt"
"strings"
"testing"
"github.com/go-sql-driver/mysql"
_ "modernc.org/sqlite"
"cmautobuy/admin/model"
)
type skuSpecConflictExecer struct {
*sql.DB
conflictKey string
inserted bool
}
func (q *skuSpecConflictExecer) Exec(query string, args ...any) (sql.Result, error) {
if !q.inserted && strings.HasPrefix(strings.TrimSpace(query), "INSERT INTO shopee_skus") {
q.inserted = true
_, err := q.DB.Exec(`INSERT INTO shopee_skus(
sku_id,goods_id,spec_raw,spec_key,color,size,advice,parse_ok,is_manual,
source,field_sources,field_observed_at,source_observed_at,created_at,updated_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
"catalog:existing", "GOODS-1", "白色,M", "白色,M", "目录白", "M", "",
1, 0, "xlsx-processed", `{"color":"xlsx-processed","size":"xlsx-processed","advice":"","sku_code":""}`,
`{"color":"2026-08-21T00:00:00Z","size":"2026-08-21T00:00:00Z","advice":"","sku_code":""}`,
"2026-08-21T00:00:00Z", "2026-08-21T00:00:00Z", "2026-08-21T00:00:00Z")
if err != nil {
return nil, err
}
return nil, &mysql.MySQLError{Number: 1062, Message: "Duplicate entry for key '" + q.conflictKey + "'"}
}
return q.DB.Exec(query, args...)
}
func (q *skuSpecConflictExecer) QueryRow(query string, args ...any) *sql.Row {
return q.DB.QueryRow(strings.TrimSuffix(query, " FOR UPDATE"), args...)
}
func newSKUConflictTestDB(t *testing.T, conflictKey string) *skuSpecConflictExecer {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
if _, err := db.Exec(`CREATE TABLE shopee_skus (
sku_id TEXT PRIMARY KEY, shopee_sku_id TEXT, goods_id TEXT NOT NULL,
spec_raw TEXT NOT NULL, spec_key TEXT NOT NULL, color TEXT, size TEXT,
advice TEXT, parse_ok INTEGER NOT NULL, sku_code TEXT, is_manual INTEGER NOT NULL,
source TEXT, field_sources TEXT, field_observed_at TEXT, source_observed_at TEXT,
created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
UNIQUE(goods_id, spec_key)
)`); err != nil {
t.Fatal(err)
}
return &skuSpecConflictExecer{DB: db, conflictKey: conflictKey}
}
func TestUpsertCatalogShopeeSKU_规格唯一键并发冲突后安全重读(t *testing.T) {
q := newSKUConflictTestDB(t, "uq_shopee_skus_spec")
outcome, err := UpsertCatalogShopeeSKU(q, CatalogShopeeSKUInput{
RecordID: "syb:incoming", GoodsID: "GOODS-1", SpecRaw: "白色,M", SpecKey: "白色,M",
Color: "顺运宝白", Size: "M", ParseOK: true, Source: "syb",
ObservedAt: "2026-08-21T00:01:00Z", Now: "2026-08-21T00:01:00Z", UpdatePolicy: "fill_missing",
})
if err != nil || outcome != CatalogSKUSkipped {
t.Fatalf("并发撞键后应重读并按既有规则跳过:outcome=%q err=%v", outcome, err)
}
var id, color, source string
if err := q.QueryRow(`SELECT sku_id,color,source FROM shopee_skus WHERE goods_id='GOODS-1' AND spec_key='白色,M'`).Scan(&id, &color, &source); err != nil {
t.Fatal(err)
}
if id != "catalog:existing" || color != "目录白" || source != "xlsx-processed" {
t.Fatalf("竞争方目录数据不得被 SYB 覆盖:id=%q color=%q source=%q", id, color, source)
}
}
func TestUpsertCatalogShopeeSKU_非规格唯一键冲突仍失败(t *testing.T) {
q := newSKUConflictTestDB(t, "uq_shopee_skus_external")
_, err := UpsertCatalogShopeeSKU(q, CatalogShopeeSKUInput{
RecordID: "syb:incoming", GoodsID: "GOODS-1", SpecRaw: "白色,M", SpecKey: "白色,M",
Color: "顺运宝白", Size: "M", ParseOK: true, Source: "syb",
ObservedAt: "2026-08-21T00:01:00Z", Now: "2026-08-21T00:01:00Z", UpdatePolicy: "fill_missing",
})
var mysqlErr *mysql.MySQLError
if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1062 {
t.Fatalf("非规格唯一键冲突必须原样失败:%v", err)
}
}
func TestCatalogImportRun_登记查询重放和冲突(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {