Files
goauto/server/app/goauto/sybinnercode/match_test.go
T

180 lines
7.9 KiB
Go

package sybinnercode
import (
"context"
"encoding/json"
"testing"
"go-admin/app/goauto/models"
"go-admin/app/goauto/sybclient"
"github.com/google/uuid"
"gorm.io/gorm"
)
type fakeMatchReader struct {
rows map[string][]sybclient.StockRow
stocks map[int64]sybclient.StockDetail
listCalls, detailCalls int
}
func (f *fakeMatchReader) ListByOrderNumber(_ context.Context, order string) ([]sybclient.StockRow, error) {
f.listCalls++
return f.rows[order], nil
}
func (f *fakeMatchReader) DetailListByStock(_ context.Context, ids []int64) ([]sybclient.StockDetail, error) {
f.detailCalls++
result := make([]sybclient.StockDetail, 0, len(ids))
for _, id := range ids {
if stock, ok := f.stocks[id]; ok {
result = append(result, stock)
}
}
return result, nil
}
func createMatchJob(t *testing.T, db *gorm.DB, records []models.SYBInnerCodeRecord) string {
t.Helper()
for i := range records {
if err := db.Create(&records[i]).Error; err != nil {
t.Fatal(err)
}
}
id := uuid.NewString()
ids := make([]uint64, 0, len(records))
for _, record := range records {
ids = append(ids, record.ID)
}
raw, _ := json.Marshal(ids)
job := models.SYBInnerCodeMatchJob{ID: id, WorkKey: "test:" + id, BusinessDate: "2026-08-28", RecordIDsJSON: string(raw), Status: "pending", Total: len(records)}
if err := db.Create(&job).Error; err != nil {
t.Fatal(err)
}
return id
}
func TestRunMatchJobOnlyProcessesScopedRecords(t *testing.T) {
db := testDB(t)
selected := matchRecord("ORDER-1", "SKU-1", "A#1", "IC-1")
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{selected})
outside := matchRecord("ORDER-2", "SKU-2", "B#2", "IC-2")
if err := db.Create(&outside).Error; err != nil {
t.Fatal(err)
}
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10}}}, stocks: map[int64]sybclient.StockDetail{10: {ID: 10, Details: []sybclient.DetailItem{detail(20, "黑色,L", 1, "SKU-1", "A#1", "")}}}}
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
t.Fatal(err)
}
var untouched models.SYBInnerCodeRecord
if err := db.First(&untouched, outside.ID).Error; err != nil {
t.Fatal(err)
}
if untouched.Status != models.SYBInnerCodePending {
t.Fatalf("out-of-scope record changed to %s", untouched.Status)
}
}
func detail(id int64, spec string, qty int, sku, variation, code string) sybclient.DetailItem {
return sybclient.DetailItem{ID: id, ProductSpec: spec, ProductQty: qty, Raw: map[string]any{"sku": sku, "variationSku": variation, "innerExpCode": code, "purchasePlatform": "", "purchaseCode": ""}}
}
func matchRecord(order, sku, stall, code string) models.SYBInnerCodeRecord {
return models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: order, Stall: stall, SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: sku, Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: code, Ordinal: 1, SourceRow: 2}}}
}
func TestRunMatchJobPlansMultiItemWithoutAnyWriteSurface(t *testing.T) {
db := testDB(t)
record := models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: "ORDER-1", Stall: "A#1", SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: "SKU-1", Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: "IC-1", Ordinal: 1, SourceRow: 2}, {BusinessDate: "2026-08-28", Code: "IC-2", Ordinal: 2, SourceRow: 3}}}
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{record})
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10, Code: "ORDER-1"}}}, stocks: map[int64]sybclient.StockDetail{10: {ID: 10, Code: "ORDER-1", Details: []sybclient.DetailItem{detail(20, "黑色,L", 2, "SKU-1", "A#1", "")}}}}
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
t.Fatal(err)
}
var saved models.SYBInnerCodeRecord
if err := db.Preload("Items").First(&saved).Error; err != nil {
t.Fatal(err)
}
if saved.Status != models.SYBInnerCodeReady {
t.Fatalf("status=%s message=%s", saved.Status, saved.ResultMessage)
}
var plan models.SYBInnerCodePlan
if err := db.First(&plan, "record_id = ?", saved.ID).Error; err != nil {
t.Fatal(err)
}
if plan.DetailID != 20 || plan.PlaceholderCount != 1 || plan.EvidenceHash == "" {
t.Fatalf("plan=%+v", plan)
}
if reader.listCalls != 1 || reader.detailCalls != 1 {
t.Fatalf("calls list=%d detail=%d", reader.listCalls, reader.detailCalls)
}
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
t.Fatal(err)
}
if reader.listCalls != 1 {
t.Fatalf("completed job reran")
}
}
func TestRunMatchJobRejectsSKUAndStallConflict(t *testing.T) {
db := testDB(t)
record := models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: "ORDER-1", Stall: "B#2", SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: "SKU-A", Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: "IC-1", Ordinal: 1, SourceRow: 2}}}
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{record})
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10, Code: "ORDER-1"}}}, stocks: map[int64]sybclient.StockDetail{10: {ID: 10, Details: []sybclient.DetailItem{detail(20, "黑色,L", 1, "SKU-A", "A#1", ""), detail(21, "黑色,L", 1, "SKU-B", "B#2", "")}}}}
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
t.Fatal(err)
}
var saved models.SYBInnerCodeRecord
db.First(&saved)
if saved.Status != models.SYBInnerCodeSkipped || saved.ResultMessage != "原始 SKU 与档口货号冲突,不能自动选择" {
t.Fatalf("record=%+v", saved)
}
}
func TestRunMatchJobReservesDifferentDetailsForRecordsInOneBatch(t *testing.T) {
db := testDB(t)
base := func(order, sku, stall, code string) models.SYBInnerCodeRecord {
return models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: order, Stall: stall, SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: sku, Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: code, Ordinal: 1, SourceRow: 2}}}
}
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{base("ORDER-1", "SKU-A", "A#1", "IC-A"), base("ORDER-1", "SKU-B", "B#2", "IC-B")})
stock := sybclient.StockDetail{ID: 10, Details: []sybclient.DetailItem{detail(20, "黑色,L", 1, "SKU-A", "A#1", ""), detail(21, "黑色,L", 1, "SKU-B", "B#2", "")}}
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10, Code: "ORDER-1"}}}, stocks: map[int64]sybclient.StockDetail{10: stock}}
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
t.Fatal(err)
}
var plans []models.SYBInnerCodePlan
db.Order("record_id").Find(&plans)
if len(plans) != 2 || plans[0].DetailID == plans[1].DetailID {
t.Fatalf("plans=%+v", plans)
}
}
type captureStarter struct {
jobID string
committed bool
}
func (c *captureStarter) Start(_ context.Context, db *gorm.DB, jobID string) error {
c.jobID = jobID
var count int64
db.Model(&models.SYBInnerCodeRecord{}).Count(&count)
c.committed = count > 0
return nil
}
func TestImportCreatesOneMatchJobAndStartsAfterCommit(t *testing.T) {
db := testDB(t)
starter := &captureStarter{}
service := NewService(db).WithMatchEnqueuer(starter)
rows := [][]any{{"2026-08-28", "ORDER-1", "店铺", "A#1", "SKU-1", "黑色,L", "IC-01", 1}}
result, err := importRows(t, service, uuid.NewString(), rows)
if err != nil {
t.Fatal(err)
}
if result.MatchJobID == "" || starter.jobID != result.MatchJobID || !starter.committed {
t.Fatalf("result=%+v starter=%+v", result, starter)
}
var count int64
db.Model(&models.SYBInnerCodeMatchJob{}).Count(&count)
if count != 1 {
t.Fatalf("jobs=%d", count)
}
}