feat(finance): 积分钱包操作 + 客服「确认履约」闭环

把「配置 → 待办 → 确认履约 → 扣额度 → 写成本快照」整条链路打通,
客服点一下就完成履约,不需要填复杂的东西。

积分钱包(P1.3):
- platform-credit-service.ts:充值 / 调整 / 消耗 / 流水查询。
- 三平台积分互不通用,每客服每平台一个账户;余额只能由流水产生,
  消耗不足直接拒绝(409),不允许负余额,与备用金口径一致。
- 换算率按平台:南航、乐玩 1 积分 = 1 分,ROCK 1 积分 = 1 元。
- 充值 / 调整只对 admin/operator/finance 开放;客服只能看自己的钱包。
- 备用金现金不在这里,走现有 fund_* 链路。

确认履约(P1.4):
- manual-fulfillment-service.ts:待办列表 + 确认履约。
- 待办口径:order_items.fulfillment_mode='manual' 且已配 fulfillment_config_id。
  人工单**不建 fulfillment_tasks**——那是自动发货通道的体系,人工单历来不走它
  (本地 102,955 个人工单的任务行数为 0)。
- 「已履约」判定:该订单项存在有效 manual_case 成本快照。
- 固定成本(消耗积分 > 0)→ 零输入;成本不固定(= 0)→ 只填实际成本金额。
- 确认时:积分平台扣积分钱包、备用金现金走 writeFundExpense,
  随后写 actual 成本快照(主体 = 积分流水 id 或备用金支出单 id)。
- 渠道与消耗积分永远提前配好,客服不选;重复履约直接拒绝。

前端:
- 新增「人工履约」页(客服菜单):待确认 / 已履约 / 全部,确认履约弹窗。
- 备用金页新增「平台积分」Tab(客服端与管理端各一份):
  余额卡片、积分流水、管理员充值与调整。

E2E 实测:配置 ROCK 100 积分 → 成本 ¥100.00(1 积分 = 1 元);充值 5000 积分
= ¥5000;确认履约后积分 5000 → 4900,成本快照 actual ¥100.00 写入;
重复履约被拒绝。测试数据已清理,登记表保留 357 条扫描结果供运营使用。

