fix(admin): decouple SYB collection eligibility (#183)
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Business-Rules-and-Glossary
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.-
|
||||
wiki_revision: a49579d398b21029aa23cb42201b54a3bed92643
|
||||
synchronized_at: 2026-08-31T16:06:48Z
|
||||
wiki_revision: 51de401da3555517a5e560ca72e064fb11bd8d89
|
||||
synchronized_at: 2026-09-01T00:58:04Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# 业务规则与术语
|
||||
@@ -136,6 +136,7 @@ synchronized_at: 2026-08-31T16:06:48Z
|
||||
- 价格保护保存参考单价、最低单价、最高单价和币种,执行时以 PDD App 实际单价判断;价格越界明确失败,不考虑优惠券。
|
||||
- Admin 从 SYB 当前页选择商品,可在“创建采购”和“重新解析”两种批量用途间切换;采购模式只允许选择预检合格行,表头全选不跨页。批量预检和正式创建均最多 100 条并逐条重新校验,每条 SYB 明细只创建一个独立任务;部分失败不回滚其他成功项。
|
||||
- SYB 商品列表在列表数据返回后立即展示,采购准备状态独立异步加载;只读批量预检只执行批量数据库读取、已确认映射和本地确定性匹配,禁止调用 AI Provider。正式创建仍按最新数据完整复核,只有创建阶段无法唯一确定规格时才允许调用 AI。
|
||||
- SYB 商品页的 PDD 采集资格独立于采购处理阶段:只要已关联的 PDD 商品未停用、没有 `pending` / `running` 采集任务且存在可用采集规则,即可创建采集任务;因此已完成采集并进入“可创建采购”的商品也可以重新采集。相同 PDD 商品在批量创建前按商品去重,服务端创建时仍按最新状态复核。
|
||||
- 批量创建读取管理员选定的当前采购规则并重新执行契约校验,每条任务保存不可变快照;当前规则缺失或无效时明确阻断,不回退到代码常量。设备默认人工指定,也可以留空由符合能力的空闲设备领取。
|
||||
- 批量创建的价格保护来自 PDD 商品档案而不是 SYB/Shopee 的 TWD 售价:当前采购规则可用 `priceGuard.minRatio`(0.1~1.0)和 `maxRatio`(1.0~3.0)配置比例,缺省仍为 0.2 / 1.5;最低价向下取整、最高价向上取整到人民币分。已确认颜色映射时按该颜色计算,待规格探测时以全部可用颜色的最低/最高价计算,参考价取最高价;没有可用颜色价格时不能创建。
|
||||
- 地址后缀为 `_cg{purchase_task.id}`。选好规格和数量后,Agent 仍停留在当前规格/下单面板;若收货地址被面板裁切,只允许在唯一、可见且占据主要宽度的纵向滚动容器内有限向下拉动来显示地址,禁止按页面最大滚动区域盲目滑动。Agent 只在唯一地址入口、唯一修改按钮和唯一详细地址输入框均成立时修改;脱敏手机号文字本身不可点击时,仍以该唯一文字节点的中心坐标执行一次精确手势点击,不沿用可能覆盖整个下单面板的可点击祖先。“修改”“保存”和“提交订单”等文字节点即使依赖可点击父节点,也必须保留原始文字节点作为每次重新定位的锚点,父节点只用于验证存在可点击路径。近乎完全重叠的无障碍重复节点按一个目标处理,仍有多个独立目标或页面切换超时则明确失败。按首个 `-` 或 `_` 截取地址主体后追加当前后缀,保存后必须回读完整新地址。修改或回读失败时禁止创建订单,地址全文和控件树不落库。
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件)
|
||||
wiki_page: Android-Agent-API-Contract
|
||||
wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Android-Agent-API-Contract.-
|
||||
wiki_revision: b074c3ec1b7fc605af3cfa982b60a8e6b6ff3664
|
||||
synchronized_at: 2026-08-31T16:07:32Z
|
||||
wiki_revision: 94a99754058be22ec1fbb4e2e661b91a23fc026d
|
||||
synchronized_at: 2026-09-01T00:58:35Z
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
# MVP 共享 API 契约
|
||||
@@ -501,7 +501,7 @@ POST /api/agent/v1/tasks/{taskId}/fail
|
||||
| `GET` | `/api/admin/v1/purchase-tasks/{taskId}` | 只读任务详情与 attempt 历史;不返回规则原文、Token、凭据、完整地址、控件树或截图 |
|
||||
| `POST` | `/api/admin/v1/purchase-tasks` | `requestId`;单条创建 |
|
||||
| `POST` | `/api/admin/v1/purchase-tasks/stock` | 创建备货采购;`requestId`、`executionMode`、`pddProductId`、可选 `deviceId` / `pddAccountId`、`color`、可选 `size`、`quantity`、`minUnitPriceCent`、`maxUnitPriceCent`。服务端使用内置规则并从所选颜色归档派生参考价;相同 `requestId` 幂等返回原任务 |
|
||||
| `POST` | `/api/admin/v1/purchase-tasks/batch-preview` | `sybProductIds`(1~100)和可选 `deviceId`;逐条返回是否可创建、价格区间、原因和下一步,不创建任务 |
|
||||
| `POST` | `/api/admin/v1/purchase-tasks/batch-preview` | `sybProductIds`(1~100)和可选 `deviceId`;逐条返回采购是否可创建、价格区间、原因和下一步,并独立返回 PDD 采集资格 `collectionEligible` 与可选禁用原因 `collectionDisabledReason`,不创建任务 |
|
||||
| `POST` | `/api/admin/v1/purchase-tasks/batch` | 批次 `requestId`、`sybProductIds`(1~100)和可选 `deviceId`;每条派生稳定幂等键并独立创建,部分失败不回滚成功项 |
|
||||
| `POST` | `/api/admin/v1/purchase-tasks/{taskId}/authorize-repurchase` | 一次性授权;创建新任务后自动消耗 |
|
||||
| `POST` | `/api/admin/v1/purchase-tasks/{taskId}/payment-review` | `paid` 或 `unpaid`,管理员和采购员可操作 |
|
||||
|
||||
@@ -32,32 +32,34 @@ type BatchCreateRequest struct {
|
||||
}
|
||||
|
||||
type BatchPreviewItem struct {
|
||||
SYBProductID uint64 `json:"sybProductId"`
|
||||
OrderCode string `json:"orderCode,omitempty"`
|
||||
ShopeeProductID *uint64 `json:"shopeeProductId,omitempty"`
|
||||
ShopeeItemID string `json:"shopeeItemId,omitempty"`
|
||||
ProductTitle string `json:"productTitle,omitempty"`
|
||||
PDDProductID *uint64 `json:"pddProductId,omitempty"`
|
||||
PDDGoodsID string `json:"pddGoodsId,omitempty"`
|
||||
PDDTitle string `json:"pddTitle,omitempty"`
|
||||
TargetColor string `json:"targetColor,omitempty"`
|
||||
TargetSize string `json:"targetSize,omitempty"`
|
||||
MappedColor string `json:"mappedColor,omitempty"`
|
||||
MappedSize string `json:"mappedSize,omitempty"`
|
||||
Quantity int64 `json:"quantity,omitempty"`
|
||||
ReferenceUnitPriceCent int64 `json:"referenceUnitPriceCent,omitempty"`
|
||||
MinUnitPriceCent int64 `json:"minUnitPriceCent,omitempty"`
|
||||
MaxUnitPriceCent int64 `json:"maxUnitPriceCent,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
Eligible bool `json:"eligible"`
|
||||
ReasonCode string `json:"reasonCode,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
NextAction string `json:"nextAction,omitempty"`
|
||||
ActiveTaskID *uint64 `json:"activeTaskId,omitempty"`
|
||||
ProcessStage string `json:"processStage,omitempty"`
|
||||
ProcessStageLabel string `json:"processStageLabel,omitempty"`
|
||||
ProcessStageReason string `json:"processStageReason,omitempty"`
|
||||
ProcessNextAction string `json:"processNextAction,omitempty"`
|
||||
SYBProductID uint64 `json:"sybProductId"`
|
||||
OrderCode string `json:"orderCode,omitempty"`
|
||||
ShopeeProductID *uint64 `json:"shopeeProductId,omitempty"`
|
||||
ShopeeItemID string `json:"shopeeItemId,omitempty"`
|
||||
ProductTitle string `json:"productTitle,omitempty"`
|
||||
PDDProductID *uint64 `json:"pddProductId,omitempty"`
|
||||
PDDGoodsID string `json:"pddGoodsId,omitempty"`
|
||||
PDDTitle string `json:"pddTitle,omitempty"`
|
||||
TargetColor string `json:"targetColor,omitempty"`
|
||||
TargetSize string `json:"targetSize,omitempty"`
|
||||
MappedColor string `json:"mappedColor,omitempty"`
|
||||
MappedSize string `json:"mappedSize,omitempty"`
|
||||
Quantity int64 `json:"quantity,omitempty"`
|
||||
ReferenceUnitPriceCent int64 `json:"referenceUnitPriceCent,omitempty"`
|
||||
MinUnitPriceCent int64 `json:"minUnitPriceCent,omitempty"`
|
||||
MaxUnitPriceCent int64 `json:"maxUnitPriceCent,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
Eligible bool `json:"eligible"`
|
||||
ReasonCode string `json:"reasonCode,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
NextAction string `json:"nextAction,omitempty"`
|
||||
ActiveTaskID *uint64 `json:"activeTaskId,omitempty"`
|
||||
ProcessStage string `json:"processStage,omitempty"`
|
||||
ProcessStageLabel string `json:"processStageLabel,omitempty"`
|
||||
ProcessStageReason string `json:"processStageReason,omitempty"`
|
||||
ProcessNextAction string `json:"processNextAction,omitempty"`
|
||||
CollectionEligible bool `json:"collectionEligible"`
|
||||
CollectionDisabledReason string `json:"collectionDisabledReason,omitempty"`
|
||||
}
|
||||
|
||||
type BatchPreviewResponse struct {
|
||||
@@ -102,6 +104,7 @@ func (s *Service) BatchPreview(ctx context.Context, req BatchPreviewRequest) (Ba
|
||||
eligible := 0
|
||||
for _, id := range ids {
|
||||
item := s.previewFromDataset(ctx, id, dataset, false, guard)
|
||||
item.applyCollectionEligibility(id, dataset)
|
||||
if ruleReason != "" && item.Eligible {
|
||||
item.Eligible = false
|
||||
item.ReasonCode, item.Reason, item.NextAction = purchaserule.CodeCurrentRuleMissing, ruleReason, "refresh"
|
||||
@@ -120,16 +123,19 @@ func (s *Service) BatchPreview(ctx context.Context, req BatchPreviewRequest) (Ba
|
||||
}
|
||||
|
||||
type batchPreviewDataset struct {
|
||||
sybByID map[uint64]models.SYBProduct
|
||||
shopeeByID map[uint64]models.ShopeeProduct
|
||||
pddByID map[uint64]models.PDDProduct
|
||||
latestTaskBySYB map[uint64]models.PurchaseTask
|
||||
latestCollectionByPDD map[uint64]models.CollectionTask
|
||||
sybByID map[uint64]models.SYBProduct
|
||||
shopeeByID map[uint64]models.ShopeeProduct
|
||||
pddByID map[uint64]models.PDDProduct
|
||||
latestTaskBySYB map[uint64]models.PurchaseTask
|
||||
latestCollectionByPDD map[uint64]models.CollectionTask
|
||||
activeCollectionByPDD map[uint64]uint64
|
||||
collectionRuleAvailable bool
|
||||
}
|
||||
|
||||
// loadBatchPreviewDataset keeps the read-only preview on bounded bulk queries.
|
||||
// Pending PDD products add one collection-task query; active products retain
|
||||
// the original four-query fast path. Creation still revalidates each item.
|
||||
// Collection eligibility adds bounded active-task and rule queries; pending
|
||||
// products additionally load their latest task for the user-facing stage.
|
||||
// Creation still revalidates each item.
|
||||
func (s *Service) loadBatchPreviewDataset(ctx context.Context, ids []uint64) (batchPreviewDataset, error) {
|
||||
dataset := batchPreviewDataset{
|
||||
sybByID: make(map[uint64]models.SYBProduct, len(ids)),
|
||||
@@ -137,6 +143,7 @@ func (s *Service) loadBatchPreviewDataset(ctx context.Context, ids []uint64) (ba
|
||||
pddByID: make(map[uint64]models.PDDProduct),
|
||||
latestTaskBySYB: make(map[uint64]models.PurchaseTask),
|
||||
latestCollectionByPDD: make(map[uint64]models.CollectionTask),
|
||||
activeCollectionByPDD: make(map[uint64]uint64),
|
||||
}
|
||||
var sybProducts []models.SYBProduct
|
||||
if err := s.DB.WithContext(ctx).Where("id IN ?", ids).Find(&sybProducts).Error; err != nil {
|
||||
@@ -169,6 +176,22 @@ func (s *Service) loadBatchPreviewDataset(ctx context.Context, ids []uint64) (ba
|
||||
for _, pdd := range pddProducts {
|
||||
dataset.pddByID[pdd.ID] = pdd
|
||||
}
|
||||
var activeCollectionTasks []models.CollectionTask
|
||||
if err := s.DB.WithContext(ctx).
|
||||
Where("pdd_product_id IN ? AND status IN ?", pddIDs, []string{models.TaskStatusPending, models.TaskStatusRunning}).
|
||||
Find(&activeCollectionTasks).Error; err != nil {
|
||||
return dataset, err
|
||||
}
|
||||
for _, task := range activeCollectionTasks {
|
||||
if task.PDDProductID != nil {
|
||||
dataset.activeCollectionByPDD[*task.PDDProductID] = task.ID
|
||||
}
|
||||
}
|
||||
var collectionRuleCount int64
|
||||
if err := s.DB.WithContext(ctx).Model(&models.CollectionRule{}).Count(&collectionRuleCount).Error; err != nil {
|
||||
return dataset, err
|
||||
}
|
||||
dataset.collectionRuleAvailable = collectionRuleCount > 0
|
||||
pendingPDDIDs := make([]uint64, 0)
|
||||
for _, pdd := range pddProducts {
|
||||
if pdd.Status == "pending" {
|
||||
|
||||
@@ -44,6 +44,62 @@ func TestBatchPreviewUsesPDDPriceAndExplainsIneligibleRows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchPreviewExposesIndependentCollectionEligibility(t *testing.T) {
|
||||
db := testDB(t)
|
||||
fixture := seed(t, db, liveCaps(), true)
|
||||
setCollectedPDDPrice(t, db, fixture.pdd.ID)
|
||||
service := testService(db)
|
||||
preview := func() BatchPreviewItem {
|
||||
response, err := service.BatchPreview(context.Background(), BatchPreviewRequest{SYBProductIDs: []uint64{fixture.syb.ID}})
|
||||
if err != nil || len(response.Items) != 1 {
|
||||
t.Fatalf("preview failed: %+v %v", response, err)
|
||||
}
|
||||
return response.Items[0]
|
||||
}
|
||||
|
||||
withoutRule := preview()
|
||||
if !withoutRule.Eligible || withoutRule.ProcessStage != ProcessStagePurchaseReady || withoutRule.CollectionEligible || withoutRule.CollectionDisabledReason != "没有可用采集规则" {
|
||||
t.Fatalf("collection eligibility was coupled to purchase readiness: %+v", withoutRule)
|
||||
}
|
||||
|
||||
rule := models.CollectionRule{Name: "采集规则", ContentJSON: `{}`}
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ready := preview()
|
||||
if !ready.CollectionEligible || ready.CollectionDisabledReason != "" || ready.PDDProductID == nil || *ready.PDDProductID != fixture.pdd.ID {
|
||||
t.Fatalf("active idle PDD was not collection eligible: %+v", ready)
|
||||
}
|
||||
|
||||
activeSlot := uint8(1)
|
||||
task := models.CollectionTask{
|
||||
PDDProductID: &fixture.pdd.ID, RuleID: rule.ID, Status: models.TaskStatusPending, ActiveSlot: &activeSlot,
|
||||
URLSnapshot: fixture.pdd.URL, GoodsIDSnapshot: fixture.pdd.GoodsID, RuleSnapshot: rule.ContentJSON,
|
||||
}
|
||||
if err := db.Create(&task).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
blocked := preview()
|
||||
if blocked.CollectionEligible || blocked.CollectionDisabledReason != "已有待执行或执行中的采集任务" {
|
||||
t.Fatalf("active collection task did not block preview: %+v", blocked)
|
||||
}
|
||||
|
||||
if err := db.Model(&task).Updates(map[string]any{"status": models.TaskStatusFailed, "active_slot": nil}).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if item := preview(); !item.CollectionEligible {
|
||||
t.Fatalf("terminal collection task kept preview blocked: %+v", item)
|
||||
}
|
||||
|
||||
if err := db.Model(&models.PDDProduct{}).Where("id = ?", fixture.pdd.ID).Update("status", "disabled").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
disabled := preview()
|
||||
if disabled.CollectionEligible || disabled.CollectionDisabledReason != "商品已停用" {
|
||||
t.Fatalf("disabled product remained collection eligible: %+v", disabled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreateFreezesAbsolutePriceCeilingWhenRatiosAreDisabled(t *testing.T) {
|
||||
db := testDB(t)
|
||||
fixture := seed(t, db, liveCaps(), true)
|
||||
@@ -208,8 +264,8 @@ func TestBatchPreviewBulkLoadsAndNeverCallsAIMatcher(t *testing.T) {
|
||||
if matcher.calls != 0 {
|
||||
t.Fatalf("read-only preview called AI matcher %d times", matcher.calls)
|
||||
}
|
||||
if queries != 5 {
|
||||
t.Fatalf("batch preview used %d queries, want 5 bounded queries including current purchase rule", queries)
|
||||
if queries != 7 {
|
||||
t.Fatalf("batch preview used %d queries, want 7 bounded queries including collection eligibility and current purchase rule", queries)
|
||||
}
|
||||
if len(response.Items) != 2 || !response.Items[0].Eligible || !response.Items[1].Eligible {
|
||||
t.Fatalf("unresolved row should remain selectable for full create-time matching: %+v", response)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package purchase
|
||||
|
||||
func (item *BatchPreviewItem) applyCollectionEligibility(id uint64, dataset batchPreviewDataset) {
|
||||
item.CollectionEligible = false
|
||||
item.CollectionDisabledReason = ""
|
||||
|
||||
syb, ok := dataset.sybByID[id]
|
||||
if !ok {
|
||||
item.CollectionDisabledReason = "SYB 商品不存在或已删除"
|
||||
return
|
||||
}
|
||||
if syb.ShopeeProductID == nil {
|
||||
item.CollectionDisabledReason = "尚未关联蝦皮商品"
|
||||
return
|
||||
}
|
||||
shopee, ok := dataset.shopeeByID[*syb.ShopeeProductID]
|
||||
if !ok {
|
||||
item.CollectionDisabledReason = "关联的蝦皮商品不存在"
|
||||
return
|
||||
}
|
||||
if shopee.PDDProductID == nil {
|
||||
item.CollectionDisabledReason = "尚未关联 PDD 商品"
|
||||
return
|
||||
}
|
||||
|
||||
item.PDDProductID = shopee.PDDProductID
|
||||
pdd, ok := dataset.pddByID[*shopee.PDDProductID]
|
||||
if !ok {
|
||||
item.CollectionDisabledReason = "关联的 PDD 商品不存在"
|
||||
return
|
||||
}
|
||||
item.PDDGoodsID, item.PDDTitle = pdd.GoodsID, pdd.Title
|
||||
if pdd.Status == "disabled" {
|
||||
item.CollectionDisabledReason = "商品已停用"
|
||||
return
|
||||
}
|
||||
if _, ok := dataset.activeCollectionByPDD[pdd.ID]; ok {
|
||||
item.CollectionDisabledReason = "已有待执行或执行中的采集任务"
|
||||
return
|
||||
}
|
||||
if !dataset.collectionRuleAvailable {
|
||||
item.CollectionDisabledReason = "没有可用采集规则"
|
||||
return
|
||||
}
|
||||
item.CollectionEligible = true
|
||||
}
|
||||
@@ -239,7 +239,7 @@ export default {
|
||||
},
|
||||
handleSelectionChange(rows) { this.selectedProducts = rows },
|
||||
isPurchaseCandidate(row) { const ready = this.purchaseReady(row); return ready.eligible === true && ready.processStage === 'purchase_ready' },
|
||||
isCollectionCandidate(row) { const ready = this.purchaseReady(row); return ready.processStage === 'pdd_pending' && Number.isInteger(ready.pddProductId) && ready.pddProductId > 0 },
|
||||
isCollectionCandidate(row) { const ready = this.purchaseReady(row); return ready.collectionEligible === true && Number.isInteger(ready.pddProductId) && ready.pddProductId > 0 },
|
||||
rowSelectable(row) { return this.canPurchase && !this.purchaseReadinessLoading && (this.isPurchaseCandidate(row) || this.isCollectionCandidate(row)) },
|
||||
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)}` },
|
||||
|
||||
@@ -13,7 +13,7 @@ test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context
|
||||
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-shops')) return route.fulfill({ json: { code: 200, data: { items: [{ id: 1, displayName: '大碼臻選', enabled: true }, { id: 2, displayName: '歷史店鋪', enabled: false }], total: 2, enabledCount: 1, page: 1, pageSize: 200 }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/syb-products')) { listURLs.push(url); return route.fulfill({ json: { code: 200, data: { items: [{ id: 1, orderCode: 'SYB001', shopeeItemId: '26154802794', shopeeProductId: 9, productTitle: '用于验证加宽后的虾皮商品标题区域', shopName: '大碼臻選', targetColor: '黑色', targetSize: 'XL', quantity: 2, unitPriceCent: 1000, parseStatus: 'success', manuallyConfirmed: false }, { id: 2, orderCode: 'SYB002', shopeeItemId: '26154802795', productTitle: '尚未关联的商品', shopName: '大碼臻選', targetColor: '白色', targetSize: 'L', quantity: 1, unitPriceCent: 900, parseStatus: 'success', manuallyConfirmed: false }], 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: 1, eligible: true, minUnitPriceCent: 200, maxUnitPriceCent: 1500, processStage: 'purchase_ready', processStageLabel: '可创建采购', processStageReason: '当前条件可以创建采购任务' }, { sybProductId: 2, eligible: false, reason: '尚未关联蝦皮商品', processStage: 'manual_action', processStageLabel: '待人工处理', processStageReason: '尚未关联蝦皮商品' }], eligibleCount: 1, skippedCount: 1 }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) return route.fulfill({ json: { code: 200, data: { items: [{ sybProductId: 1, pddProductId: 17, eligible: true, collectionEligible: true, minUnitPriceCent: 200, maxUnitPriceCent: 1500, processStage: 'purchase_ready', processStageLabel: '可创建采购', processStageReason: '当前条件可以创建采购任务' }, { sybProductId: 2, eligible: false, collectionEligible: false, collectionDisabledReason: '尚未关联 PDD 商品', reason: '尚未关联蝦皮商品', processStage: 'manual_action', processStageLabel: '待人工处理', processStageReason: '尚未关联蝦皮商品' }], eligibleCount: 1, skippedCount: 1 }}})
|
||||
return route.fulfill({ json: { code: 200, data: [] }})
|
||||
})
|
||||
|
||||
@@ -57,6 +57,7 @@ test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context
|
||||
await page.locator('.el-table__body-wrapper tbody tr').first().locator('.el-checkbox').click()
|
||||
await expect(page.getByText('已选择 1 条', { exact: true })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: '创建采购(1)', exact: true })).toBeEnabled()
|
||||
await expect(page.getByRole('button', { name: '创建 PDD 采集(1)', exact: true })).toBeEnabled()
|
||||
|
||||
const widths = await page.locator('.el-table__header-wrapper colgroup col').evaluateAll(cols => cols.map(col => Number(col.getAttribute('width'))))
|
||||
expect(widths[3]).toBe(260)
|
||||
@@ -108,7 +109,7 @@ test('PDD 待采集的下一步打开对应 PDD 商品而不是蝦皮商品', as
|
||||
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: 31, orderCode: 'SYB-PDD-31', shopeeItemId: '26154802794', shopeeProductId: 9, productTitle: '待采集商品', shopName: '大碼臻選', targetColor: '黑色', targetSize: 'XL', quantity: 1, unitPriceCent: 1000, parseStatus: 'success' }], total: 1, page: 1, pageSize: 20 }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) return route.fulfill({ json: { code: 200, data: { items: [{ sybProductId: 31, pddProductId: 17, eligible: false, reason: 'PDD 商品待采集', processStage: 'pdd_pending', processStageLabel: 'PDD 待采集', processStageReason: 'PDD 商品待采集', processNextAction: 'open_pdd' }], eligibleCount: 0, skippedCount: 1 }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) return route.fulfill({ json: { code: 200, data: { items: [{ sybProductId: 31, pddProductId: 17, eligible: false, collectionEligible: true, reason: 'PDD 商品待采集', processStage: 'pdd_pending', processStageLabel: 'PDD 待采集', processStageReason: 'PDD 商品待采集', processNextAction: 'open_pdd' }], eligibleCount: 0, skippedCount: 1 }}})
|
||||
return route.fulfill({ json: { code: 200, data: [] }})
|
||||
})
|
||||
|
||||
@@ -186,9 +187,9 @@ test('混选后采购与采集分别提交候选子集且采集按 PDD 去重',
|
||||
if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) {
|
||||
const ids = route.request().postDataJSON().sybProductIds
|
||||
const all = [
|
||||
{ sybProductId: 201, pddProductId: 301, eligible: true, processStage: 'purchase_ready', processStageLabel: '可创建采购', processStageReason: '当前条件可以创建采购任务' },
|
||||
{ sybProductId: 202, pddProductId: 302, pddGoodsId: '88002', eligible: false, processStage: 'pdd_pending', processStageLabel: 'PDD 待采集', processStageReason: 'PDD 商品待采集', processNextAction: 'open_pdd' },
|
||||
{ sybProductId: 203, pddProductId: 302, pddGoodsId: '88002', eligible: false, processStage: 'pdd_pending', processStageLabel: 'PDD 待采集', processStageReason: 'PDD 商品待采集', processNextAction: 'open_pdd' }
|
||||
{ sybProductId: 201, pddProductId: 301, eligible: true, collectionEligible: false, collectionDisabledReason: '已有待执行或执行中的采集任务', processStage: 'purchase_ready', processStageLabel: '可创建采购', processStageReason: '当前条件可以创建采购任务' },
|
||||
{ sybProductId: 202, pddProductId: 302, pddGoodsId: '88002', eligible: false, collectionEligible: true, processStage: 'pdd_pending', processStageLabel: 'PDD 待采集', processStageReason: 'PDD 商品待采集', processNextAction: 'open_pdd' },
|
||||
{ sybProductId: 203, pddProductId: 302, pddGoodsId: '88002', eligible: false, collectionEligible: true, processStage: 'pdd_pending', processStageLabel: 'PDD 待采集', processStageReason: 'PDD 商品待采集', processNextAction: 'open_pdd' }
|
||||
]
|
||||
const items = all.filter(item => ids.includes(item.sybProductId))
|
||||
return route.fulfill({ json: { code: 200, data: { items, eligibleCount: items.filter(item => item.eligible).length, skippedCount: items.filter(item => !item.eligible).length }}})
|
||||
@@ -238,7 +239,7 @@ test('去重后超过 100 个 PDD 商品时给出提示且不调用批量接口'
|
||||
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: rows, total: rows.length, page: 1, pageSize: 200 }}})
|
||||
if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) {
|
||||
const items = rows.map((row, index) => ({ sybProductId: row.id, pddProductId: 500 + index, pddGoodsId: `88${index}`, eligible: false, processStage: 'pdd_pending', processStageLabel: 'PDD 待采集', processStageReason: 'PDD 商品待采集', processNextAction: 'open_pdd' }))
|
||||
const items = rows.map((row, index) => ({ sybProductId: row.id, pddProductId: 500 + index, pddGoodsId: `88${index}`, eligible: false, collectionEligible: true, processStage: 'pdd_pending', processStageLabel: 'PDD 待采集', processStageReason: 'PDD 商品待采集', processNextAction: 'open_pdd' }))
|
||||
return route.fulfill({ json: { code: 200, data: { items, eligibleCount: 0, skippedCount: items.length }}})
|
||||
}
|
||||
if (url.pathname.endsWith('/api/admin/v1/collection-tasks/batch')) { collectionCalls.push(route.request().postDataJSON()); return route.fulfill({ json: { code: 200, data: { items: [], successCount: 0, failureCount: 0 }}}) }
|
||||
|
||||
Reference in New Issue
Block a user