feat(claim): 人工履约单领取页只显示「人工处理中,请等待客服」

未命中 AFFILIATE_DASH_SKU_MAPPING 的抖音单显式回落 manual_dispatch(人工),这类单没有
自动发货通道(无 claim token、无绑定二维码),但卡券 cert_link 仍指向 /h5/<订单号> 领取页;
原页面会让买家「输 UID」,提交后没有任何自动发货动作,属于误导买家。

- 后端 kuaishou-cloud-claim-context.ts:新增 MANUAL_FULFILLMENT_MESSAGE 与
  resolveManualFulfillmentMessage(executorKey)(manual_dispatch → 等待话术,其它执行器空串);
  三个 payload builder 统一输出 manualFulfillment / manualFulfillmentMessage
- 前端 claim-snapshot.ts:输出 isManualFulfillment / manualFulfillmentMessage,
  progressText 同步显示等待话术
- 前端 ClaimPage.tsx:在「渠道已下线」之后、「输 UID」之前插入人工单卡片(只读 + 刷新状态),
  人工单不会自动轮询(无 expectedUid → 不安排下一次轮询)
- types/claim.ts:补齐两个可选字段;话术只在后端维护一份,前端只做兜底

验证:后端 npm run check(701 单测)与前端 npm run check(格式/lint/类型/143 单测/构建)全绿
This commit is contained in:
yml2213
2026-10-13 13:53:00 +08:00
parent 319e082254
commit e8fbb28cf4
5 changed files with 79 additions and 0 deletions
@@ -3,10 +3,25 @@ import test from 'node:test'
import {
CLAIM_TERMINAL_STATUSES,
MANUAL_FULFILLMENT_MESSAGE,
resolveClaimProductImageUrl,
resolveManualFulfillmentMessage,
} from './kuaishou-cloud-claim-context.js'
import { TASK_STATUS } from '../../domain/task-status.js'
test('人工履约提示:manual_dispatch 给等待客服话术,自动履约与已下线渠道给空串', () => {
// 人工单(未命中自动发货映射 → 回落 manual_dispatch):领取页不许再让买家输 UID
assert.equal(resolveManualFulfillmentMessage('manual_dispatch'), MANUAL_FULFILLMENT_MESSAGE)
assert.equal(resolveManualFulfillmentMessage(' manual_dispatch '), MANUAL_FULFILLMENT_MESSAGE)
assert.equal(MANUAL_FULFILLMENT_MESSAGE, '人工处理中,请等待客服')
// 自动发货 / 已下线渠道 / 空值都不算人工履约
assert.equal(resolveManualFulfillmentMessage('affiliate_dash'), '')
assert.equal(resolveManualFulfillmentMessage('kuaishou_feifei'), '')
assert.equal(resolveManualFulfillmentMessage(''), '')
assert.equal(resolveManualFulfillmentMessage(undefined), '')
})
test('CLAIM_TERMINAL_STATUSES 包含 closed 与 expired 状态', () => {
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.CLOSED), true)
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.EXPIRED), true)
@@ -15,6 +15,7 @@ import { buildClaimUrl } from './claim-service.js'
import { resolveProductImageUrl } from '../platforms/affiliate-dash/product-image.js'
import { buildClaimIdentityPayload } from './claim-identity.js'
import { normalizeAffiliateDashFlow } from '../fulfillment/affiliate-dash/index.js'
import { isManualDispatchExecutor } from '../fulfillment/executors/types.js'
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
import type { JsonObject } from '../../types/json.js'
@@ -25,6 +26,20 @@ const RETIRED_CHANNEL_MESSAGE: Record<string, string> = {
kuaishou_ct_assisted: '乐玩履约渠道已下线,本链接仅可查看历史信息,无法继续领取',
}
/** 领取页给人工履约单买家看的话术(人工派单 / 未命中自动发货映射)。 */
export const MANUAL_FULFILLMENT_MESSAGE = '人工处理中,请等待客服'
/**
* 人工履约提示话术:`manual_dispatch` 返回文案,其余执行器返回空串。
*
* 为什么要在领取详情里显式区分:人工单没有自动发货通道(没有 claim token、也不会有
* affiliate_dash 绑定二维码),买家在领取页输 UID 既无用又误导。前端据此隐藏「输 UID」步骤、
* 只显示等待客服的说明;话术只在此处维护一份,前端只做兜底。
*/
export function resolveManualFulfillmentMessage(executorKey: unknown): string {
return isManualDispatchExecutor(executorKey) ? MANUAL_FULFILLMENT_MESSAGE : ''
}
export type ClaimContext = {
claimToken: ClaimTokenRow
task: TaskRow
@@ -290,6 +305,8 @@ function buildAffiliateDashClaimDetailPayload({
flowType: 'affiliate_dash' as const,
channelOffline: false,
channelOfflineMessage: '',
manualFulfillment: false,
manualFulfillmentMessage: '',
claimIdentity,
task: buildClaimTaskPayload(task, claimToken),
order: buildClaimOrderPayload(order),
@@ -372,6 +389,8 @@ function buildRetiredClaimDetailPayload({ claimToken, task, order, orderItem }:
: ('kuaishou_cloud' as const),
channelOffline: true,
channelOfflineMessage: offlineMessage,
manualFulfillment: false,
manualFulfillmentMessage: '',
claimIdentity,
task: buildClaimTaskPayload(task, claimToken),
order: buildClaimOrderPayload(order),
@@ -413,6 +432,7 @@ function buildGenericClaimDetailPayload({ claimToken, task, order, orderItem }:
const context = parseTaskContext(task)
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
const productTitle = String(orderItem.sku_name || orderItem.sku_code || '').trim()
const manualFulfillmentMessage = resolveManualFulfillmentMessage(task.executor_key)
return {
tokenStatus: claimToken.status,
@@ -420,6 +440,10 @@ function buildGenericClaimDetailPayload({ claimToken, task, order, orderItem }:
flowType: String(task.executor_key || 'manual_dispatch').trim() || 'manual_dispatch',
channelOffline: false,
channelOfflineMessage: '',
// 人工履约单:买家在本页**无事可做**(没有自动发货通道可用),页面只展示等待话术,
// 不许再让买家输 UID ——否则会误导买家以为提交后就会自动发货。
manualFulfillment: Boolean(manualFulfillmentMessage),
manualFulfillmentMessage,
claimIdentity,
task: buildClaimTaskPayload(task, claimToken),
order: buildClaimOrderPayload(order),