package service import ( "context" "database/sql" "errors" "testing" _ "modernc.org/sqlite" "cmautobuy/admin/model" "cmautobuy/admin/syb" ) type fakeInnerCodeWriter struct { stock syb.StockDetail readErr error deleteErr error createErr error updateErr error readCount int deleteCount int createCount int updateCount int updatedCode string nextDetailID int64 } func TestQueueInnerCodeApplyBatch_超过20条可一次排队(t *testing.T) { if innerCodeApplyChunkSize != 20 { t.Fatalf("后台数据库读取分组应固定为 20,实际 %d", innerCodeApplyChunkSize) } db, err := sql.Open("sqlite", "file:inner_code_queue_over_20?mode=memory&cache=shared") if err != nil { t.Fatal(err) } defer db.Close() if _, err := db.Exec(`CREATE TABLE syb_inner_code_records ( id INTEGER PRIMARY KEY,status TEXT NOT NULL,apply_batch_id TEXT,apply_queued_at TEXT, applied_by_user_id TEXT,result_message TEXT,apply_started_at TEXT,updated_at TEXT, deleted_at TEXT )`); err != nil { t.Fatal(err) } ids := make([]int64, 45) for i := range ids { ids[i] = int64(i + 1) if _, err := db.Exec(`INSERT INTO syb_inner_code_records(id,status,updated_at) VALUES(?,'ready','old')`, ids[i]); err != nil { t.Fatal(err) } } batch, err := QueueInnerCodeApplyBatch(db, ids, "user-1") if err != nil || batch.Count != 45 || batch.ID == "" { t.Fatalf("batch=%+v err=%v", batch, err) } var queued, batches int if err := db.QueryRow(`SELECT COUNT(*),COUNT(DISTINCT apply_batch_id) FROM syb_inner_code_records WHERE status='queued'`).Scan(&queued, &batches); err != nil { t.Fatal(err) } if queued != 45 || batches != 1 { t.Fatalf("queued=%d batches=%d", queued, batches) } } func (f *fakeInnerCodeWriter) DetailListByStock(context.Context, []int64) ([]syb.StockDetail, error) { f.readCount++ if f.readErr != nil { return nil, f.readErr } return []syb.StockDetail{f.stock}, nil } func (f *fakeInnerCodeWriter) DeleteInnerCode(_ context.Context, detailID int64) error { f.deleteCount++ if f.deleteErr != nil { return f.deleteErr } for index := range f.stock.Details { if f.stock.Details[index].ID == detailID { f.stock.Details[index].Raw["innerExpCode"] = "" } } return nil } func (f *fakeInnerCodeWriter) CreateInnerCodeDetail(_ context.Context, stockID int64, title string) (int64, error) { f.createCount++ if f.createErr != nil { return 0, f.createErr } if f.nextDetailID == 0 { f.nextDetailID = 100 } f.nextDetailID++ f.stock.Details = append(f.stock.Details, syb.DetailItem{ID: f.nextDetailID, ProductTitle: title, ProductQty: 1, ProductPrice: 0, Raw: map[string]any{"innerExpCode": ""}}) return f.nextDetailID, nil } func (f *fakeInnerCodeWriter) UpdateDetailCode(_ context.Context, _, detailID int64, code string) error { f.updateCount++ f.updatedCode = code if f.updateErr != nil { return f.updateErr } for index := range f.stock.Details { if f.stock.Details[index].ID == detailID { f.stock.Details[index].Raw["innerExpCode"] = code } } return nil } func TestApplyClaimedInnerCode_两个单件码写入两个明细(t *testing.T) { record := innerCodeApplyTestRecord("") record.InnerCode = "DK260815A160101,DK260815A160102" record.SourceDuplicateCount = 2 writer := innerCodeApplyTestWriter(record, "") outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeUpdated || writer.updateCount != 2 || writer.createCount != 1 || outcome.RemoteCode != record.InnerCode || writer.readCount != 3 { t.Fatalf("outcome=%+v update=%d create=%d read=%d", outcome, writer.updateCount, writer.createCount, writer.readCount) } } func TestApplyClaimedInnerCode_两个单件码写入两条现成相同明细(t *testing.T) { record := innerCodeApplyTestRecord("") record.InnerCode = "DK-001,DK-002" record.SourceDuplicateCount = 2 record.SybSKU = "A档#1-黑色-M" record.SybVariationSKU = "A档#1" record.RemoteItemsJSON = `[{"code":"DK-001","detail_id":20,"source":"existing_matched","status":"planned"},{"code":"DK-002","detail_id":21,"source":"existing_matched","status":"planned"}]` writer := innerCodeApplyTestWriter(record, "") writer.stock.Details[0].ProductQty = 1 writer.stock.Details[0].Raw["sku"] = record.SybSKU writer.stock.Details[0].Raw["variationSku"] = record.SybVariationSKU writer.stock.Details = append(writer.stock.Details, syb.DetailItem{ ID: 21, ProductSpec: record.SybSpec, ProductQty: 1, Raw: map[string]any{"sku": record.SybSKU, "variationSku": record.SybVariationSKU, "innerExpCode": "", "purchasePlatform": "", "purchaseCode": ""}, }) outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeUpdated || writer.updateCount != 2 || writer.createCount != 0 || writer.deleteCount != 0 || writer.readCount != 3 { t.Fatalf("outcome=%+v update=%d create=%d delete=%d read=%d", outcome, writer.updateCount, writer.createCount, writer.deleteCount, writer.readCount) } if got := innerCodeDetailCode(writer.stock, 20); got != "DK-001" { t.Fatalf("第一条现成明细写码错误: %q", got) } if got := innerCodeDetailCode(writer.stock, 21); got != "DK-002" { t.Fatalf("第二条现成明细写码错误: %q", got) } } func TestApplyClaimedInnerCode_现成多明细身份变化时零写入(t *testing.T) { record := innerCodeApplyTestRecord("") record.InnerCode = "DK-001,DK-002" record.SourceDuplicateCount = 2 record.SybSKU = "A档#1-黑色-M" record.RemoteItemsJSON = `[{"code":"DK-001","detail_id":20,"source":"existing_matched","status":"planned"},{"code":"DK-002","detail_id":21,"source":"existing_matched","status":"planned"}]` writer := innerCodeApplyTestWriter(record, "") writer.stock.Details[0].ProductQty = 1 writer.stock.Details[0].Raw["sku"] = record.SybSKU writer.stock.Details = append(writer.stock.Details, syb.DetailItem{ ID: 21, ProductSpec: record.SybSpec, ProductQty: 1, Raw: map[string]any{"sku": "B档#2", "innerExpCode": "", "purchasePlatform": "", "purchaseCode": ""}, }) outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeSkipped || writer.updateCount != 0 || writer.createCount != 0 || writer.deleteCount != 0 { t.Fatalf("身份变化必须零写入: outcome=%+v update=%d create=%d delete=%d", outcome, writer.updateCount, writer.createCount, writer.deleteCount) } } func TestApplyClaimedInnerCode_三个单件码创建两条零价明细(t *testing.T) { record := innerCodeApplyTestRecord("") record.InnerCode = "DK-001,DK-002,DK-003" record.SourceDuplicateCount = 3 writer := innerCodeApplyTestWriter(record, "") outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeUpdated || writer.updateCount != 3 || writer.createCount != 2 { t.Fatalf("outcome=%+v update=%d create=%d", outcome, writer.updateCount, writer.createCount) } for _, detail := range writer.stock.Details[1:] { if detail.ProductQty != 1 || detail.ProductPrice != 0 { t.Fatalf("占位明细必须数量 1、价格 0: %+v", detail) } } } func TestApplyClaimedInnerCode_复用人工明细已有目标码(t *testing.T) { record := innerCodeApplyTestRecord("") record.InnerCode = "DK-001,DK-002" record.SourceDuplicateCount = 2 writer := innerCodeApplyTestWriter(record, "") writer.stock.Details = append(writer.stock.Details, syb.DetailItem{ID: 21, ProductTitle: "人工明细", ProductQty: 1, Raw: map[string]any{"innerExpCode": "DK-002"}}) outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeUpdated || writer.updateCount != 1 || writer.createCount != 0 || writer.deleteCount != 0 { t.Fatalf("outcome=%+v update=%d create=%d delete=%d", outcome, writer.updateCount, writer.createCount, writer.deleteCount) } } func TestApplyClaimedInnerCode_数量不一致零写入(t *testing.T) { record := innerCodeApplyTestRecord("") record.InnerCode = "DK-001,DK-002" record.SourceDuplicateCount = 2 writer := innerCodeApplyTestWriter(record, "") writer.stock.Details[0].ProductQty = 1 outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeFailed || writer.updateCount != 0 || writer.createCount != 0 || writer.deleteCount != 0 { t.Fatalf("outcome=%+v update=%d create=%d delete=%d", outcome, writer.updateCount, writer.createCount, writer.deleteCount) } } func TestApplyClaimedInnerCode_创建结果未知立即停止(t *testing.T) { record := innerCodeApplyTestRecord("") record.InnerCode = "DK-001,DK-002" record.SourceDuplicateCount = 2 writer := innerCodeApplyTestWriter(record, "") writer.createErr = fmtUnknownInnerCodeWriteError() checkpoints := 0 outcome := applyClaimedInnerCode(context.Background(), writer, record, func([]innerCodeRemoteItem, string) error { checkpoints++ return nil }) if outcome.Status != model.InnerCodeNeedsCheck || writer.createCount != 1 || writer.updateCount != 1 || checkpoints < 3 { t.Fatalf("outcome=%+v create=%d update=%d checkpoints=%d", outcome, writer.createCount, writer.updateCount, checkpoints) } } func TestApplyClaimedInnerCode_崩溃遗留占位明细会复用而不重复创建(t *testing.T) { record := innerCodeApplyTestRecord("") record.InnerCode = "DK-001,DK-002" record.SourceDuplicateCount = 2 writer := innerCodeApplyTestWriter(record, "") writer.stock.Details = append(writer.stock.Details, syb.DetailItem{ID: 99, ProductTitle: innerCodePlaceholderTitle(record.ID, 2), ProductQty: 1, ProductPrice: 0, Raw: map[string]any{"innerExpCode": ""}}) outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeUpdated || writer.createCount != 0 || writer.updateCount != 2 { t.Fatalf("outcome=%+v create=%d update=%d", outcome, writer.createCount, writer.updateCount) } } func TestApplyClaimedInnerCode_空旧值直接写并复读确认(t *testing.T) { record := innerCodeApplyTestRecord("") writer := innerCodeApplyTestWriter(record, "") outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeUpdated || writer.deleteCount != 0 || writer.updateCount != 1 || writer.readCount != 2 { t.Fatalf("outcome=%+v counts read=%d delete=%d update=%d", outcome, writer.readCount, writer.deleteCount, writer.updateCount) } } func TestApplyClaimedInnerCode_有旧值先删后写(t *testing.T) { record := innerCodeApplyTestRecord("OLD") writer := innerCodeApplyTestWriter(record, "OLD") outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeUpdated || writer.deleteCount != 1 || writer.updateCount != 1 { t.Fatalf("outcome=%+v delete=%d update=%d", outcome, writer.deleteCount, writer.updateCount) } } func TestApplyClaimedInnerCode_规划后远端值变化不写入(t *testing.T) { record := innerCodeApplyTestRecord("OLD") writer := innerCodeApplyTestWriter(record, "OTHER") outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeNeedsCheck || writer.deleteCount != 0 || writer.updateCount != 0 { t.Fatalf("outcome=%+v delete=%d update=%d", outcome, writer.deleteCount, writer.updateCount) } } func TestApplyClaimedInnerCode_删除明确失败不继续写(t *testing.T) { record := innerCodeApplyTestRecord("OLD") writer := innerCodeApplyTestWriter(record, "OLD") writer.deleteErr = errors.New("已打单数据不能清除") outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeFailed || writer.deleteCount != 1 || writer.updateCount != 0 { t.Fatalf("outcome=%+v delete=%d update=%d", outcome, writer.deleteCount, writer.updateCount) } } func TestApplyClaimedInnerCode_写入未知结果不复读也不重试(t *testing.T) { record := innerCodeApplyTestRecord("") writer := innerCodeApplyTestWriter(record, "") writer.updateErr = fmtUnknownInnerCodeWriteError() outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeNeedsCheck || writer.updateCount != 1 || writer.readCount != 1 { t.Fatalf("outcome=%+v update=%d read=%d", outcome, writer.updateCount, writer.readCount) } } func TestApplyClaimedInnerCode_采购字段出现后停止(t *testing.T) { record := innerCodeApplyTestRecord("") writer := innerCodeApplyTestWriter(record, "") writer.stock.Details[0].Raw["purchaseCode"] = "CG-1" outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeSkipped || writer.updateCount != 0 { t.Fatalf("outcome=%+v update=%d", outcome, writer.updateCount) } } func TestApplyClaimedInnerCode_档口身份变化后停止(t *testing.T) { record := innerCodeApplyTestRecord("") record.SybSKU = "A#1" writer := innerCodeApplyTestWriter(record, "") writer.stock.Details[0].Raw["sku"] = "B#2" outcome := applyClaimedInnerCode(context.Background(), writer, record, nil) if outcome.Status != model.InnerCodeSkipped || writer.updateCount != 0 { t.Fatalf("outcome=%+v update=%d", outcome, writer.updateCount) } } func innerCodeApplyTestRecord(oldCode string) model.InnerCodeRecord { return model.InnerCodeRecord{ID: 1, StockID: 10, DetailID: 20, SybSpec: "黑色,M", InnerCode: "DK-001", SourceDuplicateCount: 1, RemoteInnerCode: oldCode, Status: model.InnerCodeApplying} } func innerCodeApplyTestWriter(record model.InnerCodeRecord, currentCode string) *fakeInnerCodeWriter { item := syb.DetailItem{ID: record.DetailID, ProductSpec: record.SybSpec, ProductQty: record.SourceDuplicateCount, Raw: map[string]any{ "id": record.DetailID, "productSpec": record.SybSpec, "innerExpCode": currentCode, "purchasePlatform": "", "purchaseCode": "", }} return &fakeInnerCodeWriter{stock: syb.StockDetail{ID: record.StockID, Details: []syb.DetailItem{item}}} } func fmtUnknownInnerCodeWriteError() error { return errors.Join(syb.ErrWriteResultUnknown, errors.New("timeout")) }