feat: 新增客服备用金台账与后台用户中文显示名
- 新增备用金账户、流水、支出、对账与权限 - 后台用户支持中文显示名并同步备用金姓名 - 审计、登录记录、任务及工单操作人优先展示中文名 - 补充备用金与人工支出方案文档
This commit is contained in:
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user