diff --git a/apps/backend/src/services/cost-center/cost-center-constants.ts b/apps/backend/src/services/cost-center/cost-center-constants.ts index 2de776df..e687ae92 100644 --- a/apps/backend/src/services/cost-center/cost-center-constants.ts +++ b/apps/backend/src/services/cost-center/cost-center-constants.ts @@ -55,7 +55,7 @@ export const COST_MODEL_CODE_BASE: Record = { other: 'other_cost', } -/** 履约方式:唯一真源是 order_items.fulfillment_mode。 */ +/** 履约渠道:唯一真源是 order_items.fulfillment_channel + fulfillment_status。 */ export type FulfillmentMode = 'work_order' | 'affiliate_dash' | 'legacy_platform' | 'manual' export const FULFILLMENT_MODES = new Set([ diff --git a/apps/backend/src/services/cost-center/cost-center-overview-service.ts b/apps/backend/src/services/cost-center/cost-center-overview-service.ts index 004c11c4..bcc78e49 100644 --- a/apps/backend/src/services/cost-center/cost-center-overview-service.ts +++ b/apps/backend/src/services/cost-center/cost-center-overview-service.ts @@ -80,7 +80,7 @@ export async function getAdminCostCenterOverview(params: { scope?: string } = {} AND ( $1::text IS NULL OR ce.order_item_id IN ( - SELECT oi.id FROM order_items oi WHERE oi.fulfillment_mode = $1 + SELECT oi.id FROM order_items oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $1 ) ) ) AS open_exception_count, @@ -89,7 +89,7 @@ export async function getAdminCostCenterOverview(params: { scope?: string } = {} FROM order_items oi WHERE oi.canonical_product_id IS NULL AND oi.fulfillment_status <> 'archived' - AND ($1::text IS NULL OR oi.fulfillment_mode = $1) + AND ($1::text IS NULL OR COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $1) ) AS unmapped_order_item_count `, [scope], @@ -121,7 +121,12 @@ export async function getAdminCostCenterOverview(params: { scope?: string } = {} } } -const COST_CENTER_SCOPES = new Set(['affiliate_dash', 'work_order', 'manual']) +const COST_CENTER_SCOPES = new Set([ + 'affiliate_dash', + 'work_order', + 'third_party_voucher', + 'manual', +]) function normalizeScope(raw: unknown): CostCenterScope | null { const value = String(raw ?? '').trim() diff --git a/apps/backend/src/services/cost-center/cost-center-shared.ts b/apps/backend/src/services/cost-center/cost-center-shared.ts index d20b0511..5725f1ad 100644 --- a/apps/backend/src/services/cost-center/cost-center-shared.ts +++ b/apps/backend/src/services/cost-center/cost-center-shared.ts @@ -2,7 +2,7 @@ * 成本中心共享层入口。 * * 口径(2026-10-23 重写): - * 1. 履约方式只有 order_items.fulfillment_mode 一个真源; + * 1. 履约渠道只有 order_items.fulfillment_channel/status 一个真源; * 2. 成本项用 cost_model 表达「成本怎么算」,与履约方式正交; * 3. 商品来源由 canonical_sku 前缀推导,商品表不再存履约载体; * 4. 快照与异常表都不再存履约载体,join order_items 即可拿到。 diff --git a/apps/backend/src/services/cost-center/cost-definition-service.ts b/apps/backend/src/services/cost-center/cost-definition-service.ts index 1d079a64..0b5c8964 100644 --- a/apps/backend/src/services/cost-center/cost-definition-service.ts +++ b/apps/backend/src/services/cost-center/cost-definition-service.ts @@ -567,7 +567,7 @@ export async function listAdminCostExceptions(payload: JsonObject = {}) { params.push(since) clauses.push(`ce.created_at >= $${params.length}::timestamptz`) } - // 履约方式筛选:异常表不再存载体,改为 join order_items 读 fulfillment_mode。 + // 履约渠道筛选:异常表不存渠道,改为 join order_items 读 channel/status。 // 用 IN 半连接而不是 EXISTS:248k 异常 × 316k 订单项下,EXISTS 会逐行回表, // 半连接能一次物化候选集合(实测 2.7s -> 0.4s)。 const fulfillmentMode = text(payload.fulfillmentMode ?? payload.fulfillment_mode) @@ -576,7 +576,7 @@ export async function listAdminCostExceptions(payload: JsonObject = {}) { params.push(fulfillmentMode) clauses.push( `ce.order_item_id IN ( - SELECT oi.id FROM order_items oi WHERE oi.fulfillment_mode = $${params.length} + SELECT oi.id FROM order_items oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${params.length} )`, ) } @@ -591,7 +591,7 @@ export async function listAdminCostExceptions(payload: JsonObject = {}) { ` SELECT ce.*, - oi.fulfillment_mode + COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS fulfillment_mode FROM cost_exceptions ce LEFT JOIN order_items oi ON oi.id = ce.order_item_id ${where} @@ -635,7 +635,7 @@ export async function summarizeAdminCostExceptions( params.push(fulfillmentMode) clauses.push( `ce.order_item_id IN ( - SELECT oi.id FROM order_items oi WHERE oi.fulfillment_mode = $${params.length} + SELECT oi.id FROM order_items oi WHERE COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${params.length} )`, ) } diff --git a/apps/backend/src/services/cost-center/cost-resolution-service.ts b/apps/backend/src/services/cost-center/cost-resolution-service.ts index 7f2b7e11..a1cb5798 100644 --- a/apps/backend/src/services/cost-center/cost-resolution-service.ts +++ b/apps/backend/src/services/cost-center/cost-resolution-service.ts @@ -250,7 +250,7 @@ export async function createAdminCostSnapshot(payload: JsonObject = {}, _session subject_type, subject_id, order_item_id, work_order_id, reason, status, detail_json, created_at ) VALUES ('order_item', $1, $1, $2, $3, 'open', $4::jsonb, $5) - ON CONFLICT (subject_type, subject_id, reason) DO NOTHING + ON CONFLICT (scope_key) WHERE status <> 'resolved' DO NOTHING `, [ orderItemId, @@ -315,7 +315,7 @@ async function resolveCostContext(payload: JsonObject): Promise>'provider' AS provider, ce.detail_json->>'shopId' AS shop_id, COALESCE(ce.detail_json->>'skuCode', '') AS sku_code, - oi.fulfillment_mode AS fulfillment_mode, + COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS fulfillment_mode, COUNT(*)::int AS item_count FROM cost_exceptions ce JOIN order_items oi ON oi.id = ce.order_item_id WHERE ce.reason = 'product_mapping_missing' AND ce.status = 'open' - AND oi.fulfillment_mode = $1 + AND COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $1 GROUP BY 1, 2, 3, 4 ) SELECT @@ -716,7 +718,7 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { FROM cost_exceptions ce JOIN order_items oi ON oi.id = ce.order_item_id WHERE ${clauses.join(' AND ')} - AND oi.fulfillment_mode = $${totalParams.length} + AND COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${totalParams.length} GROUP BY 1, 2, 3 ) SELECT @@ -744,7 +746,9 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { const pageIndex = params.length + (modeIndex ? 2 : 1) const offsetIndex = pageIndex + 1 const listParams = [...params, ...(fulfillmentMode ? [fulfillmentMode] : []), pageSize, offset] - const modeClause = modeIndex ? `AND oi.fulfillment_mode = $${modeIndex}` : '' + const modeClause = modeIndex + ? `AND COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) = $${modeIndex}` + : '' const result = await query<{ provider: string shop_id: string @@ -760,7 +764,7 @@ export async function listAdminUnmappedMappingGroups(payload: JsonObject = {}) { ce.detail_json->>'provider' AS provider, ce.detail_json->>'shopId' AS shop_id, COALESCE(ce.detail_json->>'skuCode', '') AS sku_code, - oi.fulfillment_mode AS fulfillment_mode, + COALESCE(oi.fulfillment_channel, CASE WHEN oi.fulfillment_status = 'archived' THEN 'archived' ELSE 'pending' END) AS fulfillment_mode, MAX(ce.detail_json->>'skuName') AS sku_name, MAX(NULLIF(ce.detail_json->>'basisAt', '')) AS last_seen_at, COUNT(*)::int AS item_count @@ -1522,10 +1526,12 @@ export async function setAdminProductPrice( } } - // 3. 其他标准商品 (wo: 或 man:) + // 3. 其他标准商品 (wo: / tpv: / man:) const costItemCode = product.canonical_sku.startsWith('wo:') ? 'sys_work_order_reward' - : 'sys_manual_cost' + : product.canonical_sku.startsWith('tpv:') + ? 'sys_third_party_voucher_cost' + : 'sys_manual_service' const costItemResult = await query<{ id: number }>( 'SELECT id FROM cost_items WHERE code = $1 LIMIT 1', [costItemCode], @@ -1647,13 +1653,12 @@ export async function syncAdminProductPrice(productIdInput: unknown, session: Ad } if (product.canonical_sku.startsWith('aff:')) { - const syncRes = await syncAdminAffiliateDashCostSnapshots() + // 成本快照由成本快照管道按缺补算,这里不再同步触发。 return { success: true, - type: 'affiliate_snapshots_synced', + type: 'affiliate_price_saved', product, - snapshots: syncRes, - message: `已重算成本快照(生成 ${syncRes.snapshotResult.createdCount} 笔快照,消除 ${syncRes.snapshotResult.resolvedExceptionCount} 笔异常),并重算销售事实`, + message: '价格已保存,成本快照会在下一轮管道运行后自动补齐', } } diff --git a/apps/backend/src/services/cost-center/product-normalize-service.test.ts b/apps/backend/src/services/cost-center/product-normalize-service.test.ts index f0714185..0f73d9af 100644 --- a/apps/backend/src/services/cost-center/product-normalize-service.test.ts +++ b/apps/backend/src/services/cost-center/product-normalize-service.test.ts @@ -46,6 +46,7 @@ const index = buildMappingIndex( mappings, [{ sku: 'aff-sku', productId: 21 }], [{ ruleKey: 'rule-abc', productId: 31 }], + [{ sku: 'voucher-sku', productId: 41 }], ) function item(patch: Record = {}) { @@ -123,3 +124,13 @@ test('商品归一:人工渠道直接使用配置里指定的商品', () => { 44, ) }) + +test('商品归一:第三方卡券按 tpv:SKU 直接命中', () => { + assert.equal( + resolveProductId( + item({ fulfillment_channel: 'third_party_voucher', sku_code: 'voucher-sku' }), + index, + ), + 41, + ) +}) diff --git a/apps/backend/src/services/cost-center/product-normalize-service.ts b/apps/backend/src/services/cost-center/product-normalize-service.ts index 31f70148..05159aa6 100644 --- a/apps/backend/src/services/cost-center/product-normalize-service.ts +++ b/apps/backend/src/services/cost-center/product-normalize-service.ts @@ -32,6 +32,7 @@ export type MappingIndex = { bySkuSeries: Map byNameSeries: Map affSkus: Map + voucherSkus: Map templateRules: Map } @@ -56,6 +57,7 @@ export function buildMappingIndex( mappings: MappingRow[], affiliateProducts: Array<{ sku: string; productId: number }>, templateProducts: Array<{ ruleKey: string; productId: number }> = [], + voucherProducts: Array<{ sku: string; productId: number }> = [], ): MappingIndex { const index: MappingIndex = { bySku: new Map(), @@ -63,6 +65,7 @@ export function buildMappingIndex( bySkuSeries: new Map(), byNameSeries: new Map(), affSkus: new Map(), + voucherSkus: new Map(), templateRules: new Map(), } @@ -99,6 +102,10 @@ export function buildMappingIndex( index.templateRules.set(String(product.ruleKey).trim(), product.productId) } + for (const product of voucherProducts) { + index.voucherSkus.set(String(product.sku).trim(), product.productId) + } + return index } @@ -126,6 +133,12 @@ export function resolveProductId(item: ItemRow, index: MappingIndex): number | n if (aff) return aff } + // 第三方卡券:与自动发货同构,商品编码 tpv:。 + if (item.fulfillment_channel === 'third_party_voucher' && skuCode) { + const voucher = index.voucherSkus.get(skuCode) + if (voucher) return voucher + } + const exactWithShop = index.bySku.get(key(provider, shopId, skuCode)) if (exactWithShop) return exactWithShop const exactGlobal = index.bySku.get(key(provider, '', skuCode)) @@ -265,6 +278,14 @@ async function loadMappingIndex(): Promise { `, ) + const voucherProducts = await query<{ canonical_sku: string; id: number }>( + ` + SELECT id, canonical_sku + FROM product_catalog + WHERE status = 'active' AND canonical_sku LIKE 'tpv:%' + `, + ) + return buildMappingIndex( mappings.rows, affiliateProducts.rows.map((row) => ({ @@ -275,5 +296,9 @@ async function loadMappingIndex(): Promise { ruleKey: String(row.canonical_sku).slice(4), productId: Number(row.id), })), + voucherProducts.rows.map((row) => ({ + sku: String(row.canonical_sku).slice(4), + productId: Number(row.id), + })), ) } diff --git a/apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx b/apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx index d330841e..06c64371 100644 --- a/apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx +++ b/apps/frontend/src/pages/admin/AdminSalesProfitPage.tsx @@ -38,15 +38,20 @@ const FULFILLMENT_MODE_OPTIONS = [ { value: '', label: '全部履约方式' }, { value: 'work_order', label: '接单平台' }, { value: 'affiliate_dash', label: '自动发货' }, - { value: 'legacy_platform', label: '历史平台' }, + { value: 'third_party_voucher', label: '第三方卡券' }, { value: 'manual', label: '人工客服' }, + { value: 'unclassified', label: '未分类' }, + { value: 'archived', label: '历史归档' }, ] const FULFILLMENT_MODE_LABEL: Record = { work_order: '接单平台', affiliate_dash: '自动发货', - legacy_platform: '历史平台', + third_party_voucher: '第三方卡券', + legacy_platform: '历史归档', manual: '人工客服', + archived: '历史归档', + unclassified: '未分类', } const COST_STAGE_LABEL: Record = { diff --git a/apps/frontend/src/pages/admin/cost-center/CostCenterContext.ts b/apps/frontend/src/pages/admin/cost-center/CostCenterContext.ts index 81850d8e..df4e3017 100644 --- a/apps/frontend/src/pages/admin/cost-center/CostCenterContext.ts +++ b/apps/frontend/src/pages/admin/cost-center/CostCenterContext.ts @@ -20,7 +20,7 @@ export type CostCenterContextValue = { /** 商品/映射按 canonical_sku 前缀筛选。 */ productSourceFilter?: ProductSource /** 成本项/版本/异常按履约方式筛选。 */ - fulfillmentModeFilter?: 'affiliate_dash' | 'work_order' | 'manual' + fulfillmentModeFilter?: 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual' options?: { providers: string[] diff --git a/apps/frontend/src/pages/admin/cost-center/CostCenterShell.tsx b/apps/frontend/src/pages/admin/cost-center/CostCenterShell.tsx index 66a855bf..39ce86d5 100644 --- a/apps/frontend/src/pages/admin/cost-center/CostCenterShell.tsx +++ b/apps/frontend/src/pages/admin/cost-center/CostCenterShell.tsx @@ -48,6 +48,11 @@ const SCOPES: Array<{ key: CostScope; label: string; hint: string }> = [ label: '接单平台', hint: '需要打手接单履约的商品;成本以打手报酬为主,按接单模板配价。', }, + { + key: 'third_party_voucher', + label: '第三方卡券', + hint: '只统计渠道:不建任务、不发领取页;成本按商品采购价版本核算。', + }, { key: 'manual', label: '人工客服', @@ -104,7 +109,9 @@ export default function CostCenterShell() { const scopeHint = SCOPES.find((item) => item.key === scope)?.hint ?? '' const productSourceFilter = scope === 'all' ? undefined : scope const fulfillmentModeFilter = - scope === 'all' ? undefined : (scope as 'affiliate_dash' | 'work_order' | 'manual') + scope === 'all' + ? undefined + : (scope as 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual') const mappingsApplicable = scope === 'all' || scope === 'work_order' return ( diff --git a/apps/frontend/src/pages/admin/cost-center/CostConfigPanel.tsx b/apps/frontend/src/pages/admin/cost-center/CostConfigPanel.tsx index 82bce6e4..1132459e 100644 --- a/apps/frontend/src/pages/admin/cost-center/CostConfigPanel.tsx +++ b/apps/frontend/src/pages/admin/cost-center/CostConfigPanel.tsx @@ -4,9 +4,10 @@ * 三个来源的成本形态完全不同,所以这里不做统一表格,而是按来源切换: * 自动发货 → 上游 SKU 采购价(按 aff: 归一) * 接单平台 → 接单模板发单金额(只读核对,改价去接单模板页) + * 第三方卡券 → 卡券商品采购价(按 tpv: 归一,只统计) * 人工客服 → 内部 SKU 的履约渠道与消耗积分 * - * 来源在页面顶部工具栏统一选择;选「全部」时这里给出三个入口, + * 来源在页面顶部工具栏统一选择;选「全部」时这里给出四个入口, * 避免把三套配置竖着堆在一页里。 */ import { Button, Card, Col, Row, Space, Typography } from 'antd' @@ -14,6 +15,7 @@ import { Button, Card, Col, Row, Space, Typography } from 'antd' import { AffiliateSkuPanel } from './AffiliateSkuPanel' import { useCostCenter } from './CostCenterContext' import { ManualSkuConfigPanel } from './ManualSkuConfigPanel' +import VoucherSkuPanel from './VoucherSkuPanel' import { WorkOrderPricePanel } from './WorkOrderPricePanel' import type { CostScope } from './shared' @@ -32,6 +34,11 @@ const CONFIG_ENTRIES: Array<{ title: '接单平台发单金额', description: '接单模板 → 发单金额的当前生效版本,只读核对;改价请改模板后重新同步。', }, + { + key: 'third_party_voucher', + title: '第三方卡券采购价', + description: '按 tpv: 配置卡券采购价;该渠道只统计收入与成本,不产生履约动作。', + }, { key: 'manual', title: '人工客服 SKU 消耗', @@ -44,6 +51,7 @@ export default function CostConfigPanel() { if (scope === 'affiliate_dash') return if (scope === 'work_order') return + if (scope === 'third_party_voucher') return if (scope === 'manual') return return ( diff --git a/apps/frontend/src/pages/admin/cost-center/CostExceptionPanel.tsx b/apps/frontend/src/pages/admin/cost-center/CostExceptionPanel.tsx index c3e0f8be..8b66fb50 100644 --- a/apps/frontend/src/pages/admin/cost-center/CostExceptionPanel.tsx +++ b/apps/frontend/src/pages/admin/cost-center/CostExceptionPanel.tsx @@ -52,8 +52,11 @@ const FULFILLMENT_MODE_LABELS: Record = { } const REASON_LABELS: Record = { + classification_missing: '待分类', + routing_conflict: '渠道规则冲突', product_mapping_missing: '商品未归一', cost_missing: '成本缺失', + manual_config_missing: '人工 SKU 未配置', manual_cost_missing: '人工成本未记录', } diff --git a/apps/frontend/src/pages/admin/cost-center/VoucherSkuPanel.tsx b/apps/frontend/src/pages/admin/cost-center/VoucherSkuPanel.tsx new file mode 100644 index 00000000..5450071b --- /dev/null +++ b/apps/frontend/src/pages/admin/cost-center/VoucherSkuPanel.tsx @@ -0,0 +1,142 @@ +/** + * 第三方卡券采购价:只统计渠道,不产生履约动作。 + * + * 商品编码固定为 `tpv:`;成本写入 `sys_third_party_voucher_cost` 的 + * 商品级版本,由成本快照管道消费。 + */ +import { ReloadOutlined } from '@ant-design/icons' +import { Button, Card, Form, Input, InputNumber, Modal, Tag, Typography, message } from 'antd' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useState } from 'react' + +import PercentTable from '@/components/common/PercentTable' +import { fetchAdminProductCatalog, setAdminProductPrice } from '@/services/admin' +import type { ProductCatalogItem } from '@/types/admin' +import { useCostCenter } from './CostCenterContext' + +const PAGE_SIZE = 100 + +export default function VoucherSkuPanel() { + const queryClient = useQueryClient() + const { canConfig, refreshAll } = useCostCenter() + const [page] = useState(1) + const [selected, setSelected] = useState(null) + const [form] = Form.useForm<{ amountFen: number; changeReason?: string }>() + + const query = useQuery({ + queryKey: ['cost-center-voucher-products', page], + queryFn: () => + fetchAdminProductCatalog({ + carrierType: 'third_party_voucher', + page, + pageSize: PAGE_SIZE, + }), + }) + + const saveMutation = useMutation({ + mutationFn: (values: { amountFen: number; changeReason?: string }) => + setAdminProductPrice(selected!.id, { + amountFen: Math.round(values.amountFen * 100), + changeReason: values.changeReason || '第三方卡券采购价调整', + }), + onSuccess: () => { + message.success('采购价已保存') + setSelected(null) + form.resetFields() + void queryClient.invalidateQueries({ queryKey: ['cost-center-voucher-products'] }) + refreshAll() + }, + onError: (error) => message.error(error instanceof Error ? error.message : '保存失败'), + }) + + const items = query.data?.data.items || [] + const total = query.data?.data.pagination.total || 0 + + const columns = [ + { title: '商品', dataIndex: 'canonicalName', ellipsis: true }, + { title: '编码', dataIndex: 'canonicalSku', width: 220 }, + { + title: '采购价', + width: 140, + render: (_: unknown, row: ProductCatalogItem) => + row.activeCostAmountFen === null || row.activeCostAmountFen === undefined ? ( + 未配价 + ) : ( + `¥${(row.activeCostAmountFen / 100).toFixed(2)}` + ), + }, + { + title: '生效时间', + dataIndex: 'activeCostEffectiveFrom', + width: 180, + render: (value?: string | null) => value || '—', + }, + { + title: '操作', + width: 110, + render: (_: unknown, row: ProductCatalogItem) => ( + + ), + }, + ] + + return ( + } onClick={() => void query.refetch()}> + 刷新 + + } + > + + 卡券渠道只统计:命中规则后不创建任务、不发领取页,只记收入与采购成本。 商品编码以{' '} + tpv: 开头。 + + + + setSelected(null)} + onOk={() => form.submit()} + confirmLoading={saveMutation.isPending} + > +
saveMutation.mutate(values)}> + + + + + + +
+
+
+ ) +} diff --git a/apps/frontend/src/pages/admin/cost-center/shared.ts b/apps/frontend/src/pages/admin/cost-center/shared.ts index a0ee25da..2d873990 100644 --- a/apps/frontend/src/pages/admin/cost-center/shared.ts +++ b/apps/frontend/src/pages/admin/cost-center/shared.ts @@ -16,8 +16,8 @@ import type { ProductSource, } from '@/types/admin' -/** 顶部来源切换:全部 / 自动发货 / 接单平台 / 人工。 */ -export type CostScope = 'all' | 'affiliate_dash' | 'work_order' | 'manual' +/** 顶部来源切换:全部 / 自动发货 / 接单平台 / 第三方卡券 / 人工。 */ +export type CostScope = 'all' | 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual' export type CostItemPreset = { key: string @@ -89,12 +89,14 @@ export const COST_MODEL_OPTIONS = [ export const PRODUCT_SOURCE_OPTIONS = [ { value: 'affiliate_dash', label: '自动发货' }, { value: 'work_order', label: '接单平台' }, + { value: 'third_party_voucher', label: '第三方卡券' }, { value: 'manual', label: '人工客服' }, ] export const PRODUCT_SOURCE_TAG_COLORS: Record = { affiliate_dash: 'green', work_order: 'blue', + third_party_voucher: 'cyan', manual: 'orange', '': 'default', } @@ -125,15 +127,21 @@ export type MappingPrefill = { export const FULFILLMENT_MODE_LABELS: Record = { work_order: '接单平台', affiliate_dash: '自动发货', - legacy_platform: '历史平台', + third_party_voucher: '第三方卡券', + legacy_platform: '历史归档', manual: '人工客服', + archived: '历史归档', + unclassified: '未分类', } export const FULFILLMENT_MODE_COLORS: Record = { work_order: 'blue', affiliate_dash: 'green', + third_party_voucher: 'cyan', legacy_platform: 'default', manual: 'orange', + archived: 'default', + unclassified: 'gold', } export const PROVIDER_LABELS: Record = { diff --git a/apps/frontend/src/types/admin/cost-center.ts b/apps/frontend/src/types/admin/cost-center.ts index be991a04..f44ddda8 100644 --- a/apps/frontend/src/types/admin/cost-center.ts +++ b/apps/frontend/src/types/admin/cost-center.ts @@ -1,11 +1,16 @@ /** - * 履约方式:唯一真源是 order_items.fulfillment_mode。 - * 核销码不参与履约判定。 + * 履约渠道:v2 唯一真源是 order_items.fulfillment_channel; + * legacy_platform 仅历史数据兜底显示。 */ -export type FulfillmentMode = 'work_order' | 'affiliate_dash' | 'legacy_platform' | 'manual' +export type FulfillmentMode = + | 'work_order' + | 'affiliate_dash' + | 'third_party_voucher' + | 'legacy_platform' + | 'manual' /** 商品来源:由 canonical_sku 前缀推导,空串表示未分类。 */ -export type ProductSource = 'affiliate_dash' | 'work_order' | 'manual' | '' +export type ProductSource = 'affiliate_dash' | 'work_order' | 'third_party_voucher' | 'manual' | '' /** * 成本模型:只表达「这笔成本怎么算」,与履约方式正交。