资金申请增加渠道筛选合计与审核留痕

This commit is contained in:
yml2213
2026-08-22 09:57:28 +08:00
parent 3618df74c5
commit 985e61209d
10 changed files with 273 additions and 14 deletions
@@ -471,6 +471,8 @@ export type FinanceRequestListInput = {
/** 申请时间区间(YYYY-MM-DD,上海时区,含起止日);后台列表筛选使用。 */
createdFrom?: string
createdTo?: string
/** 收款渠道筛选:alipay / wechat */
accountChannel?: string
}
export type CreateWorkerInput = {
@@ -205,6 +205,7 @@ export async function listWorkerFinanceRequests({
keyword = '',
createdFrom = '',
createdTo = '',
accountChannel = '',
}: FinanceRequestListInput = {}): Promise<{ items: WorkerFinanceRequestRow[]; total: number }> {
const { whereClause, params } = buildWorkerFinanceRequestWhere({
requestId,
@@ -214,6 +215,7 @@ export async function listWorkerFinanceRequests({
keyword,
createdFrom,
createdTo,
accountChannel,
})
const totalResult = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM worker_finance_requests wfr
@@ -236,11 +238,46 @@ export async function listWorkerFinanceRequests({
}
}
/** 按当前列表筛选条件汇总各收款渠道的笔数与金额,供后台合计条展示。 */
export async function summarizeWorkerFinanceRequests(
input: FinanceRequestListInput = {},
): Promise<Array<{ channel: string; count: number; amount: number }>> {
const { whereClause, params } = buildWorkerFinanceRequestWhere({
requestId: input.requestId || 0,
workerId: input.workerId || 0,
status: input.status || '',
requestType: input.requestType || '',
keyword: input.keyword || '',
createdFrom: input.createdFrom || '',
createdTo: input.createdTo || '',
accountChannel: input.accountChannel || '',
})
const result = await query<{ channel: string; total: number; amount: string | number }>(
`
SELECT
COALESCE(NULLIF(wfr.account_channel, ''), 'other') AS channel,
COUNT(*)::int AS total,
COALESCE(SUM(wfr.amount), 0)::bigint AS amount
FROM worker_finance_requests wfr
LEFT JOIN worker_users wu ON wu.id = wfr.worker_id
${whereClause}
GROUP BY 1
`,
params,
)
return result.rows.map((row) => ({
channel: String(row.channel || 'other'),
count: Number(row.total || 0),
amount: Number(row.amount || 0),
}))
}
export async function reviewWorkerFinanceRequest(input: {
requestId: number
status: string
reviewedNote: string
now: string
reviewedBy?: string
}): Promise<{
request: WorkerFinanceRequestRow | null
failureReason: 'request_not_pending' | 'withdraw_insufficient' | null
@@ -357,10 +394,24 @@ export async function reviewWorkerFinanceRequest(input: {
status = $1,
reviewed_note = $2,
reviewed_at = $3,
updated_at = $3
updated_at = $3,
payload_json = payload_json || $5::jsonb
WHERE id = $4
`,
[input.status, input.reviewedNote, input.now, input.requestId],
[
input.status,
input.reviewedNote,
input.now,
input.requestId,
JSON.stringify({
review: {
by: String(input.reviewedBy || '').trim(),
at: input.now,
status: input.status,
note: input.reviewedNote,
},
}),
],
)
return {
@@ -421,9 +472,14 @@ function buildWorkerFinanceRequestWhere({
keyword = '',
createdFrom = '',
createdTo = '',
accountChannel = '',
}: FinanceRequestListInput) {
const filters: string[] = []
const params: unknown[] = []
if (accountChannel) {
params.push(accountChannel)
filters.push(`wfr.account_channel = $${params.length}`)
}
if (createdFrom) {
params.push(createdFrom)
filters.push(
@@ -272,6 +272,7 @@ export {
listWorkerFinanceRequests,
listWorkerWithdrawalAccounts,
reviewWorkerFinanceRequest,
summarizeWorkerFinanceRequests,
upsertWorkerWithdrawalAccount,
} from './worker-finance-repo.js'
@@ -22,7 +22,12 @@ router.post(
'/worker-platform/finance-requests/:requestId/review',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) => reviewAdminWorkerFinanceRequest(String(req.params.requestId || ''), req.body || {}),
(req) =>
reviewAdminWorkerFinanceRequest(
String(req.params.requestId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '资金申请已处理',
errorMessage: '处理资金申请失败',
@@ -1,4 +1,5 @@
import {
summarizeWorkerFinanceRequests,
addWorkerWalletCredit,
getWorkerFinanceRequestById,
listWorkerFinanceRequests,
@@ -169,25 +170,39 @@ export async function listAdminWorkerFinanceRequests(query: JsonObject = {}) {
const keyword = String(query.keyword || '').trim()
const createdFrom = normalizeDateString(query.createdFrom)
const createdTo = normalizeDateString(query.createdTo)
const { items, total } = await listWorkerFinanceRequests({
page,
pageSize,
const accountChannel = ['alipay', 'wechat'].includes(String(query.accountChannel || ''))
? String(query.accountChannel)
: ''
const listFilters = {
requestId,
status,
requestType,
keyword,
...(createdFrom ? { createdFrom } : {}),
...(createdTo ? { createdTo } : {}),
})
...(accountChannel ? { accountChannel } : {}),
}
const [listResult, channels] = await Promise.all([
listWorkerFinanceRequests({ page, pageSize, ...listFilters }),
summarizeWorkerFinanceRequests(listFilters),
])
const { items, total } = listResult
const summary = {
channels,
totalCount: channels.reduce((sum, item) => sum + item.count, 0),
totalAmount: channels.reduce((sum, item) => sum + item.amount, 0),
}
return {
items: items.map(mapFinanceRequest),
pagination: { page, pageSize, total },
summary,
}
}
export async function reviewAdminWorkerFinanceRequest(
requestId: number | string,
payload: JsonObject = {},
reviewedBy = '',
) {
const current = await getWorkerFinanceRequestById(requestId)
if (!current) {
@@ -204,6 +219,7 @@ export async function reviewAdminWorkerFinanceRequest(
status,
reviewedNote,
now: nowIso(),
reviewedBy: String(reviewedBy || '').trim(),
})
if (reviewed.failureReason === 'request_not_pending') {
@@ -72,6 +72,7 @@ export default function FinancePanel() {
const [keywordInput, setKeywordInput] = useState('')
const [keyword, setKeyword] = useState('')
const [createdRange, setCreatedRange] = useState<[string, string] | null>(null)
const [accountChannel, setAccountChannel] = useState('')
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [savingConfig, setSavingConfig] = useState(false)
@@ -114,6 +115,7 @@ export default function FinancePanel() {
pageSize,
createdRange ? createdRange[0] : '',
createdRange ? createdRange[1] : '',
accountChannel,
],
queryFn: () =>
fetchAdminWorkerFinanceRequests({
@@ -286,7 +288,9 @@ export default function FinancePanel() {
<div className="cell-stack">
<Typography.Text>{formatAdminDateTime(row.createdAt)}</Typography.Text>
<Typography.Text type="secondary">
{row.reviewedAt ? `审核:${formatAdminDateTime(row.reviewedAt)}` : '待审核'}
{row.reviewedAt
? `审核:${formatAdminDateTime(row.reviewedAt)}${resolveFinanceReviewer(row) ? `${resolveFinanceReviewer(row)}` : ''}`
: '待审核'}
</Typography.Text>
</div>
),
@@ -472,6 +476,19 @@ export default function FinancePanel() {
setPage(1)
}}
/>
<Select
value={accountChannel}
style={{ width: 130 }}
options={[
{ value: '', label: '全部渠道' },
{ value: 'alipay', label: '支付宝' },
{ value: 'wechat', label: '微信' },
]}
onChange={(nextChannel) => {
setAccountChannel(nextChannel)
setPage(1)
}}
/>
<AdminRangePicker
value={createdRange ? [dayjs(createdRange[0]), dayjs(createdRange[1])] : null}
onChange={(dates) => {
@@ -494,6 +511,11 @@ export default function FinancePanel() {
</Space>
}
>
<FinanceSummaryBar
channels={financeRequestsQuery.data?.data.summary?.channels}
totalCount={financeRequestsQuery.data?.data.summary?.totalCount}
totalAmount={financeRequestsQuery.data?.data.summary?.totalAmount}
/>
<Table<WorkerFinanceRequest>
rowKey="requestId"
loading={financeRequestsQuery.isLoading}
@@ -776,3 +798,49 @@ function maskFinanceAccount(value: string) {
if (text.length <= 8) return text
return `${text.slice(0, 4)} **** ${text.slice(-4)}`
}
type FinanceSummaryChannel = { channel: string; count: number; amount: number }
/** 当前筛选条件下的渠道合计条:支付宝 / 微信 / 其他分列展示笔数与金额。 */
function FinanceSummaryBar({
channels,
totalCount,
totalAmount,
}: {
channels?: FinanceSummaryChannel[]
totalCount?: number
totalAmount?: number
}) {
const items = channels || []
const alipay = items.find((item) => item.channel === 'alipay')
const wechat = items.find((item) => item.channel === 'wechat')
const others = items.filter((item) => item.channel !== 'alipay' && item.channel !== 'wechat')
const otherCount = others.reduce((sum, item) => sum + item.count, 0)
const otherAmount = others.reduce((sum, item) => sum + item.amount, 0)
return (
<Space size={24} wrap style={{ marginBottom: 12 }}>
<span className="finance-summary-item">
<strong>{alipay ? alipay.count : 0}</strong> /{' '}
<strong>{formatMoney(alipay ? alipay.amount : 0)}</strong>
</span>
<span className="finance-summary-item">
<strong>{wechat ? wechat.count : 0}</strong> /{' '}
<strong>{formatMoney(wechat ? wechat.amount : 0)}</strong>
</span>
<span className="finance-summary-item">
<strong>{otherCount}</strong> / <strong>{formatMoney(otherAmount)}</strong>
</span>
<span className="finance-summary-item">
<strong>{totalCount || 0}</strong> /{' '}
<strong>{formatMoney(totalAmount || 0)}</strong>
</span>
<Typography.Text type="secondary"></Typography.Text>
</Space>
)
}
function resolveFinanceReviewer(request: WorkerFinanceRequest): string {
const review = asRecord(asRecord(request.payload).review)
return String(review.by || '').trim()
}
@@ -441,10 +441,15 @@ export function saveAdminWorkerPlatformNotificationConfig(
}
export function fetchAdminWorkerFinanceRequests(params?: Record<string, unknown>) {
return apiGet<WorkerListResponse<WorkerFinanceRequest>>(
'/api/v1/admin/worker-platform/finance-requests',
params,
)
return apiGet<
WorkerListResponse<WorkerFinanceRequest> & {
summary?: {
channels: { channel: string; count: number; amount: number }[]
totalCount: number
totalAmount: number
}
}
>('/api/v1/admin/worker-platform/finance-requests', params)
}
export function reviewAdminWorkerFinanceRequest(
+9
View File
@@ -2190,3 +2190,12 @@ body {
font-size: 13px;
white-space: nowrap;
}
.finance-summary-item {
color: #4b5563;
font-size: 13px;
}
.finance-summary-item strong {
color: #111827;
}
+48
View File
@@ -28,6 +28,44 @@ export function formatAuditAction(action: string) {
task_complete_manual_dispatch: '完成人工履约',
platform_shop_config_updated: '更新店铺配置',
platform_fulfillment_config_updated: '更新履约配置',
worker_finance_request_reviewed: '资金申请审核',
worker_cancel_request_reviewed: '撤单申请审核',
worker_reviewed: '打手账号审核',
worker_wallet_credited: '打手钱包手动加款',
worker_password_reset: '重置打手密码',
worker_withdrawal_account_saved: '打手提现账户保存',
work_order_published: '工单发布',
work_order_unpublished: '工单下架',
work_order_updated: '工单信息更新',
work_order_deleted: '工单删除',
work_order_accepted: '工单验收',
work_orders_batch_accepted: '批量验收',
work_order_marked_problem: '工单标记问题',
work_order_problem_resolved: '问题单处置',
work_order_reopened: '工单重新打开',
work_order_pin_toggled: '工单置顶切换',
work_order_worker_assigned: '工单指派打手',
work_order_worker_unassigned: '工单取消指派',
work_order_material_saved: '工单资料保存',
work_order_share_accepted: '拼单份额验收',
work_order_share_cancelled: '拼单份额取消',
work_order_sharing_config_updated: '拼单配置更新',
work_order_pending_deposit_deducted: '押金扣除',
after_sales_case_created: '售后问题单创建',
after_sales_case_resolved: '售后问题单处置',
worker_level_saved: '接单等级保存',
worker_level_deleted: '接单等级删除',
work_category_saved: '接单分类保存',
work_category_deleted: '接单分类删除',
work_product_rule_saved: '接单模板保存',
work_product_rule_deleted: '接单模板删除',
work_product_rule_price_synced: '模板价格同步',
work_product_mapping_saved: '商品映射保存',
worker_finance_config_saved: '资金配置保存',
worker_hall_config_saved: '大厅配置保存',
worker_announcement_saved: '公告保存',
worker_platform_notification_config_saved: '通知配置保存',
worker_product_match_config_saved: '商品匹配配置保存',
}
return formatStatusWithRaw(action, labelMap)
@@ -38,6 +76,16 @@ export function formatAuditTargetType(targetType: string) {
admin_user: '后台用户',
task: '交付任务',
platform_config: '平台配置',
worker: '打手',
work_order: '接单工单',
work_order_share: '拼单份额',
worker_finance_request: '资金申请',
worker_cancel_request: '撤单申请',
after_sales_case: '售后问题单',
worker_level: '接单等级',
work_category: '接单分类',
work_product_rule: '接单模板',
work_product_mapping: '商品映射',
}
return formatStatusWithRaw(targetType, labelMap)
+51 -2
View File
@@ -35,7 +35,11 @@ export const adminUserStatusOptions = [
]
export const adminAuditActionOptions = [
{ label: '全部', value: '' },
{ label: '资金申请审核(通过/拒绝)', value: 'worker_finance_request_reviewed' },
{ label: '撤单申请审核', value: 'worker_cancel_request_reviewed' },
{ label: '打手账号审核', value: 'worker_reviewed' },
{ label: '打手钱包手动加款', value: 'worker_wallet_credited' },
{ label: '重置打手密码', value: 'worker_password_reset' },
{ label: '创建后台用户', value: 'admin_user_created' },
{ label: '修改用户角色', value: 'admin_user_role_updated' },
{ label: '修改用户状态', value: 'admin_user_status_updated' },
@@ -43,10 +47,55 @@ export const adminAuditActionOptions = [
{ label: '换绑角色', value: 'task_kuaishou_cloud_rebind_role' },
{ label: '关闭任务', value: 'task_closed' },
{ label: '转人工处理', value: 'task_mark_manual_review' },
{ label: '工单发布', value: 'work_order_published' },
{ label: '工单下架', value: 'work_order_unpublished' },
{ label: '工单信息更新', value: 'work_order_updated' },
{ label: '工单删除', value: 'work_order_deleted' },
{ label: '工单验收', value: 'work_order_accepted' },
{ label: '批量验收', value: 'work_orders_batch_accepted' },
{ label: '工单标记问题', value: 'work_order_marked_problem' },
{ label: '问题单处置', value: 'work_order_problem_resolved' },
{ label: '工单重新打开', value: 'work_order_reopened' },
{ label: '工单置顶切换', value: 'work_order_pin_toggled' },
{ label: '工单指派打手', value: 'work_order_worker_assigned' },
{ label: '工单取消指派', value: 'work_order_worker_unassigned' },
{ label: '工单资料保存', value: 'work_order_material_saved' },
{ label: '拼单份额验收', value: 'work_order_share_accepted' },
{ label: '拼单份额取消', value: 'work_order_share_cancelled' },
{ label: '拼单配置更新', value: 'work_order_sharing_config_updated' },
{ label: '押金扣除', value: 'work_order_pending_deposit_deducted' },
{ label: '售后问题单创建', value: 'after_sales_case_created' },
{ label: '售后问题单处置', value: 'after_sales_case_resolved' },
{ label: '接单等级保存', value: 'worker_level_saved' },
{ label: '接单等级删除', value: 'worker_level_deleted' },
{ label: '接单分类保存', value: 'work_category_saved' },
{ label: '接单分类删除', value: 'work_category_deleted' },
{ label: '接单模板保存', value: 'work_product_rule_saved' },
{ label: '接单模板删除', value: 'work_product_rule_deleted' },
{ label: '模板价格同步', value: 'work_product_rule_price_synced' },
{ label: '商品映射保存', value: 'work_product_mapping_saved' },
{ label: '打手提现账户保存', value: 'worker_withdrawal_account_saved' },
{ label: '资金配置保存', value: 'worker_finance_config_saved' },
{ label: '大厅配置保存', value: 'worker_hall_config_saved' },
{ label: '公告保存', value: 'worker_announcement_saved' },
{ label: '通知配置保存', value: 'worker_platform_notification_config_saved' },
{ label: '商品匹配配置保存', value: 'worker_product_match_config_saved' },
{ label: '更新店铺配置', value: 'platform_shop_config_updated' },
{ label: '更新履约配置', value: 'platform_fulfillment_config_updated' },
]
export const adminAuditTargetTypeOptions = [
{ label: '全部', value: '' },
{ label: '资金申请', value: 'worker_finance_request' },
{ label: '撤单申请', value: 'worker_cancel_request' },
{ label: '打手', value: 'worker' },
{ label: '接单工单', value: 'work_order' },
{ label: '拼单份额', value: 'work_order_share' },
{ label: '售后问题单', value: 'after_sales_case' },
{ label: '接单等级', value: 'worker_level' },
{ label: '接单分类', value: 'work_category' },
{ label: '接单模板', value: 'work_product_rule' },
{ label: '商品映射', value: 'work_product_mapping' },
{ label: '后台用户', value: 'admin_user' },
{ label: '交付任务', value: 'task' },
{ label: '平台配置', value: 'platform_config' },
]