新增 affiliate_dash 履约 executor(阶段 2)

- executor 常量/守卫 + affiliate-dash-executor(preparePaidTask/resolveDeliveryLink)
- 履约业务实现:prepareAffiliateDashTask(幂等建单, client_order_no=task_no, data 透传 91单号/game_account)
  + syncAffiliateDashTaskStatus(状态合并: delivering→redeeming / delivered→核销→redeemed /
  ship_failed→retry_pending / cancelled→closed)
- registry 注册 + 测试(212 全绿)
- 真实建单联调通过(线上 skin.khhao.com,幂等不重复扣款)
- 修复 POST /orders 响应 data.order 映射层级
This commit is contained in:
yml2213
2026-08-05 14:56:36 +08:00
parent a6ba5f1c7f
commit 867ffd2f3c
7 changed files with 457 additions and 3 deletions
@@ -0,0 +1,70 @@
import { buildClaimUrl } from '../../claim/claim-service.js'
import { TASK_STATUS } from '../../../domain/task-status.js'
import { prepareAffiliateDashTask } from '../affiliate-dash/index.js'
import {
FULFILLMENT_EXECUTOR_KEYS,
type FulfillmentDeliveryLink,
type FulfillmentExecutor,
type FulfillmentPrepareDeps,
} from './types.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export const affiliateDashExecutor: FulfillmentExecutor = {
key: FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH,
preparePaidTask,
resolveDeliveryLink,
}
async function preparePaidTask(
task: TaskRow,
deps: FulfillmentPrepareDeps,
): Promise<TaskRow | null> {
try {
let workingTask = task
if (!task.primary_claim_token_id && !String(task.claim_token || '').trim()) {
const claimToken = await deps.createTaskClaimToken(task.id)
workingTask =
(await deps.updateTask(task.id, {
claim_token: claimToken.token,
claim_expires_at: claimToken.expired_at,
user_action_status: 'pending_claim',
updated_at: deps.nowIso(),
})) || task
}
return await prepareAffiliateDashTask(workingTask)
} catch (error) {
const message = error instanceof Error ? error.message : 'affiliate-dash 订单创建失败'
const updatedTask = await deps.updateTask(task.id, {
task_status: TASK_STATUS.MANUAL_REVIEW,
user_action_status: 'not_required',
last_error: message,
result_code: 'affiliate_dash_prepare_failed',
result_message: message,
updated_at: deps.nowIso(),
})
await deps.notifyTaskAutoManualReview({
task: updatedTask || task,
reason: message,
source: 'affiliate_dash_prepare_failed',
})
return updatedTask
}
}
/**
* 对外(91 等)统一返回本站领取链接(方案 B:自建领取页,不返回 affiliate_dash H5)。
*/
async function resolveDeliveryLink(task: TaskRow): Promise<FulfillmentDeliveryLink | null> {
const primaryToken = String(task.primary_claim_token || task.claim_token || '').trim()
const expireTime = String(task.primary_claim_expires_at || task.claim_expires_at || '').trim()
if (!primaryToken) {
return null
}
return {
claimUrl: buildClaimUrl(primaryToken),
expireTime,
}
}
@@ -21,7 +21,7 @@ function makeTask(executorKey: string): TaskRow {
} as TaskRow
}
test('getFulfillmentExecutor 映射 lewan / industry / feifei / manual', () => {
test('getFulfillmentExecutor 映射 lewan / industry / feifei / affiliate_dash / manual', () => {
assert.equal(
getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD)?.key,
FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
@@ -34,6 +34,10 @@ test('getFulfillmentExecutor 映射 lewan / industry / feifei / manual', () => {
getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI)?.key,
FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI,
)
assert.equal(
getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH)?.key,
FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH,
)
assert.equal(
getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH)?.key,
FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH,
@@ -41,6 +45,16 @@ test('getFulfillmentExecutor 映射 lewan / industry / feifei / manual', () => {
assert.equal(getFulfillmentExecutor('unknown'), null)
})
test('affiliate_dash executor 暴露 preparePaidTask 与 resolveDeliveryLink', () => {
const executor = getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH)
assert.ok(executor)
assert.equal(typeof executor.preparePaidTask, 'function')
assert.equal(typeof executor.resolveDeliveryLink, 'function')
// affiliate_dash 不需要 lewan 专属动作
assert.equal(executor.prepareBinding, undefined)
assert.equal(executor.redeemTask, undefined)
})
test('lewan executor 暴露完整履约动作', () => {
const executor = getFulfillmentExecutor(FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD)
assert.ok(executor)
@@ -2,6 +2,7 @@ import { isPaidPreparationStableStatus } from '../../../domain/task-status.js'
import { kuaishouCloudExecutor } from './kuaishou-cloud-executor.js'
import { kuaishouFeifeiExecutor } from './kuaishou-feifei-executor.js'
import { manualDispatchExecutor } from './manual-executor.js'
import { affiliateDashExecutor } from './affiliate-dash-executor.js'
import {
FULFILLMENT_EXECUTOR_KEYS,
isManualDispatchExecutor,
@@ -17,6 +18,7 @@ const EXECUTORS = new Map<string, FulfillmentExecutor>([
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD, kuaishouCloudExecutor],
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY, kuaishouCloudExecutor],
[FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI, kuaishouFeifeiExecutor],
[FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH, affiliateDashExecutor],
[FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH, manualDispatchExecutor],
])
@@ -6,6 +6,7 @@ export const FULFILLMENT_EXECUTOR_KEYS = {
KUAISHOU_CLOUD: 'kuaishou_ct_assisted',
KUAISHOU_INDUSTRY: 'kuaishou-industry',
KUAISHOU_FEIFEI: 'kuaishou_feifei',
AFFILIATE_DASH: 'affiliate_dash',
} as const
export type FulfillmentExecutorKey =
@@ -111,6 +112,10 @@ export function isKuaishouFeifeiExecutor(value: unknown): boolean {
return normalizeExecutorKey(value) === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI
}
export function isAffiliateDashExecutor(value: unknown): boolean {
return normalizeExecutorKey(value) === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH
}
export function isManualDispatchExecutor(value: unknown): boolean {
return normalizeExecutorKey(value) === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH
}