feat(finance): 完善客服钱包与人工履约成本流水

This commit is contained in:
yml2213
2026-10-24 14:34:28 +08:00
parent f0f8c92e67
commit a96229a106
11 changed files with 359 additions and 78 deletions
@@ -419,8 +419,8 @@ export async function listPlatformCreditOwners() {
SELECT
au.id, au.username, COALESCE(NULLIF(au.display_name, ''), au.username) AS display_name
FROM admin_users au
WHERE au.status = 'active'
ORDER BY au.id
WHERE au.status = 'active' AND au.role = 'support'
ORDER BY display_name, au.id
`,
)
return {
@@ -0,0 +1,63 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
buildManualFulfillmentNote,
buildManualFulfillmentReference,
} from './manual-fulfillment-note.js'
test('人工履约编号同时带工单号与平台单号', () => {
assert.equal(
buildManualFulfillmentReference({
workOrderNo: 'WK26102414033739',
platformOrderId: '2628501763808879',
orderItemId: 10,
}),
'工单 WK26102414033739 · 平台单号 2628501763808879',
)
})
test('人工履约编号在没有工单号时回退平台单号', () => {
assert.equal(
buildManualFulfillmentReference({
workOrderNo: null,
platformOrderId: '2628501763808879',
orderItemId: 10,
}),
'平台单号 2628501763808879',
)
})
test('人工履约编号完全没有单号时回退订单项', () => {
assert.equal(
buildManualFulfillmentReference({
workOrderNo: null,
platformOrderId: '',
orderItemId: 10,
}),
'订单项 #10',
)
})
test('人工履约备注包含商品、编号与客服备注并按上限截断', () => {
const note = buildManualFulfillmentNote({
skuName: '幸运币2个',
reference: '工单 WK26102414033739 · 平台单号 2628501763808879',
note: '已发放',
maxLength: 300,
})
assert.equal(
note,
'人工履约 幸运币2个 · 工单 WK26102414033739 · 平台单号 2628501763808879 · 已发放',
)
assert.equal(
buildManualFulfillmentNote({
skuName: '幸运币2个',
reference: '工单 WK26102414033739',
note: '',
maxLength: 12,
}),
'人工履约 幸运币2个 ·',
)
})
@@ -0,0 +1,23 @@
/** 人工履约流水备注:商品、可追溯编号与客服备注。 */
export function buildManualFulfillmentReference(input: {
workOrderNo: string | null
platformOrderId: string
orderItemId: number
}) {
const parts: string[] = []
if (input.workOrderNo) parts.push(`工单 ${input.workOrderNo}`)
if (input.platformOrderId) parts.push(`平台单号 ${input.platformOrderId}`)
if (parts.length === 0) parts.push(`订单项 #${input.orderItemId}`)
return parts.join(' · ')
}
export function buildManualFulfillmentNote(input: {
skuName: string
reference: string
note: string
maxLength: number
}) {
const parts = [`人工履约 ${input.skuName}`.trim(), input.reference]
if (input.note) parts.push(input.note)
return parts.filter(Boolean).join(' · ').slice(0, input.maxLength)
}
@@ -3,6 +3,10 @@ import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
import type { AdminSession } from '../admin/admin-auth-service.js'
import {
buildManualFulfillmentNote,
buildManualFulfillmentReference,
} from './manual-fulfillment-note.js'
import {
consumePlatformCredit,
type PlatformCreditKey,
@@ -175,6 +179,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
cost_mode: string
credit_unit_price_fen: string
work_order_id: number | null
work_order_no: string | null
work_product_rule_id: number | null
fulfilled_at: string | null
}>(
@@ -203,6 +208,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
COALESCE(cfg.cost_mode, 'variable') AS cost_mode,
COALESCE((fp.config_json->>'rechargeUnitPriceFen')::bigint, 1) AS credit_unit_price_fen,
wo.id AS work_order_id,
wo.work_order_no,
wo.product_rule_id AS work_product_rule_id,
fulfilled.fulfilled_at
FROM order_items oi
@@ -210,7 +216,7 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
LEFT JOIN manual_fulfillment_configs cfg ON cfg.id = oi.fulfillment_config_id
LEFT JOIN fulfillment_profiles fp ON fp.profile_key = 'manual_' || cfg.platform_key
LEFT JOIN LATERAL (
SELECT id, product_rule_id FROM work_orders
SELECT id, product_rule_id, work_order_no FROM work_orders
WHERE order_item_id = oi.id ORDER BY id DESC LIMIT 1
) wo ON TRUE
LEFT JOIN fulfilled ON fulfilled.order_item_id = oi.id
@@ -267,6 +273,11 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
const note = optionalText(payload.note, 300)
const payeeName = optionalText(payload.payeeName ?? payload.payee_name, 120)
const fulfillmentReference = buildManualFulfillmentReference({
workOrderNo: row.work_order_no,
platformOrderId: row.platform_order_id,
orderItemId,
})
// 1. 先扣额度(积分平台走积分钱包;备用金现金走现有 fund_* 链路)。
let creditEntryId: number | null = null
@@ -288,7 +299,12 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
orderItemId,
paidAt: now,
evidence: [],
note: buildFundNote(row.sku_name, note),
note: buildManualFulfillmentNote({
skuName: row.sku_name,
reference: fulfillmentReference,
note,
maxLength: 500,
}),
actorUserId: session.userId,
})
})
@@ -300,7 +316,13 @@ export async function confirmManualFulfillment(payload: JsonObject, session: Adm
platformKey: platformKey as PlatformCreditKey,
quantity: creditQuantity,
relatedTaskId: null,
note: buildCreditNote(row.sku_name, note),
platformRef: row.work_order_no || row.platform_order_id,
note: buildManualFulfillmentNote({
skuName: row.sku_name,
reference: fulfillmentReference,
note,
maxLength: 300,
}),
})
creditEntryId = consumed.entryId
balanceAfter = consumed.balanceAfter
@@ -500,18 +522,6 @@ function mapPending(row: PendingRow) {
}
}
function buildFundNote(skuName: string, note: string) {
const parts = [`人工履约 ${skuName}`.trim()]
if (note) parts.push(note)
return parts.join(' · ').slice(0, 500)
}
function buildCreditNote(skuName: string, note: string) {
const parts = [`人工履约 ${skuName}`.trim()]
if (note) parts.push(note)
return parts.join(' · ').slice(0, 300)
}
function normalizeCostFen(value: unknown) {
const parsed = Number(value)
if (!Number.isFinite(parsed) || parsed < 0) {
+2 -2
View File
@@ -136,7 +136,7 @@ export default function AdminLayout() {
{ key: '/admin/finance', icon: <AccountBookOutlined />, label: '财务' },
{ key: '/admin/sales-profit', icon: <LineChartOutlined />, label: '销售毛利' },
{ key: '/admin/cost-center', icon: <ApartmentOutlined />, label: '商品与成本' },
{ key: '/admin/fund', icon: <WalletOutlined />, label: '备用金' },
{ key: '/admin/fund', icon: <WalletOutlined />, label: '钱包管理' },
{ key: '/admin/worker-platform', icon: <TrophyOutlined />, label: '资金审核' },
]
: [
@@ -145,7 +145,7 @@ export default function AdminLayout() {
{
key: '/admin/fund',
icon: <WalletOutlined />,
label: role === 'support' ? '我的钱包' : '备用金',
label: role === 'support' ? '我的钱包' : '钱包管理',
},
...(role === 'support'
? []
@@ -87,7 +87,10 @@ export default function AdminManualFulfillmentPage() {
void queryClient.invalidateQueries({
queryKey: ['admin-manual-fulfillment-pending'],
})
void queryClient.invalidateQueries({ queryKey: ['admin-fund-my'] })
void queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] })
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 : '确认履约失败')
@@ -156,6 +156,7 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
const canIssue = role === 'admin' || role === 'operator'
const canReconcile = role === 'admin' || role === 'operator' || role === 'finance'
const canExpense = role === 'admin'
const canManageCredits = role === 'admin' || role === 'finance'
const [activeTab, setActiveTab] = useState('overview')
const [submitting, setSubmitting] = useState(false)
@@ -875,8 +876,8 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
return (
<section className="page-stack">
<PageHeader
title="备用金"
description="主管资金池、客服备用金、支出与对账;余额由流水自动计算。"
title="钱包管理"
description="统管客服的南航 / 乐玩 / ROCK 积分钱包与备用金现金;可分配积分、发放现金、查看流水与对账。"
extra={
<Button icon={<ReloadOutlined />} loading={overviewQuery.isFetching} onClick={refreshAll}>
刷新
@@ -902,7 +903,7 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
items={[
{
key: 'overview',
label: '总览',
label: '钱包总览',
children: (
<Space direction="vertical" size={16} style={{ width: '100%' }}>
<Row gutter={[16, 16]}>
@@ -997,14 +998,25 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
发放备用金
</Button>
) : null}
{canManageCredits ? (
<Button
icon={<WalletOutlined />}
onClick={() => setActiveTab('platform-credits')}
>
分配客服积分
</Button>
) : null}
{canExpense ? (
<Button icon={<PlusOutlined />} onClick={() => openExpense()}>
登记支出
</Button>
) : null}
{canReconcile ? (
<Button icon={<CheckCircleOutlined />} onClick={() => openReconcile()}>
发起对账
<Button
icon={<CheckCircleOutlined />}
onClick={() => setActiveTab('reconciliations')}
>
对账中心
</Button>
) : null}
{isAdmin ? <Button onClick={() => openFunding()}>公司拨付</Button> : null}
@@ -1024,7 +1036,7 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
},
{
key: 'agents',
label: '客服余额',
label: '客服现金钱包',
children: (
<Card
title="客服备用金账户"
@@ -1050,7 +1062,7 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
},
{
key: 'supervisors',
label: '主管资金池',
label: '现金资金池',
children: (
<Card
title="主管资金池"
@@ -1076,12 +1088,12 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
},
{
key: 'platform-credits',
label: '平台积分',
label: '客服积分钱包',
children: <PlatformCreditPanel />,
},
{
key: 'expenses',
label: '支出明细',
label: '现金支出',
children: (
<Card
title="备用金支出"
@@ -1149,10 +1161,10 @@ export default function FundManagementView({ role }: { role: ManagementRole }) {
},
{
key: 'reconciliations',
label: '对账',
label: '对账中心',
children: (
<Card
title="备用金对账"
title="现金对账"
extra={
canReconcile ? (
<Button type="primary" onClick={() => openReconcile()}>
@@ -96,13 +96,18 @@ export default function FundSupportView() {
const entries = data?.entries || []
const expenses = data?.expenses || []
// 全局 staleTime=Infinity:钱包页每次进入都重新拉取,停留期间也轮询最新流水。
const creditAccountsQuery = useQuery({
queryKey: ['admin-platform-credit-accounts'],
queryFn: fetchAdminPlatformCreditAccounts,
refetchOnMount: 'always',
refetchInterval: 15_000,
})
const creditEntriesQuery = useQuery({
queryKey: ['admin-platform-credit-entries', 'mine'],
queryFn: () => fetchAdminPlatformCreditEntries({ pageSize: 50 }),
refetchOnMount: 'always',
refetchInterval: 15_000,
})
const creditAccounts = creditAccountsQuery.data?.data.items || []
const creditEntries = creditEntriesQuery.data?.data.items || []
@@ -247,7 +252,8 @@ export default function FundSupportView() {
{
title: '平台',
dataIndex: 'platformKey',
width: 100,
width: 110,
align: 'center',
render: (value: string, row) => (
<Tag color={value === 'rock' ? 'cyan' : value === 'lewan' ? 'purple' : 'blue'}>
{row.platformLabel}
@@ -257,7 +263,8 @@ export default function FundSupportView() {
{
title: '类型',
dataIndex: 'entryType',
width: 80,
width: 88,
align: 'center',
render: (value: string) => (
<Tag color={value === 'grant' ? 'green' : value === 'consume' ? 'red' : 'gold'}>
{value === 'grant'
@@ -273,8 +280,8 @@ export default function FundSupportView() {
{
title: '变动',
dataIndex: 'quantityDelta',
width: 100,
align: 'right',
width: 110,
align: 'center',
render: (value: number) =>
value > 0 ? (
<Typography.Text style={{ color: '#389e0d' }}>+{value}</Typography.Text>
@@ -285,26 +292,32 @@ export default function FundSupportView() {
{
title: '余额',
dataIndex: 'balanceAfter',
width: 100,
align: 'right',
width: 120,
align: 'center',
},
{
title: '折合金额',
dataIndex: 'amountFen',
width: 110,
align: 'right',
width: 120,
align: 'center',
render: (value: number) => `¥${(Number(value || 0) / 100).toFixed(2)}`,
},
{
title: '备注',
dataIndex: 'note',
width: 320,
align: 'center',
ellipsis: true,
render: (value: string) => value || '-',
},
{
title: '时间',
dataIndex: 'createdAt',
width: 150,
render: (value: string) => formatAdminDateTime(value),
width: 180,
align: 'center',
render: (value: string) => (
<span style={{ whiteSpace: 'nowrap' }}>{formatAdminDateTime(value)}</span>
),
},
]
@@ -378,7 +391,9 @@ export default function FundSupportView() {
extra={
<Button
icon={<ReloadOutlined />}
loading={myQuery.isFetching || creditAccountsQuery.isFetching}
loading={
myQuery.isFetching || creditAccountsQuery.isFetching || creditEntriesQuery.isFetching
}
onClick={refresh}
>
刷新
@@ -489,7 +504,8 @@ export default function FundSupportView() {
dataSource={creditEntries}
loading={creditEntriesQuery.isLoading}
pagination={false}
scroll={{ x: 900 }}
scroll={{ x: 1048 }}
tableLayout="fixed"
locale={{ emptyText: '还没有积分流水' }}
/>
),
@@ -35,9 +35,10 @@ import {
adjustAdminPlatformCredit,
fetchAdminPlatformCreditAccounts,
fetchAdminPlatformCreditEntries,
fetchAdminPlatformCreditOwners,
grantAdminPlatformCredit,
} from '@/services/admin'
import type { PlatformCreditAccount, PlatformCreditEntry } from '@/types/admin'
import type { PlatformCreditAccount, PlatformCreditEntry, PlatformCreditOwner } from '@/types/admin'
import { hasAdminPermission } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
@@ -88,9 +89,16 @@ export function PlatformCreditPanel() {
const canManage = hasAdminPermission('finance.config')
const ownersQuery = useQuery({
queryKey: ['admin-platform-credit-owners'],
queryFn: fetchAdminPlatformCreditOwners,
enabled: canManage,
})
const accountsQuery = useQuery({
queryKey: ['admin-platform-credit-accounts'],
queryFn: fetchAdminPlatformCreditAccounts,
refetchOnMount: 'always',
refetchInterval: 30_000,
})
const entriesQuery = useQuery({
queryKey: ['admin-platform-credit-entries', platformFilter],
@@ -99,6 +107,8 @@ export function PlatformCreditPanel() {
platformKey: platformFilter || undefined,
pageSize: 50,
}),
refetchOnMount: 'always',
refetchInterval: 30_000,
})
const grantMutation = useMutation({
@@ -113,14 +123,14 @@ export function PlatformCreditPanel() {
onSuccess: (response) => {
const result = response.data
message.success(
`已充值 ${result.quantity} 积分,折合 ¥${(result.amountFen / 100).toFixed(2)}`,
`已分配 ${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 : '充值失败')
message.error(error instanceof Error ? error.message : '分配失败')
},
})
@@ -147,18 +157,128 @@ export function PlatformCreditPanel() {
const accounts = accountsQuery.data?.data.items || []
const platforms = accountsQuery.data?.data.platforms || []
const entries = entriesQuery.data?.data.items || []
const owners: PlatformCreditOwner[] = ownersQuery.data?.data.items || []
const ownerOptions = owners.map((owner) => ({
value: owner.id,
label:
owner.displayName && owner.displayName !== owner.username
? `${owner.displayName}(${owner.username})`
: owner.username,
}))
const visibleAccounts = (
platformFilter ? accounts.filter((item) => item.platformKey === platformFilter) : accounts
)
.slice()
.sort(
(left, right) =>
left.ownerName.localeCompare(right.ownerName, 'zh-CN') ||
left.platformKey.localeCompare(right.platformKey),
)
function openGrant(account?: PlatformCreditAccount) {
grantForm.resetFields()
if (account) {
grantForm.setFieldsValue({
ownerAdminUserId: account.ownerAdminUserId,
platformKey: account.platformKey,
})
}
setGrantOpen(true)
}
function openAdjust(account?: PlatformCreditAccount) {
adjustForm.resetFields()
if (account) {
adjustForm.setFieldsValue({
ownerAdminUserId: account.ownerAdminUserId,
platformKey: account.platformKey,
})
}
setAdjustOpen(true)
}
const accountColumns: TableColumnsType<PlatformCreditAccount> = [
{
title: '客服',
dataIndex: 'ownerName',
width: 160,
align: 'center',
render: (value: string) => value || '-',
},
{
title: '平台',
dataIndex: 'platformKey',
width: 130,
align: 'center',
render: (value: string, row) => (
<Tag color={PLATFORM_TAG_COLORS[value] || 'default'}>{row.platformLabel}</Tag>
),
},
{
title: '积分余额',
dataIndex: 'balanceQuantity',
width: 140,
align: 'center',
render: (value: number, row) => `${value} ${row.unitName}`,
},
{
title: '折合金额',
dataIndex: 'balanceAmountFen',
width: 130,
align: 'center',
render: (value: number) => `¥${(Number(value || 0) / 100).toFixed(2)}`,
},
{
title: '状态',
dataIndex: 'status',
width: 100,
align: 'center',
render: (value: string) =>
value === 'active' ? <Tag color="green">正常</Tag> : <Tag>{value}</Tag>,
},
{
title: '更新时间',
dataIndex: 'updatedAt',
width: 180,
align: 'center',
render: (value: string) => (
<span style={{ whiteSpace: 'nowrap' }}>{formatAdminDateTime(value)}</span>
),
},
{
title: '操作',
key: 'actions',
width: 150,
align: 'center',
render: (_, account) =>
canManage ? (
<Space size={4}>
<Button type="link" size="small" onClick={() => openGrant(account)}>
分配
</Button>
<Button type="link" size="small" onClick={() => openAdjust(account)}>
调整
</Button>
</Space>
) : (
'-'
),
},
]
const entryColumns: TableColumnsType<PlatformCreditEntry> = [
{
title: '客服',
dataIndex: 'ownerName',
width: 110,
align: 'center',
render: (value: string) => value || '-',
},
{
title: '平台',
dataIndex: 'platformKey',
width: 100,
align: 'center',
render: (value: string, row) => (
<Tag color={PLATFORM_TAG_COLORS[value] || 'default'}>{row.platformLabel}</Tag>
),
@@ -167,6 +287,7 @@ export function PlatformCreditPanel() {
title: '类型',
dataIndex: 'entryType',
width: 80,
align: 'center',
render: (value: string) => (
<Tag color={ENTRY_TYPE_COLORS[value] || 'default'}>{ENTRY_TYPE_LABELS[value] || value}</Tag>
),
@@ -175,7 +296,7 @@ export function PlatformCreditPanel() {
title: '变动',
dataIndex: 'quantityDelta',
width: 100,
align: 'right',
align: 'center',
render: (value: number) =>
value > 0 ? (
<Typography.Text style={{ color: '#389e0d' }}>+{value}</Typography.Text>
@@ -187,45 +308,49 @@ export function PlatformCreditPanel() {
title: '余额',
dataIndex: 'balanceAfter',
width: 100,
align: 'right',
align: 'center',
},
{
title: '金额',
dataIndex: 'amountFen',
width: 110,
align: 'right',
align: 'center',
render: (value: number) => `¥${(Number(value || 0) / 100).toFixed(2)}`,
},
{
title: '备注',
dataIndex: 'note',
align: 'center',
render: (value: string) => value || '-',
},
{
title: '时间',
dataIndex: 'createdAt',
width: 150,
render: (value: string) => formatAdminDateTime(value),
width: 180,
align: 'center',
render: (value: string) => (
<span style={{ whiteSpace: 'nowrap' }}>{formatAdminDateTime(value)}</span>
),
},
]
return (
<Card bordered={false} title="平台积分钱包">
<Card bordered={false} title="客服积分钱包">
<Alert
type="info"
showIcon
style={{ marginBottom: 14 }}
message="南航 / 乐玩 / ROCK 三平台积分互不通用,每平台一个钱包"
description="换算率按平台:南航、乐玩 1 积分 = 1 分;ROCK 1 积分 = 1 元。余额只能由流水产生,消耗不足会直接拒绝,不允许负余额。备用金现金不在这里,走「我的备用金」。"
description="换算率按平台:南航、乐玩 1 积分 = 1 分;ROCK 1 积分 = 1 元。可在下方按客服直接分配或调整积分;备用金现金在「现金钱包 / 对账中心」管理。"
/>
<Row gutter={12} style={{ marginBottom: 12 }}>
<Row gutter={[12, 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}>
<Col xs={24} md={8} key={platform.key}>
<Card size="small">
<Statistic
title={`${platform.label}总积分`}
@@ -246,24 +371,10 @@ export function PlatformCreditPanel() {
<Space wrap style={{ marginBottom: 12 }}>
{canManage ? (
<>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => {
grantForm.resetFields()
setGrantOpen(true)
}}
>
充值积分
</Button>
<Button
onClick={() => {
adjustForm.resetFields()
setAdjustOpen(true)
}}
>
调整余额
<Button type="primary" icon={<PlusOutlined />} onClick={() => openGrant()}>
分配积分
</Button>
<Button onClick={() => openAdjust()}>调整余额</Button>
</>
) : null}
<Select
@@ -285,6 +396,23 @@ export function PlatformCreditPanel() {
</Button>
</Space>
<Typography.Title level={5} style={{ margin: '8px 0 12px' }}>
客服积分余额
</Typography.Title>
<Table<PlatformCreditAccount>
rowKey="id"
size="small"
columns={accountColumns}
dataSource={visibleAccounts}
loading={accountsQuery.isLoading}
scroll={{ x: 1080 }}
pagination={{ pageSize: 20, showSizeChanger: true }}
locale={{ emptyText: '还没有客服积分账户' }}
/>
<Typography.Title level={5} style={{ margin: '24px 0 12px' }}>
积分流水
</Typography.Title>
<Table
rowKey="id"
size="small"
@@ -297,8 +425,8 @@ export function PlatformCreditPanel() {
<Modal
open={grantOpen}
title="充值积分"
okText="确认充值"
title="分配积分"
okText="确认分配"
cancelText="取消"
confirmLoading={grantMutation.isPending}
onOk={() => {
@@ -313,9 +441,14 @@ export function PlatformCreditPanel() {
name="ownerAdminUserId"
label="客服"
rules={[{ required: true, message: '请选择客服' }]}
extra="客服 ID(管理员用户 id)"
>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
<Select
showSearch
optionFilterProp="label"
placeholder="按姓名或账号搜索"
loading={ownersQuery.isLoading}
options={ownerOptions}
/>
</Form.Item>
<Form.Item
name="platformKey"
@@ -331,7 +464,7 @@ export function PlatformCreditPanel() {
</Form.Item>
<Form.Item
name="quantity"
label="积分数"
label="分配积分数"
rules={[{ required: true, message: '请输入积分数' }]}
>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
@@ -347,7 +480,7 @@ export function PlatformCreditPanel() {
<Modal
open={adjustOpen}
title="调整积分余额"
title="调整积分"
okText="确认调整"
cancelText="取消"
confirmLoading={adjustMutation.isPending}
@@ -364,7 +497,13 @@ export function PlatformCreditPanel() {
label="客服"
rules={[{ required: true, message: '请选择客服' }]}
>
<InputNumber min={1} precision={0} style={{ width: '100%' }} />
<Select
showSearch
optionFilterProp="label"
placeholder="按姓名或账号搜索"
loading={ownersQuery.isLoading}
options={ownerOptions}
/>
</Form.Item>
<Form.Item
name="platformKey"
@@ -17,6 +17,7 @@ import type {
ManualPlatformOption,
PlatformCreditAccountListResult,
PlatformCreditEntryListResult,
PlatformCreditOwnerListResult,
ManualRematchResult,
ManualSkuListResult,
ManualSkuOverview,
@@ -298,6 +299,10 @@ export function fetchAdminPlatformCreditAccounts() {
)
}
export function fetchAdminPlatformCreditOwners() {
return apiGet<PlatformCreditOwnerListResult>('/api/v1/admin/cost-center/platform-credits/owners')
}
export function fetchAdminPlatformCreditEntries(
params: {
page?: number
@@ -418,6 +418,16 @@ export interface PlatformCreditAccountListResult {
}>
}
export interface PlatformCreditOwner {
id: number
username: string
displayName: string
}
export interface PlatformCreditOwnerListResult {
items: PlatformCreditOwner[]
}
export interface PlatformCreditEntry {
id: number
accountId: number