上一轮我把皮肤交付做成了「另建一套执行器 + 独立领取接口 + 只提交账号就结束」, 跟买家实际要走的过程不一致(真正的完成信号是「扫码绑定后的 UID == 买家填的 UID」), 而且核销还触发了老通知逻辑,给买家**又发了一条 /redeem** —— 买家收到两条链接。 本次按用户要求改回**完全复用仓库现成的 affiliate_dash 通道**(= 旧的快手皮肤自动发货)。 **删掉** - 执行器 `douyin_mashida_skin`(含 registry/types/planner/bootstrap/路由白名单注册) - `services/claim/claim-skin-service.ts`、`routes/claim-skin.ts`、`/api/v1/claim-skin` 挂载 - 迁移 096 的 `claim_submission_json` 全链路(task-repo / rows / inputs / task-detail / admin-read-service 的 claimSubmission),并新增 **097 兜底迁移**把该列 DROP 掉 - 顺带清掉上一轮专为已删接口加的死代码(utils/http.ts 的 buildMessageSuccessPayload 等) **改回复用** - `resolveOrderItemClaimLink`:通用解析(只看订单项、不看执行器)→ `buildConfiguredClaimUrl(claimLinkBase, token)`;多履约单元取 task id 最小并告警; **绝不回退** hash 形态的 `buildClaimUrl` - `cert_link` 与付款后那条**唯一**消息共用同一个解析结果 - **核销后不再给抖音买家发第二条消息**(`shouldNotifyBuyerAfterVerify` 只放行非抖音 provider) - 买家侧走既有流程:输 UID → 二维码 → 扫码绑定 → 比对 UID → delivered/REDEEMED; 在 delivered 分支 best-effort 核销平台券(幂等、失败只 warn,不拖慢买家响应) - 后台回显链接改按 **order.provider** 判断(抖音用 `claimLinkBase`) **SQL 守卫**:允许「删除历史迁移」,但强制要求「同时新增兜底迁移」配对 (修改历史迁移仍禁止),并把改动来源换成 `git status --porcelain`(否则未跟踪的新迁移看不见) `npm run check`:tests 671 / pass 668 / fail 0 / skipped 3(含新增的物品图回退用例)
208 lines
6.0 KiB
TypeScript
208 lines
6.0 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
|
|
import { planFulfillmentTaskForOrderItem } from './planner.js'
|
|
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
|
|
|
test('planFulfillmentTaskForOrderItem 为 affiliate-dash 商品生成履约计划', async () => {
|
|
const plan = await planFulfillmentTaskForOrderItem({
|
|
order: createOrder(),
|
|
item: createOrderItem({
|
|
sku_code: 'AFF-1001',
|
|
sku_name: '幸运币90个',
|
|
item_snapshot_json: {
|
|
fulfillmentRoute: {
|
|
selectedExecutorKey: 'affiliate_dash',
|
|
},
|
|
affiliateDash: {
|
|
sku: 'AFF-1001',
|
|
skuName: '幸运币90个',
|
|
matchMode: 'affiliate_dash_sku_mapping',
|
|
},
|
|
},
|
|
}),
|
|
getProfileByKey: async (profileKey) => ({
|
|
id: 11,
|
|
profile_key: profileKey,
|
|
name: 'affiliate-dash 履约',
|
|
executor_key: profileKey,
|
|
requires_claim: false,
|
|
auto_dispatch: true,
|
|
}),
|
|
})
|
|
|
|
assert.ok(plan)
|
|
assert.equal(plan.executorKey, 'affiliate_dash')
|
|
assert.equal(plan.requiresClaim, true)
|
|
assert.equal(plan.autoDispatch, false)
|
|
assert.equal(plan.context.profileKey, 'affiliate_dash')
|
|
assert.equal((plan.context.affiliateDash as { sku?: string }).sku, 'AFF-1001')
|
|
assert.equal((plan.context.affiliateDash as { productName?: string }).productName, '幸运币90个')
|
|
})
|
|
|
|
test('planFulfillmentTaskForOrderItem 支持履约路由转人工', async () => {
|
|
const plan = await planFulfillmentTaskForOrderItem({
|
|
order: createOrder(),
|
|
item: createOrderItem({
|
|
sku_code: 'MANUAL-1',
|
|
sku_name: '人工商品',
|
|
item_snapshot_json: {
|
|
fulfillmentRoute: {
|
|
selectedExecutorKey: 'manual_dispatch',
|
|
},
|
|
},
|
|
}),
|
|
getProfileByKey: async (profileKey) => ({
|
|
id: 1,
|
|
profile_key: profileKey,
|
|
name: '人工发货',
|
|
executor_key: 'manual_dispatch',
|
|
requires_claim: true,
|
|
auto_dispatch: true,
|
|
}),
|
|
})
|
|
|
|
assert.ok(plan)
|
|
assert.equal(plan.executorKey, 'manual_dispatch')
|
|
assert.equal(plan.requiresClaim, false)
|
|
assert.equal(plan.autoDispatch, false)
|
|
assert.equal(plan.context.affiliateDash, null)
|
|
})
|
|
|
|
test('planFulfillmentTaskForOrderItem 未命中平台配置时返回 null', async () => {
|
|
const plan = await planFulfillmentTaskForOrderItem({
|
|
order: createOrder(),
|
|
item: createOrderItem({
|
|
item_snapshot_json: {
|
|
affiliateDash: null,
|
|
},
|
|
}),
|
|
getProfileByKey: async () => null,
|
|
})
|
|
|
|
assert.equal(plan, null)
|
|
})
|
|
|
|
test('planFulfillmentTaskForOrderItem affiliate 缺 sku 时返回 null', async () => {
|
|
const plan = await planFulfillmentTaskForOrderItem({
|
|
order: createOrder(),
|
|
item: createOrderItem({
|
|
item_snapshot_json: {
|
|
fulfillmentRoute: {
|
|
selectedExecutorKey: 'affiliate_dash',
|
|
},
|
|
affiliateDash: {
|
|
skuName: '无 sku 商品',
|
|
},
|
|
},
|
|
}),
|
|
getProfileByKey: async (profileKey) => ({
|
|
id: 11,
|
|
profile_key: profileKey,
|
|
name: 'affiliate-dash 履约',
|
|
executor_key: profileKey,
|
|
requires_claim: true,
|
|
auto_dispatch: false,
|
|
}),
|
|
})
|
|
|
|
assert.equal(plan, null)
|
|
})
|
|
|
|
test('planFulfillmentTaskForOrderItem 为抖音命中商品生成 affiliate_dash 履约计划', async () => {
|
|
const plan = await planFulfillmentTaskForOrderItem({
|
|
order: createOrder({ provider: 'douyin_mashida', platform: 'doudian' }),
|
|
item: createOrderItem({
|
|
sku_code: '幸运币2个',
|
|
sku_name: '【测试专用】和平精英皮肤小店入口',
|
|
item_snapshot_json: {
|
|
fulfillmentRoute: { selectedExecutorKey: 'affiliate_dash' },
|
|
affiliateDash: {
|
|
matchMode: 'affiliate_dash_sku_mapping',
|
|
productNo: '幸运币2个',
|
|
sku: 'lucky_coin_x2',
|
|
skuName: '幸运币2个',
|
|
},
|
|
},
|
|
}),
|
|
getProfileByKey: async (profileKey) => ({
|
|
id: 21,
|
|
profile_key: profileKey,
|
|
name: 'affiliate-dash 履约',
|
|
executor_key: profileKey,
|
|
requires_claim: false,
|
|
auto_dispatch: false,
|
|
}),
|
|
})
|
|
|
|
assert.ok(plan)
|
|
assert.equal(plan.executorKey, 'affiliate_dash')
|
|
assert.equal(plan.profileKey, 'affiliate_dash')
|
|
assert.equal(plan.requiresClaim, true)
|
|
assert.equal(plan.autoDispatch, false)
|
|
|
|
const affiliateDash = plan.context.affiliateDash as Record<string, unknown> | null
|
|
assert.ok(affiliateDash)
|
|
assert.equal(affiliateDash.sku, 'lucky_coin_x2')
|
|
assert.equal(affiliateDash.productName, '幸运币2个')
|
|
})
|
|
|
|
test('planFulfillmentTaskForOrderItem 抖音订单缺 affiliate_dash sku 时返回 null', async () => {
|
|
const plan = await planFulfillmentTaskForOrderItem({
|
|
order: createOrder({ provider: 'douyin_mashida', platform: 'doudian' }),
|
|
item: createOrderItem({
|
|
sku_code: '',
|
|
sku_name: '无内部 SKU',
|
|
item_snapshot_json: {
|
|
fulfillmentRoute: { selectedExecutorKey: 'affiliate_dash' },
|
|
},
|
|
}),
|
|
getProfileByKey: async (profileKey) => ({
|
|
id: 21,
|
|
profile_key: profileKey,
|
|
name: 'affiliate-dash 履约',
|
|
executor_key: profileKey,
|
|
}),
|
|
})
|
|
|
|
assert.equal(plan, null)
|
|
})
|
|
|
|
function createOrder(patch: Partial<OrderRow> = {}): OrderRow {
|
|
return {
|
|
id: 1,
|
|
provider: '91kaquan',
|
|
platform: 'kuaishou',
|
|
shop_id: '91kaquan',
|
|
shop_name: '91卡券',
|
|
platform_order_id: '2614900602069169',
|
|
order_status: 'paid',
|
|
pay_status: 'paid',
|
|
buyer_id: '',
|
|
buyer_name: '',
|
|
receiver_contact: '',
|
|
total_amount: 0,
|
|
currency: 'CNY',
|
|
raw_payload_json: '{}',
|
|
paid_at: '2026-07-08T00:00:00.000Z',
|
|
created_at: '2026-07-08T00:00:00.000Z',
|
|
updated_at: '2026-07-08T00:00:00.000Z',
|
|
...patch,
|
|
}
|
|
}
|
|
|
|
function createOrderItem(patch: Partial<OrderItemRow> = {}): OrderItemRow {
|
|
return {
|
|
id: 10,
|
|
order_id: 1,
|
|
sku_code: 'SKU-1',
|
|
sku_name: '测试商品',
|
|
quantity: 1,
|
|
spec_json: '{}',
|
|
item_snapshot_json: {},
|
|
created_at: '2026-07-08T00:00:00.000Z',
|
|
updated_at: '2026-07-08T00:00:00.000Z',
|
|
...patch,
|
|
}
|
|
}
|