Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cb78ad8cd |
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user