Compare commits

..
2 changed files with 156 additions and 1 deletions
+3 -1
View File
@@ -255,7 +255,9 @@ func (service *Service) IdentifyCurrentPage(ctx context.Context, taskID uint64,
return serviceError(CodeCurrentPageIdentityConflict, "当前商品与任务已识别商品不一致")
}
now := service.Now()
if err := tx.Model(&models.CollectionTask{}).Where("id = ?", record.ID).Updates(map[string]any{
// Validate the locked task's real state; an empty model fails BeforeSave
// when a retried current-page task already has a product binding.
if err := tx.Model(&record).Where("id = ?", record.ID).Updates(map[string]any{
"identify_request_id": request.RequestID, "identity_resolved_at": now,
}).Error; err != nil {
return internalError(err)
@@ -0,0 +1,153 @@
package task
import (
"context"
"reflect"
"testing"
"go-admin/app/goauto/models"
"go-admin/app/goauto/rulecontract"
"github.com/google/uuid"
)
func TestCurrentPageReidentifyAfterSameTaskRetry(t *testing.T) {
db := openTaskDatabase(t)
_, token := registerTaskDeviceWithCapabilities(t, db, "retry-device", []string{
rulecontract.CapabilitySchemaV2,
rulecontract.CapabilityPDDProductDetailV1,
rulecontract.CapabilityPDDCurrentPageShareV1,
})
_, otherToken := registerTaskDevice(t, db, "other-device")
rule := models.CollectionRule{Name: "retry-rule", ContentJSON: v2TaskRuleSnapshot()}
if err := db.Create(&rule).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&models.AgentManualCollectionSetting{ID: 1, RuleID: rule.ID}).Error; err != nil {
t.Fatal(err)
}
service := newTaskService(db)
ctx := context.Background()
created, err := service.CreateCurrentPage(ctx, CurrentPageCreateRequest{RequestID: uuid.NewString()}, token)
if err != nil {
t.Fatal(err)
}
load := func() models.CollectionTask {
t.Helper()
var record models.CollectionTask
if err := db.First(&record, created.TaskID).Error; err != nil {
t.Fatal(err)
}
return record
}
request := CurrentPageIdentifyRequest{
RequestID: uuid.NewString(), ShareURL: "https://mobile.yangkeduo.com/goods.html?goods_id=700000000255",
}
// Direct links resolve locally: the regression never calls PDD or a live DB.
identity, err := service.IdentifyCurrentPage(ctx, created.TaskID, request, token)
if err != nil {
t.Fatal(err)
}
const failureCode = "SPEC_PANEL_EVIDENCE_NOT_MATCHED"
if _, err := service.SubmitFailure(ctx, created.TaskID, FailRequest{
RequestID: uuid.NewString(), Code: failureCode, Message: "Synthetic panel failure",
}, token); err != nil {
t.Fatal(err)
}
reset, err := service.ResetForDevice(ctx, created.TaskID, ActionRequest{RequestID: uuid.NewString()}, token)
if err != nil || reset.TaskID != created.TaskID || reset.AttemptNumber != 2 || reset.Status != models.TaskStatusPending {
t.Fatalf("same-task reset: %+v, %v", reset, err)
}
resetRecord := load()
if resetRecord.PDDProductID == nil || *resetRecord.PDDProductID != identity.PDDProductID ||
resetRecord.IdentifyRequestID != nil || resetRecord.IdentityResolvedAt != nil {
t.Fatal("reset must keep the product and clear per-attempt identification")
}
if _, err := service.Claim(ctx, created.TaskID, ActionRequest{RequestID: uuid.NewString()}, token); err != nil {
t.Fatal(err)
}
if _, err := service.Start(ctx, created.TaskID, ActionRequest{RequestID: uuid.NewString()}, token); err != nil {
t.Fatal(err)
}
before := load()
request.RequestID = uuid.NewString()
for _, tc := range []struct {
name, token, url, code string
}{
{"other device", otherToken, request.ShareURL, CodeTaskNotFound},
{"other product", token, "https://mobile.yangkeduo.com/goods.html?goods_id=700000000256", CodeCurrentPageIdentityConflict},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := service.IdentifyCurrentPage(ctx, created.TaskID, CurrentPageIdentifyRequest{RequestID: uuid.NewString(), ShareURL: tc.url}, tc.token)
if taskErrorCode(t, err) != tc.code {
t.Fatalf("expected %s, got %v", tc.code, err)
}
if !reflect.DeepEqual(before, load()) {
t.Fatal("rejected identification changed the task")
}
})
}
reidentified, err := service.IdentifyCurrentPage(ctx, created.TaskID, request, token)
if err != nil {
t.Fatalf("reidentify same product after retry: %v", err)
}
if reidentified.TaskID != created.TaskID || reidentified.PDDProductID != identity.PDDProductID ||
reidentified.GoodsID != identity.GoodsID || reidentified.URL != identity.URL || !reidentified.Replayed {
t.Fatalf("identity changed: %+v", reidentified)
}
after := load()
if after.IdentifyRequestID == nil || *after.IdentifyRequestID != request.RequestID || after.IdentityResolvedAt == nil {
t.Fatal("new attempt identification was not persisted")
}
// Apart from identity metadata and the normal update timestamp, every field
// (including status, device, rule, lease and uniqueness slots) must survive.
expected := before
expected.IdentifyRequestID = after.IdentifyRequestID
expected.IdentityResolvedAt = after.IdentityResolvedAt
expected.UpdatedAt = after.UpdatedAt
if !reflect.DeepEqual(expected, after) {
t.Fatal("reidentification changed unrelated task fields")
}
service.ResolveCurrentPageShare = func(context.Context, string) (ResolvedPDDShare, error) {
t.Fatal("replay must not resolve the link again")
return ResolvedPDDShare{}, nil
}
replay, err := service.IdentifyCurrentPage(ctx, created.TaskID, request, token)
if err != nil || replay != reidentified || !reflect.DeepEqual(after, load()) {
t.Fatalf("replay changed identity or task: %+v, %v", replay, err)
}
service.ResolveCurrentPageShare = nil
title := "Synthetic collected product"
result, err := service.SubmitResult(ctx, created.TaskID, ResultRequest{
RequestID: uuid.NewString(), Status: models.TaskStatusCompleted,
Product: ResultProduct{PDDGoodsID: identity.GoodsID, Title: &title},
Dimensions: []ResultDimension{{Key: "color", Name: "颜色", Values: []string{"黑色"}}},
ColorPrices: []ResultColorPrice{{Color: "黑色", PriceCent: 1200}},
SKUs: []ResultSKU{{Specs: map[string]string{"color": "黑色"}, PriceCent: 1200, Available: true}},
}, token)
if err != nil || result.Task.ID != created.TaskID || result.Task.Status != models.TaskStatusCompleted {
t.Fatalf("complete retried collection: %+v, %v", result, err)
}
terminal := load()
request.RequestID = uuid.NewString()
if _, err := service.IdentifyCurrentPage(ctx, created.TaskID, request, token); taskErrorCode(t, err) != CodeTaskStateConflict {
t.Fatalf("expected terminal-state rejection: %v", err)
}
if !reflect.DeepEqual(terminal, load()) {
t.Fatal("terminal identification changed the task")
}
var archived []models.CollectionTaskAttempt
if err := db.Where("task_id = ?", created.TaskID).Find(&archived).Error; err != nil {
t.Fatal(err)
}
if len(archived) != 1 || archived[0].AttemptNumber != 1 || archived[0].Status != models.TaskStatusFailed ||
archived[0].ErrorCode == nil || *archived[0].ErrorCode != failureCode {
t.Fatal("original failed attempt was not preserved")
}
for _, model := range []any{&models.CollectionTask{}, &models.PDDProduct{}} {
var count int64
if err := db.Model(model).Count(&count).Error; err != nil || count != 1 {
t.Fatalf("retry duplicated task/product: count=%d, %v", count, err)
}
}
}