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
@@ -46,6 +46,7 @@ export async function getAdminAuditLogs(query: JsonObject = {}) {
logId: Number(item.id),
actorUserId: Number(item.actor_user_id || 0),
actorUsername: item.actor_username,
actorDisplayName: String(item.actor_display_name || item.actor_username || ''),
actorRole: item.actor_role,
action: item.action,
targetType: item.target_type,
@@ -28,6 +28,7 @@ export type AdminSession = {
sessionId: string
userId: number
username: string
displayName: string
role: AdminRole
expiresAt: string
sessionVersion: number
@@ -48,6 +49,7 @@ export async function ensureAdminUsersBootstrapped(): Promise<void> {
const username = String(configuredUser?.username || '')
.trim()
.toLowerCase()
const displayName = normalizeDisplayName(configuredUser?.displayName, username) || username
const password = String(configuredUser?.password || '').trim()
const role = normalizeAdminRole(configuredUser?.role)
@@ -62,6 +64,7 @@ export async function ensureAdminUsersBootstrapped(): Promise<void> {
const now = nowIso()
await createAdminUser({
username,
displayName,
passwordHash: hashAdminPassword(password),
role,
status: 'active',
@@ -229,6 +232,7 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
sessionId,
userId: Number(user.id),
username: String(user.username || ''),
displayName: String(user.display_name || user.username || ''),
role: normalizeAdminRole(user.role),
expiresAt,
sessionVersion: currentSessionVersion,
@@ -251,6 +255,7 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
user: {
userId: session.userId,
username: session.username,
displayName: session.displayName,
role: session.role,
permissions: session.permissions,
},
@@ -277,7 +282,7 @@ export async function getAdminUserList(query: JsonObject = {}): Promise<JsonObje
const { items, total } = await listAdminUsers({
page,
pageSize,
username: String(query.username || '').trim(),
keyword: String(query.keyword || query.username || '').trim(),
role: normalizeRoleQuery(query.role),
status: normalizeStatusQuery(query.status),
})
@@ -290,6 +295,7 @@ export async function getAdminUserList(query: JsonObject = {}): Promise<JsonObje
export async function createManagedAdminUser(payload: JsonObject = {}): Promise<JsonObject> {
const username = normalizeUsername(payload.username)
const displayName = normalizeDisplayName(payload.displayName, username) || username
const password = normalizePassword(payload.password)
if (!username) {
@@ -319,6 +325,7 @@ export async function createManagedAdminUser(payload: JsonObject = {}): Promise<
const now = nowIso()
const created = await createAdminUser({
username,
displayName,
passwordHash: hashAdminPassword(password),
role: normalizeAdminRole(payload.role),
status: normalizeAdminUserStatus(payload.status || 'active'),
@@ -401,6 +408,28 @@ export async function updateManagedAdminUserStatus(
}
}
export async function updateManagedAdminUserDisplayName(
userId: number | string,
payload: JsonObject = {},
): Promise<JsonObject> {
const user = await getRequiredAdminUser(userId)
const displayName = normalizeDisplayName(payload.displayName, user.username) || user.username
const updated = await updateAdminUser(user.id, {
display_name: displayName,
updated_at: nowIso(),
})
if (!updated) {
throw createHttpError('后台用户更新失败', {
statusCode: 500,
errorCode: 'admin_user_update_failed',
})
}
return {
user: mapAdminUser(updated),
}
}
export async function resetManagedAdminUserPassword(
userId: number | string,
payload: JsonObject = {},
@@ -472,6 +501,7 @@ function createAdminSession(user: AdminUserRow): JsonObject {
user: {
userId: Number(user.id),
username: String(user.username || ''),
displayName: String(user.display_name || user.username || ''),
role: normalizeAdminRole(user.role),
},
}
@@ -559,6 +589,27 @@ function normalizeUsername(username: unknown): string {
.toLowerCase()
}
function normalizeDisplayName(displayName: unknown, fallback = ''): string {
const normalized = String(displayName ?? '')
.trim()
.replace(/\s+/g, ' ')
if (!normalized) {
return fallback
}
const characters = Array.from(normalized)
const hasControlCharacter = characters.some((character) => {
const codePoint = character.codePointAt(0) || 0
return codePoint <= 0x1f || codePoint === 0x7f
})
if (characters.length > 32 || hasControlCharacter) {
throw createHttpError('姓名格式无效,最多 32 个字符且不能包含控制字符', {
statusCode: 400,
errorCode: 'admin_user_display_name_invalid',
})
}
return normalized
}
function normalizePassword(password: unknown): string {
return String(password || '').trim()
}
@@ -634,6 +685,7 @@ function mapAdminUser(user: AdminUserRow): JsonObject {
return {
userId: Number(user.id),
username: String(user.username || ''),
displayName: String(user.display_name || user.username || ''),
role: normalizeAdminRole(user.role),
status: normalizeAdminUserStatus(user.status),
createdAt: user.created_at,
File diff suppressed because it is too large Load Diff
@@ -67,6 +67,7 @@ export async function getAdminLoginLogs(query: JsonObject = {}) {
logId: Number(item.id),
userId: Number(item.user_id || 0) || null,
username: String(item.username || '').trim(),
displayName: String(item.display_name || item.username || '').trim(),
role: String(item.role || '').trim(),
ip: String(item.ip || '').trim(),
location: String(item.location || '').trim(),
@@ -6,6 +6,12 @@ export const ADMIN_PERMISSION_KEYS = [
'finance.review',
'finance.config',
'finance.export',
'fund.view',
'fund.issue',
'fund.expense',
'fund.reconcile',
'fund.adjust',
'fund.export',
'worker_order.view',
'worker_order.edit',
'worker_order.configure_share',
@@ -5,6 +5,7 @@ import {
canViewerCloseTask,
createAdminViewerContext,
mapKuaishouCloudFulfillmentContext,
mapManualDispatchContext,
resolveAdminTaskScreenshotUrl,
resolveOrderItemSkuNick,
resolveOrderItemTitle,
@@ -68,6 +69,23 @@ test('createAdminViewerContext lets support rebind-assisted actions without full
assert.equal(operator.canManageTaskLifecycle, true)
})
test('mapManualDispatchContext prefers display name and falls back to username', () => {
const withDisplayName = mapManualDispatchContext({
completedBy: { userId: 3, username: 'kf_1', displayName: '客服-1', role: 'support' },
})
assert.deepEqual(withDisplayName?.completedBy, {
userId: 3,
username: 'kf_1',
displayName: '客服-1',
role: 'support',
})
const legacy = mapManualDispatchContext({
completedBy: { userId: 3, username: 'kf_1', role: 'support' },
})
assert.equal(legacy?.completedBy?.displayName, 'kf_1')
})
test('mapKuaishouCloudFulfillmentContext exposes cloud account display labels', () => {
const flow = mapKuaishouCloudFulfillmentContext(
{
@@ -64,6 +64,9 @@ export function mapManualDispatchContext(
? {
userId: Number(record.completedBy.userId || 0) || 0,
username: String(record.completedBy.username || '').trim(),
displayName: String(
record.completedBy.displayName || record.completedBy.username || '',
).trim(),
role: String(record.completedBy.role || '').trim(),
}
: null,
@@ -333,6 +333,7 @@ function buildAdminActionActor(session: AdminViewerSessionInput | null) {
? {
userId: Number(session.userId || 0) || 0,
username: String(session.username || '').trim(),
displayName: String(session.displayName || session.username || '').trim(),
role: String(session.role || '').trim(),
}
: null
@@ -59,6 +59,12 @@ export async function updateAdminOrderClaimStatus(
claimStatus: updated.claim_status || payload.status,
claimStatusReason: updated.claim_status_reason || reason,
claimStatusUpdatedAt: updated.claim_status_updated_at || now,
operator: session ? { userId: session.userId, username: session.username } : null,
operator: session
? {
userId: session.userId,
username: session.username,
displayName: session.displayName || session.username,
}
: null,
}
}
@@ -96,6 +96,7 @@ export async function closeAdminTask(
? {
userId: session.userId,
username: session.username,
displayName: session.displayName || session.username,
role: session.role,
}
: null,
@@ -140,6 +141,7 @@ export async function markAdminTaskManualReview(
? {
userId: session.userId,
username: session.username,
displayName: session.displayName || session.username,
role: session.role,
}
: null,
@@ -215,6 +217,7 @@ export async function retryAdminTask(
? {
userId: session.userId,
username: session.username,
displayName: session.displayName || session.username,
role: session.role,
}
: null,
@@ -335,6 +338,7 @@ export async function regenerateAdminTaskClaimLink(
? {
userId: session.userId,
username: session.username,
displayName: session.displayName || session.username,
role: session.role,
}
: null,
@@ -394,6 +398,7 @@ export async function completeAdminTaskManualDispatch(
? {
userId: Number(session.userId || 0),
username: String(session.username || ''),
displayName: String(session.displayName || session.username || ''),
role: String(session.role || ''),
}
: null,
@@ -29,7 +29,11 @@ import { getWorkerRiskConfig, saveWorkerRiskConfig } from './worker-risk-config-
* - **留痕层**:worker_risk_events(只记显著事件)。持久、可追溯。
*/
export type WorkerRiskActor = { userId?: number | null; username?: string }
export type WorkerRiskActor = {
userId?: number | null
username?: string
displayName?: string
}
const OVERVIEW_SUMMARY_WINDOW_MS = 24 * 60 * 60 * 1000
@@ -123,7 +127,7 @@ export async function banAdminWorkerRisk(
reason,
action: 'manual',
actorUserId: actor.userId ?? null,
actorUsername: actor.username || '',
actorUsername: actor.displayName || actor.username || '',
})
return { workerId: id, status: 'frozen', reason }
@@ -162,7 +166,7 @@ export async function unbanAdminWorkerRisk(
reason,
action: 'manual',
actorUserId: actor.userId ?? null,
actorUsername: actor.username || '',
actorUsername: actor.displayName || actor.username || '',
})
return { workerId: id, status: 'active', reason }
@@ -182,7 +186,7 @@ export async function releaseAdminWorkerRiskTracker(
reason: '后台仅释放内存风控状态(账号状态未变更)',
action: 'manual',
actorUserId: actor.userId ?? null,
actorUsername: actor.username || '',
actorUsername: actor.displayName || actor.username || '',
})
return { workerId: id }
}