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:
@@ -3,10 +3,25 @@ import test from 'node:test'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CLAIM_TERMINAL_STATUSES,
|
CLAIM_TERMINAL_STATUSES,
|
||||||
|
MANUAL_FULFILLMENT_MESSAGE,
|
||||||
resolveClaimProductImageUrl,
|
resolveClaimProductImageUrl,
|
||||||
|
resolveManualFulfillmentMessage,
|
||||||
} from './kuaishou-cloud-claim-context.js'
|
} from './kuaishou-cloud-claim-context.js'
|
||||||
import { TASK_STATUS } from '../../domain/task-status.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 状态', () => {
|
test('CLAIM_TERMINAL_STATUSES 包含 closed 与 expired 状态', () => {
|
||||||
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.CLOSED), true)
|
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.CLOSED), true)
|
||||||
assert.equal(CLAIM_TERMINAL_STATUSES.has(TASK_STATUS.EXPIRED), 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 { resolveProductImageUrl } from '../platforms/affiliate-dash/product-image.js'
|
||||||
import { buildClaimIdentityPayload } from './claim-identity.js'
|
import { buildClaimIdentityPayload } from './claim-identity.js'
|
||||||
import { normalizeAffiliateDashFlow } from '../fulfillment/affiliate-dash/index.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 { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
import type { JsonObject } from '../../types/json.js'
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
|
||||||
@@ -25,6 +26,20 @@ const RETIRED_CHANNEL_MESSAGE: Record<string, string> = {
|
|||||||
kuaishou_ct_assisted: '乐玩履约渠道已下线,本链接仅可查看历史信息,无法继续领取',
|
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 = {
|
export type ClaimContext = {
|
||||||
claimToken: ClaimTokenRow
|
claimToken: ClaimTokenRow
|
||||||
task: TaskRow
|
task: TaskRow
|
||||||
@@ -290,6 +305,8 @@ function buildAffiliateDashClaimDetailPayload({
|
|||||||
flowType: 'affiliate_dash' as const,
|
flowType: 'affiliate_dash' as const,
|
||||||
channelOffline: false,
|
channelOffline: false,
|
||||||
channelOfflineMessage: '',
|
channelOfflineMessage: '',
|
||||||
|
manualFulfillment: false,
|
||||||
|
manualFulfillmentMessage: '',
|
||||||
claimIdentity,
|
claimIdentity,
|
||||||
task: buildClaimTaskPayload(task, claimToken),
|
task: buildClaimTaskPayload(task, claimToken),
|
||||||
order: buildClaimOrderPayload(order),
|
order: buildClaimOrderPayload(order),
|
||||||
@@ -372,6 +389,8 @@ function buildRetiredClaimDetailPayload({ claimToken, task, order, orderItem }:
|
|||||||
: ('kuaishou_cloud' as const),
|
: ('kuaishou_cloud' as const),
|
||||||
channelOffline: true,
|
channelOffline: true,
|
||||||
channelOfflineMessage: offlineMessage,
|
channelOfflineMessage: offlineMessage,
|
||||||
|
manualFulfillment: false,
|
||||||
|
manualFulfillmentMessage: '',
|
||||||
claimIdentity,
|
claimIdentity,
|
||||||
task: buildClaimTaskPayload(task, claimToken),
|
task: buildClaimTaskPayload(task, claimToken),
|
||||||
order: buildClaimOrderPayload(order),
|
order: buildClaimOrderPayload(order),
|
||||||
@@ -413,6 +432,7 @@ function buildGenericClaimDetailPayload({ claimToken, task, order, orderItem }:
|
|||||||
const context = parseTaskContext(task)
|
const context = parseTaskContext(task)
|
||||||
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
|
||||||
const productTitle = String(orderItem.sku_name || orderItem.sku_code || '').trim()
|
const productTitle = String(orderItem.sku_name || orderItem.sku_code || '').trim()
|
||||||
|
const manualFulfillmentMessage = resolveManualFulfillmentMessage(task.executor_key)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tokenStatus: claimToken.status,
|
tokenStatus: claimToken.status,
|
||||||
@@ -420,6 +440,10 @@ function buildGenericClaimDetailPayload({ claimToken, task, order, orderItem }:
|
|||||||
flowType: String(task.executor_key || 'manual_dispatch').trim() || 'manual_dispatch',
|
flowType: String(task.executor_key || 'manual_dispatch').trim() || 'manual_dispatch',
|
||||||
channelOffline: false,
|
channelOffline: false,
|
||||||
channelOfflineMessage: '',
|
channelOfflineMessage: '',
|
||||||
|
// 人工履约单:买家在本页**无事可做**(没有自动发货通道可用),页面只展示等待话术,
|
||||||
|
// 不许再让买家输 UID ——否则会误导买家以为提交后就会自动发货。
|
||||||
|
manualFulfillment: Boolean(manualFulfillmentMessage),
|
||||||
|
manualFulfillmentMessage,
|
||||||
claimIdentity,
|
claimIdentity,
|
||||||
task: buildClaimTaskPayload(task, claimToken),
|
task: buildClaimTaskPayload(task, claimToken),
|
||||||
order: buildClaimOrderPayload(order),
|
order: buildClaimOrderPayload(order),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
ClockCircleOutlined,
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
@@ -301,6 +302,25 @@ export default function ClaimPage() {
|
|||||||
<ExclamationCircleOutlined className="claim-large-icon claim-icon-error" />
|
<ExclamationCircleOutlined className="claim-large-icon claim-icon-error" />
|
||||||
<p>{snapshot.channelOfflineMessage || '该履约渠道已下线,无法继续领取'}</p>
|
<p>{snapshot.channelOfflineMessage || '该履约渠道已下线,无法继续领取'}</p>
|
||||||
</Card>
|
</Card>
|
||||||
|
) : snapshot.isManualFulfillment ? (
|
||||||
|
// 人工履约单(未命中自动发货映射 → manual_dispatch):没有自动发货通道,
|
||||||
|
// 买家输 UID 也没人自动发货,这里只如实告知「等客服」,不给任何操作入口。
|
||||||
|
<Card className="claim-content-card claim-state-card">
|
||||||
|
<ClockCircleOutlined className="claim-large-icon claim-icon-info" />
|
||||||
|
<h3 style={{ marginTop: 16 }}>
|
||||||
|
{snapshot.manualFulfillmentMessage || '人工处理中,请等待客服'}
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: '#64748b' }}>
|
||||||
|
该订单由人工客服处理,本页无需任何操作;处理完成后刷新本页即可查看结果。
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
icon={<ReloadOutlined />}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
onClick={() => void loadDetail()}
|
||||||
|
>
|
||||||
|
刷新状态
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
) : snapshot.currentStep === 1 ? (
|
) : snapshot.currentStep === 1 ? (
|
||||||
<ClaimUidStep
|
<ClaimUidStep
|
||||||
uidInput={uidInput}
|
uidInput={uidInput}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export function createClaimSnapshot(detail: ClaimDetailData | null) {
|
|||||||
const hasExpectedUid = Boolean(expectedUid || claimIdentity?.ready)
|
const hasExpectedUid = Boolean(expectedUid || claimIdentity?.ready)
|
||||||
const channelOffline = Boolean(detail?.channelOffline)
|
const channelOffline = Boolean(detail?.channelOffline)
|
||||||
const channelOfflineMessage = String(detail?.channelOfflineMessage || '').trim()
|
const channelOfflineMessage = String(detail?.channelOfflineMessage || '').trim()
|
||||||
|
// 人工履约单(manual_dispatch):买家无需操作,页面只显示等待客服话术
|
||||||
|
const isManualFulfillment = Boolean(detail?.manualFulfillment)
|
||||||
|
const manualFulfillmentMessage = String(detail?.manualFulfillmentMessage || '').trim()
|
||||||
const isAffiliateDashFlow = detail?.flowType === 'affiliate_dash'
|
const isAffiliateDashFlow = detail?.flowType === 'affiliate_dash'
|
||||||
const affiliateDashExpectedUid = String(
|
const affiliateDashExpectedUid = String(
|
||||||
expectedUid || affiliateDash?.expectedGameAccount || affiliateDash?.gameAccount || '',
|
expectedUid || affiliateDash?.expectedGameAccount || affiliateDash?.gameAccount || '',
|
||||||
@@ -67,6 +70,8 @@ export function createClaimSnapshot(detail: ClaimDetailData | null) {
|
|||||||
const progressText = resolveProgressText({
|
const progressText = resolveProgressText({
|
||||||
channelOffline,
|
channelOffline,
|
||||||
channelOfflineMessage,
|
channelOfflineMessage,
|
||||||
|
isManualFulfillment,
|
||||||
|
manualFulfillmentMessage,
|
||||||
affiliateDash,
|
affiliateDash,
|
||||||
isAffiliateDashFlow,
|
isAffiliateDashFlow,
|
||||||
hasExpectedUid,
|
hasExpectedUid,
|
||||||
@@ -102,6 +107,8 @@ export function createClaimSnapshot(detail: ClaimDetailData | null) {
|
|||||||
hasExpectedUid,
|
hasExpectedUid,
|
||||||
channelOffline,
|
channelOffline,
|
||||||
channelOfflineMessage,
|
channelOfflineMessage,
|
||||||
|
isManualFulfillment,
|
||||||
|
manualFulfillmentMessage,
|
||||||
roleName: '',
|
roleName: '',
|
||||||
roleId: '',
|
roleId: '',
|
||||||
isFeifeiFlow: false,
|
isFeifeiFlow: false,
|
||||||
@@ -165,6 +172,8 @@ function resolveCurrentStep(options: {
|
|||||||
function resolveProgressText(options: {
|
function resolveProgressText(options: {
|
||||||
channelOffline: boolean
|
channelOffline: boolean
|
||||||
channelOfflineMessage: string
|
channelOfflineMessage: string
|
||||||
|
isManualFulfillment: boolean
|
||||||
|
manualFulfillmentMessage: string
|
||||||
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
affiliateDash: ClaimAffiliateDashFlowInfo | null
|
||||||
isAffiliateDashFlow: boolean
|
isAffiliateDashFlow: boolean
|
||||||
hasExpectedUid: boolean
|
hasExpectedUid: boolean
|
||||||
@@ -175,6 +184,10 @@ function resolveProgressText(options: {
|
|||||||
if (options.channelOffline) {
|
if (options.channelOffline) {
|
||||||
return options.channelOfflineMessage || '该履约渠道已下线'
|
return options.channelOfflineMessage || '该履约渠道已下线'
|
||||||
}
|
}
|
||||||
|
// 人工履约单:买家不需要填任何信息,直接显示等待客服
|
||||||
|
if (options.isManualFulfillment) {
|
||||||
|
return options.manualFulfillmentMessage || '人工处理中,请等待客服'
|
||||||
|
}
|
||||||
if (!options.hasExpectedUid) {
|
if (!options.hasExpectedUid) {
|
||||||
return '请填写游戏编号'
|
return '请填写游戏编号'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,13 @@ export interface ClaimDetailData {
|
|||||||
| (string & {})
|
| (string & {})
|
||||||
channelOffline?: boolean
|
channelOffline?: boolean
|
||||||
channelOfflineMessage?: string
|
channelOfflineMessage?: string
|
||||||
|
/**
|
||||||
|
* 人工履约单(未命中自动发货映射 → `manual_dispatch`):买家在领取页**无需也不能操作**,
|
||||||
|
* 页面只展示 `manualFulfillmentMessage`,不再出现「输 UID」步骤。
|
||||||
|
*/
|
||||||
|
manualFulfillment?: boolean
|
||||||
|
/** 人工履约等待话术(如「人工处理中,请等待客服」);非人工单为空串 */
|
||||||
|
manualFulfillmentMessage?: string
|
||||||
claimIdentity: ClaimIdentityInfo | null
|
claimIdentity: ClaimIdentityInfo | null
|
||||||
task: ClaimTaskInfo
|
task: ClaimTaskInfo
|
||||||
order: ClaimOrderInfo
|
order: ClaimOrderInfo
|
||||||
|
|||||||
Reference in New Issue
Block a user