fix(finance): 客服只能使用自己的备用金账户

This commit is contained in:
yml2213
2026-10-23 11:44:04 +08:00
parent 2aeb789e20
commit a7f60414a4
6 changed files with 126 additions and 57 deletions
@@ -1507,19 +1507,24 @@ export async function createAdminFundReturn(payload: JsonObject, session: AdminS
})
}
export async function getOwnAgentAccountId(userId: number): Promise<number> {
const own = await query<FundAccountRow>(
"SELECT * FROM fund_accounts WHERE owner_admin_user_id = $1 AND account_type = 'agent' AND status <> 'closed' ORDER BY id LIMIT 1",
[userId],
)
return own.rows[0] ? Number(own.rows[0].id) : 0
}
async function resolveAgentAccountId(payload: JsonObject, session: AdminSession): Promise<number> {
if (session.role === 'support') {
const own = await query<FundAccountRow>(
"SELECT * FROM fund_accounts WHERE owner_admin_user_id = $1 AND account_type = 'agent' LIMIT 1",
[session.userId],
)
if (!own.rows[0]) {
const ownAccountId = await getOwnAgentAccountId(session.userId)
if (!ownAccountId) {
throw createHttpError('当前账号还没有备用金账户', {
statusCode: 404,
errorCode: 'fund_agent_account_not_found',
})
}
return Number(own.rows[0].id)
return ownAccountId
}
const accountId = toId(payload.agentAccountId)
@@ -7,7 +7,7 @@ import { resolveFinanceDateRange } from '../../utils/finance-date-range.js'
import { generateManualExpenseNo } from '../../utils/random.js'
import { nowIso } from '../../utils/time.js'
import type { AdminSession } from '../admin/admin-auth-service.js'
import { writeFundExpense } from '../admin/admin-fund-service.js'
import { getOwnAgentAccountId, writeFundExpense } from '../admin/admin-fund-service.js'
const EXPENSE_STATUSES = new Set([
'draft',
@@ -372,9 +372,26 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
inferExpenseCategory(costItem?.costType),
'支出分类无效',
)
const fundAccountId = normalizeOptionalId(
const requestedFundAccountId = normalizeOptionalId(
payload.fundAccountId ?? payload.fund_account_id ?? payload.agentAccountId,
)
let fundAccountId = requestedFundAccountId
if (session.role === 'support') {
const ownAccountId = await getOwnAgentAccountId(session.userId)
if (!ownAccountId) {
throw createHttpError('当前账号还没有备用金账户', {
statusCode: 404,
errorCode: 'fund_agent_account_not_found',
})
}
if (requestedFundAccountId && requestedFundAccountId !== ownAccountId) {
throw createHttpError('只能使用自己的备用金账户', {
statusCode: 403,
errorCode: 'manual_expense_fund_account_forbidden',
})
}
fundAccountId = ownAccountId
}
if (!fundAccountId) {
throw createHttpError('请选择备用金账户,人工支出必须从备用金扣款', {
statusCode: 400,
@@ -462,10 +479,10 @@ export async function createManualExpense(payload: JsonObject, session: AdminSes
export async function createManualOrderCost(payload: JsonObject, session: AdminSession) {
const orderId = normalizeId(payload.orderId ?? payload.order_id)
const requestedItemId = normalizeOptionalId(payload.orderItemId ?? payload.order_item_id)
const fundAccountId = normalizeOptionalId(
const requestedFundAccountId = normalizeOptionalId(
payload.fundAccountId ?? payload.fund_account_id ?? payload.agentAccountId,
)
if (!fundAccountId) {
if (!requestedFundAccountId && session.role !== 'support') {
throw createHttpError('请选择备用金账户,人工录单必须从备用金扣款', {
statusCode: 400,
errorCode: 'manual_order_cost_fund_account_required',
@@ -535,7 +552,7 @@ export async function createManualOrderCost(payload: JsonObject, session: AdminS
orderId,
orderItemId,
costItemId,
fundAccountId,
fundAccountId: requestedFundAccountId,
amountFen,
expenseCategory,
payeeName,
@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { createAdminManualOrderCost, fetchAdminFundAccounts } from '@/services/admin'
import { hasAdminPermission } from '@/utils/admin-auth'
import { getAdminRole, hasAdminPermission } from '@/utils/admin-auth'
export type ManualOrderCostItem = {
orderItemId: number
@@ -77,6 +77,8 @@ export default function ManualOrderCostModal({
})
const fundAccounts = fundAccountsQuery.data?.data.items || []
const soleFundAccountId = fundAccounts.length === 1 ? fundAccounts[0]?.id : undefined
const isSupport = getAdminRole() === 'support'
const ownFundAccount = isSupport ? fundAccounts[0] : undefined
useEffect(() => {
if (!open) return
@@ -161,25 +163,45 @@ export default function ManualOrderCostModal({
<Form.Item label="订单号">
<Input value={orderNo} readOnly />
</Form.Item>
<Form.Item
name="fundAccountId"
label="备用金账户"
rules={[{ required: true, message: '请选择备用金账户' }]}
extra="人工支出会直接从该客服的备用金余额扣减"
>
<Select
showSearch
optionFilterProp="label"
loading={fundAccountsQuery.isLoading}
placeholder="请选择这笔支出从哪个客服备用金账户出"
options={fundAccounts.map((account) => ({
value: account.id,
label: `${account.ownerName} · ${account.accountNo} · 余额 ¥${(
account.balanceFen / 100
).toFixed(2)}`,
}))}
/>
</Form.Item>
{isSupport ? (
<>
<Form.Item label="备用金账户" extra="只能使用自己的备用金账户">
<Input
readOnly
value={
ownFundAccount
? `${ownFundAccount.ownerName} · ${ownFundAccount.accountNo} · 余额 ¥${(
ownFundAccount.balanceFen / 100
).toFixed(2)}`
: '当前账号还没有备用金账户'
}
/>
</Form.Item>
<Form.Item name="fundAccountId" hidden>
<Input />
</Form.Item>
</>
) : (
<Form.Item
name="fundAccountId"
label="备用金账户"
rules={[{ required: true, message: '请选择备用金账户' }]}
extra="人工支出会直接从该客服的备用金余额扣减"
>
<Select
showSearch
optionFilterProp="label"
loading={fundAccountsQuery.isLoading}
placeholder="请选择这笔支出从哪个客服备用金账户出"
options={fundAccounts.map((account) => ({
value: account.id,
label: `${account.ownerName} · ${account.accountNo} · 余额 ¥${(
account.balanceFen / 100
).toFixed(2)}`,
}))}
/>
</Form.Item>
)}
{!fundAccountsQuery.isLoading && fundAccounts.length === 0 ? (
<Alert
type="warning"
@@ -46,7 +46,7 @@ import {
} from '@/services/admin'
import type { AdminManualExpense, AdminManualExpenseStatus } from '@/types/admin'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import { hasAdminPermission } from '@/utils/admin-auth'
import { hasAdminPermission, getAdminRole } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import { formatMoney } from './panels/shared'
@@ -150,6 +150,8 @@ export default function AdminManualExpensePage() {
const overview = overviewQuery.data?.data?.overview
const costItems = costItemsQuery.data?.data.items || []
const fundAccounts = fundAccountsQuery.data?.data.items || []
const isSupport = getAdminRole() === 'support'
const ownFundAccount = isSupport ? fundAccounts[0] : undefined
useEffect(() => {
const orderId = Number(searchParams.get('orderId') || 0)
@@ -170,11 +172,13 @@ export default function AdminManualExpensePage() {
}, [costItems, createOpen, expenseForm])
useEffect(() => {
if (!createOpen || fundAccounts.length !== 1) return
if (!createOpen) return
if (isSupport && !ownFundAccount) return
if (fundAccounts.length !== 1 && !ownFundAccount) return
if (!expenseForm.getFieldValue('fundAccountId')) {
expenseForm.setFieldValue('fundAccountId', fundAccounts[0]?.id)
expenseForm.setFieldValue('fundAccountId', ownFundAccount?.id || fundAccounts[0]?.id)
}
}, [createOpen, expenseForm, fundAccounts])
}, [createOpen, expenseForm, fundAccounts, isSupport, ownFundAccount])
function refreshAll() {
void queryClient.invalidateQueries({ queryKey: ['admin-manual-expenses'] })
@@ -624,25 +628,45 @@ export default function AdminManualExpensePage() {
</Form.Item>
</Col>
</Row>
<Form.Item
name="fundAccountId"
label="备用金账户"
rules={[{ required: true, message: '请选择备用金账户' }]}
extra="人工支出会直接从该客服的备用金余额扣减,并同步出现在「备用金」支出明细里"
>
<Select
showSearch
optionFilterProp="label"
loading={fundAccountsQuery.isLoading}
placeholder="请选择这笔支出从哪个客服备用金账户出"
options={fundAccounts.map((account) => ({
value: account.id,
label: `${account.ownerName} · ${account.accountNo} · 余额 ${formatMoney(
account.balanceFen,
)}`,
}))}
/>
</Form.Item>
{isSupport ? (
<>
<Form.Item label="备用金账户" extra="只能使用自己的备用金账户">
<Input
readOnly
value={
ownFundAccount
? `${ownFundAccount.ownerName} · ${ownFundAccount.accountNo} · 余额 ${formatMoney(
ownFundAccount.balanceFen,
)}`
: '当前账号还没有备用金账户'
}
/>
</Form.Item>
<Form.Item name="fundAccountId" hidden>
<Input />
</Form.Item>
</>
) : (
<Form.Item
name="fundAccountId"
label="备用金账户"
rules={[{ required: true, message: '请选择备用金账户' }]}
extra="人工支出会直接从该客服的备用金余额扣减,并同步出现在「备用金」支出明细里"
>
<Select
showSearch
optionFilterProp="label"
loading={fundAccountsQuery.isLoading}
placeholder="请选择这笔支出从哪个客服备用金账户出"
options={fundAccounts.map((account) => ({
value: account.id,
label: `${account.ownerName} · ${account.accountNo} · 余额 ${formatMoney(
account.balanceFen,
)}`,
}))}
/>
</Form.Item>
)}
{!fundAccountsQuery.isLoading && fundAccounts.length === 0 ? (
<Alert
type="warning"