@@ -12,8 +12,7 @@ import (
|
||||
const (
|
||||
CodeOriginNotEligible = "REPLACEMENT_ORIGIN_NOT_ELIGIBLE"
|
||||
|
||||
ErrorPDDLinkInvalid = "PDD_LINK_INVALID"
|
||||
ErrorPDDGoodsSoldOut = "PDD_GOODS_SOLD_OUT"
|
||||
collectionPurchaseInProgressReason = "该商品有采购任务正在执行或结果待核对,暂不能替换,请稍后重试"
|
||||
)
|
||||
|
||||
type OriginInspection struct {
|
||||
@@ -32,8 +31,8 @@ func (service *Service) InspectOrigin(ctx context.Context, originType string, ta
|
||||
return OriginInspection{}, fail(CodeInvalidRequest, "替换来源无效")
|
||||
}
|
||||
result := OriginInspection{}
|
||||
var status string
|
||||
var errorCode *string
|
||||
var purchaseStatus string
|
||||
var purchaseErrorCode *string
|
||||
switch originType {
|
||||
case models.ReplacementOriginCollection:
|
||||
var task models.CollectionTask
|
||||
@@ -45,7 +44,6 @@ func (service *Service) InspectOrigin(ctx context.Context, originType string, ta
|
||||
if task.PDDProductID != nil {
|
||||
result.SourceProductID = *task.PDDProductID
|
||||
}
|
||||
status, errorCode = task.Status, task.ErrorCode
|
||||
case models.ReplacementOriginPurchase:
|
||||
var task models.PurchaseTask
|
||||
if err := service.DB.WithContext(ctx).Where("id = ? AND device_id = ?", taskID, deviceID).First(&task).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -61,7 +59,7 @@ func (service *Service) InspectOrigin(ctx context.Context, originType string, ta
|
||||
if task.ShopeeProductID != nil {
|
||||
result.ShopeeProductID = *task.ShopeeProductID
|
||||
}
|
||||
status, errorCode = task.Status, task.ErrorCode
|
||||
purchaseStatus, purchaseErrorCode = task.Status, task.ErrorCode
|
||||
}
|
||||
if result.SourceProductID == 0 {
|
||||
result.DisabledReason = "原任务没有可替换的拼多多商品"
|
||||
@@ -101,12 +99,28 @@ func (service *Service) InspectOrigin(ctx context.Context, originType string, ta
|
||||
}
|
||||
result.CorrectionReplacementID = correctionID
|
||||
}
|
||||
failed := status == models.TaskStatusFailed || status == models.PurchaseTaskStatusFailed
|
||||
if !failed {
|
||||
if originType == models.ReplacementOriginCollection {
|
||||
var count int64
|
||||
if err := service.DB.WithContext(ctx).Model(&models.PurchaseTask{}).
|
||||
Where("pdd_product_id = ? AND status IN ?", result.SourceProductID, []string{
|
||||
models.PurchaseTaskStatusRunning,
|
||||
models.PurchaseTaskStatusOrderSubmitStarted,
|
||||
models.PurchaseTaskStatusOrderResultUnknown,
|
||||
}).Count(&count).Error; err != nil {
|
||||
return OriginInspection{}, internal(err)
|
||||
}
|
||||
if count > 0 {
|
||||
result.DisabledReason = collectionPurchaseInProgressReason
|
||||
return result, nil
|
||||
}
|
||||
result.Eligible = true
|
||||
return result, nil
|
||||
}
|
||||
if purchaseStatus != models.PurchaseTaskStatusFailed {
|
||||
result.DisabledReason = "只有失败任务可以替换商品"
|
||||
return result, nil
|
||||
}
|
||||
if errorCode == nil || (*errorCode != ErrorPDDLinkInvalid && *errorCode != ErrorPDDGoodsSoldOut) {
|
||||
if purchaseErrorCode == nil || (*purchaseErrorCode != "PDD_LINK_INVALID" && *purchaseErrorCode != "PDD_GOODS_SOLD_OUT") {
|
||||
result.DisabledReason = "当前失败原因不属于商品失效或售罄"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -99,10 +99,11 @@ func replacementCode(err error) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestInspectOriginUsesExactFailureCodesAndDeviceBoundary(t *testing.T) {
|
||||
func TestInspectCollectionOriginAllowsAnyTaskStatusAndFailureCode(t *testing.T) {
|
||||
fixture := seedReplacementFixture(t)
|
||||
linkInvalid := ErrorPDDLinkInvalid
|
||||
if err := fixture.db.Session(&gorm.Session{SkipHooks: true}).Model(&models.CollectionTask{}).Where("id = ?", fixture.origin.ID).Update("error_code", linkInvalid).Error; err != nil {
|
||||
genericFailure := "RULE_NOT_MATCHED"
|
||||
if err := fixture.db.Session(&gorm.Session{SkipHooks: true}).Model(&models.CollectionTask{}).Where("id = ?", fixture.origin.ID).
|
||||
Updates(map[string]any{"status": models.TaskStatusCompleted, "error_code": genericFailure}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -111,15 +112,6 @@ func TestInspectOriginUsesExactFailureCodesAndDeviceBoundary(t *testing.T) {
|
||||
t.Fatalf("eligible collection origin: inspection=%+v error=%v", inspection, err)
|
||||
}
|
||||
|
||||
notExact := "PDD_LINK_INVALID_RETRY"
|
||||
if err := fixture.db.Session(&gorm.Session{SkipHooks: true}).Model(&models.CollectionTask{}).Where("id = ?", fixture.origin.ID).Update("error_code", notExact).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspection, err = fixture.service.InspectOrigin(context.Background(), models.ReplacementOriginCollection, fixture.origin.ID, fixture.device.ID)
|
||||
if err != nil || inspection.Eligible || inspection.DisabledReason == "" {
|
||||
t.Fatalf("non-exact code must be disabled: inspection=%+v error=%v", inspection, err)
|
||||
}
|
||||
|
||||
otherDevice := fixture.device
|
||||
otherDevice.ID = 0
|
||||
otherDevice.InstallID = uuid.NewString()
|
||||
@@ -133,6 +125,96 @@ func TestInspectOriginUsesExactFailureCodesAndDeviceBoundary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectCollectionOriginBlocksPurchasesInProgress(t *testing.T) {
|
||||
blockedStatuses := []string{
|
||||
models.PurchaseTaskStatusRunning,
|
||||
models.PurchaseTaskStatusOrderSubmitStarted,
|
||||
models.PurchaseTaskStatusOrderResultUnknown,
|
||||
}
|
||||
for _, status := range blockedStatuses {
|
||||
t.Run(status, func(t *testing.T) {
|
||||
fixture := seedReplacementFixture(t)
|
||||
shopee := models.ShopeeProduct{ShopeeItemID: "SP-BLOCKED", PDDProductID: &fixture.source.ID, SpecsJSON: "[]", Currency: "CNY"}
|
||||
if err := fixture.db.Create(&shopee).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
syb := models.SYBProduct{OrderCode: "SYB-BLOCKED", DetailID: 1, StockID: 1, ShopeeItemID: shopee.ShopeeItemID, ShopeeProductID: &shopee.ID, Quantity: 1, UnitPriceCent: 100, ParseStatus: models.SYBParseStatusSuccess, RawJSON: `{}`}
|
||||
if err := fixture.db.Create(&syb).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
purchase := models.PurchaseTask{
|
||||
SYBProductID: &syb.ID, ShopeeProductID: &shopee.ID, PDDProductID: fixture.source.ID, DeviceID: &fixture.device.ID,
|
||||
ExecutionMode: models.PurchaseExecutionModeLive, Status: status,
|
||||
ShopeeItemIDSnapshot: "SP-BLOCKED", PDDURLSnapshot: fixture.source.URL, PDDGoodsIDSnapshot: fixture.source.GoodsID,
|
||||
SpecDecisionSnapshot: `{}`, Quantity: 1, Currency: "CNY", RuleType: "pddPurchase", RuleSchemaVersion: 1,
|
||||
RequiredCapabilitiesJSON: `[]`, RuleSnapshot: `{}`, CreateRequestID: uuid.NewString(),
|
||||
PaymentReviewStatus: models.PurchasePaymentReviewPending, LogisticsStatus: models.PurchaseLogisticsStatusPending,
|
||||
WritebackStatus: models.PurchaseWritebackStatusNotSelected,
|
||||
}
|
||||
if err := fixture.db.Create(&purchase).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inspection, err := fixture.service.InspectOrigin(context.Background(), models.ReplacementOriginCollection, fixture.origin.ID, fixture.device.ID)
|
||||
if err != nil || inspection.Eligible || inspection.DisabledReason != collectionPurchaseInProgressReason {
|
||||
t.Fatalf("status=%s inspection=%+v error=%v", status, inspection, err)
|
||||
}
|
||||
|
||||
if err := fixture.db.Session(&gorm.Session{SkipHooks: true}).Model(&purchase).Update("status", models.PurchaseTaskStatusFailed).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspection, err = fixture.service.InspectOrigin(context.Background(), models.ReplacementOriginCollection, fixture.origin.ID, fixture.device.ID)
|
||||
if err != nil || !inspection.Eligible {
|
||||
t.Fatalf("terminal purchase must restore eligibility: inspection=%+v error=%v", inspection, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectPurchaseOriginKeepsFailureStatusAndCodeRequirements(t *testing.T) {
|
||||
fixture := seedReplacementFixture(t)
|
||||
shopee := models.ShopeeProduct{ShopeeItemID: "SP-PURCHASE", PDDProductID: &fixture.source.ID, SpecsJSON: "[]", Currency: "CNY"}
|
||||
if err := fixture.db.Create(&shopee).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
syb := models.SYBProduct{OrderCode: "SYB-PURCHASE", DetailID: 1, StockID: 1, ShopeeItemID: shopee.ShopeeItemID, ShopeeProductID: &shopee.ID, Quantity: 1, UnitPriceCent: 100, ParseStatus: models.SYBParseStatusSuccess, RawJSON: `{}`}
|
||||
if err := fixture.db.Create(&syb).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
errorCode := "PDD_LINK_INVALID"
|
||||
purchase := models.PurchaseTask{
|
||||
SYBProductID: &syb.ID, ShopeeProductID: &shopee.ID, PDDProductID: fixture.source.ID, DeviceID: &fixture.device.ID,
|
||||
ExecutionMode: models.PurchaseExecutionModeLive, Status: models.PurchaseTaskStatusFailed, ErrorCode: &errorCode,
|
||||
ShopeeItemIDSnapshot: shopee.ShopeeItemID, PDDURLSnapshot: fixture.source.URL, PDDGoodsIDSnapshot: fixture.source.GoodsID,
|
||||
SpecDecisionSnapshot: `{}`, Quantity: 1, Currency: "CNY", RuleType: "pddPurchase", RuleSchemaVersion: 1,
|
||||
RequiredCapabilitiesJSON: `[]`, RuleSnapshot: `{}`, CreateRequestID: uuid.NewString(),
|
||||
PaymentReviewStatus: models.PurchasePaymentReviewPending, LogisticsStatus: models.PurchaseLogisticsStatusPending,
|
||||
WritebackStatus: models.PurchaseWritebackStatusNotSelected,
|
||||
}
|
||||
if err := fixture.db.Create(&purchase).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inspection, err := fixture.service.InspectOrigin(context.Background(), models.ReplacementOriginPurchase, purchase.ID, fixture.device.ID)
|
||||
if err != nil || !inspection.Eligible {
|
||||
t.Fatalf("eligible purchase origin: inspection=%+v error=%v", inspection, err)
|
||||
}
|
||||
if err := fixture.db.Session(&gorm.Session{SkipHooks: true}).Model(&purchase).Updates(map[string]any{"error_code": "RULE_NOT_MATCHED"}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspection, err = fixture.service.InspectOrigin(context.Background(), models.ReplacementOriginPurchase, purchase.ID, fixture.device.ID)
|
||||
if err != nil || inspection.Eligible || inspection.DisabledReason != "当前失败原因不属于商品失效或售罄" {
|
||||
t.Fatalf("purchase failure code boundary changed: inspection=%+v error=%v", inspection, err)
|
||||
}
|
||||
if err := fixture.db.Session(&gorm.Session{SkipHooks: true}).Model(&purchase).Updates(map[string]any{"status": models.PurchaseTaskStatusOrderCreated, "error_code": errorCode}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspection, err = fixture.service.InspectOrigin(context.Background(), models.ReplacementOriginPurchase, purchase.ID, fixture.device.ID)
|
||||
if err != nil || inspection.Eligible || inspection.DisabledReason != "只有失败任务可以替换商品" {
|
||||
t.Fatalf("purchase status boundary changed: inspection=%+v error=%v", inspection, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterAndQueryCollectionOrigin(t *testing.T) {
|
||||
fixture := seedReplacementFixture(t)
|
||||
request := fixture.request()
|
||||
|
||||
@@ -54,6 +54,7 @@ type AgentCollectionDetail struct {
|
||||
ReviewCount *int64 `json:"reviewCount,omitempty"`
|
||||
Dimensions []DetailDimension `json:"dimensions"`
|
||||
ColorPrices []AgentColorPrice `json:"colorPrices"`
|
||||
ColorImages []AgentColorImage `json:"colorImages"`
|
||||
SKUs []AgentCollectionSKU `json:"skus"`
|
||||
Missing []string `json:"missing"`
|
||||
ReplacementEligible bool `json:"replacementEligible"`
|
||||
@@ -79,6 +80,13 @@ type AgentColorPrice struct {
|
||||
PriceCent int64 `json:"priceCent"`
|
||||
}
|
||||
|
||||
type AgentColorImage struct {
|
||||
Color string `json:"color"`
|
||||
ImagePath string `json:"imagePath"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
type AgentCollectionSKU struct {
|
||||
Specs map[string]string `json:"specs"`
|
||||
PriceCent int64 `json:"priceCent"`
|
||||
@@ -163,6 +171,14 @@ func (service *Service) AgentHistoryDetail(ctx context.Context, taskID uint64, t
|
||||
for _, value := range detail.ColorPrices {
|
||||
colorPrices = append(colorPrices, AgentColorPrice{Color: value.Color, PriceCent: value.PriceCent})
|
||||
}
|
||||
var imageRows []models.PDDProductColorImage
|
||||
if err := service.DB.WithContext(ctx).Where("source_task_id = ?", taskID).Order("color ASC").Find(&imageRows).Error; err != nil {
|
||||
return AgentCollectionDetail{}, internalError(err)
|
||||
}
|
||||
colorImages := make([]AgentColorImage, 0, len(imageRows))
|
||||
for _, value := range imageRows {
|
||||
colorImages = append(colorImages, AgentColorImage{Color: value.Color, ImagePath: value.ImagePath, Width: value.Width, Height: value.Height})
|
||||
}
|
||||
skus := make([]AgentCollectionSKU, 0, len(detail.SKUs))
|
||||
for _, value := range detail.SKUs {
|
||||
skus = append(skus, AgentCollectionSKU{Specs: value.Specs, PriceCent: value.PriceCent, Available: value.Available, Complete: value.Complete})
|
||||
@@ -177,7 +193,7 @@ func (service *Service) AgentHistoryDetail(ctx context.Context, taskID uint64, t
|
||||
}
|
||||
return AgentCollectionDetail{
|
||||
Task: agentCollectionItem(record), Attempts: attempts, ShopName: record.ShopName, SalesText: record.SalesText,
|
||||
ReviewCount: record.ReviewCount, Dimensions: detail.Dimensions, ColorPrices: colorPrices,
|
||||
ReviewCount: record.ReviewCount, Dimensions: detail.Dimensions, ColorPrices: colorPrices, ColorImages: colorImages,
|
||||
SKUs: skus, Missing: detail.Missing,
|
||||
ReplacementEligible: inspection.Eligible, ReplacementDisabledReason: inspection.DisabledReason,
|
||||
ReplacementMappingStatus: inspection.MappingStatus, ReplacementActivationStatus: inspection.ActivationStatus,
|
||||
|
||||
@@ -95,3 +95,40 @@ func TestAgentHistoryDetailReturnsArrayForLegacyNullMissing(t *testing.T) {
|
||||
t.Fatalf("history detail did not preserve array contract: %s %v", raw, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentHistoryDetailReturnsOnlyImagesOwnedByTask(t *testing.T) {
|
||||
db := openTaskDatabase(t)
|
||||
deviceRecord, token := registerTaskDevice(t, db, "history-color-images")
|
||||
taskA := createTask(t, db, &deviceRecord.ID)
|
||||
taskB := createTask(t, db, &deviceRecord.ID)
|
||||
if taskA.PDDProductID == nil || taskB.PDDProductID == nil {
|
||||
t.Fatal("collection tasks must reference products")
|
||||
}
|
||||
images := []models.PDDProductColorImage{
|
||||
{PDDProductID: *taskA.PDDProductID, Color: "红色", ImagePath: "/static/uploadfile/goauto-color/red.jpg", ContentType: "image/jpeg", ByteSize: 120, Width: 300, Height: 300, SourceTaskID: taskA.ID, SourceDeviceID: deviceRecord.ID},
|
||||
{PDDProductID: *taskB.PDDProductID, Color: "蓝色", ImagePath: "/static/uploadfile/goauto-color/blue.jpg", ContentType: "image/jpeg", ByteSize: 140, Width: 320, Height: 280, SourceTaskID: taskB.ID, SourceDeviceID: deviceRecord.ID},
|
||||
}
|
||||
if err := db.Create(&images).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
detail, err := newTaskService(db).AgentHistoryDetail(context.Background(), taskA.ID, token)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(detail.ColorImages) != 1 || detail.ColorImages[0].Color != "红色" || detail.ColorImages[0].ImagePath != images[0].ImagePath {
|
||||
t.Fatalf("unexpected task images: %#v", detail.ColorImages)
|
||||
}
|
||||
|
||||
if err := db.Model(&images[0]).Update("source_task_id", taskB.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
detail, err = newTaskService(db).AgentHistoryDetail(context.Background(), taskA.ID, token)
|
||||
if err != nil || detail.ColorImages == nil || len(detail.ColorImages) != 0 {
|
||||
t.Fatalf("overwritten image must disappear from old task: %#v %v", detail.ColorImages, err)
|
||||
}
|
||||
raw, err := json.Marshal(detail)
|
||||
if err != nil || !strings.Contains(string(raw), `"colorImages":[]`) {
|
||||
t.Fatalf("empty image array contract changed: %s %v", raw, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user