fix: 明确拦截档口入库码唯一冲突 (#231)

This commit is contained in:
chengma
2026-08-15 09:12:23 +08:00
parent 9ca7f83289
commit c0eb957097
3 changed files with 44 additions and 2 deletions
+28 -2
View File
@@ -6,9 +6,14 @@ import (
"fmt"
"strings"
"github.com/go-sql-driver/mysql"
"cmautobuy/admin/model"
)
// ErrInnerCodeUniqueConflict 表示同一日期的入库码已属于另一个业务键。
var ErrInnerCodeUniqueConflict = errors.New("同一业务日期的档口入库码已被其他记录使用")
// InnerCodeSybSnapshot 是本地顺运宝明细用于解析货运单 stock id 的最小快照。
type InnerCodeSybSnapshot struct {
OrderNumber string
@@ -40,6 +45,17 @@ const (
// UpsertInnerCodeImportRow 按已确认业务键写入一行。
// 必须在事务中调用;先锁定业务键,避免另一个唯一键冲突时更新错行。
func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now string) (InnerCodeImportOutcome, error) {
var codeRecordID int64
var codeOrder, codeStall, codeSpecKey string
codeErr := tx.QueryRow(`SELECT id,order_number,stall,spec_key FROM syb_inner_code_records
WHERE business_date=? AND inner_code=? FOR UPDATE`, row.BusinessDate, row.InnerCode).
Scan(&codeRecordID, &codeOrder, &codeStall, &codeSpecKey)
if codeErr != nil && !errors.Is(codeErr, sql.ErrNoRows) {
return "", fmt.Errorf("核对档口入库码唯一性失败: %w", codeErr)
}
if codeErr == nil && (codeOrder != row.OrderNumber || codeStall != row.Stall || codeSpecKey != row.SpecKey) {
return "", fmt.Errorf("%w(记录 %d)", ErrInnerCodeUniqueConflict, codeRecordID)
}
var id int64
err := tx.QueryRow(`
SELECT id FROM syb_inner_code_records
@@ -59,7 +75,7 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
nullableString(row.ShopName), row.Stall, row.SpecRaw, row.SpecKey, row.InnerCode,
row.SourceDuplicateCount, row.CreatedByUserID, now, now)
if err != nil {
return "", fmt.Errorf("新增档口入库码记录失败: %w", err)
return "", innerCodeImportWriteError("新增档口入库码记录失败", err)
}
return InnerCodeImportCreated, nil
}
@@ -85,11 +101,21 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
row.SourceRow, nullablePositiveInt(row.PrintSequence), nullableString(row.ShopName), row.SpecRaw,
row.InnerCode, row.SourceDuplicateCount, now, id)
if err != nil {
return "", fmt.Errorf("更新档口入库码导入记录失败: %w", err)
return "", innerCodeImportWriteError("更新档口入库码导入记录失败", err)
}
return InnerCodeImportUpdated, nil
}
func innerCodeImportWriteError(action string, err error) error {
// 并发导入可能都在 SELECT 时看不到对方,最终仍由唯一索引裁决。
// 不把 MySQL 索引名或 SQL 原文显示给操作员。
var mysqlError *mysql.MySQLError
if errors.As(err, &mysqlError) && mysqlError.Number == 1062 {
return fmt.Errorf("%s: %w", action, ErrInnerCodeUniqueConflict)
}
return fmt.Errorf("%s: %w", action, err)
}
// ListInnerCodeRecordsForPlanning 返回某日允许重新规划的记录。
func ListInnerCodeRecordsForPlanning(q Execer, businessDate string) ([]model.InnerCodeRecord, error) {
rows, err := q.Query(`