新增打手改昵称卡功能默认一张且一年冷却

This commit is contained in:
yml2213
2026-08-22 10:20:10 +08:00
parent 985e61209d
commit ba35d27976
13 changed files with 315 additions and 2 deletions
@@ -5,6 +5,7 @@ export * from './worker-auth-policy.js'
export * from './worker-session-view.js'
export * from './worker-session-auth-service.js'
export * from './worker-credentials-service.js'
export * from './worker-rename-service.js'
export * from './worker-sms-service.js'
export * from './worker-registration-service.js'
export * from './worker-login-service.js'
@@ -16,6 +16,7 @@ import {
import { resolveVisibleDelaySeconds, resolveWorkerPermissions } from './worker-permission-utils.js'
import { normalizeUploadedFiles } from './worker-input-normalizers.js'
import { mapAcceptanceForResponse } from './work-order-material-mappers.js'
import { WORKER_RENAME_COOLDOWN_MS } from './worker-rename-service.js'
export const DEFAULT_LEVEL_KEY = 'vip1'
export const DEFAULT_LEVEL_NAME = 'VIP1'
@@ -97,6 +98,11 @@ export function mapWorkerUser(worker: WorkerUserRow) {
createdAt: worker.created_at,
updatedAt: worker.updated_at,
reviewedAt: worker.reviewed_at,
renameCards: Number(worker.rename_cards ?? 1),
lastRenamedAt: worker.last_renamed_at || null,
renameUnlockedAt: worker.last_renamed_at
? new Date(Date.parse(worker.last_renamed_at) + WORKER_RENAME_COOLDOWN_MS).toISOString()
: null,
}
}
@@ -0,0 +1,94 @@
import {
consumeWorkerRenameCard,
getWorkerUserByDisplayName,
} from '../../repositories/worker-platform/index.js'
import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js'
import { mapWorkerUser } from './mappers.js'
import { getRequiredWorker, type WorkerSession } from './worker-session-context-service.js'
/** 改名冷却期:修改昵称后一年内禁止再次修改。 */
export const WORKER_RENAME_COOLDOWN_MS = 365 * 24 * 60 * 60 * 1000
const MAX_DISPLAY_NAME_LENGTH = 32
function normalizeDisplayName(value: unknown): string {
return String(value || '').trim()
}
/** 打手使用改名卡修改昵称;默认每人 1 张卡,修改后一年内锁定。昵称全平台唯一。 */
export async function renameWorkerAccount(payload: JsonObject = {}, session: WorkerSession) {
const worker = await getRequiredWorker(session.workerId)
const nextDisplayName = normalizeDisplayName(payload.displayName || payload.nickname)
if (!nextDisplayName) {
throw createHttpError('请输入新的昵称', {
statusCode: 400,
errorCode: 'worker_rename_display_name_required',
})
}
if (nextDisplayName.length > MAX_DISPLAY_NAME_LENGTH) {
throw createHttpError(`昵称最长 ${MAX_DISPLAY_NAME_LENGTH} 个字符`, {
statusCode: 400,
errorCode: 'worker_rename_display_name_invalid',
})
}
if (nextDisplayName === String(worker.display_name || '')) {
throw createHttpError('新昵称不能与当前昵称一致', {
statusCode: 400,
errorCode: 'worker_rename_display_name_unchanged',
})
}
if (Number(worker.rename_cards ?? 1) <= 0) {
throw createHttpError('改名卡已用完,无法再次修改昵称', {
statusCode: 409,
errorCode: 'worker_rename_card_exhausted',
})
}
const lastRenamedMs = worker.last_renamed_at ? Date.parse(worker.last_renamed_at) : 0
if (Number.isFinite(lastRenamedMs) && lastRenamedMs > 0) {
const unlockedAtMs = lastRenamedMs + WORKER_RENAME_COOLDOWN_MS
if (Date.now() < unlockedAtMs) {
const unlockedAt = new Date(unlockedAtMs).toISOString()
throw createHttpError(
`修改昵称后一年内无法再次修改,${new Date(unlockedAtMs).toLocaleString('zh-CN')} 后才能修改`,
{ statusCode: 409, errorCode: 'worker_rename_cooldown', context: { unlockedAt } },
)
}
}
const existing = await getWorkerUserByDisplayName(nextDisplayName)
if (existing && Number(existing.id) !== Number(worker.id)) {
throw createHttpError('该昵称已被使用,请更换昵称', {
statusCode: 409,
errorCode: 'worker_display_name_taken',
})
}
const updated = await consumeWorkerRenameCard({
workerId: Number(worker.id),
displayName: nextDisplayName,
now: nowIso(),
}).catch((error: unknown) => {
if ((error as { code?: string }).code === '23505') {
throw createHttpError('该昵称已被使用,请更换昵称', {
statusCode: 409,
errorCode: 'worker_display_name_taken',
})
}
throw error
})
if (!updated) {
throw createHttpError('改名卡已用完,无法再次修改昵称', {
statusCode: 409,
errorCode: 'worker_rename_card_exhausted',
})
}
return {
worker: mapWorkerUser(updated),
reloginRequired: false,
}
}