新增打手改昵称卡功能默认一张且一年冷却
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
-- 打手改昵称卡:默认每人 1 张,修改昵称后一年内锁定。
|
||||
|
||||
ALTER TABLE worker_users
|
||||
ADD COLUMN IF NOT EXISTS rename_cards INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN IF NOT EXISTS last_renamed_at TIMESTAMPTZ;
|
||||
|
||||
COMMENT ON COLUMN worker_users.rename_cards IS '剩余改昵称卡数量,注册默认 1 张';
|
||||
COMMENT ON COLUMN worker_users.last_renamed_at IS '最近一次修改昵称时间,修改后一年内禁止再次修改';
|
||||
@@ -38,6 +38,8 @@ export type WorkerUserRow = {
|
||||
pending_unfreeze_amount?: number
|
||||
total_credited_amount?: number
|
||||
total_settled_amount?: number
|
||||
rename_cards?: number
|
||||
last_renamed_at?: string | null
|
||||
}
|
||||
|
||||
export type WorkerSessionRow = {
|
||||
|
||||
@@ -325,3 +325,28 @@ function buildWorkerUserWhere({
|
||||
params,
|
||||
}
|
||||
}
|
||||
|
||||
/** 消耗一张改名卡并更新昵称;条件更新保证并发下不会超发。 */
|
||||
export async function consumeWorkerRenameCard(input: {
|
||||
workerId: number
|
||||
displayName: string
|
||||
now: string
|
||||
}): Promise<WorkerUserRow | null> {
|
||||
const result = await query<{ id: number }>(
|
||||
`
|
||||
UPDATE worker_users
|
||||
SET
|
||||
display_name = $1,
|
||||
rename_cards = rename_cards - 1,
|
||||
last_renamed_at = $2,
|
||||
updated_at = $2
|
||||
WHERE id = $3 AND rename_cards > 0
|
||||
RETURNING id
|
||||
`,
|
||||
[input.displayName, input.now, input.workerId],
|
||||
)
|
||||
if (!result.rows[0]) {
|
||||
return null
|
||||
}
|
||||
return getWorkerUserById(input.workerId)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { uploadSingleFile } from '../file-upload.js'
|
||||
import { uploadFileAsset } from '../../services/file-storage/file-storage-service.js'
|
||||
import {
|
||||
changeWorkerPassword,
|
||||
renameWorkerAccount,
|
||||
createWorkerRechargeRequest,
|
||||
createWorkerWithdrawRequest,
|
||||
createWorkerWithdrawalAccount,
|
||||
@@ -26,6 +27,16 @@ router.get(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/profile/rename',
|
||||
requireActiveWorker,
|
||||
createRouteHandler((req) => renameWorkerAccount(req.body || {}, getRequiredWorkerSession(req)), {
|
||||
successMessage: '昵称已修改',
|
||||
errorMessage: '修改昵称失败',
|
||||
scope: '[worker/profile/rename]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/profile/wallet-ledgers',
|
||||
createRouteHandler(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user