feat: 支持勾选部分档口入库码匹配 (#235)

This commit is contained in:
chengma
2026-08-15 09:41:45 +08:00
parent 05b59974e8
commit 63f8407780
10 changed files with 342 additions and 55 deletions
+133 -16
View File
@@ -4,6 +4,7 @@ import (
"database/sql"
"errors"
"fmt"
"sort"
"strings"
"github.com/go-sql-driver/mysql"
@@ -116,32 +117,71 @@ func innerCodeImportWriteError(action string, err error) error {
return fmt.Errorf("%s: %w", action, err)
}
// ListInnerCodeRecordsForPlanning 返回某日允许重新规划的记录。
func ListInnerCodeRecordsForPlanning(q Execer, businessDate string) ([]model.InnerCodeRecord, error) {
rows, err := q.Query(`
SELECT id,business_date,source_row,COALESCE(print_sequence,0),order_number,
COALESCE(shop_name,''),stall,spec_raw,spec_key,inner_code,source_duplicate_count,
status,COALESCE(result_message,''),created_by_user_id,created_at,updated_at
// ListInnerCodeRecordsForPlanning 返回某日选中且允许重新规划的记录。
func ListInnerCodeRecordsForPlanning(q Execer, businessDate string, ids []int64) ([]model.InnerCodeRecord, error) {
if len(ids) == 0 {
return nil, nil
}
placeholders := make([]string, len(ids))
args := make([]any, 0, len(ids)+1)
args = append(args, businessDate)
for index, id := range ids {
placeholders[index] = "?"
args = append(args, id)
}
rows, err := q.Query(`SELECT `+innerCodeListColumns+`
FROM syb_inner_code_records
WHERE business_date=? AND status IN ('pending','ready','skipped','failed')
ORDER BY source_row,id`, businessDate)
WHERE business_date=? AND id IN (`+strings.Join(placeholders, ",")+`)
AND status IN ('pending','ready','skipped','failed')
ORDER BY source_row,id`, args...)
if err != nil {
return nil, fmt.Errorf("查询待规划档口入库码失败: %w", err)
return nil, fmt.Errorf("查询选中档口入库码失败: %w", err)
}
defer rows.Close()
result := make([]model.InnerCodeRecord, 0)
for rows.Next() {
var row model.InnerCodeRecord
if err := rows.Scan(&row.ID, &row.BusinessDate, &row.SourceRow, &row.PrintSequence,
&row.OrderNumber, &row.ShopName, &row.Stall, &row.SpecRaw, &row.SpecKey,
&row.InnerCode, &row.SourceDuplicateCount, &row.Status, &row.ResultMessage,
&row.CreatedByUserID, &row.CreatedAt, &row.UpdatedAt); err != nil {
return nil, fmt.Errorf("读取待规划档口入库码失败: %w", err)
row, err := scanInnerCodeRecord(rows)
if err != nil {
return nil, err
}
result = append(result, row)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("遍历待规划档口入库码失败: %w", err)
return nil, fmt.Errorf("遍历选中档口入库码失败: %w", err)
}
return result, nil
}
// ListInnerCodePlanningContext 返回选中订单的全部同日记录,用于预留未选记录已经占用的远端明细。
func ListInnerCodePlanningContext(q Execer, businessDate string, orderNumbers []string) ([]model.InnerCodeRecord, error) {
if len(orderNumbers) == 0 {
return nil, nil
}
placeholders := make([]string, len(orderNumbers))
args := make([]any, 0, len(orderNumbers)+1)
args = append(args, businessDate)
for index, orderNumber := range orderNumbers {
placeholders[index] = "?"
args = append(args, orderNumber)
}
rows, err := q.Query(`SELECT `+innerCodeListColumns+`
FROM syb_inner_code_records
WHERE business_date=? AND order_number IN (`+strings.Join(placeholders, ",")+`)
ORDER BY source_row,id`, args...)
if err != nil {
return nil, fmt.Errorf("查询档口入库码匹配上下文失败: %w", err)
}
defer rows.Close()
result := make([]model.InnerCodeRecord, 0)
for rows.Next() {
row, err := scanInnerCodeRecord(rows)
if err != nil {
return nil, err
}
result = append(result, row)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("遍历档口入库码匹配上下文失败: %w", err)
}
return result, nil
}
@@ -184,6 +224,9 @@ func SaveInnerCodePlans(db *sql.DB, plans []model.InnerCodeRecord, plannedAt str
return fmt.Errorf("开始保存档口入库码规划事务失败: %w", err)
}
defer tx.Rollback()
if err := lockAndValidateInnerCodePlanClaims(tx, plans); err != nil {
return err
}
for _, plan := range plans {
result, err := tx.Exec(`
UPDATE syb_inner_code_records
@@ -209,6 +252,80 @@ func SaveInnerCodePlans(db *sql.DB, plans []model.InnerCodeRecord, plannedAt str
return nil
}
type innerCodePlanOrderKey struct {
BusinessDate string
OrderNumber string
}
// lockAndValidateInnerCodePlanClaims 串行化同订单的部分匹配,并拒绝覆盖未选记录已有的明细占用。
func lockAndValidateInnerCodePlanClaims(tx *sql.Tx, plans []model.InnerCodeRecord) error {
selected := make(map[int64]bool, len(plans))
keySet := make(map[innerCodePlanOrderKey]bool)
for _, plan := range plans {
selected[plan.ID] = true
keySet[innerCodePlanOrderKey{BusinessDate: plan.BusinessDate, OrderNumber: plan.OrderNumber}] = true
}
keys := make([]innerCodePlanOrderKey, 0, len(keySet))
for key := range keySet {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].BusinessDate == keys[j].BusinessDate {
return keys[i].OrderNumber < keys[j].OrderNumber
}
return keys[i].BusinessDate < keys[j].BusinessDate
})
reserved := make(map[innerCodePlanOrderKey]map[int64]int64, len(keys))
for _, key := range keys {
rows, err := tx.Query(`SELECT id,COALESCE(detail_id,0),status
FROM syb_inner_code_records
WHERE business_date=? AND order_number=? ORDER BY id FOR UPDATE`, key.BusinessDate, key.OrderNumber)
if err != nil {
return fmt.Errorf("锁定档口入库码订单匹配上下文失败: %w", err)
}
for rows.Next() {
var id, detailID int64
var status model.InnerCodeStatus
if err := rows.Scan(&id, &detailID, &status); err != nil {
rows.Close()
return fmt.Errorf("读取档口入库码订单匹配上下文失败: %w", err)
}
if selected[id] || detailID <= 0 || !innerCodeStatusHoldsDetail(status) {
continue
}
if reserved[key] == nil {
reserved[key] = make(map[int64]int64)
}
reserved[key][detailID] = id
}
if err := rows.Err(); err != nil {
rows.Close()
return fmt.Errorf("遍历档口入库码订单匹配上下文失败: %w", err)
}
rows.Close()
}
for _, plan := range plans {
if plan.DetailID <= 0 || !innerCodeStatusHoldsDetail(plan.Status) {
continue
}
key := innerCodePlanOrderKey{BusinessDate: plan.BusinessDate, OrderNumber: plan.OrderNumber}
if ownerID := reserved[key][plan.DetailID]; ownerID > 0 {
return fmt.Errorf("档口入库码记录 %d 的顺运宝商品已被记录 %d 占用,请刷新后重新匹配", plan.ID, ownerID)
}
}
return nil
}
func innerCodeStatusHoldsDetail(status model.InnerCodeStatus) bool {
switch status {
case model.InnerCodeReady, model.InnerCodeApplying, model.InnerCodeUpdated,
model.InnerCodeAlreadyFilled, model.InnerCodeNeedsCheck:
return true
default:
return false
}
}
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(stock_id,0),COALESCE(detail_id,0),COALESCE(syb_spec,''),COALESCE(syb_sku,''),