fix(#63): keep AI matching out of purchase queries

This commit is contained in:
QiuSW
2026-08-21 18:02:58 +08:00
parent 0e3be46c4c
commit ec9f47f2ab
3 changed files with 81 additions and 6 deletions
+2 -2
View File
@@ -145,7 +145,7 @@ func (s *Service) AdminList(ctx context.Context, req AdminListRequest) (AdminLis
}
items := make([]AdminTaskItem, 0, len(tasks))
for _, task := range tasks {
items = append(items, adminTaskItem(task, deviceNames, s.retryEligibility(ctx, task, true)))
items = append(items, adminTaskItem(task, deviceNames, s.retryQueryEligibility(ctx, task, true)))
}
return AdminListResponse{Items: items, Total: total, Page: req.Page, PageSize: req.PageSize}, nil
}
@@ -175,7 +175,7 @@ func (s *Service) AdminDetail(ctx context.Context, taskID uint64) (AdminDetailRe
StartedAt: attempt.StartedAt, FinishedAt: attempt.FinishedAt, CreatedAt: attempt.CreatedAt,
})
}
return AdminDetailResponse{Task: adminTaskItem(task, deviceNames, s.retryEligibility(ctx, task, true)), Attempts: items}, nil
return AdminDetailResponse{Task: adminTaskItem(task, deviceNames, s.retryQueryEligibility(ctx, task, true)), Attempts: items}, nil
}
func loadDeviceNames(db *gorm.DB, tasks []models.PurchaseTask) (map[uint64]string, error) {
+36 -4
View File
@@ -143,6 +143,36 @@ func validateRetryTaskIDs(raw []uint64) ([]uint64, error) {
}
func (s *Service) retryEligibility(ctx context.Context, task models.PurchaseTask, checkDeviceBusy bool) retryDecision {
decision := s.retryStateEligibility(ctx, task)
if !decision.Allowed {
return decision
}
preview := s.previewOne(ctx, *task.SYBProductID)
if !preview.Eligible {
return retryDecision{ReasonCode: preview.ReasonCode, Reason: preview.Reason}
}
decision = s.retryDeviceEligibility(ctx, task, checkDeviceBusy)
if !decision.Allowed {
return decision
}
return retryDecision{Allowed: true, Preview: preview}
}
// retryQueryEligibility is deliberately limited to local task and device
// safety checks. Admin list/detail queries must never call previewOne because
// previewOne can fall back to the external AI provider for unresolved specs.
// The batch-retry command runs retryEligibility and performs that complete,
// current-archive validation only after the operator submits the retry.
func (s *Service) retryQueryEligibility(ctx context.Context, task models.PurchaseTask, checkDeviceBusy bool) retryDecision {
decision := s.retryStateEligibility(ctx, task)
if !decision.Allowed {
return decision
}
return s.retryDeviceEligibility(ctx, task, checkDeviceBusy)
}
func (s *Service) retryStateEligibility(ctx context.Context, task models.PurchaseTask) retryDecision {
deny := func(code, message string) retryDecision {
return retryDecision{ReasonCode: code, Reason: message}
}
@@ -166,10 +196,12 @@ func (s *Service) retryEligibility(ctx context.Context, task models.PurchaseTask
if latest.ID != task.ID {
return deny(CodeRetryStale, fmt.Sprintf("同一 SYB 商品已有更新任务 %s", taskNumber(latest.ID)))
}
return retryDecision{Allowed: true}
}
preview := s.previewOne(ctx, *task.SYBProductID)
if !preview.Eligible {
return deny(preview.ReasonCode, preview.Reason)
func (s *Service) retryDeviceEligibility(ctx context.Context, task models.PurchaseTask, checkDeviceBusy bool) retryDecision {
deny := func(code, message string) retryDecision {
return retryDecision{ReasonCode: code, Reason: message}
}
if task.DeviceID != nil {
if err := s.validateBatchDevice(ctx, task.DeviceID); err != nil {
@@ -189,7 +221,7 @@ func (s *Service) retryEligibility(ctx context.Context, task models.PurchaseTask
}
}
}
return retryDecision{Allowed: true, Preview: preview}
return retryDecision{Allowed: true}
}
func retryItemRequestID(batchRequestID string, sourceTaskID uint64) string {
+43
View File
@@ -5,12 +5,20 @@ import (
"strings"
"testing"
"go-admin/app/goauto/aimatching"
"go-admin/app/goauto/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type countingRetryMatcher struct{ calls int }
func (matcher *countingRetryMatcher) Resolve(context.Context, aimatching.MatchRequest) (aimatching.MatchResult, error) {
matcher.calls++
return aimatching.MatchResult{}, &aimatching.Error{Code: aimatching.CodeNoMatch, Message: "no match"}
}
func failedLiveTask(t *testing.T, db *gorm.DB, service *Service, f fixture) models.PurchaseTask {
t.Helper()
task, err := createLive(t, service, f)
@@ -181,6 +189,41 @@ func TestAdminListRetryEligibilityRejectsBusyOriginalDevice(t *testing.T) {
}
}
func TestAdminQueriesDoNotResolveSpecsButBatchRetryStillDoes(t *testing.T) {
db := testDB(t)
f := seed(t, db, liveCaps(), false)
service := testService(db)
failed := failedLiveTask(t, db, service, f)
if err := db.Model(&models.PDDProduct{}).Where("id = ?", f.pdd.ID).Update("specs_json", `[{"name":"颜色","role":"color","values":[{"name":"白色","selectable":true,"priceCent":2000}]},{"name":"尺码","role":"size","values":[{"name":"L","selectable":true}]}]`).Error; err != nil {
t.Fatal(err)
}
matcher := &countingRetryMatcher{}
service.Matcher = matcher
list, err := service.AdminList(context.Background(), AdminListRequest{Page: 1, PageSize: 20, TaskID: failed.ID})
if err != nil {
t.Fatal(err)
}
if len(list.Items) != 1 || !list.Items[0].Retryable || matcher.calls != 0 {
t.Fatalf("admin list resolved specs: items=%+v matcherCalls=%d", list.Items, matcher.calls)
}
detail, err := service.AdminDetail(context.Background(), failed.ID)
if err != nil {
t.Fatal(err)
}
if !detail.Task.Retryable || matcher.calls != 0 {
t.Fatalf("admin detail resolved specs: task=%+v matcherCalls=%d", detail.Task, matcher.calls)
}
retry, err := service.BatchRetry(context.Background(), BatchRetryRequest{RequestID: uuid.NewString(), TaskIDs: []uint64{failed.ID}})
if err != nil {
t.Fatal(err)
}
if matcher.calls != 1 || retry.CreatedCount != 0 || retry.FailedCount != 1 || retry.Items[0].ReasonCode != CodeMappingRequired {
t.Fatalf("batch retry skipped full matching: result=%+v matcherCalls=%d", retry, matcher.calls)
}
}
func TestFailedTaskNoLongerNeedsRepurchaseAuthorization(t *testing.T) {
db := testDB(t)
f := seed(t, db, liveCaps(), true)