feat: 规划档口入库码确定性匹配 (#232)
This commit is contained in:
@@ -4,10 +4,17 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
)
|
||||
|
||||
// InnerCodeSybSnapshot 是本地顺运宝明细用于解析货运单 stock id 的最小快照。
|
||||
type InnerCodeSybSnapshot struct {
|
||||
OrderNumber string
|
||||
SybData string
|
||||
}
|
||||
|
||||
// InnerCodeImportOutcome 说明幂等导入是新增还是更新。
|
||||
type InnerCodeImportOutcome string
|
||||
|
||||
@@ -69,6 +76,99 @@ func UpsertInnerCodeImportRow(tx *sql.Tx, row model.InnerCodeImportRow, now stri
|
||||
return InnerCodeImportUpdated, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
FROM syb_inner_code_records
|
||||
WHERE business_date=? AND status IN ('pending','ready','skipped','failed')
|
||||
ORDER BY source_row,id`, businessDate)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("遍历待规划档口入库码失败: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListInnerCodeSybSnapshots 查询订单号对应的本地 SYB 原始快照。
|
||||
func ListInnerCodeSybSnapshots(q Execer, orderNumbers []string) ([]InnerCodeSybSnapshot, error) {
|
||||
if len(orderNumbers) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
placeholders := make([]string, len(orderNumbers))
|
||||
args := make([]any, len(orderNumbers))
|
||||
for index, orderNumber := range orderNumbers {
|
||||
placeholders[index] = "?"
|
||||
args[index] = orderNumber
|
||||
}
|
||||
rows, err := q.Query(`SELECT order_no,syb_data FROM syb_orders WHERE order_no IN (`+
|
||||
strings.Join(placeholders, ",")+`) ORDER BY order_no,syb_id`, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询档口入库码对应顺运宝快照失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make([]InnerCodeSybSnapshot, 0)
|
||||
for rows.Next() {
|
||||
var row InnerCodeSybSnapshot
|
||||
if err := rows.Scan(&row.OrderNumber, &row.SybData); err != nil {
|
||||
return nil, fmt.Errorf("读取顺运宝快照失败: %w", err)
|
||||
}
|
||||
result = append(result, row)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("遍历顺运宝快照失败: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SaveInnerCodePlans 在同一事务中保存一批只读规划结果。
|
||||
func SaveInnerCodePlans(db *sql.DB, plans []model.InnerCodeRecord, plannedAt string) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始保存档口入库码规划事务失败: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
for _, plan := range plans {
|
||||
result, err := tx.Exec(`
|
||||
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=?,updated_at=?
|
||||
WHERE id=? AND status IN ('pending','ready','skipped','failed')`,
|
||||
nullablePositiveInt64(plan.StockID), nullablePositiveInt64(plan.DetailID),
|
||||
nullableString(plan.SybSpec), nullableString(plan.SybSKU), nullableString(plan.SybVariationSKU),
|
||||
nullableString(plan.PurchasePlatform), nullableString(plan.PurchaseCode),
|
||||
nullableString(plan.RemoteInnerCode), plan.Status, nullableString(plan.ResultMessage),
|
||||
plannedAt, plannedAt, plan.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存档口入库码记录 %d 规划失败: %w", plan.ID, err)
|
||||
}
|
||||
if affected, err := result.RowsAffected(); err != nil || affected != 1 {
|
||||
return fmt.Errorf("档口入库码记录 %d 状态已变化,规划整体未保存", plan.ID)
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("提交档口入库码规划失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nullablePositiveInt(value int) any {
|
||||
if value <= 0 {
|
||||
return nil
|
||||
@@ -76,6 +176,13 @@ func nullablePositiveInt(value int) any {
|
||||
return value
|
||||
}
|
||||
|
||||
func nullablePositiveInt64(value int64) any {
|
||||
if value <= 0 {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func nullableString(value string) any {
|
||||
if value == "" {
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
"cmautobuy/admin/repository"
|
||||
"cmautobuy/admin/syb"
|
||||
)
|
||||
|
||||
// InnerCodeDetailReader 只开放规划所需的顺运宝只读调用,方便无网络测试。
|
||||
type InnerCodeDetailReader interface {
|
||||
DetailListByStock(context.Context, []int64) ([]syb.StockDetail, error)
|
||||
}
|
||||
|
||||
// InnerCodePlanResult 是一次规划的逐状态统计。
|
||||
type InnerCodePlanResult struct {
|
||||
Total int
|
||||
Ready int
|
||||
AlreadyFilled int
|
||||
Skipped int
|
||||
Failed int
|
||||
}
|
||||
|
||||
// PlanInnerCodeRecords 对某个业务日期的可重规划记录读取最新远端详情并原子保存计划。
|
||||
// 本函数绝不调用删除或更新顺运宝接口。
|
||||
func PlanInnerCodeRecords(ctx context.Context, db *sql.DB, reader InnerCodeDetailReader, businessDate string) (*InnerCodePlanResult, error) {
|
||||
if _, err := time.Parse("2006-01-02", businessDate); err != nil {
|
||||
return nil, fmt.Errorf("业务日期格式应为 YYYY-MM-DD")
|
||||
}
|
||||
records, err := repository.ListInnerCodeRecordsForPlanning(db, businessDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &InnerCodePlanResult{Total: len(records)}
|
||||
if len(records) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
orders := uniqueInnerCodeOrders(records)
|
||||
snapshots, err := repository.ListInnerCodeSybSnapshots(db, orders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stockIDsByOrder := innerCodeStockIDsByOrder(snapshots)
|
||||
requested := uniqueInnerCodeStockIDs(records, stockIDsByOrder)
|
||||
detailsByID := make(map[int64]syb.StockDetail, len(requested))
|
||||
for start := 0; start < len(requested); start += 100 {
|
||||
end := start + 100
|
||||
if end > len(requested) {
|
||||
end = len(requested)
|
||||
}
|
||||
batch := requested[start:end]
|
||||
details, err := reader.DetailListByStock(ctx, batch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取顺运宝最新商品详情失败,本次规划未保存: %w", err)
|
||||
}
|
||||
for _, detail := range details {
|
||||
if _, duplicate := detailsByID[detail.ID]; duplicate {
|
||||
return nil, fmt.Errorf("顺运宝重复返回货运单 id=%d,本次规划未保存", detail.ID)
|
||||
}
|
||||
detailsByID[detail.ID] = detail
|
||||
}
|
||||
for _, id := range batch {
|
||||
if _, exists := detailsByID[id]; !exists {
|
||||
return nil, fmt.Errorf("顺运宝响应缺少货运单 id=%d,本次规划未保存", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plans := planInnerCodeRows(records, stockIDsByOrder, detailsByID)
|
||||
for _, plan := range plans {
|
||||
switch plan.Status {
|
||||
case model.InnerCodeReady:
|
||||
result.Ready++
|
||||
case model.InnerCodeAlreadyFilled:
|
||||
result.AlreadyFilled++
|
||||
case model.InnerCodeFailed:
|
||||
result.Failed++
|
||||
default:
|
||||
result.Skipped++
|
||||
}
|
||||
}
|
||||
if err := repository.SaveInnerCodePlans(db, plans, model.NowISO()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func uniqueInnerCodeOrders(records []model.InnerCodeRecord) []string {
|
||||
seen := make(map[string]bool, len(records))
|
||||
result := make([]string, 0, len(records))
|
||||
for _, record := range records {
|
||||
if !seen[record.OrderNumber] {
|
||||
seen[record.OrderNumber] = true
|
||||
result = append(result, record.OrderNumber)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func innerCodeStockIDsByOrder(snapshots []repository.InnerCodeSybSnapshot) map[string][]int64 {
|
||||
sets := make(map[string]map[int64]bool)
|
||||
for _, snapshot := range snapshots {
|
||||
stockID, ok := innerCodeStockIDFromJSON(snapshot.SybData)
|
||||
if !ok || stockID <= 0 {
|
||||
continue
|
||||
}
|
||||
if sets[snapshot.OrderNumber] == nil {
|
||||
sets[snapshot.OrderNumber] = make(map[int64]bool)
|
||||
}
|
||||
sets[snapshot.OrderNumber][stockID] = true
|
||||
}
|
||||
result := make(map[string][]int64, len(sets))
|
||||
for order, set := range sets {
|
||||
for id := range set {
|
||||
result[order] = append(result[order], id)
|
||||
}
|
||||
sort.Slice(result[order], func(i, j int) bool { return result[order][i] < result[order][j] })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func innerCodeStockIDFromJSON(raw string) (int64, bool) {
|
||||
var envelope struct {
|
||||
Stock map[string]any `json:"stock"`
|
||||
}
|
||||
decoder := json.NewDecoder(strings.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
if err := decoder.Decode(&envelope); err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return innerCodeInt64(envelope.Stock["id"])
|
||||
}
|
||||
|
||||
func uniqueInnerCodeStockIDs(records []model.InnerCodeRecord, byOrder map[string][]int64) []int64 {
|
||||
seen := make(map[int64]bool)
|
||||
result := make([]int64, 0)
|
||||
for _, record := range records {
|
||||
ids := byOrder[record.OrderNumber]
|
||||
if len(ids) != 1 || seen[ids[0]] {
|
||||
continue
|
||||
}
|
||||
seen[ids[0]] = true
|
||||
result = append(result, ids[0])
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool { return result[i] < result[j] })
|
||||
return result
|
||||
}
|
||||
|
||||
func planInnerCodeRows(records []model.InnerCodeRecord, stockIDsByOrder map[string][]int64, detailsByID map[int64]syb.StockDetail) []model.InnerCodeRecord {
|
||||
used := make(map[string]map[int64]bool)
|
||||
plans := make([]model.InnerCodeRecord, 0, len(records))
|
||||
for _, record := range records {
|
||||
plan := record
|
||||
plan.StockID, plan.DetailID = 0, 0
|
||||
plan.SybSpec, plan.SybSKU, plan.SybVariationSKU = "", "", ""
|
||||
plan.PurchasePlatform, plan.PurchaseCode, plan.RemoteInnerCode = "", "", ""
|
||||
if strings.TrimSpace(record.SpecRaw) == "" {
|
||||
plans = append(plans, innerCodeSkippedPlan(plan, "Excel 清洗后规格为空,跳过"))
|
||||
continue
|
||||
}
|
||||
if record.SourceDuplicateCount > 1 {
|
||||
plans = append(plans, innerCodeSkippedPlan(plan, "同订单同规格同档口在 Excel 中出现多次,跳过"))
|
||||
continue
|
||||
}
|
||||
stockIDs := stockIDsByOrder[record.OrderNumber]
|
||||
if len(stockIDs) == 0 {
|
||||
plan.Status = model.InnerCodeFailed
|
||||
plan.ResultMessage = "本地未找到对应的顺运宝货运单"
|
||||
plans = append(plans, plan)
|
||||
continue
|
||||
}
|
||||
if len(stockIDs) > 1 {
|
||||
plans = append(plans, innerCodeSkippedPlan(plan, "同一订单号命中多张顺运宝货运单,不能自动选择"))
|
||||
continue
|
||||
}
|
||||
stockID := stockIDs[0]
|
||||
stock := detailsByID[stockID]
|
||||
if used[record.OrderNumber] == nil {
|
||||
used[record.OrderNumber] = make(map[int64]bool)
|
||||
}
|
||||
eligible := make([]syb.DetailItem, 0, len(stock.Details))
|
||||
for _, item := range stock.Details {
|
||||
if used[record.OrderNumber][item.ID] {
|
||||
continue
|
||||
}
|
||||
if innerCodeRawText(item.Raw["purchasePlatform"]) == "" && innerCodeRawText(item.Raw["purchaseCode"]) == "" {
|
||||
eligible = append(eligible, item)
|
||||
}
|
||||
}
|
||||
if len(eligible) == 0 {
|
||||
plans = append(plans, innerCodeSkippedPlan(plan, "没有采购平台和采购单号都为空的顺运宝商品"))
|
||||
continue
|
||||
}
|
||||
matches := matchInnerCodeItems(record.SpecRaw, record.Stall, eligible)
|
||||
if len(matches) == 0 {
|
||||
reason := "候选商品中没有相同规格"
|
||||
if len(matchInnerCodeItems(record.SpecRaw, "", eligible)) > 0 {
|
||||
reason = "规格能匹配,但档口及货号与候选商品不一致"
|
||||
}
|
||||
plans = append(plans, innerCodeSkippedPlan(plan, reason))
|
||||
continue
|
||||
}
|
||||
if len(matches) > 1 {
|
||||
plans = append(plans, innerCodeSkippedPlan(plan, "同一订单存在多条相同规格候选商品,不能自动选择"))
|
||||
continue
|
||||
}
|
||||
matched := matches[0]
|
||||
used[record.OrderNumber][matched.ID] = true
|
||||
plan.StockID = stockID
|
||||
plan.DetailID = matched.ID
|
||||
plan.SybSpec = matched.ProductSpec
|
||||
plan.SybSKU = innerCodeRawText(matched.Raw["sku"])
|
||||
plan.SybVariationSKU = innerCodeRawText(matched.Raw["variationSku"])
|
||||
plan.PurchasePlatform = innerCodeRawText(matched.Raw["purchasePlatform"])
|
||||
plan.PurchaseCode = innerCodeRawText(matched.Raw["purchaseCode"])
|
||||
plan.RemoteInnerCode = innerCodeRawText(matched.Raw["innerExpCode"])
|
||||
if plan.RemoteInnerCode == record.InnerCode {
|
||||
plan.Status = model.InnerCodeAlreadyFilled
|
||||
plan.ResultMessage = "远端已是相同入库码,无需重复写入"
|
||||
} else {
|
||||
plan.Status = model.InnerCodeReady
|
||||
plan.ResultMessage = "唯一匹配,等待操作员确认回写"
|
||||
}
|
||||
plans = append(plans, plan)
|
||||
}
|
||||
return plans
|
||||
}
|
||||
|
||||
func innerCodeSkippedPlan(plan model.InnerCodeRecord, message string) model.InnerCodeRecord {
|
||||
plan.Status = model.InnerCodeSkipped
|
||||
plan.ResultMessage = message
|
||||
return plan
|
||||
}
|
||||
|
||||
func matchInnerCodeItems(spec, stall string, eligible []syb.DetailItem) []syb.DetailItem {
|
||||
matches := make([]syb.DetailItem, 0)
|
||||
for _, item := range eligible {
|
||||
if item.ProductSpec == spec {
|
||||
matches = append(matches, item)
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 {
|
||||
key := NormalizeInnerCodeSpecKey(spec)
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
for _, item := range eligible {
|
||||
if NormalizeInnerCodeSpecKey(item.ProductSpec) == key {
|
||||
matches = append(matches, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filterInnerCodeItemsByStall(matches, stall)
|
||||
}
|
||||
|
||||
func filterInnerCodeItemsByStall(items []syb.DetailItem, stall string) []syb.DetailItem {
|
||||
stall = strings.TrimSpace(stall)
|
||||
if stall == "" {
|
||||
return items
|
||||
}
|
||||
matched := make([]syb.DetailItem, 0)
|
||||
for _, item := range items {
|
||||
if innerCodeStallMatches(stall, item) {
|
||||
matched = append(matched, item)
|
||||
}
|
||||
}
|
||||
if len(matched) > 0 {
|
||||
return matched
|
||||
}
|
||||
for _, item := range items {
|
||||
if innerCodeRawText(item.Raw["sku"]) == "" && innerCodeRawText(item.Raw["variationSku"]) == "" {
|
||||
matched = append(matched, item)
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
func innerCodeStallMatches(stall string, item syb.DetailItem) bool {
|
||||
sku := innerCodeRawText(item.Raw["sku"])
|
||||
variation := innerCodeRawText(item.Raw["variationSku"])
|
||||
blob := sku + " " + variation + " " + item.ProductSpec
|
||||
if strings.Contains(blob, stall) {
|
||||
return true
|
||||
}
|
||||
name, article, hasArticle := strings.Cut(stall, "#")
|
||||
if !hasArticle {
|
||||
return false
|
||||
}
|
||||
name, article = strings.TrimSpace(name), strings.TrimSpace(article)
|
||||
if article != "" {
|
||||
if strings.Contains(blob, "#"+article) || strings.HasPrefix(item.ProductSpec, article+" ") || strings.HasPrefix(item.ProductSpec, article+",") {
|
||||
return true
|
||||
}
|
||||
pattern := regexp.MustCompile(`(?:^|[#\-_/\s])` + regexp.QuoteMeta(article) + `(?:$|[#\-_/,\s])`)
|
||||
return pattern.MatchString(sku) || pattern.MatchString(variation)
|
||||
}
|
||||
return name != "" && (strings.Contains(sku, name) || strings.Contains(variation, name))
|
||||
}
|
||||
|
||||
func innerCodeRawText(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return strings.TrimSpace(typed)
|
||||
case json.Number:
|
||||
return strings.TrimSpace(typed.String())
|
||||
case float64:
|
||||
if typed == float64(int64(typed)) {
|
||||
return strconv.FormatInt(int64(typed), 10)
|
||||
}
|
||||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprint(typed))
|
||||
}
|
||||
}
|
||||
|
||||
func innerCodeInt64(value any) (int64, bool) {
|
||||
switch typed := value.(type) {
|
||||
case json.Number:
|
||||
result, err := typed.Int64()
|
||||
return result, err == nil
|
||||
case float64:
|
||||
return int64(typed), true
|
||||
case string:
|
||||
result, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
|
||||
return result, err == nil
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"cmautobuy/admin/model"
|
||||
"cmautobuy/admin/syb"
|
||||
)
|
||||
|
||||
func TestInnerCodeStockIDFromJSON(t *testing.T) {
|
||||
for _, raw := range []string{
|
||||
`{"stock":{"id":75104587},"detail":{"id":1}}`,
|
||||
`{"stock":{"id":"75104587"},"detail":{"id":1}}`,
|
||||
} {
|
||||
if got, ok := innerCodeStockIDFromJSON(raw); !ok || got != 75104587 {
|
||||
t.Fatalf("解析 stock id 失败 got=%d ok=%v", got, ok)
|
||||
}
|
||||
}
|
||||
if _, ok := innerCodeStockIDFromJSON(`{"stock":{}}`); ok {
|
||||
t.Fatal("缺少 stock.id 时不应成功")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanInnerCodeRows_精确与标准化规格均受档口约束(t *testing.T) {
|
||||
records := []model.InnerCodeRecord{
|
||||
{ID: 1, OrderNumber: "ORDER-1", Stall: "A档#027", SpecRaw: "A-027 黑色,M碼 建議45-55公斤", InnerCode: "DK1", SourceDuplicateCount: 1},
|
||||
{ID: 2, OrderNumber: "ORDER-1", Stall: "B档#102", SpecRaw: "杏色,XL", InnerCode: "DK2", SourceDuplicateCount: 1},
|
||||
}
|
||||
detail := syb.StockDetail{ID: 100, Details: []syb.DetailItem{
|
||||
innerCodeTestItem(11, "A-027 黑色,M", map[string]any{"sku": "A档#027"}),
|
||||
innerCodeTestItem(12, "杏色,XL", map[string]any{"variationSku": "B档-102"}),
|
||||
}}
|
||||
plans := planInnerCodeRows(records, map[string][]int64{"ORDER-1": {100}}, map[int64]syb.StockDetail{100: detail})
|
||||
if len(plans) != 2 || plans[0].Status != model.InnerCodeReady || plans[0].DetailID != 11 || plans[1].DetailID != 12 {
|
||||
t.Fatalf("规划结果不正确: %+v", plans)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanInnerCodeRows_重复空规格采购占用和歧义全部阻断(t *testing.T) {
|
||||
records := []model.InnerCodeRecord{
|
||||
{ID: 1, OrderNumber: "DUP", SpecRaw: "黑色,M", InnerCode: "DK1", SourceDuplicateCount: 2},
|
||||
{ID: 2, OrderNumber: "EMPTY", SpecRaw: "", InnerCode: "DK2", SourceDuplicateCount: 1},
|
||||
{ID: 3, OrderNumber: "PURCHASED", SpecRaw: "黑色,M", InnerCode: "DK3", SourceDuplicateCount: 1},
|
||||
{ID: 4, OrderNumber: "AMBIGUOUS", SpecRaw: "黑色,M", InnerCode: "DK4", SourceDuplicateCount: 1},
|
||||
{ID: 5, OrderNumber: "MISSING", SpecRaw: "黑色,M", InnerCode: "DK5", SourceDuplicateCount: 1},
|
||||
}
|
||||
stocks := map[string][]int64{"DUP": {1}, "EMPTY": {2}, "PURCHASED": {3}, "AMBIGUOUS": {4}}
|
||||
details := map[int64]syb.StockDetail{
|
||||
1: {ID: 1, Details: []syb.DetailItem{innerCodeTestItem(1, "黑色,M", nil)}},
|
||||
2: {ID: 2, Details: []syb.DetailItem{innerCodeTestItem(2, "黑色,M", nil)}},
|
||||
3: {ID: 3, Details: []syb.DetailItem{innerCodeTestItem(3, "黑色,M", map[string]any{"purchasePlatform": "PDD"})}},
|
||||
4: {ID: 4, Details: []syb.DetailItem{innerCodeTestItem(4, "黑色,M", nil), innerCodeTestItem(5, "黑色,M", nil)}},
|
||||
}
|
||||
plans := planInnerCodeRows(records, stocks, details)
|
||||
wants := []model.InnerCodeStatus{model.InnerCodeSkipped, model.InnerCodeSkipped, model.InnerCodeSkipped, model.InnerCodeSkipped, model.InnerCodeFailed}
|
||||
for index, want := range wants {
|
||||
if plans[index].Status != want || plans[index].Status == model.InnerCodeReady {
|
||||
t.Errorf("第 %d 条 status=%s message=%s,期望 %s", index, plans[index].Status, plans[index].ResultMessage, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanInnerCodeRows_同一远端明细不会占用两次(t *testing.T) {
|
||||
records := []model.InnerCodeRecord{
|
||||
{ID: 1, OrderNumber: "ORDER", SpecRaw: "黑色,M", InnerCode: "DK1", SourceDuplicateCount: 1},
|
||||
{ID: 2, OrderNumber: "ORDER", SpecRaw: "黑色,M", InnerCode: "DK2", SourceDuplicateCount: 1},
|
||||
}
|
||||
detail := syb.StockDetail{ID: 10, Details: []syb.DetailItem{innerCodeTestItem(99, "黑色,M", nil)}}
|
||||
plans := planInnerCodeRows(records, map[string][]int64{"ORDER": {10}}, map[int64]syb.StockDetail{10: detail})
|
||||
if plans[0].Status != model.InnerCodeReady || plans[1].Status != model.InnerCodeSkipped {
|
||||
t.Fatalf("占用门禁失败: %+v", plans)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanInnerCodeRows_远端已是目标码(t *testing.T) {
|
||||
record := model.InnerCodeRecord{ID: 1, OrderNumber: "ORDER", SpecRaw: "黑色,M", InnerCode: "DK1", SourceDuplicateCount: 1}
|
||||
item := innerCodeTestItem(1, "黑色,M", map[string]any{"innerExpCode": "DK1"})
|
||||
plans := planInnerCodeRows([]model.InnerCodeRecord{record}, map[string][]int64{"ORDER": {10}}, map[int64]syb.StockDetail{10: {ID: 10, Details: []syb.DetailItem{item}}})
|
||||
if plans[0].Status != model.InnerCodeAlreadyFilled || plans[0].RemoteInnerCode != "DK1" {
|
||||
t.Fatalf("已存在判断失败: %+v", plans[0])
|
||||
}
|
||||
}
|
||||
|
||||
func innerCodeTestItem(id int64, spec string, raw map[string]any) syb.DetailItem {
|
||||
if raw == nil {
|
||||
raw = make(map[string]any)
|
||||
}
|
||||
raw["id"] = id
|
||||
raw["productSpec"] = spec
|
||||
return syb.DetailItem{ID: id, ProductSpec: spec, Raw: raw}
|
||||
}
|
||||
Reference in New Issue
Block a user