资金申请增加渠道筛选合计与审核留痕
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user