feat(finance): 完善客服钱包与人工履约成本流水
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user