feat: 档口入库码支持软删除与重导恢复 (#238)

This commit is contained in:
chengma
2026-08-15 10:49:28 +08:00
parent 4cbdc81a30
commit 2aea22b44d
13 changed files with 515 additions and 26 deletions
@@ -2,6 +2,7 @@ package repository
import (
"database/sql"
"errors"
"fmt"
"os"
"strings"
@@ -1117,6 +1118,117 @@ func TestMySQLMigrate_V22升级V23且唯一约束生效(t *testing.T) {
}
}
func TestMySQLMigrate_V23升级V24且软删除字段有效(t *testing.T) {
db := openMySQLMigrationTestDB(t)
defer db.Close()
cleanMySQLTestSchema(t, db)
defer cleanMySQLTestSchema(t, db)
if err := MigrateMySQL(db); err != nil {
t.Fatal(err)
}
mustExec(t, db, `ALTER TABLE syb_inner_code_records DROP FOREIGN KEY fk_inner_code_deleted_by`)
mustExec(t, db, `ALTER TABLE syb_inner_code_records DROP INDEX idx_inner_code_deleted`)
mustExec(t, db, `ALTER TABLE syb_inner_code_records DROP COLUMN deleted_by_user_id,DROP COLUMN deleted_at`)
mustExec(t, db, `DELETE FROM schema_migrations WHERE version=24`)
if err := MigrateMySQL(db); err != nil {
t.Fatalf("v23 升级 v24 失败: %v", err)
}
if err := MigrateMySQL(db); err != nil {
t.Fatalf("v24 重复迁移失败: %v", err)
}
if err := checkMySQLV24Shape(db); err != nil {
t.Fatal(err)
}
}
func TestUpsertInnerCodeImportRow_软删除记录按状态安全恢复(t *testing.T) {
db := openMySQLMigrationTestDB(t)
defer db.Close()
cleanMySQLTestSchema(t, db)
defer cleanMySQLTestSchema(t, db)
if err := MigrateMySQL(db); err != nil {
t.Fatal(err)
}
now := "2026-08-15T03:00:00Z"
mustExec(t, db, `INSERT INTO users(user_id,username,password_hash,role,status,password_changed_at,created_at,updated_at)
VALUES('restore-user','restore-user','hash','purchaser','active',?,?,?)`, now, now, now)
for _, row := range []struct {
orderNumber, stall, specKey, innerCode, status string
}{
{"ORDER-F", "A#1", "黑色,M", "DK-F", "failed"},
{"ORDER-U", "B#2", "白色,L", "DK-U", "updated"},
{"ORDER-N", "C#3", "杏色,S", "DK-N", "needs_check"},
} {
mustExec(t, db, `INSERT INTO syb_inner_code_records
(business_date,source_row,order_number,stall,spec_raw,spec_key,inner_code,stock_id,status,
result_message,created_by_user_id,deleted_at,deleted_by_user_id,created_at,updated_at)
VALUES('2026-08-15',2,?,?,?,?,?,99,?,'旧审计','restore-user',?,'restore-user',?,?)`,
row.orderNumber, row.stall, row.specKey, row.specKey, row.innerCode, row.status, now, now, now)
}
failedRow := model.InnerCodeImportRow{BusinessDate: "2026-08-15", SourceRow: 8, OrderNumber: "ORDER-F",
Stall: "A#1", SpecRaw: "黑色,M", SpecKey: "黑色,M", InnerCode: "DK-F-NEW",
SourceDuplicateCount: 1, CreatedByUserID: "restore-user"}
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
outcome, err := UpsertInnerCodeImportRow(tx, failedRow, "2026-08-15T04:00:00Z")
if err != nil || outcome != InnerCodeImportRestored {
tx.Rollback()
t.Fatalf("失败记录恢复不正确 outcome=%s err=%v", outcome, err)
}
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
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 {
t.Fatal(err)
}
if status != "pending" || innerCode != "DK-F-NEW" || stockID.Valid || deletedAt.Valid || message.Valid {
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",
SourceDuplicateCount: 1, CreatedByUserID: "restore-user"}
tx, err = db.Begin()
if err != nil {
t.Fatal(err)
}
outcome, err = UpsertInnerCodeImportRow(tx, updatedRow, "2026-08-15T04:00:00Z")
if err != nil || outcome != InnerCodeImportRestored {
tx.Rollback()
t.Fatalf("已完成记录恢复不正确 outcome=%s err=%v", outcome, err)
}
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 {
t.Fatal(err)
}
if status != "updated" || !stockID.Valid || deletedAt.Valid || !message.Valid || message.String != "旧审计" {
t.Fatalf("已完成记录恢复后未保留审计: status=%s stock=%+v deleted=%+v message=%+v", status, stockID, deletedAt, message)
}
conflictRow := model.InnerCodeImportRow{BusinessDate: "2026-08-15", SourceRow: 10, OrderNumber: "ORDER-N",
Stall: "C#3", SpecRaw: "杏色,S", SpecKey: "杏色,S", InnerCode: "DK-N-CHANGED",
SourceDuplicateCount: 1, CreatedByUserID: "restore-user"}
tx, err = db.Begin()
if err != nil {
t.Fatal(err)
}
_, err = UpsertInnerCodeImportRow(tx, conflictRow, "2026-08-15T04:00:00Z")
tx.Rollback()
if !errors.Is(err, ErrInnerCodeRestoreConflict) {
t.Fatalf("需核对记录换入库码应拒绝,实际 %v", err)
}
}
func mustExec(t *testing.T, db *sql.DB, query string, args ...any) {
t.Helper()
if _, err := db.Exec(query, args...); err != nil {