抖音码速达的皮肤类商品改为「自动发货 + 领取链接」:买家不用输订单号,点链接直接领。
**分流(按内部 SKU)**
- 平台闸门放行 `provider='douyin_mashida' && platform='doudian'`,走**独立分支**
(原有 91/快手路径一行未动,避免外溢)
- 判据:内部 SKU(`sku_order_list[].code`,即 order_items.sku_code)命中权威表
`AFFILIATE_DASH_SKU_MAPPING` → 新执行器 `douyin_mashida_skin`;未命中 → `manual_dispatch`
- 皮肤判定与商品图同源(都用那张权威表),保证「能路由 ⇒ 有图」
- 履约路由新增 `internal_sku` 匹配维度(`rules[].internalSku`,NFKC+去空白+小写,exact/contains);
旧规则缺 `matchField` 时默认 `product_name`,**归一化/优先级/回退顺序完全不变**
**执行器与领取 token**
- 新增 `douyin-mashida-skin-executor`:建 claim token、置 `link_generated/pending_claim`,
`resolveDeliveryLink` 返回本站路径式领取链接;注册进 registry、planner 分支、
bootstrap 种子 profile(`抖音皮肤自动发货`);进 `ROUTABLE_EXECUTOR_KEYS`
- token 粒度 = 一个履约单元一个(`claim_tokens.task_id` 唯一);一单多项时
cert_link/买家消息取 task id 最小的那个并告警
**cert_link 与发消息**
- 皮肤订单且该订单项已有 token 且配了 `claimLinkBase` → `cert_link = claimLinkBase/token`;
其它情况一律用 `certLink`(老行为不变)
- 领取链接**只认** `claimLinkBase`,绝不回退 `buildClaimUrl`/`orders.claimBaseUrl`
(那是 hash 形式的打手领取页,语义不同)
- 付款建单+发券成功后按 `messageTemplate`(`{link}/{sku}/{orderNo}/{productTitle}`)
经 `order.message` 发给买家;best-effort,失败只记 warn
**买家领取接口**
- `GET/POST /api/v1/claim-skin/:token`(限流、公开);详情含内部SKU/引流标题/商品图/
数量/金额/支付时间/动态表单字段;token 失效给明确 errorCode(404/410/409),不打 500
- 提交账号:先核销平台券再落记录(避免「记账了但券没核销」的资损),已提交幂等
**隐私**
- 买家游戏账号落 `fulfillment_tasks.claim_submission_json`(迁移 096),
**不进** `work_orders.material_json`(那是发给打手的素材);详情接口不返回券码明文/手机号/地址/open_id
209 lines
6.1 KiB
TypeScript
209 lines
6.1 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 为抖音皮肤商品生成领取链接履约计划', async () => {
|
|
const plan = await planFulfillmentTaskForOrderItem({
|
|
order: createOrder({ provider: 'douyin_mashida', platform: 'doudian' }),
|
|
item: createOrderItem({
|
|
sku_code: '幸运币2个',
|
|
sku_name: '【测试专用】和平精英皮肤小店入口',
|
|
item_snapshot_json: {
|
|
fulfillmentRoute: { selectedExecutorKey: 'douyin_mashida_skin' },
|
|
douyinSkin: {
|
|
internalSku: '幸运币2个',
|
|
sku: 'lucky_coin_x2',
|
|
productTitle: '【测试专用】和平精英皮肤小店入口',
|
|
},
|
|
},
|
|
}),
|
|
getProfileByKey: async (profileKey) => ({
|
|
id: 21,
|
|
profile_key: profileKey,
|
|
name: '抖音皮肤自动发货',
|
|
executor_key: profileKey,
|
|
requires_claim: false,
|
|
auto_dispatch: false,
|
|
}),
|
|
})
|
|
|
|
assert.ok(plan)
|
|
assert.equal(plan.executorKey, 'douyin_mashida_skin')
|
|
assert.equal(plan.profileKey, 'douyin_mashida_skin')
|
|
assert.equal(plan.requiresClaim, true)
|
|
assert.equal(plan.autoDispatch, false)
|
|
assert.equal(plan.context.affiliateDash, null)
|
|
|
|
const douyinSkin = plan.context.douyinSkin as Record<string, unknown> | null
|
|
assert.ok(douyinSkin)
|
|
assert.equal(douyinSkin.internalSku, '幸运币2个')
|
|
assert.equal(douyinSkin.sku, 'lucky_coin_x2')
|
|
assert.equal(douyinSkin.imageUrl, '/images/products/lucky_coin_x2.webp')
|
|
})
|
|
|
|
test('planFulfillmentTaskForOrderItem 抖音皮肤缺内部 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: 'douyin_mashida_skin' },
|
|
},
|
|
}),
|
|
getProfileByKey: async (profileKey) => ({
|
|
id: 21,
|
|
profile_key: profileKey,
|
|
name: '抖音皮肤自动发货',
|
|
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,
|
|
}
|
|
}
|