Files
cmautobuy/admin/service/inner_code_match_test.go
T

401 lines
17 KiB
Go
Raw Normal View History

package service
import (
"context"
"errors"
"reflect"
"testing"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
"cmautobuy/admin/syb"
)
type batchingInnerCodeReader struct {
batches [][]int64
}
func (r *batchingInnerCodeReader) DetailListByStock(_ context.Context, ids []int64) ([]syb.StockDetail, error) {
r.batches = append(r.batches, append([]int64(nil), ids...))
result := make([]syb.StockDetail, 0, len(ids))
for _, id := range ids {
result = append(result, syb.StockDetail{ID: id})
}
return result, nil
}
func TestReadInnerCodeDetails_总量不限且每批最多100(t *testing.T) {
ids := make([]int64, 205)
for index := range ids {
ids[index] = int64(index + 1)
}
reader := &batchingInnerCodeReader{}
details, err := readInnerCodeDetails(context.Background(), reader, ids)
if err != nil {
t.Fatal(err)
}
if len(details) != len(ids) || len(reader.batches) != 3 {
t.Fatalf("只读匹配应处理全部选择并分 3 批,details=%d batches=%d", len(details), len(reader.batches))
}
for index, want := range []int{100, 100, 5} {
if len(reader.batches[index]) != want {
t.Errorf("第 %d 批数量=%d,期望 %d", index+1, len(reader.batches[index]), want)
}
}
}
type orderQueryInnerCodeReader struct {
rowsByOrder map[string][]syb.StockRow
detailsByID map[int64]syb.StockDetail
queries []string
detailCalls [][]int64
err error
}
func (r *orderQueryInnerCodeReader) ListByOrderNumber(_ context.Context, order string) ([]syb.StockRow, error) {
r.queries = append(r.queries, order)
if r.err != nil {
return nil, r.err
}
return r.rowsByOrder[order], nil
}
func (r *orderQueryInnerCodeReader) DetailListByStock(_ context.Context, ids []int64) ([]syb.StockDetail, error) {
r.detailCalls = append(r.detailCalls, append([]int64(nil), ids...))
result := make([]syb.StockDetail, 0, len(ids))
for _, id := range ids {
if detail, exists := r.detailsByID[id]; exists {
result = append(result, detail)
}
}
return result, nil
}
func TestReadInnerCodeStockIDs_按去重订单直查远端并保留零和多命中(t *testing.T) {
reader := &orderQueryInnerCodeReader{rowsByOrder: map[string][]syb.StockRow{
"ORDER-1": {{ID: 20, Code: "ORDER-1"}, {ID: 10, Code: "ORDER-1"}},
"ORDER-2": {},
}}
got, err := readInnerCodeStockIDs(context.Background(), reader, []string{"ORDER-1", "ORDER-2"})
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(reader.queries, []string{"ORDER-1", "ORDER-2"}) ||
!reflect.DeepEqual(got["ORDER-1"], []int64{10, 20}) || len(got["ORDER-2"]) != 0 {
t.Fatalf("远端货运单定位结果不正确 queries=%v got=%v", reader.queries, got)
}
}
func TestReadInnerCodeStockIDs_协议或网络错误中止整批(t *testing.T) {
reader := &orderQueryInnerCodeReader{err: errors.New("network down")}
if _, err := readInnerCodeStockIDs(context.Background(), reader, []string{"ORDER"}); err == nil {
t.Fatal("远端查询失败时必须中止整批")
}
}
func TestUniqueInnerCodeOrders_同一货运单只查询一次(t *testing.T) {
orders := uniqueInnerCodeOrders([]model.InnerCodeRecord{
{OrderNumber: "ORDER-1"}, {OrderNumber: "ORDER-1"}, {OrderNumber: "ORDER-2"},
})
if !reflect.DeepEqual(orders, []string{"ORDER-1", "ORDER-2"}) {
t.Fatalf("订单号去重结果不正确: %v", orders)
}
}
func TestPlanInnerCodeRecords_本地没有SybOrder仍可直查远端生成计划(t *testing.T) {
db := newTestDB(t)
now := model.NowISO()
user := model.User{
UserID: "INNER-CODE-REMOTE-USER", Username: "inner-code-remote-user", PasswordHash: "test",
Role: model.RoleAdmin, Status: model.UserActive, PasswordChangedAt: now, CreatedAt: now, UpdatedAt: now,
}
if err := repository.CreateUser(db, user); err != nil {
t.Fatal(err)
}
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
_, err = repository.UpsertInnerCodeImportRow(tx, model.InnerCodeImportRow{
BusinessDate: "2026-08-15", SourceRow: 4, OrderNumber: "REMOTE-ORDER",
Stall: "", SpecRaw: "黑色,M", SpecKey: NormalizeInnerCodeSpecKey("黑色,M"),
InnerCode: "DK260815TEST01", SourceDuplicateCount: 1, CreatedByUserID: user.UserID,
}, now)
if err != nil {
tx.Rollback()
t.Fatal(err)
}
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
var recordID int64
if err := db.QueryRow(`SELECT id FROM syb_inner_code_records WHERE order_number='REMOTE-ORDER'`).Scan(&recordID); err != nil {
t.Fatal(err)
}
var localSybCount int
if err := db.QueryRow(`SELECT COUNT(*) FROM syb_orders WHERE order_no='REMOTE-ORDER'`).Scan(&localSybCount); err != nil {
t.Fatal(err)
}
if localSybCount != 0 {
t.Fatal("测试前提失败:本地不应存在该货运单")
}
item := innerCodeTestItem(9002, "黑色,M", nil)
reader := &orderQueryInnerCodeReader{
rowsByOrder: map[string][]syb.StockRow{"REMOTE-ORDER": {{ID: 9001, Code: "REMOTE-ORDER"}}},
detailsByID: map[int64]syb.StockDetail{9001: {ID: 9001, Code: "REMOTE-ORDER", Details: []syb.DetailItem{item}}},
}
result, err := PlanInnerCodeRecords(context.Background(), db, reader, "2026-08-15", []int64{recordID})
if err != nil {
t.Fatal(err)
}
if result.Ready != 1 || !reflect.DeepEqual(reader.queries, []string{"REMOTE-ORDER"}) ||
!reflect.DeepEqual(reader.detailCalls, [][]int64{{9001}}) {
t.Fatalf("远端规划编排不正确 result=%+v queries=%v detailCalls=%v", result, reader.queries, reader.detailCalls)
}
var status model.InnerCodeStatus
var stockID, detailID int64
if err := db.QueryRow(`SELECT status,stock_id,detail_id FROM syb_inner_code_records WHERE id=?`, recordID).
Scan(&status, &stockID, &detailID); err != nil {
t.Fatal(err)
}
if status != model.InnerCodeReady || stockID != 9001 || detailID != 9002 {
t.Fatalf("保存计划不正确 status=%s stock=%d detail=%d", status, stockID, detailID)
}
}
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_原始SKU精确命中VariationSKU(t *testing.T) {
record := model.InnerCodeRecord{ID: 31, OrderNumber: "26081387A2EC2N",
Stall: "上寮-青春网购工厂店#8916", SourceSKURaw: "上寮-青春网购工厂店8916##(&9)",
SpecRaw: "紫色,XL", InnerCode: "DK-31", SourceDuplicateCount: 1}
item := innerCodeTestItem(310, "紫色,XL", map[string]any{"variationSku": "上寮-青春网购工厂店8916##(&9)"})
plans := planInnerCodeRows([]model.InnerCodeRecord{record},
map[string][]int64{record.OrderNumber: {300}}, map[int64]syb.StockDetail{300: {ID: 300, Details: []syb.DetailItem{item}}})
if len(plans) != 1 || plans[0].Status != model.InnerCodeReady || plans[0].DetailID != 310 {
t.Fatalf("原始 SKU 未生成可回写计划: %+v", plans)
}
}
func TestMatchInnerCodeItemsWithEvidence_原始SKU可命中SKU且保留旧规则兜底(t *testing.T) {
items := []syb.DetailItem{
innerCodeTestItem(1, "黑色,M", map[string]any{"sku": "源SKU-1"}),
innerCodeTestItem(2, "黑色,M", map[string]any{"sku": "B档#2"}),
}
matched, reason := matchInnerCodeItemsWithEvidence("不匹配#9", " 源SKU-1\n", items)
if reason != "" || len(matched) != 1 || matched[0].ID != 1 {
t.Fatalf("原始 SKU 精确命中失败 matched=%+v reason=%q", matched, reason)
}
matched, reason = matchInnerCodeItemsWithEvidence("B档#2", "不存在", items)
if reason != "" || len(matched) != 1 || matched[0].ID != 2 {
t.Fatalf("旧档口规则兜底失败 matched=%+v reason=%q", matched, reason)
}
}
func TestMatchInnerCodeItemsWithEvidence_重复和证据冲突均阻断(t *testing.T) {
duplicate := []syb.DetailItem{
innerCodeTestItem(1, "黑色,M", map[string]any{"sku": "同一SKU"}),
innerCodeTestItem(2, "黑色,M", map[string]any{"variationSku": "同一SKU"}),
}
if matched, reason := matchInnerCodeItemsWithEvidence("", "同一SKU", duplicate); len(matched) != 0 || reason != "原始 SKU 候选重复,不能自动选择" {
t.Fatalf("重复候选未阻断 matched=%+v reason=%q", matched, reason)
}
conflict := []syb.DetailItem{
innerCodeTestItem(1, "黑色,M", map[string]any{"sku": "源SKU"}),
innerCodeTestItem(2, "黑色,M", map[string]any{"sku": "B档#2"}),
}
if matched, reason := matchInnerCodeItemsWithEvidence("B档#2", "源SKU", conflict); len(matched) != 0 || reason != "原始 SKU 与档口货号冲突,不能自动选择" {
t.Fatalf("证据冲突未阻断 matched=%+v reason=%q", matched, reason)
}
}
func TestInnerCodeStallMatches_中文括号货号与数字前导零(t *testing.T) {
tests := []struct {
name string
stall string
item syb.DetailItem
want bool
}{
{
name: "067在中文括号前为独立货号",
stall: "将客大码网批#067",
item: innerCodeTestItem(1, "黑色(有口袋),2XL【建議67.5-82.5公斤】", map[string]any{
"sku": "067【将客大码网批】-黑色(有口袋)-2XL",
}),
want: true,
},
{
name: "同档口67与067等价",
stall: "将客大码网批#067",
item: innerCodeTestItem(2, "黑色,2XL", map[string]any{
"variationSku": "汽配-将客大码网批#67#(&19)",
}),
want: true,
},
{
name: "同货号不同档口不匹配",
stall: "将客大码网批#067",
item: innerCodeTestItem(3, "黑色,2XL", map[string]any{
"sku": "067【其他档口】-黑色-2XL", "variationSku": "汽配-其他档口#67",
}),
want: false,
},
{
name: "规格重量数字不能冒充货号",
stall: "将客大码网批#067",
item: innerCodeTestItem(6, "黑色,2XL【建議67.5-82.5公斤】", map[string]any{
"sku": "将客大码网批-黑色-2XL",
}),
want: false,
},
{
name: "非数字货号不做类似转换",
stall: "A档#A01",
item: innerCodeTestItem(4, "黑色,M", map[string]any{"sku": "A档#A1"}),
want: false,
},
{
name: "非数字货号精确匹配",
stall: "A档#A01",
item: innerCodeTestItem(5, "黑色,M", map[string]any{"sku": "A档#A01"}),
want: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := innerCodeStallMatches(test.stall, test.item); got != test.want {
t.Fatalf("innerCodeStallMatches(%q)=%t,期望 %t", test.stall, got, test.want)
}
})
}
}
func TestPlanInnerCodeRows_将客档口067唯一匹配生产样例(t *testing.T) {
record := model.InnerCodeRecord{ID: 255, OrderNumber: "260814A21AAB1G",
Stall: "将客大码网批#067", SpecRaw: "黑色(有口袋),2XL【建議67.5-82.5公斤】",
InnerCode: "DK-255", SourceDuplicateCount: 1}
item := innerCodeTestItem(147048059, record.SpecRaw, map[string]any{
"sku": "067【将客大码网批】-黑色(有口袋)-2XL",
"variationSku": "汽配-将客大码网批#67#(&19)",
})
plans := planInnerCodeRows([]model.InnerCodeRecord{record},
map[string][]int64{record.OrderNumber: {76159471}},
map[int64]syb.StockDetail{76159471: {ID: 76159471, Details: []syb.DetailItem{item}}})
if len(plans) != 1 || plans[0].Status != model.InnerCodeReady || plans[0].DetailID != item.ID {
t.Fatalf("将客档口记录未生成可回写计划: %+v", plans)
}
}
func TestPlanInnerCodeRows_多件数量一致放行且其他异常继续阻断(t *testing.T) {
records := []model.InnerCodeRecord{
{ID: 1, OrderNumber: "DUP", SpecRaw: "黑色,M", InnerCode: "DK1,DK2", 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{func() syb.DetailItem { item := innerCodeTestItem(1, "黑色,M", nil); item.ProductQty = 2; return item }()}},
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.InnerCodeReady, model.InnerCodeSkipped, model.InnerCodeSkipped, model.InnerCodeSkipped, model.InnerCodeFailed}
for index, want := range wants {
if plans[index].Status != want {
t.Errorf("第 %d 条 status=%s message=%s,期望 %s", index, plans[index].Status, plans[index].ResultMessage, want)
}
}
if plans[4].ResultMessage != "顺运宝未找到货运单" {
t.Fatalf("零命中提示不正确: %q", plans[4].ResultMessage)
}
}
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) {
selected := model.InnerCodeRecord{
ID: 1, BusinessDate: "2026-08-15", OrderNumber: "ORDER",
SpecRaw: "黑色,M", InnerCode: "DK1", SourceDuplicateCount: 1,
}
contextRecords := []model.InnerCodeRecord{
selected,
{ID: 2, BusinessDate: "2026-08-15", OrderNumber: "ORDER", DetailID: 99, Status: model.InnerCodeReady},
}
reserved := innerCodeReservedDetails(contextRecords, map[int64]bool{selected.ID: true})
detail := syb.StockDetail{ID: 10, Details: []syb.DetailItem{innerCodeTestItem(99, "黑色,M", nil)}}
plans := planInnerCodeRowsWithReserved(
[]model.InnerCodeRecord{selected},
map[string][]int64{"ORDER": {10}},
map[int64]syb.StockDetail{10: detail},
reserved,
)
if len(plans) != 1 || plans[0].Status != model.InnerCodeSkipped ||
plans[0].ResultMessage != "符合条件的顺运宝商品已被同订单其他记录占用" {
t.Fatalf("未选记录的明细占用没有生效: %+v", plans)
}
}
func TestInnerCodeReservedDetails_选中记录释放自身旧占用(t *testing.T) {
records := []model.InnerCodeRecord{
{ID: 1, OrderNumber: "ORDER", DetailID: 99, Status: model.InnerCodeReady},
{ID: 2, OrderNumber: "ORDER", DetailID: 100, Status: model.InnerCodeNeedsCheck},
{ID: 3, OrderNumber: "ORDER", DetailID: 101, Status: model.InnerCodeSkipped},
}
reserved := innerCodeReservedDetails(records, map[int64]bool{1: true})
if reserved["ORDER"][99] || !reserved["ORDER"][100] || reserved["ORDER"][101] {
t.Fatalf("明细占用集合不正确: %+v", reserved)
}
}
func TestUniquePositiveInnerCodeIDs_过滤无效和重复编号(t *testing.T) {
got := uniquePositiveInnerCodeIDs([]int64{3, 0, -1, 3, 2})
if len(got) != 2 || got[0] != 3 || got[1] != 2 {
t.Fatalf("选中编号清理结果不正确: %v", got)
}
}
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, ProductQty: 1, Raw: raw}
}