校验:后端 765 项 / 0 失败,前端 143 项全通过(含 tsc -b 与 build)。

Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml2213
2026-10-24 13:21:34 +08:00
co-authored by Claude Opus 4.5
parent 4bcb4e6b57
commit d07d88e25c
12 changed files with 2221 additions and 0 deletions
+2
View File
@@ -23,6 +23,7 @@ const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage')
const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage'))
const AdminSalesProfitPage = lazy(() => import('@/pages/admin/AdminSalesProfitPage'))
const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage'))
const AdminManualFulfillmentPage = lazy(() => import('@/pages/admin/AdminManualFulfillmentPage'))
const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage'))
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
@@ -144,6 +145,7 @@ function AdminApplication() {
<Route path="orders/:orderId" element={<AdminOrderDetailPage />} />
<Route path="tasks" element={<AdminTasksPage />} />
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
<Route path="manual-fulfillment" element={<AdminManualFulfillmentPage />} />
</Route>
<Route element={<RequireRole roles={['admin', 'operator', 'finance']} />}>
<Route path="finance" element={<AdminFinancePage />} />
@@ -1,4 +1,5 @@
import {
CheckCircleOutlined,
AuditOutlined,
AccountBookOutlined,
ApartmentOutlined,
@@ -162,6 +163,11 @@ export default function AdminLayout() {
},
]),
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
{
key: '/admin/manual-fulfillment',
icon: <CheckCircleOutlined />,
label: '人工履约',
},
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '接单平台' },
{
key: '/admin/kuaishou-industry',
@@ -0,0 +1,340 @@
/**
* 人工履约待办页:客服确认履约。
*
* 口径(2026-10-24 确认):
* * 渠道与消耗积分在成本中心提前配好,客服不选、不填;
* * 固定成本 → 点「确认履约」零输入;
* * 成本不固定(消耗积分 = 0)→ 只填本次实际成本;
* * 确认后自动扣对应平台的积分并写成本快照。
*/
import { CheckOutlined, ReloadOutlined } from '@ant-design/icons'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
App,
Button,
Card,
Form,
Input,
InputNumber,
Modal,
Space,
Statistic,
Table,
Tag,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import PageHeader from '@/components/admin/PageHeader'
import { confirmAdminManualFulfillment, fetchAdminManualFulfillmentPending } from '@/services/admin'
import type { ManualFulfillmentPendingItem } from '@/types/admin'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import { formatAdminDateTime } from '@/utils/admin-time'
type ConfirmFormValues = {
costYuan?: number
payeeName?: string
note?: string
}
const PLATFORM_TAG_COLORS: Record<string, string> = {
nanhang: 'blue',
lewan: 'purple',
rock: 'cyan',
fund_cash: 'orange',
}
export default function AdminManualFulfillmentPage() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [form] = Form.useForm<ConfirmFormValues>()
const [status, setStatus] = useState<'pending' | 'fulfilled' | ''>('pending')
const [keyword, setKeyword] = useState('')
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [confirming, setConfirming] = useState<ManualFulfillmentPendingItem | null>(null)
const [showCostInput, setShowCostInput] = useState(false)
const listQuery = useQuery({
queryKey: ['admin-manual-fulfillment-pending', status, keyword, page, pageSize],
queryFn: () =>
fetchAdminManualFulfillmentPending({
status: status || undefined,
keyword: keyword || undefined,
page,
pageSize,
}),
})
const confirmMutation = useMutation({
mutationFn: (values: ConfirmFormValues) =>
confirmAdminManualFulfillment({
orderItemId: confirming?.orderItemId || 0,
costFen: values.costYuan === undefined ? undefined : Math.round(values.costYuan * 100),
payeeName: values.payeeName || '',
note: values.note || '',
}),
onSuccess: (response) => {
const result = response.data
message.success(
`履约已确认,扣 ${result.creditQuantity} 积分,成本 ¥${(result.costFen / 100).toFixed(2)}`,
)
setConfirming(null)
setShowCostInput(false)
void queryClient.invalidateQueries({
queryKey: ['admin-manual-fulfillment-pending'],
})
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-accounts'] })
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '确认履约失败')
},
})
function openConfirm(item: ManualFulfillmentPendingItem) {
setConfirming(item)
setShowCostInput(item.costFen === null)
form.resetFields()
}
const columns: TableColumnsType<ManualFulfillmentPendingItem> = [
{
title: '商品',
key: 'sku',
width: 260,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong ellipsis={{ tooltip: row.skuName }}>
{row.skuName || row.skuCode}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.internalSku || row.skuCode}
</Typography.Text>
</div>
),
},
{
title: '平台单号',
dataIndex: 'platformOrderId',
width: 170,
render: (value: string) => (
<Typography.Text copyable={{ text: value }} style={{ fontSize: 12 }}>
{value}
</Typography.Text>
),
},
{
title: '履约渠道',
key: 'platform',
width: 110,
render: (_, row) => (
<Tag color={PLATFORM_TAG_COLORS[row.platformKey] || 'default'}>{row.platformLabel}</Tag>
),
},
{
title: '消耗',
key: 'credit',
width: 120,
render: (_, row) =>
row.isFundCash ? (
<Typography.Text type="secondary">—</Typography.Text>
) : (
<Typography.Text>{row.creditQuantity} 积分</Typography.Text>
),
},
{
title: '成本',
key: 'cost',
width: 120,
align: 'right',
render: (_, row) =>
row.costFen === null ? (
<Tag color="orange">待填写</Tag>
) : (
<Typography.Text strong>¥{(row.costFen / 100).toFixed(2)}</Typography.Text>
),
},
{
title: '订单金额',
key: 'revenue',
width: 100,
align: 'right',
render: (_, row) => `¥${(row.revenueFen / 100).toFixed(2)}`,
},
{
title: '支付时间',
dataIndex: 'paidAt',
width: 150,
render: (value: string) => formatAdminDateTime(value),
},
{
title: '操作',
key: 'actions',
width: 110,
fixed: 'right',
render: (_, row) =>
row.fulfilled ? (
<Tag color="green">已履约</Tag>
) : (
<Button
type="primary"
size="small"
icon={<CheckOutlined />}
onClick={() => openConfirm(row)}
>
确认履约
</Button>
),
},
]
const total = listQuery.data?.data.pagination.total ?? 0
return (
<section>
<PageHeader
title="人工履约待办"
description="渠道与消耗积分已提前配好,确认即扣额度并计入成本。"
/>
<Alert
type="info"
showIcon
style={{ marginBottom: 14 }}
message="确认履约会从对应平台的积分钱包扣减,并写入成本快照"
description="固定成本的商品直接确认即可;成本不固定的商品需要填写本次实际成本。"
/>
<Space wrap style={{ marginBottom: 12 }}>
<Button icon={<ReloadOutlined />} onClick={() => void listQuery.refetch()}>
刷新
</Button>
<Button
type={status === 'pending' ? 'primary' : 'default'}
onClick={() => {
setStatus('pending')
setPage(1)
}}
>
待确认
</Button>
<Button
type={status === 'fulfilled' ? 'primary' : 'default'}
onClick={() => {
setStatus('fulfilled')
setPage(1)
}}
>
已履约
</Button>
<Button
type={status === '' ? 'primary' : 'default'}
onClick={() => {
setStatus('')
setPage(1)
}}
>
全部
</Button>
</Space>
<Card bordered={false}>
<Table
rowKey="orderItemId"
size="small"
columns={columns}
dataSource={listQuery.data?.data.items || []}
loading={listQuery.isLoading}
scroll={{ x: 1080 }}
pagination={buildAdminTablePagination({
page,
pageSize,
total,
onChange: (nextPage, nextPageSize) => {
setPage(nextPage)
if (nextPageSize !== pageSize) setPageSize(nextPageSize)
},
})}
/>
</Card>
<Modal
open={Boolean(confirming)}
title="确认履约"
okText="确认"
cancelText="取消"
confirmLoading={confirmMutation.isPending}
onOk={() => {
void form.validateFields().then((values) => confirmMutation.mutate(values))
}}
onCancel={() => {
setConfirming(null)
setShowCostInput(false)
}}
destroyOnHidden
width={520}
>
{confirming ? (
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<div>
<Typography.Text type="secondary">商品:</Typography.Text>
<Typography.Text strong>{confirming.skuName || confirming.skuCode}</Typography.Text>
</div>
<div>
<Typography.Text type="secondary">内部 SKU:</Typography.Text>
<Typography.Text copyable={{ text: confirming.internalSku }}>
{confirming.internalSku}
</Typography.Text>
</div>
<div>
<Typography.Text type="secondary">履约渠道:</Typography.Text>
<Tag color={PLATFORM_TAG_COLORS[confirming.platformKey] || 'default'}>
{confirming.platformLabel}
</Tag>
{!confirming.isFundCash ? (
<Typography.Text>消耗 {confirming.creditQuantity} 积分</Typography.Text>
) : null}
</div>
{showCostInput ? (
<Alert type="warning" showIcon message="该商品成本不固定,请填写本次实际成本" />
) : (
<Statistic
title="本次成本"
value={(confirming.costFen || 0) / 100}
precision={2}
prefix="¥"
/>
)}
</Space>
) : null}
<Form<ConfirmFormValues> form={form} layout="vertical">
{showCostInput ? (
<Form.Item
name="costYuan"
label="实际成本(元)"
rules={[{ required: true, message: '请填写实际成本' }]}
>
<InputNumber
min={0.01}
precision={2}
style={{ width: '100%' }}
placeholder="例如 12.50"
prefix="¥"
/>
</Form.Item>
) : null}
<Form.Item name="payeeName" label="收款方 / 处理人">
<Input placeholder="例如:客服小王、上游商家" maxLength={120} />
</Form.Item>
<Form.Item name="note" label="备注">
<Input.TextArea rows={2} maxLength={300} showCount />
</Form.Item>
</Form>
</Modal>
</section>
)
}
@@ -34,6 +34,7 @@ import dayjs from 'dayjs'
import type { Dayjs } from 'dayjs'
import { useState } from 'react'
import { PlatformCreditPanel } from './PlatformCreditPanel'
import ImagePreviewList from '@/components/files/ImagePreviewList'
import ImageUpload from '@/components/files/ImageUpload'
import PageHeader from '@/components/admin/PageHeader'
@@ -1073,6 +1074,11 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
</Card>
),
},
{
key: 'platform-credits',
label: '平台积分',
children: <PlatformCreditPanel />,
},
{
key: 'expenses',
label: '支出明细',
@@ -43,6 +43,7 @@ import type {
} from '@/types/admin'
import type { UploadedFile } from '@/types/worker-platform'
import { formatAdminDateTime } from '@/utils/admin-time'
import { PlatformCreditPanel } from './PlatformCreditPanel'
import {
FUND_ACCOUNT_STATUS_LABELS,
FUND_CATEGORY_LABELS,
@@ -409,6 +410,11 @@ export default function FundSupportView() {
/>
),
},
{
key: 'platform-credits',
label: '平台积分',
children: <PlatformCreditPanel />,
},
]}
/>
</Card>
@@ -0,0 +1,395 @@
/**
* 平台积分钱包面板:南航 / 乐玩 / ROCK 三平台的客服积分。
*
* 口径(2026-10-24 确认):
* * 三平台积分**互不通用**,每客服每平台一个钱包;
* * 换算率按平台不同:南航、乐玩 1 积分 = 1 分,ROCK 1 积分 = 1 元;
* * 余额只能由流水产生,不允许手工改;消耗不足直接拒绝。
*
* 客服看自己的积分余额与流水;管理员/财务可以充值、调整、看全部。
*/
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
Alert,
App,
Button,
Card,
Col,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
Space,
Statistic,
Table,
Tag,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import {
adjustAdminPlatformCredit,
fetchAdminPlatformCreditAccounts,
fetchAdminPlatformCreditEntries,
grantAdminPlatformCredit,
} from '@/services/admin'
import type { PlatformCreditAccount, PlatformCreditEntry } from '@/types/admin'
import { hasAdminPermission } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
const PLATFORM_TAG_COLORS: Record<string, string> = {
nanhang: 'blue',
lewan: 'purple',
rock: 'cyan',
}
const ENTRY_TYPE_LABELS: Record<string, string> = {
grant: '充值',
consume: '消耗',
adjust: '调整',
return: '退回',
}
const ENTRY_TYPE_COLORS: Record<string, string> = {
grant: 'green',
consume: 'red',
adjust: 'gold',
return: 'orange',
}
type GrantFormValues = {
ownerAdminUserId: number
platformKey: string
quantity: number
platformRef?: string
note?: string
}
type AdjustFormValues = {
ownerAdminUserId: number
platformKey: string
quantity: number
note: string
}
export function PlatformCreditPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [grantForm] = Form.useForm<GrantFormValues>()
const [adjustForm] = Form.useForm<AdjustFormValues>()
const [grantOpen, setGrantOpen] = useState(false)
const [adjustOpen, setAdjustOpen] = useState(false)
const [platformFilter, setPlatformFilter] = useState('')
const canManage = hasAdminPermission('finance.config')
const accountsQuery = useQuery({
queryKey: ['admin-platform-credit-accounts'],
queryFn: fetchAdminPlatformCreditAccounts,
})
const entriesQuery = useQuery({
queryKey: ['admin-platform-credit-entries', platformFilter],
queryFn: () =>
fetchAdminPlatformCreditEntries({
platformKey: platformFilter || undefined,
pageSize: 50,
}),
})
const grantMutation = useMutation({
mutationFn: (values: GrantFormValues) =>
grantAdminPlatformCredit({
ownerAdminUserId: values.ownerAdminUserId,
platformKey: values.platformKey,
quantity: values.quantity,
platformRef: values.platformRef || '',
note: values.note || '',
}),
onSuccess: (response) => {
const result = response.data
message.success(
`已充值 ${result.quantity} 积分,折合 ¥${(result.amountFen / 100).toFixed(2)}`,
)
setGrantOpen(false)
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-accounts'] })
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-entries'] })
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '充值失败')
},
})
const adjustMutation = useMutation({
mutationFn: (values: AdjustFormValues) =>
adjustAdminPlatformCredit({
ownerAdminUserId: values.ownerAdminUserId,
platformKey: values.platformKey,
quantity: values.quantity,
note: values.note || '',
}),
onSuccess: (response) => {
const result = response.data
message.success(`已调整,余额 ${result.balanceAfter} 积分`)
setAdjustOpen(false)
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-accounts'] })
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-entries'] })
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '调整失败')
},
})
const accounts = accountsQuery.data?.data.items || []
const platforms = accountsQuery.data?.data.platforms || []
const entries = entriesQuery.data?.data.items || []
const entryColumns: TableColumnsType<PlatformCreditEntry> = [
{
title: '客服',
dataIndex: 'ownerName',
width: 110,
render: (value: string) => value || '-',
},
{
title: '平台',
dataIndex: 'platformKey',
width: 100,
render: (value: string, row) => (
<Tag color={PLATFORM_TAG_COLORS[value] || 'default'}>{row.platformLabel}</Tag>
),
},
{
title: '类型',
dataIndex: 'entryType',
width: 80,
render: (value: string) => (
<Tag color={ENTRY_TYPE_COLORS[value] || 'default'}>{ENTRY_TYPE_LABELS[value] || value}</Tag>
),
},
{
title: '变动',
dataIndex: 'quantityDelta',
width: 100,
align: 'right',
render: (value: number) =>
value > 0 ? (
<Typography.Text style={{ color: '#389e0d' }}>+{value}</Typography.Text>
) : (
<Typography.Text style={{ color: '#cf1322' }}>{value}</Typography.Text>
),
},
{
title: '余额',
dataIndex: 'balanceAfter',
width: 100,
align: 'right',
},
{
title: '金额',
dataIndex: 'amountFen',
width: 110,
align: 'right',
render: (value: number) => `¥${(Number(value || 0) / 100).toFixed(2)}`,
},
{
title: '备注',
dataIndex: 'note',
render: (value: string) => value || '-',
},
{
title: '时间',
dataIndex: 'createdAt',
width: 150,
render: (value: string) => formatAdminDateTime(value),
},
]
return (
<Card bordered={false} title="平台积分钱包">
<Alert
type="info"
showIcon
style={{ marginBottom: 14 }}
message="南航 / 乐玩 / ROCK 三平台积分互不通用,每平台一个钱包"
description="换算率按平台:南航、乐玩 1 积分 = 1 分;ROCK 1 积分 = 1 元。余额只能由流水产生,消耗不足会直接拒绝,不允许负余额。备用金现金不在这里,走「我的备用金」。"
/>
<Row gutter={12} style={{ marginBottom: 12 }}>
{platforms.map((platform) => {
const total = accounts
.filter((item) => item.platformKey === platform.key)
.reduce((sum, item) => sum + item.balanceQuantity, 0)
return (
<Col span={8} key={platform.key}>
<Card size="small">
<Statistic
title={`${platform.label}总积分`}
value={total}
suffix="积分"
prefix={
<Tag color={PLATFORM_TAG_COLORS[platform.key] || 'default'}>
1积分={platform.creditUnitPriceFen >= 100 ? '1元' : '1分'}
</Tag>
}
/>
</Card>
</Col>
)
})}
</Row>
<Space wrap style={{ marginBottom: 12 }}>
{canManage ? (
<>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
grantForm.resetFields()
setGrantOpen(true)
}}
>
充值积分
</Button>
<Button
onClick={() => {
adjustForm.resetFields()
setAdjustOpen(true)
}}
>
调整余额
</Button>
</>
) : null}
<Select
value={platformFilter}
allowClear
placeholder="全部平台"
style={{ width: 140 }}
onChange={(value) => setPlatformFilter(value || '')}
options={platforms.map((item) => ({ value: item.key, label: item.label }))}
/>
<Button
icon={<ReloadOutlined />}
onClick={() => {
void accountsQuery.refetch()
void entriesQuery.refetch()
}}
>
刷新
</Button>
</Space>
<Table
rowKey="id"
size="small"
columns={entryColumns}
dataSource={entries}
loading={entriesQuery.isLoading}
scroll={{ x: 980 }}
pagination={false}
/>
<Modal
open={grantOpen}
title="充值积分"
okText="确认充值"
cancelText="取消"
confirmLoading={grantMutation.isPending}
onOk={() => {
void grantForm.validateFields().then((values) => grantMutation.mutate(values))
}}
onCancel={() => setGrantOpen(false)}
destroyOnHidden
width={480}
>
<Form<GrantFormValues> form={grantForm} layout="vertical">
<Form.Item
name="ownerAdminUserId"
label="客服"
rules={[{ required: true, message: '请选择客服' }]}
extra="客服 ID(管理员用户 id)"
>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="platformKey"
label="平台"
rules={[{ required: true, message: '请选择平台' }]}
>
<Select
options={platforms.map((item) => ({
value: item.key,
label: `${item.label}(1积分=${item.creditUnitPriceFen >= 100 ? '1元' : '1分'})`,
}))}
/>
</Form.Item>
<Form.Item
name="quantity"
label="积分数"
rules={[{ required: true, message: '请输入积分数' }]}
>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="platformRef" label="平台流水号">
<Input placeholder="平台侧充值流水号" maxLength={100} />
</Form.Item>
<Form.Item name="note" label="备注">
<Input.TextArea rows={2} maxLength={300} showCount />
</Form.Item>
</Form>
</Modal>
<Modal
open={adjustOpen}
title="调整积分余额"
okText="确认调整"
cancelText="取消"
confirmLoading={adjustMutation.isPending}
onOk={() => {
void adjustForm.validateFields().then((values) => adjustMutation.mutate(values))
}}
onCancel={() => setAdjustOpen(false)}
destroyOnHidden
width={480}
>
<Form<AdjustFormValues> form={adjustForm} layout="vertical">
<Form.Item
name="ownerAdminUserId"
label="客服"
rules={[{ required: true, message: '请选择客服' }]}
>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="platformKey"
label="平台"
rules={[{ required: true, message: '请选择平台' }]}
>
<Select options={platforms.map((item) => ({ value: item.key, label: item.label }))} />
</Form.Item>
<Form.Item
name="quantity"
label="调整积分数"
rules={[{ required: true, message: '请输入调整值' }]}
extra="正数增加,负数扣减;调整后余额不能为负。"
>
<InputNumber precision={0} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name="note"
label="调整原因"
rules={[{ required: true, message: '请填写调整原因' }]}
>
<Input.TextArea rows={2} maxLength={300} showCount />
</Form.Item>
</Form>
</Modal>
</Card>
)
}
@@ -12,7 +12,11 @@ import type {
CostVersion,
CostVersionListResult,
ManualFulfillmentConfig,
ManualFulfillmentConfirmResult,
ManualFulfillmentPendingListResult,
ManualPlatformOption,
PlatformCreditAccountListResult,
PlatformCreditEntryListResult,
ManualRematchResult,
ManualSkuListResult,
ManualSkuOverview,
@@ -283,3 +287,87 @@ export function deleteAdminManualConfig(skuKey: string) {
export function rematchAdminManualFulfillments(payload: { skuKey?: string } = {}) {
return apiPost<ManualRematchResult>('/api/v1/admin/cost-center/manual-configs/rematch', payload)
}
/**
* 客服平台积分钱包(南航 / 乐玩 / ROCK)。
* 三平台积分互不通用,每客服每平台一个账户;备用金现金走备用金页。
*/
export function fetchAdminPlatformCreditAccounts() {
return apiGet<PlatformCreditAccountListResult>(
'/api/v1/admin/cost-center/platform-credits/accounts',
)
}
export function fetchAdminPlatformCreditEntries(
params: {
page?: number
pageSize?: number
platformKey?: string
entryType?: string
keyword?: string
} = {},
) {
return apiGet<PlatformCreditEntryListResult>(
'/api/v1/admin/cost-center/platform-credits/entries',
params,
)
}
export function grantAdminPlatformCredit(payload: {
ownerAdminUserId: number
platformKey: string
quantity: number
platformRef?: string
note?: string
}) {
return apiPost<{
accountId: number
platformKey: string
quantity: number
balanceAfter: number
amountFen: number
entryId: number
}>('/api/v1/admin/cost-center/platform-credits/grant', payload)
}
export function adjustAdminPlatformCredit(payload: {
ownerAdminUserId: number
platformKey: string
quantity: number
note?: string
}) {
return apiPost<{
accountId: number
platformKey: string
quantity: number
balanceAfter: number
amountFen: number
entryId: number
}>('/api/v1/admin/cost-center/platform-credits/adjust', payload)
}
/**
* 人工履约待办与确认履约。
* 客服点「确认履约」即扣额度 + 写成本快照;固定成本零输入,
* 成本不固定时只填金额。
*/
export function fetchAdminManualFulfillmentPending(
params: { page?: number; pageSize?: number; keyword?: string; status?: string } = {},
) {
return apiGet<ManualFulfillmentPendingListResult>(
'/api/v1/admin/finance/manual-fulfillment/pending',
params,
)
}
export function confirmAdminManualFulfillment(payload: {
orderItemId: number
costFen?: number
payeeName?: string
note?: string
}) {
return apiPost<ManualFulfillmentConfirmResult>(
'/api/v1/admin/finance/manual-fulfillment/confirm',
payload,
)
}
@@ -389,3 +389,95 @@ export interface ManualRematchResult {
updatedCount: number
skuKey: string
}
/** 积分平台:南航 / 乐玩 / ROCK,三平台积分互不通用(不含备用金现金)。 */
export type PlatformCreditKey = 'nanhang' | 'lewan' | 'rock'
/** 客服平台积分钱包(南航 / 乐玩 / ROCK,三平台积分互不通用)。 */
export interface PlatformCreditAccount {
id: number
platformKey: PlatformCreditKey
platformLabel: string
ownerAdminUserId: number
ownerName: string
balanceQuantity: number
balanceAmountFen: number
unitName: string
creditUnitPriceFen: number
status: 'active' | 'frozen' | 'closed'
createdAt: string
updatedAt: string
}
export interface PlatformCreditAccountListResult {
items: PlatformCreditAccount[]
platforms: Array<{
key: PlatformCreditKey
label: string
creditUnitPriceFen: number
}>
}
export interface PlatformCreditEntry {
id: number
accountId: number
platformKey: PlatformCreditKey
platformLabel: string
ownerName: string
entryType: 'grant' | 'consume' | 'adjust' | 'return'
quantityDelta: number
balanceAfter: number
amountFen: number
platformRef: string
relatedTaskId: number | null
note: string
createdAt: string
}
export interface PlatformCreditEntryListResult {
items: PlatformCreditEntry[]
pagination: { page: number; pageSize: number; total: number }
}
/** 人工履约待办:客服确认履约用。 */
export interface ManualFulfillmentPendingItem {
orderItemId: number
orderId: number
platformOrderId: string
provider: string
shopId: string
skuCode: string
skuName: string
internalSku: string
revenueFen: number
paidAt: string
configId: number
platformKey: PlatformCreditKey | 'fund_cash'
platformLabel: string
isFundCash: boolean
creditQuantity: number
creditUnitPriceFen: number
costMode: ManualCostMode
/** 固定成本的金额;成本不固定时为 null,客服要手填。 */
costFen: number | null
fulfilled: boolean
fulfilledAt: string | null
}
export interface ManualFulfillmentPendingListResult {
items: ManualFulfillmentPendingItem[]
pagination: { page: number; pageSize: number; total: number }
}
export interface ManualFulfillmentConfirmResult {
orderItemId: number
orderId: number
costSnapshotId: number
platformKey: string
platformLabel: string
creditQuantity: number
costFen: number
fundExpenseId: number | null
creditEntryId: number | null
balanceAfter: number | null
}