From 92ceb9697b1d5cb3a36482cac7db31d06fd64162 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 5 Nov 2026 15:19:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=96=B9=E5=8D=A1=E5=88=B8=E6=94=B6=E5=88=B0=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=90=8E=E8=87=AA=E5=8A=A8=E6=A0=B8=E9=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../finance/manual-fulfillment-service.ts | 2 + .../fulfillment/affiliate-fallback-service.ts | 2 + .../carrier-reconciliation-service.ts | 9 +- .../fulfillment/channel-assign-service.ts | 27 ++- .../src/services/fulfillment/channel-types.ts | 2 +- .../voucher-consume-policy-service.test.ts | 80 ++++++++ .../voucher-consume-policy-service.ts | 32 +++- .../voucher-consume-queue-service.ts | 180 ++++++++++++++++-- .../admin/AdminFulfillmentChannelPage.tsx | 4 +- .../panels/VoucherConsumePolicyPanel.tsx | 13 +- .../src/services/admin/fulfillment-channel.ts | 2 + 11 files changed, 323 insertions(+), 30 deletions(-) diff --git a/apps/backend/src/services/finance/manual-fulfillment-service.ts b/apps/backend/src/services/finance/manual-fulfillment-service.ts index 25c996a8..e51f3a4f 100644 --- a/apps/backend/src/services/finance/manual-fulfillment-service.ts +++ b/apps/backend/src/services/finance/manual-fulfillment-service.ts @@ -16,6 +16,7 @@ import { } from '../cost-center/platform-credit-service.js' import { rebuildChannelSalesDailyFacts } from './sales-facts-channel-service.js' import { syncFinanceLedgerFromFacts } from './finance-ledger-service.js' +import { scheduleVoucherConsumeForOrderItems } from '../fulfillment/voucher-consume-queue-service.js' /** * 人工履约待办与「确认履约」。 @@ -510,6 +511,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm } }) + await scheduleVoucherConsumeForOrderItems([orderItemId], 'order_received').catch(() => undefined) // 成本已落快照,立刻重算该业务日的事实与总账;失败不阻断确认(定时任务会兜底)。 await refreshFactsForOrderItem(orderItemId) return result diff --git a/apps/backend/src/services/fulfillment/affiliate-fallback-service.ts b/apps/backend/src/services/fulfillment/affiliate-fallback-service.ts index 9cb83715..f68c6cfc 100644 --- a/apps/backend/src/services/fulfillment/affiliate-fallback-service.ts +++ b/apps/backend/src/services/fulfillment/affiliate-fallback-service.ts @@ -1,6 +1,7 @@ import { query, withTransaction } from '../../db/client.js' import { syncCostSnapshotsForItems } from '../cost-center/cost-snapshot-service.js' import { refreshFactsForDates } from '../finance/refresh-facts.js' +import { scheduleVoucherConsumeForOrderItems } from './voucher-consume-queue-service.js' /** * 自动发货失败 → 人工兜底。 @@ -156,6 +157,7 @@ export async function runAffiliateFallback( for (const id of switchedIds) { await syncCostSnapshotsForItems([id]).catch(() => undefined) } + await scheduleVoucherConsumeForOrderItems(switchedIds, 'order_received').catch(() => undefined) await refreshFactsForDates([...affectedDates]).catch(() => undefined) return { scanned: candidates.rows.length, switched: switchedIds.length } diff --git a/apps/backend/src/services/fulfillment/carrier-reconciliation-service.ts b/apps/backend/src/services/fulfillment/carrier-reconciliation-service.ts index 3e582919..1fcc4782 100644 --- a/apps/backend/src/services/fulfillment/carrier-reconciliation-service.ts +++ b/apps/backend/src/services/fulfillment/carrier-reconciliation-service.ts @@ -12,6 +12,7 @@ import { planInternalSkuChannels, type InternalSkuChannelRow, } from './internal-sku-channel-service.js' +import { scheduleVoucherConsumeForOrderItems } from './voucher-consume-queue-service.js' type CarrierCandidate = { id: number @@ -188,7 +189,13 @@ async function selectSafeFallback(input: { input.includeAssignedMissingCarrier, ], ) - return (result.rowCount || 0) > 0 + const updated = (result.rowCount || 0) > 0 + if (updated) { + await scheduleVoucherConsumeForOrderItems([input.orderItemId], 'order_received').catch( + () => undefined, + ) + } + return updated } /** diff --git a/apps/backend/src/services/fulfillment/channel-assign-service.ts b/apps/backend/src/services/fulfillment/channel-assign-service.ts index d58d020a..f2baa987 100644 --- a/apps/backend/src/services/fulfillment/channel-assign-service.ts +++ b/apps/backend/src/services/fulfillment/channel-assign-service.ts @@ -5,6 +5,7 @@ import { createTaskEvent } from '../../repositories/task-event-repo.js' import { createWorkOrderEvent } from '../../repositories/worker-platform/work-order-event-repo.js' import type { OrderItemRow } from '../../types/repository/rows.js' import { createHttpError } from '../../utils/http.js' +import { logWarn } from '../../utils/logger.js' import { nowIso } from '../../utils/time.js' import { upsertAggregatedException } from '../cost-center/cost-exception-service.js' import { readWatermarkItemId } from '../watermark-store.js' @@ -33,6 +34,7 @@ import { ensureCarrierForOrderItem } from './carrier-creation-service.js' import { reconcilePendingCarriers } from './carrier-reconciliation-service.js' import { TASK_STATUS } from '../../domain/task-status.js' import { canonicalizeInternalSku, containsCjk } from './internal-sku-resolver.js' +import { scheduleVoucherConsumeForOrderItems } from './voucher-consume-queue-service.js' /** * 管道 A:渠道判定。 @@ -402,7 +404,7 @@ export async function assignPendingOrderItemChannels( } if (assignments.length > 0) { - const updated = await query( + const updated = await query<{ id: number }>( ` UPDATE order_items oi SET @@ -418,6 +420,7 @@ export async function assignPendingOrderItemChannels( WHERE oi.id = input.id AND oi.fulfillment_status = 'pending' AND oi.fulfillment_locked = FALSE + RETURNING oi.id `, [ assignments.map((row) => row.id), @@ -425,8 +428,20 @@ export async function assignPendingOrderItemChannels( assignments.map((row) => row.source), ], ) - result.assigned += updated.rowCount || 0 - result.assignedItemIds = assignments.map((row) => row.id).slice(0, 2000) + const assignedItemIds = updated.rows + .map((row) => Number(row.id)) + .filter((id) => id > 0) + .slice(0, 2000) + result.assigned += assignedItemIds.length + result.assignedItemIds = assignedItemIds + await scheduleVoucherConsumeForOrderItems(assignedItemIds, 'order_received').catch( + (error) => { + logWarn('[channel-assign]', '订单收到后安排自动核销失败', { + orderItemIds: assignedItemIds, + error: error instanceof Error ? error.message : String(error), + }) + }, + ) } } @@ -566,6 +581,12 @@ export async function assignOrderItemsAfterCarrierReady(input: { } if (assignedIds.length > 0) { + await scheduleVoucherConsumeForOrderItems(assignedIds, 'order_received').catch((error) => { + logWarn('[channel-assign]', '指定渠道后安排自动核销失败', { + orderItemIds: assignedIds, + error: error instanceof Error ? error.message : String(error), + }) + }) await normalizeProductsForItems(assignedIds).catch(() => undefined) const cost = await syncCostSnapshotsForItems(assignedIds).catch(() => null) const dates = [ diff --git a/apps/backend/src/services/fulfillment/channel-types.ts b/apps/backend/src/services/fulfillment/channel-types.ts index 9b43aeaf..34513644 100644 --- a/apps/backend/src/services/fulfillment/channel-types.ts +++ b/apps/backend/src/services/fulfillment/channel-types.ts @@ -4,7 +4,7 @@ * 与旧 `channel` 的区别: * · 渠道只表达「谁来做」,不包含 pending/archived 这类状态; * · 「待分类」是状态,不是渠道; - * · 「第三方卡券」是渠道,但没有履约载体,只做统计。 + * · 「第三方卡券」由外部卡券库履约,不建内部履约载体;平台电子凭证仍可配置核销。 */ export const FULFILLMENT_CHANNELS = [ diff --git a/apps/backend/src/services/fulfillment/voucher-consume-policy-service.test.ts b/apps/backend/src/services/fulfillment/voucher-consume-policy-service.test.ts index f71600d9..63a9706e 100644 --- a/apps/backend/src/services/fulfillment/voucher-consume-policy-service.test.ts +++ b/apps/backend/src/services/fulfillment/voucher-consume-policy-service.test.ts @@ -25,6 +25,11 @@ test('默认核销策略:自动发货 5 秒,工单发布或指派后 10 秒 mode: 'after_task_created', delaySeconds: 10, }) + assert.deepEqual(policy.platforms.kuaishou_industry.channels.third_party_voucher, { + enabled: true, + mode: 'after_order_received', + delaySeconds: 5, + }) }) test('工单默认策略同时匹配发布和指派,且不匹配链接生成', () => { @@ -59,6 +64,31 @@ test('工单默认策略同时匹配发布和指派,且不匹配链接生成', ) }) +test('收到订单后事件可匹配四个渠道,第三方卡券默认延迟 5 秒', () => { + const policy = createDefaultVoucherConsumePolicy() + for (const channel of [ + 'affiliate_dash', + 'work_order', + 'manual', + 'third_party_voucher', + ] as const) { + const configured = structuredClone(policy) + configured.platforms.kuaishou_industry.channels[channel] = { + enabled: true, + mode: 'after_order_received', + delaySeconds: channel === 'third_party_voucher' ? 5 : 15, + } + const resolved = resolveVoucherConsumePolicy({ + provider: 'kuaishou_industry', + channel, + event: 'order_received', + policy: configured, + }) + assert.equal(resolved.enabled, true) + assert.equal(resolved.delaySeconds, channel === 'third_party_voucher' ? 5 : 15) + } +}) + test('策略归一化会丢弃非法模式并限制延迟范围', () => { const policy = normalizeVoucherConsumePolicy({ enabled: true, @@ -83,3 +113,53 @@ test('策略归一化会丢弃非法模式并限制延迟范围', () => { }) assert.equal(policy.platforms.douyin_mashida.enabled, true) }) + +test('旧版第三方卡券停用配置自动升级为收到订单后 5 秒', () => { + const policy = normalizeVoucherConsumePolicy({ + enabled: true, + platforms: { + kuaishou_industry: { + enabled: true, + channels: { + third_party_voucher: { + enabled: false, + mode: 'manual', + delaySeconds: 0, + }, + }, + }, + }, + }) + + assert.equal(policy.version, 2) + assert.deepEqual(policy.platforms.kuaishou_industry.channels.third_party_voucher, { + enabled: true, + mode: 'after_order_received', + delaySeconds: 5, + }) +}) + +test('新版明确关闭第三方卡券时继续尊重用户配置', () => { + const policy = normalizeVoucherConsumePolicy({ + version: 2, + enabled: true, + platforms: { + kuaishou_industry: { + enabled: true, + channels: { + third_party_voucher: { + enabled: false, + mode: 'manual', + delaySeconds: 0, + }, + }, + }, + }, + }) + + assert.deepEqual(policy.platforms.kuaishou_industry.channels.third_party_voucher, { + enabled: false, + mode: 'manual', + delaySeconds: 0, + }) +}) diff --git a/apps/backend/src/services/fulfillment/voucher-consume-policy-service.ts b/apps/backend/src/services/fulfillment/voucher-consume-policy-service.ts index d872212a..356db20c 100644 --- a/apps/backend/src/services/fulfillment/voucher-consume-policy-service.ts +++ b/apps/backend/src/services/fulfillment/voucher-consume-policy-service.ts @@ -14,6 +14,7 @@ export const VOUCHER_CONSUME_CHANNELS = [ export type VoucherConsumeChannel = (typeof VOUCHER_CONSUME_CHANNELS)[number] export const VOUCHER_CONSUME_EVENTS = [ + 'order_received', 'link_generated', 'task_created', 'work_order_published', @@ -23,6 +24,7 @@ export type VoucherConsumeEvent = (typeof VOUCHER_CONSUME_EVENTS)[number] export const VOUCHER_CONSUME_MODES = [ 'manual', + 'after_order_received', 'after_link_generated', 'after_task_created', 'after_work_order_published', @@ -43,12 +45,14 @@ export type VoucherPlatformConsumePolicy = { } export type VoucherConsumePolicy = { + version: number enabled: boolean platforms: Record } export function createDefaultVoucherConsumePolicy(): VoucherConsumePolicy { return { + version: 2, enabled: true, platforms: { kuaishou_industry: createDefaultPlatformPolicy(), @@ -77,17 +81,21 @@ export function normalizeVoucherConsumePolicy(rawValue: unknown): VoucherConsume const defaults = createDefaultVoucherConsumePolicy() const source = isPlainObject(rawValue) ? rawValue : {} const rawPlatforms = isPlainObject(source.platforms) ? source.platforms : {} + const version = normalizeInteger(source.version, 1, 1, 2) return { + version: 2, enabled: normalizeBoolean(source.enabled, defaults.enabled), platforms: { kuaishou_industry: normalizePlatformPolicy( rawPlatforms.kuaishou_industry, defaults.platforms.kuaishou_industry, + version, ), douyin_mashida: normalizePlatformPolicy( rawPlatforms.douyin_mashida, defaults.platforms.douyin_mashida, + version, ), }, } @@ -138,9 +146,9 @@ function createDefaultPlatformPolicy(): VoucherPlatformConsumePolicy { delaySeconds: 10, }, third_party_voucher: { - enabled: false, - mode: 'manual', - delaySeconds: 0, + enabled: true, + mode: 'after_order_received', + delaySeconds: 5, }, }, } @@ -149,9 +157,11 @@ function createDefaultPlatformPolicy(): VoucherPlatformConsumePolicy { function normalizePlatformPolicy( rawValue: unknown, fallback: VoucherPlatformConsumePolicy, + version: number, ): VoucherPlatformConsumePolicy { const source = isPlainObject(rawValue) ? rawValue : {} const rawChannels = isPlainObject(source.channels) ? source.channels : {} + const legacyThirdPartyDisabled = isLegacyThirdPartyDisabled(rawValue, version) return { enabled: normalizeBoolean(source.enabled, fallback.enabled), @@ -159,10 +169,11 @@ function normalizePlatformPolicy( affiliate_dash: normalizeChannelPolicy( rawChannels.affiliate_dash, fallback.channels.affiliate_dash, - ['manual', 'after_link_generated', 'after_task_created'], + ['manual', 'after_order_received', 'after_link_generated', 'after_task_created'], ), work_order: normalizeChannelPolicy(rawChannels.work_order, fallback.channels.work_order, [ 'manual', + 'after_order_received', 'after_task_created', 'after_work_order_published', 'after_work_order_assigned', @@ -170,17 +181,25 @@ function normalizePlatformPolicy( ]), manual: normalizeChannelPolicy(rawChannels.manual, fallback.channels.manual, [ 'manual', + 'after_order_received', 'after_task_created', ]), third_party_voucher: normalizeChannelPolicy( - rawChannels.third_party_voucher, + legacyThirdPartyDisabled ? {} : rawChannels.third_party_voucher, fallback.channels.third_party_voucher, - ['manual'], + ['manual', 'after_order_received'], ), }, } } +function isLegacyThirdPartyDisabled(rawPlatform: unknown, version: number): boolean { + if (version >= 2 || !isPlainObject(rawPlatform)) return false + const rawChannels = isPlainObject(rawPlatform.channels) ? rawPlatform.channels : {} + const raw = isPlainObject(rawChannels.third_party_voucher) ? rawChannels.third_party_voucher : {} + return raw.enabled === false && String(raw.mode || '').trim() === 'manual' +} + function normalizeChannelPolicy( rawValue: unknown, fallback: VoucherChannelConsumePolicy, @@ -199,6 +218,7 @@ function normalizeChannelPolicy( function modeMatchesEvent(mode: VoucherConsumeMode, event: VoucherConsumeEvent): boolean { if (mode === 'manual') return false + if (mode === 'after_order_received') return event === 'order_received' if (mode === 'after_link_generated') return event === 'link_generated' if (mode === 'after_task_created') return event === 'task_created' if (mode === 'after_work_order_published') return event === 'work_order_published' diff --git a/apps/backend/src/services/fulfillment/voucher-consume-queue-service.ts b/apps/backend/src/services/fulfillment/voucher-consume-queue-service.ts index 80155ec7..b9042aa9 100644 --- a/apps/backend/src/services/fulfillment/voucher-consume-queue-service.ts +++ b/apps/backend/src/services/fulfillment/voucher-consume-queue-service.ts @@ -88,6 +88,15 @@ type VoucherConsumeScheduleTarget = { voucherCount: number } +type VoucherOrderItemTargetRow = { + id: number + order_id: number + provider: string + platform: string + platform_order_id: string + fulfillment_channel: string +} + const RETRY_DELAYS_SECONDS = [10, 30, 60, 300, 600, 900, 1800] const PROCESSING_LOCK_SECONDS = 120 @@ -152,6 +161,140 @@ export async function scheduleVoucherConsumeForWorkOrder( }) } +/** + * Schedule after an order item has a fulfillment channel. + * + * The platform voucher may not exist yet when this runs. The queued job resolves + * the voucher again at execution time and retries while the incoming voucher is + * still being created or synchronized. + */ +export async function scheduleVoucherConsumeForOrderItems( + orderItemIds: number[], + event: VoucherConsumeEvent, +): Promise<{ targetCount: number; queuedCount: number; jobIds: number[] }> { + const ids = [...new Set(orderItemIds.map(Number).filter((id) => Number.isInteger(id) && id > 0))] + if (ids.length === 0) { + return { targetCount: 0, queuedCount: 0, jobIds: [] } + } + + const rows = await query( + ` + SELECT + oi.id, + oi.order_id, + oi.fulfillment_channel, + o.provider, + o.platform, + o.platform_order_id + FROM order_items oi + JOIN orders o ON o.id = oi.order_id + WHERE oi.id = ANY($1::bigint[]) + AND oi.fulfillment_status = 'assigned' + `, + [ids], + ) + const policy = getVoucherConsumePolicy() + const targets: Array<{ + provider: VoucherConsumeProvider + channel: VoucherConsumeChannel + trigger_event: VoucherConsumeEvent + target_key: string + platform_order_id: string + order_id: number + order_item_id: number + due_at: string + }> = [] + + for (const row of rows.rows) { + const provider = resolveVoucherProviderFromFields(row.provider, row.platform) + const channel = normalizeVoucherChannel(row.fulfillment_channel) + if (!provider || !channel) continue + + const resolved = resolveVoucherConsumePolicy({ + provider, + channel, + event, + policy, + }) + if (!resolved.enabled) continue + + targets.push({ + provider, + channel, + trigger_event: event, + target_key: buildVoucherConsumeTargetKey(row.id, `order_item:${row.id}`), + platform_order_id: String(row.platform_order_id || '').trim(), + order_id: Number(row.order_id), + order_item_id: Number(row.id), + due_at: new Date(Date.now() + resolved.delaySeconds * 1000).toISOString(), + }) + } + + if (targets.length === 0) { + return { targetCount: 0, queuedCount: 0, jobIds: [] } + } + + const inserted = await query<{ id: number }>( + ` + INSERT INTO voucher_consume_jobs ( + provider, + channel, + trigger_event, + target_key, + platform_order_id, + order_id, + order_item_id, + due_at, + status, + attempt_count, + max_attempts, + updated_at + ) + SELECT + target.provider, + target.channel, + target.trigger_event, + target.target_key, + target.platform_order_id, + target.order_id, + target.order_item_id, + target.due_at, + 'pending', + 0, + 8, + NOW() + FROM jsonb_to_recordset($1::jsonb) AS target( + provider text, + channel text, + trigger_event text, + target_key text, + platform_order_id text, + order_id bigint, + order_item_id bigint, + due_at timestamptz + ) + ON CONFLICT (provider, target_key) + WHERE status IN ('pending', 'processing', 'succeeded') + DO NOTHING + RETURNING id + `, + [JSON.stringify(targets)], + ) + + logIntegration('[voucher-consume/queue]', '订单渠道确定后批量安排核销', { + event, + targetCount: targets.length, + queuedCount: inserted.rowCount || 0, + jobIds: inserted.rows.map((row) => Number(row.id)), + }) + + return { + targetCount: targets.length, + queuedCount: inserted.rowCount || 0, + jobIds: inserted.rows.map((row) => Number(row.id)), + } +} + async function scheduleVoucherConsumeTarget( target: Omit, ): Promise { @@ -688,11 +831,18 @@ async function failVoucherConsumeJob(job: VoucherConsumeJobRow, error: unknown, } function resolveVoucherProvider(order: OrderRow): VoucherConsumeProvider | null { - if (isDouyinMashidaProvider(order.provider)) { + return resolveVoucherProviderFromFields(order.provider, order.platform) +} + +function resolveVoucherProviderFromFields( + orderProvider: unknown, + orderPlatform: unknown, +): VoucherConsumeProvider | null { + if (isDouyinMashidaProvider(orderProvider)) { return 'douyin_mashida' } - const provider = String(order.provider || '').trim() - const platform = String(order.platform || '').trim() + const provider = String(orderProvider || '').trim() + const platform = String(orderPlatform || '').trim() if ( [OPEN_91_PROVIDER, 'open_91', '91'].includes(provider) || [OPEN_91_PLATFORM, 'kuaishou'].includes(platform) @@ -702,6 +852,19 @@ function resolveVoucherProvider(order: OrderRow): VoucherConsumeProvider | null return null } +function normalizeVoucherChannel(value: unknown): VoucherConsumeChannel | null { + const channel = String(value || '').trim() + if ( + channel === 'affiliate_dash' || + channel === 'work_order' || + channel === 'manual' || + channel === 'third_party_voucher' + ) { + return channel + } + return null +} + function resolveTaskVoucherChannel( task: Pick, orderItem: OrderItemRow | null, @@ -717,16 +880,7 @@ function resolveTaskVoucherChannel( return 'manual' } - const channel = String(orderItem?.fulfillment_channel || '').trim() - if ( - channel === 'affiliate_dash' || - channel === 'work_order' || - channel === 'manual' || - channel === 'third_party_voucher' - ) { - return channel - } - return null + return normalizeVoucherChannel(orderItem?.fulfillment_channel) } function buildVoucherConsumeTargetKey(orderItemId: unknown, fallback: string): string { diff --git a/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx b/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx index 21abce6f..96d92f0e 100644 --- a/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx +++ b/apps/frontend/src/pages/admin/AdminFulfillmentChannelPage.tsx @@ -4,7 +4,7 @@ * 设计口径(2026-10-31 v2): * · 渠道由 L1 规则决定,不再由「有没有任务」反推; * · 没有命中规则的新单落在「待分类」,收入进未分类桶,不产生成本噪音; - * · 第三方卡券是「只统计」渠道:不建任务、不发领取页。 + * · 第三方卡券由外部卡券库履约:不建内部任务、不发领取页,但支持平台券自动核销。 */ import { DeleteOutlined, @@ -66,7 +66,7 @@ import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admi const CHANNEL_OPTIONS = [ { value: 'work_order', label: '接单平台' }, { value: 'affiliate_dash', label: '自动发货' }, - { value: 'third_party_voucher', label: '第三方卡券(仅统计)' }, + { value: 'third_party_voucher', label: '第三方卡券' }, { value: 'manual', label: '人工客服' }, ] diff --git a/apps/frontend/src/pages/admin/panels/VoucherConsumePolicyPanel.tsx b/apps/frontend/src/pages/admin/panels/VoucherConsumePolicyPanel.tsx index 7a2bf780..d80319ae 100644 --- a/apps/frontend/src/pages/admin/panels/VoucherConsumePolicyPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/VoucherConsumePolicyPanel.tsx @@ -46,11 +46,13 @@ const MODE_OPTIONS: Record< Array<{ value: VoucherConsumeMode; label: string }> > = { affiliate_dash: [ + { value: 'after_order_received', label: '收到订单后' }, { value: 'after_link_generated', label: '领取链接生成后' }, { value: 'after_task_created', label: '履约任务创建后' }, { value: 'manual', label: '人工核销' }, ], work_order: [ + { value: 'after_order_received', label: '收到订单后' }, { value: 'after_work_order_published_or_assigned', label: '发布或指派后' }, { value: 'after_work_order_published', label: '仅发布后' }, { value: 'after_work_order_assigned', label: '仅指派后' }, @@ -58,10 +60,14 @@ const MODE_OPTIONS: Record< { value: 'manual', label: '人工核销' }, ], manual: [ + { value: 'after_order_received', label: '收到订单后' }, { value: 'after_task_created', label: '人工任务创建后' }, { value: 'manual', label: '人工核销' }, ], - third_party_voucher: [{ value: 'manual', label: '未接入自动核销' }], + third_party_voucher: [ + { value: 'after_order_received', label: '收到订单后' }, + { value: 'manual', label: '人工核销' }, + ], } type PolicyRow = { @@ -186,7 +192,6 @@ export default function VoucherConsumePolicyPanel() { updateChannel(row.provider, row.channel, { enabled })} /> ), @@ -198,7 +203,7 @@ export default function VoucherConsumePolicyPanel() {