fix(#82): separate PDD linking from mapping

This commit is contained in:
QiuSW
2026-08-25 10:21:10 +08:00
parent 66ea01c02e
commit 7b488e2c68
7 changed files with 92 additions and 22 deletions
+2 -2
View File
@@ -315,13 +315,13 @@ func (s *Service) previewFromDataset(ctx context.Context, id uint64, dataset bat
return item
}
if shopee.PDDProductID == nil {
item.ReasonCode, item.Reason, item.NextAction = "PDD_NOT_LINKED", "尚未关联拼多多商品,请先去匹配", "open_mapping"
item.ReasonCode, item.Reason, item.NextAction = "PDD_NOT_LINKED", "尚未关联 PDD 商品,请先关联", processActionOpenPDDLink
return item
}
item.PDDProductID = shopee.PDDProductID
pdd, found := dataset.pddByID[*shopee.PDDProductID]
if !found {
item.ReasonCode, item.Reason, item.NextAction = "PDD_NOT_FOUND", "关联的拼多多商品不存在,请先重新匹配", "open_mapping"
item.ReasonCode, item.Reason, item.NextAction = "PDD_NOT_FOUND", "关联的 PDD 商品不存在,请重新关联", processActionOpenPDDLink
return item
}
item.PDDGoodsID, item.PDDTitle = pdd.GoodsID, pdd.Title
+17
View File
@@ -43,6 +43,23 @@ func TestBatchPreviewUsesPDDPriceAndExplainsIneligibleRows(t *testing.T) {
}
}
func TestBatchPreviewUsesLinkActionBeforePDDMapping(t *testing.T) {
db := testDB(t)
fixture := seed(t, db, liveCaps(), true)
if err := db.Model(&models.ShopeeProduct{}).Where("id = ?", fixture.shopee.ID).Update("pdd_product_id", nil).Error; err != nil {
t.Fatal(err)
}
response, err := testService(db).BatchPreview(context.Background(), BatchPreviewRequest{SYBProductIDs: []uint64{fixture.syb.ID}})
if err != nil {
t.Fatal(err)
}
item := response.Items[0]
if item.Eligible || item.ReasonCode != "PDD_NOT_LINKED" || item.NextAction != processActionOpenPDDLink || item.ProcessNextAction != processActionOpenPDDLink {
t.Fatalf("PDD link action missing: %+v", item)
}
}
func TestBatchCreateKeepsPartialSuccessAndReplaysCreatedItems(t *testing.T) {
db := testDB(t)
fixture := seed(t, db, liveCaps(), true)
+3 -2
View File
@@ -18,6 +18,7 @@ const (
ProcessStageTaskCreated = "task_created"
ProcessStagePurchaseSucceeded = "purchase_succeeded"
ProcessStageOrderReview = "order_review"
processActionOpenPDDLink = "open_pdd_link"
)
var processStageLabels = map[string]string{
@@ -115,11 +116,11 @@ func processStageFromDataset(id uint64, dataset batchPreviewDataset, preview Bat
return stage(ProcessStageManualAction, "关联的蝦皮商品不存在", "open_shopee")
}
if shopee.PDDProductID == nil {
return stage(ProcessStagePDDUnlinked, "请先关联拼多多商品", "open_mapping")
return stage(ProcessStagePDDUnlinked, "请先关联 PDD 商品", processActionOpenPDDLink)
}
pdd, ok := dataset.pddByID[*shopee.PDDProductID]
if !ok {
return stage(ProcessStagePDDUnlinked, "关联的拼多多商品不存在", "open_mapping")
return stage(ProcessStagePDDUnlinked, "关联的 PDD 商品不存在,请重新关联", processActionOpenPDDLink)
}
if pdd.Status == "disabled" {
return stage(ProcessStageManualAction, "拼多多商品已停用", "open_pdd")
@@ -19,48 +19,49 @@ func TestProcessStageDerivesAllUserFacingStages(t *testing.T) {
}
}
tests := []struct {
name string
want string
set func(*batchPreviewDataset, *BatchPreviewItem)
name string
want string
wantAction string
set func(*batchPreviewDataset, *BatchPreviewItem)
}{
{"待人工处理", ProcessStageManualAction, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
{"待人工处理", ProcessStageManualAction, "reparse", func(d *batchPreviewDataset, _ *BatchPreviewItem) {
row := d.sybByID[1]
row.ParseStatus = models.SYBParseStatusFailed
d.sybByID[1] = row
}},
{"未关联 PDD", ProcessStagePDDUnlinked, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
{"未关联 PDD", ProcessStagePDDUnlinked, processActionOpenPDDLink, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
row := d.shopeeByID[2]
row.PDDProductID = nil
d.shopeeByID[2] = row
}},
{"PDD 待采集", ProcessStagePDDPending, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
{"PDD 待采集", ProcessStagePDDPending, "open_pdd", func(d *batchPreviewDataset, _ *BatchPreviewItem) {
row := d.pddByID[3]
row.Status = "pending"
d.pddByID[3] = row
}},
{"PDD 采集中", ProcessStagePDDCollecting, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
{"PDD 采集中", ProcessStagePDDCollecting, "open_pdd", func(d *batchPreviewDataset, _ *BatchPreviewItem) {
row := d.pddByID[3]
row.Status = "pending"
d.pddByID[3] = row
d.latestCollectionByPDD[3] = models.CollectionTask{ID: 8, PDDProductID: 3, Status: models.TaskStatusRunning}
}},
{"PDD 采集失败", ProcessStagePDDCollectionFail, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
{"PDD 采集失败", ProcessStagePDDCollectionFail, "open_pdd", func(d *batchPreviewDataset, _ *BatchPreviewItem) {
row := d.pddByID[3]
row.Status = "pending"
d.pddByID[3] = row
d.latestCollectionByPDD[3] = models.CollectionTask{ID: 8, PDDProductID: 3, Status: models.TaskStatusFailed}
}},
{"颜色待匹配", ProcessStageColorMapping, func(_ *batchPreviewDataset, p *BatchPreviewItem) { p.ReasonCode = CodeMappingRequired }},
{"可创建采购", ProcessStagePurchaseReady, func(_ *batchPreviewDataset, p *BatchPreviewItem) { p.Eligible = true }},
{"已创建任务", ProcessStageTaskCreated, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
{"颜色待匹配", ProcessStageColorMapping, "open_mapping", func(_ *batchPreviewDataset, p *BatchPreviewItem) { p.ReasonCode = CodeMappingRequired }},
{"可创建采购", ProcessStagePurchaseReady, "", func(_ *batchPreviewDataset, p *BatchPreviewItem) { p.Eligible = true }},
{"已创建任务", ProcessStageTaskCreated, "open_task", func(d *batchPreviewDataset, _ *BatchPreviewItem) {
sybID := uint64(1)
d.latestTaskBySYB[1] = models.PurchaseTask{ID: 11, SYBProductID: &sybID, Status: models.PurchaseTaskStatusRunning}
}},
{"采购成功", ProcessStagePurchaseSucceeded, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
{"采购成功", ProcessStagePurchaseSucceeded, "open_task", func(d *batchPreviewDataset, _ *BatchPreviewItem) {
sybID := uint64(1)
d.latestTaskBySYB[1] = models.PurchaseTask{ID: 12, SYBProductID: &sybID, Status: models.PurchaseTaskStatusOrderCreated}
}},
{"待人工核对", ProcessStageOrderReview, func(d *batchPreviewDataset, _ *BatchPreviewItem) {
{"待人工核对", ProcessStageOrderReview, "open_task", func(d *batchPreviewDataset, _ *BatchPreviewItem) {
sybID := uint64(1)
d.latestTaskBySYB[1] = models.PurchaseTask{ID: 13, SYBProductID: &sybID, Status: models.PurchaseTaskStatusOrderResultUnknown}
}},
@@ -71,8 +72,8 @@ func TestProcessStageDerivesAllUserFacingStages(t *testing.T) {
preview := BatchPreviewItem{SYBProductID: 1}
tt.set(&dataset, &preview)
got := processStageFromDataset(1, dataset, preview)
if got.Stage != tt.want || got.Label != tt.name {
t.Fatalf("got stage=%q label=%q, want %q/%q", got.Stage, got.Label, tt.want, tt.name)
if got.Stage != tt.want || got.Label != tt.name || got.NextAction != tt.wantAction {
t.Fatalf("got stage=%q label=%q action=%q, want %q/%q/%q", got.Stage, got.Label, got.NextAction, tt.want, tt.name, tt.wantAction)
}
})
}
@@ -83,3 +84,20 @@ func TestValidProcessStageRejectsInternalOrUnknownValues(t *testing.T) {
t.Fatal("process stage validation accepted an internal or unknown value")
}
}
func TestProcessStageMissingPDDUsesRelinkAction(t *testing.T) {
pddID := uint64(3)
shopeeID := uint64(2)
dataset := batchPreviewDataset{
sybByID: map[uint64]models.SYBProduct{1: {ID: 1, ParseStatus: models.SYBParseStatusSuccess, ShopeeProductID: &shopeeID}},
shopeeByID: map[uint64]models.ShopeeProduct{2: {ID: 2, PDDProductID: &pddID}},
pddByID: map[uint64]models.PDDProduct{},
latestTaskBySYB: make(map[uint64]models.PurchaseTask),
latestCollectionByPDD: make(map[uint64]models.CollectionTask),
}
got := processStageFromDataset(1, dataset, BatchPreviewItem{SYBProductID: 1})
if got.Stage != ProcessStagePDDUnlinked || got.NextAction != processActionOpenPDDLink || got.Reason != "关联的 PDD 商品不存在,请重新关联" {
t.Fatalf("missing PDD did not request relink: %+v", got)
}
}
@@ -183,7 +183,7 @@ export default {
created() {
this.load()
const productId = Number(this.$route.query.productId)
if (Number.isInteger(productId) && productId > 0) this.openDetail(productId)
if (Number.isInteger(productId) && productId > 0) this.openRouteDetail(productId).catch(() => {})
},
methods: {
emptyCreate() { return { shopeeItemId: '', title: '', shopName: '', pddProductId: null } },
@@ -281,6 +281,13 @@ export default {
},
// ---------------- 详情 / 映射 ----------------
async openRouteDetail(id) {
try {
await this.openDetail(id)
} finally {
if (String(this.$route.query.action || '') === 'link_pdd' && this.detail.product?.id === id) this.openPddPicker('link')
}
},
async openDetail(id) {
this.detail = { ...this.emptyDetail(), open: true, loading: true, highlightColor: String(this.$route.query.targetColor || '').trim() }
try {
+4 -2
View File
@@ -207,7 +207,7 @@ export default {
purchaseReady(row) { return this.purchaseReadiness[row.id] || { sybProductId: row.id, eligible: false, reason: this.purchaseReadinessLoading ? '正在检查' : '请刷新后重试' } },
purchasePriceText(item) { if (item.minUnitPriceCent === undefined || item.maxUnitPriceCent === undefined) return ''; return `允许单价 ¥${(item.minUnitPriceCent / 100).toFixed(2)}~¥${(item.maxUnitPriceCent / 100).toFixed(2)}` },
processMeta(stage) { return { manual_action: { label: '待人工处理', type: 'warning' }, pdd_unlinked: { label: '未关联 PDD', type: 'info' }, pdd_pending: { label: 'PDD 待采集', type: 'info' }, pdd_collecting: { label: 'PDD 采集中', type: 'primary' }, pdd_collection_failed: { label: 'PDD 采集失败', type: 'danger' }, color_mapping: { label: '颜色待匹配', type: 'warning' }, purchase_ready: { label: '可创建采购', type: 'success' }, task_created: { label: '已创建任务', type: 'primary' }, purchase_succeeded: { label: '采购成功', type: 'success' }, order_review: { label: '待人工核对', type: 'danger' }}[stage] || { label: '待人工处理', type: 'warning' } },
purchaseActionLabel(item) { return { open_mapping: '去匹配', open_shopee: '查看蝦皮商品', open_pdd: '查看 PDD 商品', open_task: '查看任务', reparse: '查看并处理', select_device: '重新选择设备', refresh: '刷新' }[item.nextAction] || '' },
purchaseActionLabel(item) { return { open_pdd_link: '去关联', open_mapping: '去匹配', open_shopee: '查看蝦皮商品', open_pdd: '查看 PDD 商品', open_task: '查看任务', reparse: '查看并处理', select_device: '重新选择设备', refresh: '刷新' }[item.nextAction] || '' },
async loadPurchaseReadiness(ids, requestOptions = {}, generation = this.loadGeneration) {
if (generation !== this.loadGeneration) return
this.purchaseReadiness = {}
@@ -226,10 +226,11 @@ export default {
},
priceText(row) { if (row.unitPriceCent === null || row.unitPriceCent === undefined) return '—'; return `¥${(row.unitPriceCent / 100).toFixed(2)}` },
parseMeta(status) { return { success: { label: '成功', type: 'success' }, uncertain: { label: '存疑', type: 'warning' }, failed: { label: '失败', type: 'danger' }}[status] || { label: status || '-', type: 'info' } },
openShopeeDetail(shopeeProductId, targetColor = '') {
openShopeeDetail(shopeeProductId, targetColor = '', action = '') {
if (!shopeeProductId) { ElMessage.warning('该明细尚未关联虾皮商品'); return }
const query = { productId: shopeeProductId }
if (targetColor) query.targetColor = targetColor
if (action) query.action = action
const route = this.$router.resolve({ path: '/shopee-products/index', query })
window.open(route.href, '_blank', 'noopener')
},
@@ -240,6 +241,7 @@ export default {
},
openPurchaseTask(taskId) { this.$router.push({ path: '/purchase-tasks/index', query: { taskId }}) },
runPurchaseNextAction(row, readiness) {
if (readiness.nextAction === 'open_pdd_link') return this.openShopeeDetail(row.shopeeProductId, '', 'link_pdd')
if (readiness.nextAction === 'open_mapping' || readiness.nextAction === 'open_shopee') return this.openShopeeDetail(row.shopeeProductId, row.targetColor)
if (readiness.nextAction === 'open_pdd') return this.openPddDetail(readiness.pddProductId)
if (readiness.nextAction === 'open_task' && readiness.activeTaskId) return this.openPurchaseTask(readiness.activeTaskId)
+25
View File
@@ -115,6 +115,31 @@ test('PDD 待采集的下一步打开对应 PDD 商品而不是蝦皮商品', as
expect(popup.url()).not.toContain('/shopee-products/index')
})
test('未关联 PDD 显示去关联并直接打开 PDD 商品选择器', async({ page, context }) => {
await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }])
await context.route('**/api/**', async route => {
const url = new URL(route.request().url())
if (url.pathname.startsWith('/src/api/')) return route.continue()
if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] }}})
if (url.pathname.endsWith('/api/admin/v1/syb-products')) return route.fulfill({ json: { code: 200, data: { items: [{ id: 41, orderCode: 'SYB-LINK-41', shopeeItemId: '26154802794', shopeeProductId: 9, productTitle: '未关联商品', shopName: '大碼臻選', targetColor: '黑色', targetSize: 'XL', quantity: 1, unitPriceCent: 1000, parseStatus: 'success' }, { id: 42, orderCode: 'SYB-MAP-42', shopeeItemId: '26154802795', shopeeProductId: 10, productTitle: '待匹配商品', shopName: '大碼臻選', targetColor: '白色', targetSize: 'L', quantity: 1, unitPriceCent: 1200, parseStatus: 'success' }], total: 2, page: 1, pageSize: 20 }}})
if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) return route.fulfill({ json: { code: 200, data: { items: [{ sybProductId: 41, eligible: false, processStage: 'pdd_unlinked', processStageLabel: '未关联 PDD', processStageReason: '请先关联 PDD 商品', processNextAction: 'open_pdd_link' }, { sybProductId: 42, eligible: false, processStage: 'color_mapping', processStageLabel: '颜色待匹配', processStageReason: '颜色匹配无效,请重新选择', processNextAction: 'open_mapping' }], eligibleCount: 0, skippedCount: 2 }}})
if (url.pathname.endsWith('/api/admin/v1/shopee-products/9')) return route.fulfill({ json: { code: 200, data: { product: { id: 9, shopeeItemId: '26154802794', title: '未关联商品', shopName: '大碼臻選', pddProductId: null, specs: [] } } }})
if (url.pathname.endsWith('/api/admin/v1/shopee-products')) return route.fulfill({ json: { code: 200, data: { items: [], total: 0, page: 1, pageSize: 20 }}})
return route.fulfill({ json: { code: 200, data: [] }})
})
await page.goto('http://localhost:9527/#/syb-products/index')
await expect(page.getByText('请先关联 PDD 商品', { exact: true })).toBeVisible()
await expect(page.getByRole('button', { name: '去关联', exact: true })).toBeVisible()
await expect(page.getByRole('button', { name: '去匹配', exact: true })).toBeVisible()
const popupPromise = page.waitForEvent('popup')
await page.getByRole('button', { name: '去关联', exact: true }).click()
const popup = await popupPromise
await expect.poll(() => popup.url()).toContain('/#/shopee-products/index?productId=9&action=link_pdd')
await expect(popup.getByText('搜索并选择 PDD 商品', { exact: true })).toBeVisible()
})
test('SYB 商品页不读取或展示后台同步状态', async({ page, context }) => {
await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }])
let syncRunCalls = 0