From c137f5072f75b16e21782a329336f480de8e3b33 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Mon, 14 Sep 2026 14:42:11 +0800 Subject: [PATCH] fix: keep purchase readiness consistent with confirmed mappings (#283) --- server/app/goauto/purchase/batch.go | 8 ++++++++ server/app/goauto/purchase/batch_spec_match_test.go | 6 +++--- server/app/goauto/purchase/batch_test.go | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/app/goauto/purchase/batch.go b/server/app/goauto/purchase/batch.go index 683a072..26e872d 100644 --- a/server/app/goauto/purchase/batch.go +++ b/server/app/goauto/purchase/batch.go @@ -422,6 +422,14 @@ func (s *Service) previewFromDataset(id uint64, dataset batchPreviewDataset, gua } // #190:映射不完整不再拦截,任务以 unresolved 建立并交由规格探测解析。 } + // Readiness must agree with the mapping shown in the product detail. A + // deterministic suggestion is only a preview; it is not a persisted, + // confirmed mapping and must not make the row appear purchase-ready. + if source == "unresolved" || !mappingTargetsValid(candidates, syb.TargetColor, syb.TargetSize, item.MappedColor, item.MappedSize) { + // Keep Eligible for the existing live-probe path, but expose the + // unresolved state so the UI cannot label the row purchase-ready. + item.ReasonCode, item.Reason, item.NextAction = CodeMappingRequired, "采购规格尚未匹配并保存", "open_mapping" + } reference, minPrice, maxPrice, err := purchasePriceRange(pdd.SpecsJSON, item.MappedColor, guard) if err != nil { item.ReasonCode, item.Reason, item.NextAction = "PDD_PRICE_MISSING", err.Error(), "open_pdd" diff --git a/server/app/goauto/purchase/batch_spec_match_test.go b/server/app/goauto/purchase/batch_spec_match_test.go index fa7b41c..2a7aa16 100644 --- a/server/app/goauto/purchase/batch_spec_match_test.go +++ b/server/app/goauto/purchase/batch_spec_match_test.go @@ -142,7 +142,7 @@ func TestBatchSpecMatchPersistsExactMatchButPurchaseDoesNotWaitForIt(t *testing. service.Matcher = matcher before, err := service.BatchPreview(context.Background(), BatchPreviewRequest{SYBProductIDs: []uint64{f.syb.ID}}) - if err != nil || len(before.Items) != 1 || !before.Items[0].Eligible || !before.Items[0].AIMatchEligible || before.Items[0].ProcessStage != ProcessStagePurchaseReady { + if err != nil || len(before.Items) != 1 || !before.Items[0].Eligible || !before.Items[0].AIMatchEligible || before.Items[0].ProcessStage != ProcessStageColorMapping { t.Fatalf("live-probe purchase was incorrectly gated by persisted mapping: %+v err=%v", before, err) } @@ -181,7 +181,7 @@ func TestExtractedUncertainSpecsCanBeMatchedBeforePurchase(t *testing.T) { service.Matcher = matcher before, err := service.BatchPreview(context.Background(), BatchPreviewRequest{SYBProductIDs: []uint64{f.syb.ID}}) - if err != nil || len(before.Items) != 1 || !before.Items[0].Eligible || !before.Items[0].AIMatchEligible || before.Items[0].ProcessStage != ProcessStagePurchaseReady { + if err != nil || len(before.Items) != 1 || !before.Items[0].Eligible || !before.Items[0].AIMatchEligible || before.Items[0].ProcessStage != ProcessStageColorMapping { t.Fatalf("extracted uncertain specs were not purchase-ready for live probing: %+v err=%v", before, err) } matched, err := service.BatchSpecMatch(context.Background(), BatchSpecMatchRequest{SYBProductIDs: []uint64{f.syb.ID}}) @@ -231,7 +231,7 @@ func TestBatchPreviewExposesExplicitAIMatchEligibility(t *testing.T) { t.Fatalf("preview failed: %+v err=%v", preview, err) } item := preview.Items[0] - if !item.AIMatchEligible || item.AIMatchDisabledReason != "" || !item.Eligible || item.ProcessStage != ProcessStagePurchaseReady { + if !item.AIMatchEligible || item.AIMatchDisabledReason != "" || !item.Eligible || item.ProcessStage != ProcessStageColorMapping { t.Fatalf("explicit AI eligibility mismatch: %+v", item) } } diff --git a/server/app/goauto/purchase/batch_test.go b/server/app/goauto/purchase/batch_test.go index 069cf83..b523198 100644 --- a/server/app/goauto/purchase/batch_test.go +++ b/server/app/goauto/purchase/batch_test.go @@ -314,7 +314,7 @@ func TestBatchPreviewBulkLoadsAndNeverCallsAIMatcher(t *testing.T) { t.Fatalf("batch preview used %d queries, want 7 bounded queries including collection eligibility and current purchase rule", queries) } if len(response.Items) != 2 || !response.Items[0].Eligible || !response.Items[1].Eligible || response.EligibleCount != 2 { - t.Fatalf("unresolved rows must be ready for mandatory live probing: %+v", response) + t.Fatalf("unresolved rows remain eligible for live probing but are not purchase-ready: %+v", response) } } @@ -329,7 +329,7 @@ func TestBatchPreviewAllowsIncompleteArchivedCandidatesForMandatoryLiveProbe(t * } response, err := testService(db).BatchPreview(context.Background(), BatchPreviewRequest{SYBProductIDs: []uint64{fixture.syb.ID}}) - if err != nil || len(response.Items) != 1 || !response.Items[0].Eligible || response.Items[0].ProcessStage != ProcessStagePurchaseReady { + if err != nil || len(response.Items) != 1 || !response.Items[0].Eligible || response.Items[0].ProcessStage != ProcessStageManualAction { t.Fatalf("incomplete archive blocked live probe task creation: %+v err=%v", response, err) } }