From ba35d279760e776084652926b8414b5227bdf243 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 22 Aug 2026 10:20:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=89=93=E6=89=8B=E6=94=B9?= =?UTF-8?q?=E6=98=B5=E7=A7=B0=E5=8D=A1=E5=8A=9F=E8=83=BD=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=B8=80=E5=BC=A0=E4=B8=94=E4=B8=80=E5=B9=B4=E5=86=B7=E5=8D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/migrations/053_worker_rename_card.sql | 8 ++ .../src/repositories/worker-platform/types.ts | 2 + .../worker-platform/worker-repo.ts | 25 +++++ apps/backend/src/routes/worker/profile.ts | 11 +++ .../src/services/worker-platform/index.ts | 1 + .../worker-platform/worker-finance-mappers.ts | 6 ++ .../worker-platform/worker-rename-service.ts | 94 +++++++++++++++++++ .../worker/WorkerProfileAccountModals.tsx | 84 +++++++++++++++++ .../worker/WorkerProfileDesktopSections.tsx | 14 ++- .../pages/worker/WorkerProfileMobileView.tsx | 13 +++ .../src/pages/worker/WorkerProfilePage.tsx | 47 +++++++++- apps/frontend/src/services/worker.ts | 7 ++ apps/frontend/src/types/worker-platform.ts | 5 + 13 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 apps/backend/src/db/migrations/053_worker_rename_card.sql create mode 100644 apps/backend/src/services/worker-platform/worker-rename-service.ts diff --git a/apps/backend/src/db/migrations/053_worker_rename_card.sql b/apps/backend/src/db/migrations/053_worker_rename_card.sql new file mode 100644 index 00000000..55754432 --- /dev/null +++ b/apps/backend/src/db/migrations/053_worker_rename_card.sql @@ -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 '最近一次修改昵称时间,修改后一年内禁止再次修改'; diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 8351c0e4..ef2b5018 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -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 = { diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index cd9cec74..563e479e 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -325,3 +325,28 @@ function buildWorkerUserWhere({ params, } } + +/** 消耗一张改名卡并更新昵称;条件更新保证并发下不会超发。 */ +export async function consumeWorkerRenameCard(input: { + workerId: number + displayName: string + now: string +}): Promise { + 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) +} diff --git a/apps/backend/src/routes/worker/profile.ts b/apps/backend/src/routes/worker/profile.ts index 37d71684..8b378459 100644 --- a/apps/backend/src/routes/worker/profile.ts +++ b/apps/backend/src/routes/worker/profile.ts @@ -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( diff --git a/apps/backend/src/services/worker-platform/index.ts b/apps/backend/src/services/worker-platform/index.ts index 2c6d17c8..6e85b38f 100644 --- a/apps/backend/src/services/worker-platform/index.ts +++ b/apps/backend/src/services/worker-platform/index.ts @@ -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' diff --git a/apps/backend/src/services/worker-platform/worker-finance-mappers.ts b/apps/backend/src/services/worker-platform/worker-finance-mappers.ts index 581701dd..9b1ef901 100644 --- a/apps/backend/src/services/worker-platform/worker-finance-mappers.ts +++ b/apps/backend/src/services/worker-platform/worker-finance-mappers.ts @@ -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, } } diff --git a/apps/backend/src/services/worker-platform/worker-rename-service.ts b/apps/backend/src/services/worker-platform/worker-rename-service.ts new file mode 100644 index 00000000..1f0558e4 --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-rename-service.ts @@ -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, + } +} diff --git a/apps/frontend/src/pages/worker/WorkerProfileAccountModals.tsx b/apps/frontend/src/pages/worker/WorkerProfileAccountModals.tsx index e79bff47..e4330ba8 100644 --- a/apps/frontend/src/pages/worker/WorkerProfileAccountModals.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfileAccountModals.tsx @@ -1,6 +1,7 @@ import { Alert, Button, + Checkbox, Descriptions, Form, Image, @@ -24,6 +25,11 @@ export type PasswordFormValues = { confirmPassword?: string } +export type RenameFormValues = { + displayName?: string + confirmed?: boolean +} + export function WorkerProfileAccountModals({ isMobile, worker, @@ -41,6 +47,11 @@ export function WorkerProfileAccountModals({ submitPasswordChange, contactOpen, setContactOpen, + renameOpen, + setRenameOpen, + submittingRename, + renameForm, + submitRename, }: { isMobile: boolean worker: WorkerUser | undefined @@ -58,6 +69,11 @@ export function WorkerProfileAccountModals({ submitPasswordChange: (values: PasswordFormValues) => Promise contactOpen: boolean setContactOpen: (open: boolean) => void + renameOpen: boolean + setRenameOpen: (open: boolean) => void + submittingRename: boolean + renameForm: FormInstance + submitRename: (values: RenameFormValues) => Promise }) { return ( <> @@ -220,6 +236,74 @@ export function WorkerProfileAccountModals({ + + setRenameOpen(false)} + > + + + 昵称修改成功后,一年内无法再次修改,请谨慎操作! + + + 当前剩余改名卡 {worker?.renameCards ?? 1} 张,本次修改昵称将消耗 1 张。 + + + } + /> + + form={renameForm} + layout="vertical" + onFinish={(values) => void submitRename(values)} + style={{ marginTop: 16 }} + > + + + {worker?.displayName || worker?.username || '-'} + + + + value && value.trim() === String(worker?.displayName || '').trim() + ? Promise.reject(new Error('新昵称不能与当前昵称一致')) + : Promise.resolve(), + }, + ]} + > + + + + value ? Promise.resolve() : Promise.reject(new Error('请先勾选确认')), + }, + ]} + > + 我已知晓改名后将消耗 1 张改名卡,且一年内无法再次修改用户名 + + + + ) } diff --git a/apps/frontend/src/pages/worker/WorkerProfileDesktopSections.tsx b/apps/frontend/src/pages/worker/WorkerProfileDesktopSections.tsx index 02e906d5..16b26a34 100644 --- a/apps/frontend/src/pages/worker/WorkerProfileDesktopSections.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfileDesktopSections.tsx @@ -5,9 +5,10 @@ import { GlobalOutlined, LockOutlined, LogoutOutlined, + UserOutlined, WalletOutlined, } from '@ant-design/icons' -import { Avatar, Button, Card, Descriptions, Progress, Space, Tag, Typography } from 'antd' +import { Avatar, Button, Card, Descriptions, Progress, Space, Tag, Tooltip, Typography } from 'antd' import type { WorkerLevelProgress, @@ -124,6 +125,9 @@ export function WorkerProfileDesktopActions({ onAddWithdrawalAccount, onAddAlipayQrCode, onChangePassword, + onRename, + renameDisabled, + renameHint, onManageDevices, onContact, onLogout, @@ -136,6 +140,9 @@ export function WorkerProfileDesktopActions({ onAddWithdrawalAccount: () => void onAddAlipayQrCode: () => void onChangePassword: () => void + onRename: () => void + renameDisabled: boolean + renameHint: string onManageDevices: () => void onContact: () => void onLogout: () => void @@ -177,6 +184,11 @@ export function WorkerProfileDesktopActions({ 补充支付宝收款码 ) : null} + + + diff --git a/apps/frontend/src/pages/worker/WorkerProfileMobileView.tsx b/apps/frontend/src/pages/worker/WorkerProfileMobileView.tsx index fd7d3483..f67467d0 100644 --- a/apps/frontend/src/pages/worker/WorkerProfileMobileView.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfileMobileView.tsx @@ -6,6 +6,7 @@ import { LockOutlined, LogoutOutlined, ReloadOutlined, + UserSwitchOutlined, WalletOutlined, } from '@ant-design/icons' import { Alert, Avatar, Button, Progress, Tag } from 'antd' @@ -37,6 +38,8 @@ type WorkerProfileMobileViewProps = { onRecharge: () => void onWithdraw: () => void onChangePassword: () => void + onRename: () => void + renameDisabled: boolean onManageDevices: () => void onContact: () => void onAddAlipayQrCode: () => void @@ -59,6 +62,8 @@ export function WorkerProfileMobileView({ onRecharge, onWithdraw, onChangePassword, + onRename, + renameDisabled, onManageDevices, onContact, onAddAlipayQrCode, @@ -190,6 +195,14 @@ export function WorkerProfileMobileView({
+ } + color="#4338ca" + background="#eef2ff" + disabled={renameDisabled} + onClick={onRename} + /> } diff --git a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx index e7c16cb6..5641b936 100644 --- a/apps/frontend/src/pages/worker/WorkerProfilePage.tsx +++ b/apps/frontend/src/pages/worker/WorkerProfilePage.tsx @@ -15,12 +15,18 @@ import { deleteWorkerSession, fetchWorkerWalletLedgers, logoutWorker, + renameWorkerAccount, saveWorkerAlipayWithdrawalQrCode, } from '@/services/worker' import type { UploadedFile, WorkerSessionDevice } from '@/types/worker-platform' +import { formatDateTime } from '@/utils/date-time' import { useIsMobile } from '@/utils/use-is-mobile' import { clearWorkerSession } from '@/utils/worker-auth' -import { WorkerProfileAccountModals, type PasswordFormValues } from './WorkerProfileAccountModals' +import { + WorkerProfileAccountModals, + type PasswordFormValues, + type RenameFormValues, +} from './WorkerProfileAccountModals' import { WorkerProfileFinanceModals } from './WorkerProfileFinanceModals' import { WorkerProfileMobileView } from './WorkerProfileMobileView' import { @@ -80,6 +86,9 @@ export default function WorkerProfilePage() { const [withdrawalAccountOpen, setWithdrawalAccountOpen] = useState(false) const [alipayQrCodeOpen, setAlipayQrCodeOpen] = useState(false) const [passwordOpen, setPasswordOpen] = useState(false) + const [renameOpen, setRenameOpen] = useState(false) + const [submittingRename, setSubmittingRename] = useState(false) + const [renameForm] = Form.useForm() const [contactOpen, setContactOpen] = useState(false) const [devicesOpen, setDevicesOpen] = useState(false) const [submittingRecharge, setSubmittingRecharge] = useState(false) @@ -119,6 +128,15 @@ export default function WorkerProfilePage() { (account) => account.accountChannel === 'alipay', ) const canUseFinanceActions = worker?.status === 'active' + const renameCards = worker?.renameCards ?? 1 + const renameLocked = + Boolean(worker?.renameUnlockedAt) && Date.parse(worker?.renameUnlockedAt || '') > Date.now() + const canRename = renameCards > 0 && !renameLocked + const renameHint = renameLocked + ? `修改昵称后一年内不可再次修改,${formatDateTime(worker?.renameUnlockedAt || null)} 后可改` + : renameCards <= 0 + ? '改名卡已用完' + : `剩余改名卡 ${renameCards} 张,修改昵称后一年内不可再次修改` async function copyInviteCode() { if (!worker?.inviteCode) return @@ -326,6 +344,23 @@ export default function WorkerProfilePage() { } } + async function submitRename(values: RenameFormValues) { + setSubmittingRename(true) + try { + const response = await renameWorkerAccount({ + displayName: String(values.displayName || '').trim(), + }) + message.success(`昵称已修改为「${response.data.worker.displayName}」,一年内无法再次修改`) + setRenameOpen(false) + renameForm.resetFields() + await refreshAll() + } catch (error) { + message.error(error instanceof Error ? error.message : '修改昵称失败') + } finally { + setSubmittingRename(false) + } + } + async function submitPasswordChange(values: PasswordFormValues) { setSubmittingPassword(true) try { @@ -440,6 +475,8 @@ export default function WorkerProfilePage() { onRecharge={openRechargeModal} onWithdraw={openWithdrawModal} onChangePassword={() => setPasswordOpen(true)} + onRename={() => setRenameOpen(true)} + renameDisabled={!canRename} onManageDevices={() => setDevicesOpen(true)} onContact={() => setContactOpen(true)} onAddAlipayQrCode={openAlipayQrCodeModal} @@ -493,6 +530,9 @@ export default function WorkerProfilePage() { onAddWithdrawalAccount={openWithdrawalAccountModal} onAddAlipayQrCode={openAlipayQrCodeModal} onChangePassword={() => setPasswordOpen(true)} + onRename={() => setRenameOpen(true)} + renameDisabled={!canRename} + renameHint={renameHint} onManageDevices={() => setDevicesOpen(true)} onContact={() => setContactOpen(true)} onLogout={submitLogout} @@ -627,6 +667,11 @@ export default function WorkerProfilePage() { submitPasswordChange={submitPasswordChange} contactOpen={contactOpen} setContactOpen={setContactOpen} + renameOpen={renameOpen} + setRenameOpen={setRenameOpen} + submittingRename={submittingRename} + renameForm={renameForm} + submitRename={submitRename} /> ) diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index 9d878304..a89d6802 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -194,6 +194,13 @@ export function changeWorkerPassword(payload: { currentPassword: string; newPass ) } +export function renameWorkerAccount(payload: { displayName: string }) { + return apiPost<{ worker: WorkerUser; reloginRequired: boolean }>( + '/api/v1/worker/profile/rename', + payload, + ) +} + export function fetchWorkerHallOrders(params?: Record) { return apiGet('/api/v1/worker/hall/orders', params) } diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 40373dae..02ae8921 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -37,6 +37,11 @@ export type WorkerUser = { createdAt: string updatedAt: string reviewedAt: string | null + /** 剩余改名卡数量,注册默认 1 张 */ + renameCards: number + lastRenamedAt: string | null + /** 上次改名后的一年锁定期结束时间 */ + renameUnlockedAt: string | null } export type WorkerSessionDevice = {