fix: 使用原始SKU辅助档口匹配 (#259)

This commit is contained in:
chengma
2026-08-18 15:25:58 +08:00
parent 103610a012
commit 4383bb87df
9 changed files with 237 additions and 33 deletions
+18 -11
View File
@@ -128,12 +128,12 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
if errors.Is(err, sql.ErrNoRows) {
_, err = tx.Exec(`
INSERT INTO syb_inner_code_records (
business_date,source_row,print_sequence,order_number,shop_name,stall,
business_date,source_row,print_sequence,order_number,shop_name,stall,source_sku_raw,
spec_raw,spec_key,inner_code,source_duplicate_count,status,
created_by_user_id,created_at,updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,'pending',?,?,?)`,
) VALUES (?,?,?,?,?,?,?,?,?,?,?,'pending',?,?,?)`,
row.BusinessDate, row.SourceRow, nullablePositiveInt(row.PrintSequence), row.OrderNumber,
nullableString(row.ShopName), row.Stall, row.SpecRaw, row.SpecKey, row.InnerCode,
nullableString(row.ShopName), row.Stall, nullableStringPreserveSpace(row.SourceSKURaw), row.SpecRaw, row.SpecKey, row.InnerCode,
row.SourceDuplicateCount, row.CreatedByUserID, now, now)
if err != nil {
return "", innerCodeImportWriteError("新增档口入库码记录失败", err)
@@ -147,20 +147,20 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
ErrInnerCodeRestoreConflict, id, currentInnerCode, row.InnerCode)
}
_, err = tx.Exec(`UPDATE syb_inner_code_records
SET source_row=?,print_sequence=?,shop_name=?,spec_raw=?,source_duplicate_count=?,
SET source_row=?,print_sequence=?,shop_name=?,source_sku_raw=?,spec_raw=?,source_duplicate_count=?,
deleted_at=NULL,deleted_by_user_id=NULL,updated_at=?
WHERE id=?`, row.SourceRow, nullablePositiveInt(row.PrintSequence), nullableString(row.ShopName),
row.SpecRaw, row.SourceDuplicateCount, now, id)
nullableStringPreserveSpace(row.SourceSKURaw), row.SpecRaw, row.SourceDuplicateCount, now, id)
} else {
_, err = tx.Exec(`UPDATE syb_inner_code_records
SET source_row=?,print_sequence=?,shop_name=?,spec_raw=?,inner_code=?,source_duplicate_count=?,
SET source_row=?,print_sequence=?,shop_name=?,source_sku_raw=?,spec_raw=?,inner_code=?,source_duplicate_count=?,
status='pending',stock_id=NULL,detail_id=NULL,syb_spec=NULL,syb_sku=NULL,
syb_variation_sku=NULL,purchase_platform=NULL,purchase_code=NULL,
remote_inner_code=NULL,remote_items_json=NULL,result_message=NULL,planned_at=NULL,
apply_batch_id=NULL,apply_queued_at=NULL,apply_started_at=NULL,
deleted_at=NULL,deleted_by_user_id=NULL,updated_at=?
WHERE id=?`, row.SourceRow, nullablePositiveInt(row.PrintSequence), nullableString(row.ShopName),
row.SpecRaw, row.InnerCode, row.SourceDuplicateCount, now, id)
nullableStringPreserveSpace(row.SourceSKURaw), row.SpecRaw, row.InnerCode, row.SourceDuplicateCount, now, id)
}
if err != nil {
return "", innerCodeImportWriteError("恢复档口入库码导入记录失败", err)
@@ -170,7 +170,7 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
_, err = tx.Exec(`
UPDATE syb_inner_code_records
SET source_row=?,print_sequence=?,shop_name=?,spec_raw=?,inner_code=?,
SET source_row=?,print_sequence=?,shop_name=?,source_sku_raw=?,spec_raw=?,inner_code=?,
source_duplicate_count=?,
status=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check')
THEN status ELSE 'pending' END,
@@ -190,7 +190,7 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
apply_started_at=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check') THEN apply_started_at ELSE NULL END,
updated_at=?
WHERE id=?`,
row.SourceRow, nullablePositiveInt(row.PrintSequence), nullableString(row.ShopName), row.SpecRaw,
row.SourceRow, nullablePositiveInt(row.PrintSequence), nullableString(row.ShopName), nullableStringPreserveSpace(row.SourceSKURaw), row.SpecRaw,
row.InnerCode, row.SourceDuplicateCount, now, id)
if err != nil {
return "", innerCodeImportWriteError("更新档口入库码导入记录失败", err)
@@ -402,7 +402,7 @@ func innerCodeStatusHoldsDetail(status model.InnerCodeStatus) bool {
}
const innerCodeListColumns = `id,business_date,source_row,COALESCE(print_sequence,0),order_number,
COALESCE(shop_name,''),stall,spec_raw,spec_key,inner_code,source_duplicate_count,
COALESCE(shop_name,''),stall,COALESCE(source_sku_raw,''),spec_raw,spec_key,inner_code,source_duplicate_count,
COALESCE(apply_batch_id,''),COALESCE(apply_queued_at,''),
COALESCE(stock_id,0),COALESCE(detail_id,0),COALESCE(syb_spec,''),COALESCE(syb_sku,''),
COALESCE(syb_variation_sku,''),COALESCE(purchase_platform,''),COALESCE(purchase_code,''),
@@ -482,7 +482,7 @@ type innerCodeRowScanner interface {
func scanInnerCodeRecord(scanner innerCodeRowScanner) (model.InnerCodeRecord, error) {
var row model.InnerCodeRecord
err := scanner.Scan(&row.ID, &row.BusinessDate, &row.SourceRow, &row.PrintSequence,
&row.OrderNumber, &row.ShopName, &row.Stall, &row.SpecRaw, &row.SpecKey,
&row.OrderNumber, &row.ShopName, &row.Stall, &row.SourceSKURaw, &row.SpecRaw, &row.SpecKey,
&row.InnerCode, &row.SourceDuplicateCount, &row.ApplyBatchID, &row.ApplyQueuedAt,
&row.StockID, &row.DetailID,
&row.SybSpec, &row.SybSKU, &row.SybVariationSKU, &row.PurchasePlatform,
@@ -780,3 +780,10 @@ func nullableString(value string) any {
}
return value
}
func nullableStringPreserveSpace(value string) any {
if strings.TrimSpace(value) == "" {
return nil
}
return value
}
+34 -2
View File
@@ -20,7 +20,7 @@ import (
"cmautobuy/admin/spec"
)
const mysqlSchemaVersion = 27
const mysqlSchemaVersion = 28
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
@@ -748,9 +748,34 @@ func MigrateMySQL(db *sql.DB) error {
return fmt.Errorf("记录 MySQL schema v27 失败: %w", err)
}
}
if current < 28 {
if err := migrateMySQLV28(db); err != nil {
return fmt.Errorf("执行 MySQL schema v28 失败: %w", err)
}
if err := checkMySQLV28Shape(db); err != nil {
return fmt.Errorf("MySQL schema v28 自检失败,未记录版本: %w", err)
}
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 28, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
return fmt.Errorf("记录 MySQL schema v28 失败: %w", err)
}
}
return CheckMySQLSchema(db)
}
// migrateMySQLV28 保留 Excel 原始 SKU,作为确定性匹配的可审计证据。
func migrateMySQLV28(db *sql.DB) error {
exists, err := mysqlColumnExists(db, "syb_inner_code_records", "source_sku_raw")
if err != nil {
return err
}
if exists {
return nil
}
_, err = db.Exec(`ALTER TABLE syb_inner_code_records
ADD COLUMN source_sku_raw VARCHAR(500) COLLATE utf8mb4_bin NULL AFTER stall`)
return err
}
// migrateMySQLV27 保存多件档口入库码逐件远端检查点,不拆分新的业务表。
func migrateMySQLV27(db *sql.DB) error {
exists, err := mysqlColumnExists(db, "syb_inner_code_records", "remote_items_json")
@@ -2348,7 +2373,14 @@ func CheckMySQLSchema(db *sql.DB) error {
if err := checkMySQLV26Shape(db); err != nil {
return err
}
return checkMySQLV27Shape(db)
if err := checkMySQLV27Shape(db); err != nil {
return err
}
return checkMySQLV28Shape(db)
}
func checkMySQLV28Shape(db *sql.DB) error {
return checkMySQLVarcharColumn(db, "syb_inner_code_records", "source_sku_raw", 500, true, "utf8mb4_bin", "")
}
func checkMySQLV27Shape(db *sql.DB) error {
@@ -1286,7 +1286,7 @@ func TestUpsertInnerCodeImportRow_软删除记录按状态安全恢复(t *testin
}
failedRow := model.InnerCodeImportRow{BusinessDate: "2026-08-15", SourceRow: 8, OrderNumber: "ORDER-F",
Stall: "A#1", SpecRaw: "黑色,M", SpecKey: "黑色,M", InnerCode: "DK-F-NEW",
Stall: "A#1", SourceSKURaw: " SOURCE-F ", SpecRaw: "黑色,M", SpecKey: "黑色,M", InnerCode: "DK-F-NEW",
SourceDuplicateCount: 1, CreatedByUserID: "restore-user"}
tx, err := db.Begin()
if err != nil {
@@ -1302,17 +1302,17 @@ func TestUpsertInnerCodeImportRow_软删除记录按状态安全恢复(t *testin
}
var status, innerCode string
var stockID sql.NullInt64
var deletedAt, message sql.NullString
if err := db.QueryRow(`SELECT status,inner_code,stock_id,deleted_at,result_message FROM syb_inner_code_records WHERE order_number='ORDER-F'`).
Scan(&status, &innerCode, &stockID, &deletedAt, &message); err != nil {
var deletedAt, message, sourceSKU sql.NullString
if err := db.QueryRow(`SELECT status,inner_code,stock_id,deleted_at,result_message,source_sku_raw FROM syb_inner_code_records WHERE order_number='ORDER-F'`).
Scan(&status, &innerCode, &stockID, &deletedAt, &message, &sourceSKU); err != nil {
t.Fatal(err)
}
if status != "pending" || innerCode != "DK-F-NEW" || stockID.Valid || deletedAt.Valid || message.Valid {
if status != "pending" || innerCode != "DK-F-NEW" || stockID.Valid || deletedAt.Valid || message.Valid || sourceSKU.String != " SOURCE-F " {
t.Fatalf("未完成记录恢复后未重置: status=%s code=%s stock=%+v deleted=%+v message=%+v", status, innerCode, stockID, deletedAt, message)
}
updatedRow := model.InnerCodeImportRow{BusinessDate: "2026-08-15", SourceRow: 9, OrderNumber: "ORDER-U",
Stall: "B#2", SpecRaw: "白色,L", SpecKey: "白色,L", InnerCode: "DK-U",
Stall: "B#2", SourceSKURaw: "SOURCE-U", SpecRaw: "白色,L", SpecKey: "白色,L", InnerCode: "DK-U",
SourceDuplicateCount: 1, CreatedByUserID: "restore-user"}
tx, err = db.Begin()
if err != nil {
@@ -1326,11 +1326,11 @@ func TestUpsertInnerCodeImportRow_软删除记录按状态安全恢复(t *testin
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
if err := db.QueryRow(`SELECT status,stock_id,deleted_at,result_message FROM syb_inner_code_records WHERE order_number='ORDER-U'`).
Scan(&status, &stockID, &deletedAt, &message); err != nil {
if err := db.QueryRow(`SELECT status,stock_id,deleted_at,result_message,source_sku_raw FROM syb_inner_code_records WHERE order_number='ORDER-U'`).
Scan(&status, &stockID, &deletedAt, &message, &sourceSKU); err != nil {
t.Fatal(err)
}
if status != "updated" || !stockID.Valid || deletedAt.Valid || !message.Valid || message.String != "旧审计" {
if status != "updated" || !stockID.Valid || deletedAt.Valid || !message.Valid || message.String != "旧审计" || sourceSKU.String != "SOURCE-U" {
t.Fatalf("已完成记录恢复后未保留审计: status=%s stock=%+v deleted=%+v message=%+v", status, stockID, deletedAt, message)
}