fix: 多件档口入库码逐件回写 (#250)
This commit is contained in:
@@ -156,7 +156,7 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
|
||||
SET source_row=?,print_sequence=?,shop_name=?,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,result_message=NULL,planned_at=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),
|
||||
@@ -182,6 +182,7 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
|
||||
purchase_platform=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check') THEN purchase_platform ELSE NULL END,
|
||||
purchase_code=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check') THEN purchase_code ELSE NULL END,
|
||||
remote_inner_code=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check') THEN remote_inner_code ELSE NULL END,
|
||||
remote_items_json=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check') THEN remote_items_json ELSE NULL END,
|
||||
result_message=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check') THEN result_message ELSE NULL END,
|
||||
planned_at=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check') THEN planned_at ELSE NULL END,
|
||||
apply_batch_id=CASE WHEN status IN ('queued','updated','already_filled','applying','needs_check') THEN apply_batch_id ELSE NULL END,
|
||||
@@ -303,7 +304,7 @@ func SaveInnerCodePlans(db *sql.DB, plans []model.InnerCodeRecord, plannedAt str
|
||||
UPDATE syb_inner_code_records
|
||||
SET stock_id=?,detail_id=?,syb_spec=?,syb_sku=?,syb_variation_sku=?,
|
||||
purchase_platform=?,purchase_code=?,remote_inner_code=?,status=?,
|
||||
result_message=?,planned_at=?,apply_batch_id=NULL,apply_queued_at=NULL,
|
||||
remote_items_json=NULL,result_message=?,planned_at=?,apply_batch_id=NULL,apply_queued_at=NULL,
|
||||
apply_started_at=NULL,updated_at=?
|
||||
WHERE id=? AND deleted_at IS NULL AND status IN ('pending','ready','skipped','failed')`,
|
||||
nullablePositiveInt64(plan.StockID), nullablePositiveInt64(plan.DetailID),
|
||||
@@ -405,7 +406,7 @@ const innerCodeListColumns = `id,business_date,source_row,COALESCE(print_sequenc
|
||||
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,''),
|
||||
COALESCE(remote_inner_code,''),status,COALESCE(result_message,''),created_by_user_id,
|
||||
COALESCE(remote_inner_code,''),COALESCE(remote_items_json,''),status,COALESCE(result_message,''),created_by_user_id,
|
||||
COALESCE(applied_by_user_id,''),COALESCE(planned_at,''),COALESCE(apply_started_at,''),
|
||||
COALESCE(applied_at,''),COALESCE(deleted_at,''),COALESCE(deleted_by_user_id,''),created_at,updated_at`
|
||||
|
||||
@@ -485,7 +486,7 @@ func scanInnerCodeRecord(scanner innerCodeRowScanner) (model.InnerCodeRecord, er
|
||||
&row.InnerCode, &row.SourceDuplicateCount, &row.ApplyBatchID, &row.ApplyQueuedAt,
|
||||
&row.StockID, &row.DetailID,
|
||||
&row.SybSpec, &row.SybSKU, &row.SybVariationSKU, &row.PurchasePlatform,
|
||||
&row.PurchaseCode, &row.RemoteInnerCode, &row.Status, &row.ResultMessage,
|
||||
&row.PurchaseCode, &row.RemoteInnerCode, &row.RemoteItemsJSON, &row.Status, &row.ResultMessage,
|
||||
&row.CreatedByUserID, &row.AppliedByUserID, &row.PlannedAt, &row.ApplyStartedAt,
|
||||
&row.AppliedAt, &row.DeletedAt, &row.DeletedByUserID, &row.CreatedAt, &row.UpdatedAt)
|
||||
if err != nil {
|
||||
@@ -654,6 +655,21 @@ func FinishInnerCodeApply(q Execer, id int64, status model.InnerCodeStatus, mess
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveInnerCodeRemoteItems 在每个远端动作前后保存逐件检查点。
|
||||
// 只有 applying 记录可更新,避免后台执行器覆盖已经被人工处理的结果。
|
||||
func SaveInnerCodeRemoteItems(q Execer, id int64, remoteItemsJSON, message, updatedAt string) error {
|
||||
result, err := q.Exec(`UPDATE syb_inner_code_records
|
||||
SET remote_items_json=?,result_message=?,updated_at=?
|
||||
WHERE id=? AND status='applying'`, nullableString(remoteItemsJSON), nullableString(message), updatedAt, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存档口入库码逐件检查点失败: %w", err)
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil || affected != 1 {
|
||||
return fmt.Errorf("档口入库码记录 %d 已不在回写中,拒绝保存逐件检查点", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetInnerCodeForRecheck 读取一条需核对记录。
|
||||
func GetInnerCodeForRecheck(q Execer, id int64) (*model.InnerCodeRecord, error) {
|
||||
record, err := scanInnerCodeRecord(q.QueryRow(`SELECT `+innerCodeListColumns+
|
||||
@@ -705,11 +721,11 @@ func SoftDeleteInnerCodeRecords(db *sql.DB, ids []int64, actorUserID, deletedAt
|
||||
}
|
||||
|
||||
// SaveInnerCodeRecheck 保存只读重新核对的远端结果,不执行状态领取或写入。
|
||||
func SaveInnerCodeRecheck(q Execer, id int64, status model.InnerCodeStatus, message, remoteCode, checkedAt string) error {
|
||||
func SaveInnerCodeRecheck(q Execer, id int64, status model.InnerCodeStatus, message, remoteCode, remoteItemsJSON, checkedAt string) error {
|
||||
result, err := q.Exec(`UPDATE syb_inner_code_records
|
||||
SET status=?,result_message=?,remote_inner_code=?,
|
||||
SET status=?,result_message=?,remote_inner_code=?,remote_items_json=?,
|
||||
applied_at=CASE WHEN ?='updated' THEN ? ELSE applied_at END,updated_at=?
|
||||
WHERE id=? AND status='needs_check'`, status, message, nullableString(remoteCode), status, checkedAt, checkedAt, id)
|
||||
WHERE id=? AND status='needs_check'`, status, message, nullableString(remoteCode), nullableString(remoteItemsJSON), status, checkedAt, checkedAt, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存档口入库码核对结果失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"cmautobuy/admin/spec"
|
||||
)
|
||||
|
||||
const mysqlSchemaVersion = 26
|
||||
const mysqlSchemaVersion = 27
|
||||
|
||||
// OpenMySQL 打开生产 MySQL 8 数据库。错误信息绝不包含完整 DSN 或密码。
|
||||
func OpenMySQL(cfg config.DatabaseConfig) (*sql.DB, error) {
|
||||
@@ -737,9 +737,34 @@ func MigrateMySQL(db *sql.DB) error {
|
||||
return fmt.Errorf("记录 MySQL schema v26 失败: %w", err)
|
||||
}
|
||||
}
|
||||
if current < 27 {
|
||||
if err := migrateMySQLV27(db); err != nil {
|
||||
return fmt.Errorf("执行 MySQL schema v27 失败: %w", err)
|
||||
}
|
||||
if err := checkMySQLV27Shape(db); err != nil {
|
||||
return fmt.Errorf("MySQL schema v27 自检失败,未记录版本: %w", err)
|
||||
}
|
||||
if _, err := db.Exec(`INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)`, 27, time.Now().UTC().Format(time.RFC3339Nano)); err != nil {
|
||||
return fmt.Errorf("记录 MySQL schema v27 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return CheckMySQLSchema(db)
|
||||
}
|
||||
|
||||
// migrateMySQLV27 保存多件档口入库码逐件远端检查点,不拆分新的业务表。
|
||||
func migrateMySQLV27(db *sql.DB) error {
|
||||
exists, err := mysqlColumnExists(db, "syb_inner_code_records", "remote_items_json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
_, err = db.Exec(`ALTER TABLE syb_inner_code_records
|
||||
ADD COLUMN remote_items_json JSON NULL AFTER remote_inner_code`)
|
||||
return err
|
||||
}
|
||||
|
||||
// migrateMySQLV26 建立采购运行时规格解析审计。一个记录同时保存候选观察和最终决策,
|
||||
// 不覆盖 PDD 商品主数据,也不通过外键阻止任务的既有硬删除流程。
|
||||
func migrateMySQLV26(db *sql.DB) error {
|
||||
@@ -2320,7 +2345,23 @@ func CheckMySQLSchema(db *sql.DB) error {
|
||||
if err := checkMySQLV25Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV26Shape(db)
|
||||
if err := checkMySQLV26Shape(db); err != nil {
|
||||
return err
|
||||
}
|
||||
return checkMySQLV27Shape(db)
|
||||
}
|
||||
|
||||
func checkMySQLV27Shape(db *sql.DB) error {
|
||||
var dataType, nullable string
|
||||
if err := db.QueryRow(`SELECT data_type,is_nullable FROM information_schema.columns
|
||||
WHERE table_schema=DATABASE() AND table_name='syb_inner_code_records' AND column_name='remote_items_json'`).
|
||||
Scan(&dataType, &nullable); err != nil {
|
||||
return fmt.Errorf("检查档口入库码逐件检查点字段失败: %w", err)
|
||||
}
|
||||
if dataType != "json" || nullable != "YES" {
|
||||
return fmt.Errorf("档口入库码逐件检查点字段 remote_items_json 结构不正确")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMySQLV26Shape(db *sql.DB) error {
|
||||
|
||||
Reference in New Issue
Block a user