feat(finance): 支持代录指定客服账户,并上线影子总账/对账/关账/快手平台退款

- 人工确认履约:管理员/财务/运营可传 fundAccountId / creditOwnerAdminUserId
  指定客服备用金/积分账户;客服仍强制本人账户
- 新增迁移 112:finance_accounts / journal_entries / journal_lines /
  periods / reconciliations / reconciliation_items,种子 11 个科目
- 影子总账:销售事实按日生成收入/退款/成本凭证,支持手工凭证、试算平衡;
  不接管钱包余额;总账同步接入定时任务与退款重算
- 期间关账/反关账:关账先补凭证并校验试算平衡,closed 期间禁止新增凭证
- 平台对账:JSON 账单导入,按 provider + platform_order_id 匹配订单/退款,
  标记 matched/amount_mismatch/missing_order/missing_refund/duplicate
- 平台退款:快手行业(91)同意退款 API 接入,成功后落 source=platform;
  抖音/拼多多返回 501,保留手工退款
- 新增后台「财务 → 总账与对账」页面(总账/期间/对账三个 Tab)
- 集成测试扩展到代录、总账平衡、关账阻断、手工凭证、对账匹配

验证:backend check 844(840 pass / 4 skip);frontend check 172 + build;
迁移 112 在 scratch 库全量执行并重跑幂等;本地生产副本已升级到 112,
销售事实同步 56 条凭证,借贷合计 1,885,351,864 = 1,885,351,864。
This commit is contained in:
yml2213
2026-10-26 13:32:47 +08:00
parent f0eb8a5e05
commit 62763ddf0c
31 changed files with 2961 additions and 55 deletions
@@ -1,6 +1,7 @@
import {
AccountBookOutlined,
ApartmentOutlined,
AuditOutlined,
LineChartOutlined,
WalletOutlined,
} from '@ant-design/icons'
@@ -30,6 +31,11 @@ export const FINANCE_NAV_ITEMS = [
label: '收支明细',
icon: <AccountBookOutlined />,
},
{
key: '/admin/finance/ledger',
label: '总账与对账',
icon: <AuditOutlined />,
},
] as const
type FinanceNavKey = (typeof FINANCE_NAV_ITEMS)[number]['key']
@@ -26,8 +26,14 @@ import {
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { confirmAdminManualFulfillment, fetchAdminManualFulfillmentPending } from '@/services/admin'
import {
confirmAdminManualFulfillment,
fetchAdminFundAccounts,
fetchAdminManualFulfillmentPending,
fetchAdminPlatformCreditOwners,
} from '@/services/admin'
import type { ManualFulfillmentPendingItem } from '@/types/admin'
import { getAdminRole, getAdminUserId } from '@/utils/admin-auth'
type ConfirmFormValues = {
costYuan?: number
@@ -61,6 +67,11 @@ export function ManualFulfillmentConfirmModal({
const queryClient = useQueryClient()
const [form] = Form.useForm<ConfirmFormValues>()
const [selectedId, setSelectedId] = useState<number | null>(null)
const [fundAccountId, setFundAccountId] = useState<number | null>(null)
const [creditOwnerAdminUserId, setCreditOwnerAdminUserId] = useState<number | null>(null)
const role = getAdminRole()
const privileged = role !== 'support'
const currentUserId = getAdminUserId()
const pendingQuery = useQuery({
queryKey: ['admin-manual-fulfillment-pending', 'order', orderId],
@@ -81,12 +92,49 @@ export function ManualFulfillmentConfirmModal({
items[0] ||
null
const fundAccountsQuery = useQuery({
queryKey: ['admin-manual-confirm-fund-accounts'],
queryFn: () => fetchAdminFundAccounts({ page: 1, pageSize: 100 }),
enabled: open && privileged && Boolean(selected?.isFundCash),
retry: false,
})
const creditOwnersQuery = useQuery({
queryKey: ['admin-manual-confirm-credit-owners'],
queryFn: () => fetchAdminPlatformCreditOwners(),
enabled: open && privileged && Boolean(selected) && !selected?.isFundCash,
retry: false,
})
useEffect(() => {
if (!open) return
setSelectedId(initialOrderItemId || null)
form.resetFields()
}, [open, initialOrderItemId, form])
// 代录时默认选本人账户;客服没有代录权限,不展示选择。
useEffect(() => {
if (!open || !privileged) return
if (selected?.isFundCash) {
const accounts = (fundAccountsQuery.data?.data.items || []).filter(
(item) => item.accountType === 'agent' && item.status === 'active',
)
const own = accounts.find((item) => item.ownerAdminUserId === currentUserId)
setFundAccountId(own?.id ?? null)
return
}
const owners = creditOwnersQuery.data?.data.items || []
const own = owners.find((item) => item.id === currentUserId)
setCreditOwnerAdminUserId(own?.id ?? null)
}, [
open,
privileged,
selected?.orderItemId,
selected?.isFundCash,
currentUserId,
fundAccountsQuery.data,
creditOwnersQuery.data,
])
const confirmMutation = useMutation({
mutationFn: (values: ConfirmFormValues) =>
confirmAdminManualFulfillment({
@@ -94,6 +142,10 @@ export function ManualFulfillmentConfirmModal({
costFen: values.costYuan === undefined ? undefined : Math.round(values.costYuan * 100),
payeeName: values.payeeName || '',
note: values.note || '',
...(privileged && selected?.isFundCash && fundAccountId ? { fundAccountId } : {}),
...(privileged && selected && !selected.isFundCash && creditOwnerAdminUserId
? { creditOwnerAdminUserId }
: {}),
}),
onSuccess: (response) => {
const result = response.data
@@ -122,7 +174,14 @@ export function ManualFulfillmentConfirmModal({
open={open}
title="确认人工履约"
okText="确认履约"
okButtonProps={{ icon: <CheckOutlined />, disabled: !selected }}
okButtonProps={{
icon: <CheckOutlined />,
disabled:
!selected ||
(privileged &&
((selected.isFundCash && !fundAccountId) ||
(!selected.isFundCash && !creditOwnerAdminUserId))),
}}
cancelText="取消"
confirmLoading={confirmMutation.isPending}
onOk={() => {
@@ -181,6 +240,46 @@ export function ManualFulfillmentConfirmModal({
<Typography.Text>¥{((selected?.revenueFen || 0) / 100).toFixed(2)}</Typography.Text>
</div>
{privileged && selected ? (
selected.isFundCash ? (
<div>
<Typography.Text type="secondary">代录到客服备用金账户:</Typography.Text>
<Select
showSearch
optionFilterProp="label"
value={fundAccountId ?? undefined}
placeholder="请选择客服备用金账户"
style={{ width: '100%', marginTop: 6 }}
loading={fundAccountsQuery.isLoading}
onChange={(value) => setFundAccountId(value)}
options={(fundAccountsQuery.data?.data.items || [])
.filter((item) => item.accountType === 'agent' && item.status === 'active')
.map((item) => ({
value: item.id,
label: `${item.ownerName || item.accountNo} · 余额 ¥${(item.balanceFen / 100).toFixed(2)}`,
}))}
/>
</div>
) : (
<div>
<Typography.Text type="secondary">代录到客服积分账户:</Typography.Text>
<Select
showSearch
optionFilterProp="label"
value={creditOwnerAdminUserId ?? undefined}
placeholder="请选择客服账号"
style={{ width: '100%', marginTop: 6 }}
loading={creditOwnersQuery.isLoading}
onChange={(value) => setCreditOwnerAdminUserId(value)}
options={(creditOwnersQuery.data?.data.items || []).map((item) => ({
value: item.id,
label: item.displayName || item.username,
}))}
/>
</div>
)
) : null}
{showCostInput ? (
<Alert type="warning" showIcon message="该商品成本不固定,请填写本次实际成本" />
) : (