fix(#63): keep AI matching out of purchase queries
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user