fix: 使用原始SKU辅助档口匹配 (#259)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user