feat(cost-center): 第三方卡券渠道归一与采购价配置
- 归一新增 tpv:<SKU> 直达,卡券单无需手工映射 - 成本配置/总览/异常支持 third_party_voucher 与 archived 维度 - 新增卡券采购价面板,写入 sys_third_party_voucher_cost - 修正 man: 商品改价误用不存在的 sys_manual_cost
This commit is contained in:
@@ -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<string, string> = {
|
||||
work_order: '接单平台',
|
||||
affiliate_dash: '自动发货',
|
||||
legacy_platform: '历史平台',
|
||||
third_party_voucher: '第三方卡券',
|
||||
legacy_platform: '历史归档',
|
||||
manual: '人工客服',
|
||||
archived: '历史归档',
|
||||
unclassified: '未分类',
|
||||
}
|
||||
|
||||
const COST_STAGE_LABEL: Record<string, { label: string; color: string }> = {
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
* 三个来源的成本形态完全不同,所以这里不做统一表格,而是按来源切换:
|
||||
* 自动发货 → 上游 SKU 采购价(按 aff:<skuCode> 归一)
|
||||
* 接单平台 → 接单模板发单金额(只读核对,改价去接单模板页)
|
||||
* 第三方卡券 → 卡券商品采购价(按 tpv:<sku> 归一,只统计)
|
||||
* 人工客服 → 内部 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:<SKU> 配置卡券采购价;该渠道只统计收入与成本,不产生履约动作。',
|
||||
},
|
||||
{
|
||||
key: 'manual',
|
||||
title: '人工客服 SKU 消耗',
|
||||
@@ -44,6 +51,7 @@ export default function CostConfigPanel() {
|
||||
|
||||
if (scope === 'affiliate_dash') return <AffiliateSkuPanel />
|
||||
if (scope === 'work_order') return <WorkOrderPricePanel />
|
||||
if (scope === 'third_party_voucher') return <VoucherSkuPanel />
|
||||
if (scope === 'manual') return <ManualSkuConfigPanel />
|
||||
|
||||
return (
|
||||
|
||||
@@ -52,8 +52,11 @@ const FULFILLMENT_MODE_LABELS: Record<string, string> = {
|
||||
}
|
||||
|
||||
const REASON_LABELS: Record<string, string> = {
|
||||
classification_missing: '待分类',
|
||||
routing_conflict: '渠道规则冲突',
|
||||
product_mapping_missing: '商品未归一',
|
||||
cost_missing: '成本缺失',
|
||||
manual_config_missing: '人工 SKU 未配置',
|
||||
manual_cost_missing: '人工成本未记录',
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 第三方卡券采购价:只统计渠道,不产生履约动作。
|
||||
*
|
||||
* 商品编码固定为 `tpv:<sku>`;成本写入 `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<ProductCatalogItem | null>(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 ? (
|
||||
<Tag color="orange">未配价</Tag>
|
||||
) : (
|
||||
`¥${(row.activeCostAmountFen / 100).toFixed(2)}`
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '生效时间',
|
||||
dataIndex: 'activeCostEffectiveFrom',
|
||||
width: 180,
|
||||
render: (value?: string | null) => value || '—',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 110,
|
||||
render: (_: unknown, row: ProductCatalogItem) => (
|
||||
<Button
|
||||
size="small"
|
||||
disabled={!canConfig}
|
||||
onClick={() => {
|
||||
setSelected(row)
|
||||
form.setFieldsValue({
|
||||
amountFen:
|
||||
row.activeCostAmountFen === null || row.activeCostAmountFen === undefined
|
||||
? 0
|
||||
: Number((row.activeCostAmountFen / 100).toFixed(2)),
|
||||
changeReason: '第三方卡券采购价调整',
|
||||
})
|
||||
}}
|
||||
>
|
||||
改价
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={`卡券商品采购价(${total})`}
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">
|
||||
卡券渠道只统计:命中规则后不创建任务、不发领取页,只记收入与采购成本。 商品编码以{' '}
|
||||
<Typography.Text code>tpv:</Typography.Text> 开头。
|
||||
</Typography.Paragraph>
|
||||
<PercentTable
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={query.isFetching}
|
||||
dataSource={items}
|
||||
columns={columns}
|
||||
pagination={{ pageSize: 20, showSizeChanger: false }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={`设置采购价:${selected?.canonicalName || ''}`}
|
||||
open={Boolean(selected)}
|
||||
onCancel={() => setSelected(null)}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={saveMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={(values) => saveMutation.mutate(values)}>
|
||||
<Form.Item
|
||||
name="amountFen"
|
||||
label="采购价(元)"
|
||||
rules={[{ required: true, message: '请输入采购价' }]}
|
||||
>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="changeReason" label="变更原因">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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<ProductSource, string> = {
|
||||
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<string, string> = {
|
||||
work_order: '接单平台',
|
||||
affiliate_dash: '自动发货',
|
||||
legacy_platform: '历史平台',
|
||||
third_party_voucher: '第三方卡券',
|
||||
legacy_platform: '历史归档',
|
||||
manual: '人工客服',
|
||||
archived: '历史归档',
|
||||
unclassified: '未分类',
|
||||
}
|
||||
|
||||
export const FULFILLMENT_MODE_COLORS: Record<string, string> = {
|
||||
work_order: 'blue',
|
||||
affiliate_dash: 'green',
|
||||
third_party_voucher: 'cyan',
|
||||
legacy_platform: 'default',
|
||||
manual: 'orange',
|
||||
archived: 'default',
|
||||
unclassified: 'gold',
|
||||
}
|
||||
|
||||
export const PROVIDER_LABELS: Record<string, string> = {
|
||||
|
||||
@@ -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' | ''
|
||||
|
||||
/**
|
||||
* 成本模型:只表达「这笔成本怎么算」,与履约方式正交。
|
||||
|
||||
Reference in New Issue
Block a user