feat: 新增客服备用金台账与后台用户中文显示名

- 新增备用金账户、流水、支出、对账与权限
- 后台用户支持中文显示名并同步备用金姓名
- 审计、登录记录、任务及工单操作人优先展示中文名
- 补充备用金与人工支出方案文档
This commit is contained in:
yml2213
2026-10-18 13:55:24 +08:00
parent 7bacb7d5eb
commit 7542d547bf
55 changed files with 6557 additions and 105 deletions
+328
View File
@@ -0,0 +1,328 @@
import type { Request } from 'express'
import { Router } from 'express'
import {
confirmAdminFundReconciliation,
createAdminFundAccount,
createAdminFundExpense,
createAdminFundFunding,
createAdminFundIssue,
createAdminFundReconciliation,
createAdminFundReturn,
ensureAdminFundAgentAccounts,
getAdminFundAccountDetail,
getAdminFundExpenseDetail,
getAdminFundMyAccount,
getAdminFundOverview,
getAdminFundSupervisorOptions,
listAdminFundAccounts,
listAdminFundExpenses,
listAdminFundReconciliations,
listAdminFundSetupOptions,
updateAdminFundAccount,
} from '../../services/admin/admin-fund-service.js'
import {
createJsonHandler,
requireAdminPermission,
requireAdminRoles,
requireAnyAdminPermission,
} from './session.js'
import type { AdminSession } from '../../services/admin/admin-auth-service.js'
import type { JsonObject } from '../../types/json.js'
const router = Router()
function sessionOf(req: Request): AdminSession {
return req.adminSession as AdminSession
}
function accountIdOf(req: Request): number {
return Number(req.params.accountId)
}
function expenseIdOf(req: Request): number {
return Number(req.params.expenseId)
}
function reconciliationIdOf(req: Request): number {
return Number(req.params.reconciliationId)
}
function bodyWithAccountId(req: Request, key: string): JsonObject {
const body = req.body && typeof req.body === 'object' ? (req.body as JsonObject) : {}
return { ...body, [key]: accountIdOf(req) }
}
router.use('/fund', requireAdminRoles(['admin', 'operator', 'support', 'finance']))
router.get(
'/fund/overview',
requireAdminPermission('fund.view'),
createJsonHandler((req) => getAdminFundOverview(sessionOf(req)), {
successMessage: 'ok',
errorMessage: '读取备用金总览失败',
scope: '[admin/fund/overview]',
}),
)
router.get(
'/fund/my',
requireAdminPermission('fund.view'),
createJsonHandler((req) => getAdminFundMyAccount(sessionOf(req)), {
successMessage: 'ok',
errorMessage: '读取我的备用金失败',
scope: '[admin/fund/my]',
}),
)
router.get(
'/fund/supervisors',
requireAdminPermission('fund.view'),
createJsonHandler((req) => getAdminFundSupervisorOptions(sessionOf(req)), {
successMessage: 'ok',
errorMessage: '读取主管资金池失败',
scope: '[admin/fund/supervisors]',
}),
)
router.get(
'/fund/options',
requireAdminRoles(['admin']),
createJsonHandler((req) => listAdminFundSetupOptions(sessionOf(req)), {
successMessage: 'ok',
errorMessage: '读取备用金初始化选项失败',
scope: '[admin/fund/options]',
}),
)
router.get(
'/fund/accounts',
requireAdminPermission('fund.view'),
createJsonHandler((req) => listAdminFundAccounts(req.query as JsonObject, sessionOf(req)), {
successMessage: 'ok',
errorMessage: '读取备用金账户失败',
scope: '[admin/fund/accounts]',
}),
)
router.get(
'/fund/accounts/:accountId',
requireAdminPermission('fund.view'),
createJsonHandler(
(req) => getAdminFundAccountDetail(accountIdOf(req), req.query as JsonObject, sessionOf(req)),
{
successMessage: 'ok',
errorMessage: '读取备用金账户详情失败',
scope: '[admin/fund/accounts/:accountId]',
},
),
)
router.post(
'/fund/accounts',
requireAdminRoles(['admin']),
createJsonHandler((req) => createAdminFundAccount(req.body as JsonObject, sessionOf(req)), {
successMessage: '备用金账户已创建',
errorMessage: '创建备用金账户失败',
scope: '[admin/fund/accounts]',
audit: (req, data) => ({
action: 'fund_account_created',
targetType: 'fund_account',
targetId: String((data as { account?: { id?: number } })?.account?.id || ''),
data: asRecord(data),
}),
}),
)
router.post(
'/fund/accounts/ensure-agents',
requireAdminRoles(['admin']),
createJsonHandler((req) => ensureAdminFundAgentAccounts(req.body as JsonObject, sessionOf(req)), {
successMessage: '客服备用金账户已初始化',
errorMessage: '批量初始化客服备用金账户失败',
scope: '[admin/fund/accounts/ensure-agents]',
audit: (_req, data) => ({
action: 'fund_agent_accounts_ensured',
targetType: 'fund_account',
targetId: 'batch',
data: asRecord(data),
}),
}),
)
router.patch(
'/fund/accounts/:accountId',
requireAdminRoles(['admin']),
createJsonHandler(
(req) => updateAdminFundAccount(accountIdOf(req), req.body as JsonObject, sessionOf(req)),
{
successMessage: '备用金账户已更新',
errorMessage: '更新备用金账户失败',
scope: '[admin/fund/accounts/:accountId]',
audit: (req, data) => ({
action: 'fund_account_updated',
targetType: 'fund_account',
targetId: String(req.params.accountId || ''),
data: asRecord(data),
}),
},
),
)
router.post(
'/fund/accounts/:accountId/funding',
requireAdminRoles(['admin']),
createJsonHandler(
(req) => createAdminFundFunding(bodyWithAccountId(req, 'supervisorAccountId'), sessionOf(req)),
{
successMessage: '主管资金池已拨付',
errorMessage: '拨付主管资金池失败',
scope: '[admin/fund/accounts/:accountId/funding]',
audit: (req, data) => ({
action: 'fund_funding',
targetType: 'fund_account',
targetId: String(req.params.accountId || ''),
data: asRecord(data),
}),
},
),
)
router.post(
'/fund/accounts/:accountId/issue',
requireAdminPermission('fund.issue'),
createJsonHandler(
(req) => createAdminFundIssue(bodyWithAccountId(req, 'agentAccountId'), sessionOf(req)),
{
successMessage: '备用金已发放',
errorMessage: '发放备用金失败',
scope: '[admin/fund/accounts/:accountId/issue]',
audit: (req, data) => ({
action: 'fund_issue',
targetType: 'fund_account',
targetId: String(req.params.accountId || ''),
data: asRecord(data),
}),
},
),
)
router.post(
'/fund/accounts/:accountId/return',
requireAnyAdminPermission(['fund.issue', 'fund.expense']),
createJsonHandler(
(req) => createAdminFundReturn(bodyWithAccountId(req, 'agentAccountId'), sessionOf(req)),
{
successMessage: '备用金已缴回',
errorMessage: '缴回备用金失败',
scope: '[admin/fund/accounts/:accountId/return]',
audit: (req, data) => ({
action: 'fund_return',
targetType: 'fund_account',
targetId: String(req.params.accountId || ''),
data: asRecord(data),
}),
},
),
)
router.post(
'/fund/accounts/:accountId/expenses',
requireAdminPermission('fund.expense'),
createJsonHandler(
(req) => createAdminFundExpense(bodyWithAccountId(req, 'agentAccountId'), sessionOf(req)),
{
successMessage: '备用金支出已登记',
errorMessage: '登记备用金支出失败',
scope: '[admin/fund/accounts/:accountId/expenses]',
audit: (req, data) => ({
action: 'fund_expense_created',
targetType: 'fund_expense',
targetId: String((data as { expense?: { id?: number } })?.expense?.id || ''),
data: asRecord(data),
}),
},
),
)
router.get(
'/fund/expenses',
requireAdminPermission('fund.view'),
createJsonHandler((req) => listAdminFundExpenses(req.query as JsonObject, sessionOf(req)), {
successMessage: 'ok',
errorMessage: '读取备用金支出失败',
scope: '[admin/fund/expenses]',
}),
)
router.get(
'/fund/expenses/:expenseId',
requireAdminPermission('fund.view'),
createJsonHandler((req) => getAdminFundExpenseDetail(expenseIdOf(req), sessionOf(req)), {
successMessage: 'ok',
errorMessage: '读取备用金支出详情失败',
scope: '[admin/fund/expenses/:expenseId]',
}),
)
router.get(
'/fund/reconciliations',
requireAdminPermission('fund.view'),
createJsonHandler(
(req) => listAdminFundReconciliations(req.query as JsonObject, sessionOf(req)),
{
successMessage: 'ok',
errorMessage: '读取备用金对账失败',
scope: '[admin/fund/reconciliations]',
},
),
)
router.post(
'/fund/reconciliations',
requireAdminPermission('fund.reconcile'),
createJsonHandler(
(req) => createAdminFundReconciliation(req.body as JsonObject, sessionOf(req)),
{
successMessage: '对账已发起',
errorMessage: '发起备用金对账失败',
scope: '[admin/fund/reconciliations]',
audit: (_req, data) => ({
action: 'fund_reconciliation_created',
targetType: 'fund_reconciliation',
targetId: String((data as { reconciliation?: { id?: number } })?.reconciliation?.id || ''),
data: asRecord(data),
}),
},
),
)
router.post(
'/fund/reconciliations/:reconciliationId/confirm',
requireAdminPermission('fund.reconcile'),
createJsonHandler(
(req) =>
confirmAdminFundReconciliation(
reconciliationIdOf(req),
req.body as JsonObject,
sessionOf(req),
),
{
successMessage: '对账已确认',
errorMessage: '确认备用金对账失败',
scope: '[admin/fund/reconciliations/:reconciliationId/confirm]',
audit: (req, data) => ({
action: 'fund_reconciliation_confirmed',
targetType: 'fund_reconciliation',
targetId: String(req.params.reconciliationId || ''),
data: asRecord(data),
}),
},
),
)
function asRecord(data: unknown): Record<string, unknown> {
return data && typeof data === 'object' ? (data as Record<string, unknown>) : { a: data }
}
export default router
+26
View File
@@ -4,6 +4,7 @@ import {
createManagedAdminUser,
getAdminUserList,
resetManagedAdminUserPassword,
updateManagedAdminUserDisplayName,
updateManagedAdminUserRole,
updateManagedAdminUserStatus,
} from '../../services/admin/admin-auth-service.js'
@@ -13,6 +14,7 @@ type AdminUserMutationResult = {
user: {
userId: number
username: string
displayName: string
role: string
status?: string
}
@@ -82,6 +84,30 @@ router.post(
),
)
router.post(
'/users/:userId/display-name',
createJsonHandler(
(req) => updateManagedAdminUserDisplayName(String(req.params.userId || ''), req.body),
{
successMessage: '用户姓名已更新',
errorMessage: '更新用户姓名失败',
scope: '[admin/users/:userId/display-name]',
audit: (_req, data) => {
const result = data as AdminUserMutationResult
return {
action: 'admin_user_display_name_updated',
targetType: 'admin_user',
targetId: String(result.user.userId),
data: {
username: result.user.username,
displayName: result.user.displayName,
},
}
},
},
),
)
router.post(
'/users/:userId/status',
createJsonHandler(
@@ -21,7 +21,7 @@ router.post(
markAdminWorkOrderProblem(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '订单已标记为问题单',
@@ -45,7 +45,7 @@ router.post(
resolveAdminProblemWorkOrder(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '问题单已处置',
@@ -66,7 +66,10 @@ router.post(
requireAdminPermission('worker_order.accept'),
createJsonHandler(
(req) =>
acceptAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
acceptAdminWorkOrder(
String(req.params.workOrderId || ''),
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '订单已验收通过',
errorMessage: '验收接单订单失败',
@@ -89,7 +92,7 @@ router.post(
acceptAdminWorkOrderShare(
String(req.params.workOrderId || ''),
String(req.params.shareId || ''),
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '拼单份额已验收通过',
@@ -113,7 +116,7 @@ router.post(
cancelAdminWorkOrderSharing(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
req.adminSession?.permissions || [],
),
{
@@ -142,7 +145,7 @@ router.post(
String(req.params.workOrderId || ''),
String(req.params.shareId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '拼单参与者已撤单,份数已退回大厅',
@@ -166,7 +169,7 @@ router.post(
deductAdminWorkOrderPendingDeposit(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '待解冻押金已扣减',
@@ -39,7 +39,7 @@ router.post(
createAdminAfterSalesCase(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '售后问题单已创建',
@@ -63,7 +63,7 @@ router.post(
resolveAdminAfterSalesCase(
String(req.params.caseId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '售后问题单已处置',
@@ -87,7 +87,7 @@ router.post(
repayAdminAfterSalesCaseDebt(
String(req.params.caseId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '售后欠款已偿还',
@@ -167,7 +167,7 @@ router.post(
(req) =>
syncAdminWorkProductRulePrice(
String(req.params.ruleId || ''),
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '模板价格已同步',
@@ -68,7 +68,11 @@ router.post(
'/worker-platform/consignment/inventories',
requireAdminPermission('consignment.manage'),
createJsonHandler(
(req) => createAdminConsignmentInventory(req.body, req.adminSession?.username || 'admin'),
(req) =>
createAdminConsignmentInventory(
req.body,
req.adminSession?.displayName || req.adminSession?.username || 'admin',
),
{
successMessage: '寄卖物品已入库',
errorMessage: '创建寄卖物品失败',
@@ -85,7 +89,7 @@ router.put(
updateAdminConsignmentInventory(
String(req.params.id || ''),
req.body,
req.adminSession?.username || 'admin',
req.adminSession?.displayName || req.adminSession?.username || 'admin',
),
{
successMessage: '寄卖物品已更新',
@@ -127,7 +131,7 @@ router.post(
returnAdminConsignmentInventory(
String(req.params.id || ''),
req.body,
req.adminSession?.username || 'admin',
req.adminSession?.displayName || req.adminSession?.username || 'admin',
),
{
successMessage: '物品已退回复库',
@@ -167,7 +171,7 @@ router.post(
approveAdminConsignmentSubmission(
String(req.params.id || ''),
req.body,
req.adminSession?.username || 'admin',
req.adminSession?.displayName || req.adminSession?.username || 'admin',
),
{
successMessage: '审核通过,已正式入库',
@@ -185,7 +189,7 @@ router.post(
rejectAdminConsignmentSubmission(
String(req.params.id || ''),
req.body,
req.adminSession?.username || 'admin',
req.adminSession?.displayName || req.adminSession?.username || 'admin',
),
{
successMessage: '已驳回该提报',
@@ -247,7 +251,7 @@ router.post(
fulfillWorkOrderWithConsignment({
workOrderId: String(req.body?.workOrderId || ''),
inventoryId: String(req.body?.inventoryId || ''),
actorName: req.adminSession?.username || 'admin',
actorName: req.adminSession?.displayName || req.adminSession?.username || 'admin',
allowPartial: req.body?.allowPartial === true,
allowSpecMismatch: req.body?.allowSpecMismatch === true,
matchNote: String(req.body?.matchNote || ''),
@@ -269,7 +273,7 @@ router.post(
workOrderId: String(req.body?.workOrderId || ''),
inventoryId: String(req.body?.inventoryId || ''),
workerId: req.body?.workerId ? Number(req.body.workerId) : null,
actorName: req.adminSession?.username || 'admin',
actorName: req.adminSession?.displayName || req.adminSession?.username || 'admin',
allowPartial: req.body?.allowPartial === true,
allowSpecMismatch: req.body?.allowSpecMismatch === true,
matchNote: String(req.body?.matchNote || ''),
@@ -299,7 +303,7 @@ router.post(
// 押金处置由运营显式选择,非法值一律归一成"按规则自动"
workerDeposit: normalizeWorkerDepositHandling(req.body?.workerDeposit),
consignmentDeposit: normalizeConsignmentDepositHandling(req.body?.consignmentDeposit),
actorName: req.adminSession?.username || 'admin',
actorName: req.adminSession?.displayName || req.adminSession?.username || 'admin',
}),
{
successMessage: '寄售履约已撤销',
@@ -26,7 +26,7 @@ router.post(
reviewAdminWorkerFinanceRequest(
String(req.params.requestId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '资金申请已处理',
@@ -153,7 +153,7 @@ router.post(
submitAdminWorkOrderMaterial(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '订单资料已保存',
@@ -176,7 +176,7 @@ router.put(
(req) =>
updateAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}, {
canEditRequirementFields: req.adminSession?.role !== 'support',
actorName: req.adminSession?.username || '',
actorName: req.adminSession?.displayName || req.adminSession?.username || '',
}),
{
successMessage: '工单信息已保存',
@@ -200,7 +200,7 @@ router.put(
updateAdminWorkOrderNote(
String(req.params.workOrderId || ''),
req.body?.note,
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '工单备注已保存',
@@ -224,7 +224,7 @@ router.post(
assignAdminWorkOrderToWorker(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '已指派打手',
@@ -247,7 +247,7 @@ router.post(
(req) =>
unassignAdminWorkOrder(
String(req.params.workOrderId || ''),
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '已取消指派',
@@ -271,7 +271,7 @@ router.post(
cancelAdminWorkOrder(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
req.adminSession?.permissions || [],
),
{
@@ -314,7 +314,7 @@ router.post(
(req) =>
reopenAdminCancelledWorkOrder(
String(req.params.workOrderId || ''),
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '订单已恢复为未分配状态',
@@ -335,7 +335,10 @@ router.post(
requireAdminPermission('worker_order.publish'),
createJsonHandler(
(req) =>
publishAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
publishAdminWorkOrder(
String(req.params.workOrderId || ''),
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '订单已进入大厅队列',
errorMessage: '发布接单订单失败',
@@ -357,7 +360,7 @@ router.post(
(req) =>
unpublishAdminWorkOrder(
String(req.params.workOrderId || ''),
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '订单已撤回到未分配',
@@ -381,7 +384,7 @@ router.post(
pinAdminWorkOrder(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '置顶状态已更新',
@@ -404,7 +407,7 @@ router.post(
(req) =>
confirmAdminWorkOrderFriendAdded(
String(req.params.workOrderId || ''),
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '已确认加好友,赠送冷却开始',
@@ -428,7 +431,7 @@ router.post(
resetAdminWorkOrderGiftMaterial(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '已重置打手资料,等待打手重新提交',
@@ -452,7 +455,7 @@ router.post(
completeAdminWorkOrderGiftCooldown(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '已手动结束赠送冷却,打手现在可以提交验收',
@@ -496,7 +499,7 @@ router.post(
updateAdminWorkOrderSharing(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '拼单配置已保存',
@@ -516,7 +519,11 @@ router.post(
'/worker-platform/orders/accept-batch',
requireAdminPermission('worker_order.accept'),
createJsonHandler(
(req) => acceptAdminWorkOrders(req.body || {}, req.adminSession?.username || ''),
(req) =>
acceptAdminWorkOrders(
req.body || {},
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '选中订单已验收通过',
errorMessage: '批量验收接单订单失败',
@@ -38,7 +38,7 @@ router.post(
resolveAdminWorkerOrderFeedback(
String(req.params.feedbackId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '问题反馈已处理',
@@ -56,7 +56,7 @@ router.post(
reviewAdminWorkerCancelRequest(
String(req.params.requestId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '打手退单申请已审核',
@@ -18,6 +18,7 @@ function actorOf(req: Request) {
return {
userId: req.adminSession?.userId ?? null,
username: req.adminSession?.username || '',
displayName: req.adminSession?.displayName || req.adminSession?.username || '',
}
}
@@ -73,7 +73,7 @@ router.post(
reviewAdminWorkerUser(
String(req.params.workerId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '打手账号状态已更新',
@@ -97,7 +97,7 @@ router.post(
updateAdminWorkerLevelLock(
String(req.params.workerId || ''),
req.body || {},
req.adminSession?.username || '',
req.adminSession?.displayName || req.adminSession?.username || '',
),
{
successMessage: '打手等级锁定状态已更新',