diff --git a/apps/backend/src/db/migrations/016_worker_display_name_unique.sql b/apps/backend/src/db/migrations/016_worker_display_name_unique.sql new file mode 100644 index 00000000..b3b2819c --- /dev/null +++ b/apps/backend/src/db/migrations/016_worker_display_name_unique.sql @@ -0,0 +1,15 @@ +-- 打手昵称唯一:昵称作为登录凭证之一,需全局唯一。 +-- 存量空昵称账号用账号名兜底(冲突时保持空昵称,不参与唯一约束)。 + +UPDATE worker_users +SET display_name = username +WHERE display_name = '' + AND NOT EXISTS ( + SELECT 1 FROM worker_users other + WHERE other.display_name = worker_users.username + AND other.id <> worker_users.id + ); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_worker_users_display_name_unique + ON worker_users (display_name) + WHERE display_name <> ''; diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index 40d0f0ea..75b4619a 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -126,6 +126,16 @@ export async function getWorkerUserByPhone(phone: string): Promise { + const result = await query( + `${WORKER_USER_SELECT} WHERE wu.display_name = $1 LIMIT 1`, + [String(displayName || '').trim()], + ) + return result.rows[0] || null +} + export async function getWorkerUserByInviteCode( inviteCode: string, ): Promise { diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index ef56a514..fd4e08ec 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -34,6 +34,7 @@ import { getWorkOrderById, getWorkOrderByOrderItemId, getWorkOrderShare, + getWorkProductRuleByKey, grabWorkOrder, incrementWorkerSessionVersion, joinWorkOrderShare, @@ -260,11 +261,19 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { : 0 : normalizeAmountFen(payload.requiredDepositAmount ?? payload.requiredDepositAmountYuan, 0) const fields = normalizeRequirementFieldsFromPayload(payload) + const ruleKey = normalizeSlugKey( + payload.ruleKey || payload.rule_key || skuCode || productName, + `rule-${Date.now()}`, + ) + const ruleId = normalizeOptionalId(payload.ruleId ?? payload.rule_id) + if (!ruleId && (await getWorkProductRuleByKey(ruleKey))) { + throw createHttpError('规则标识已存在,请更换后重试', { + statusCode: 409, + errorCode: 'work_product_rule_key_taken', + }) + } const rule = await upsertWorkProductRule({ - ruleKey: normalizeSlugKey( - payload.ruleKey || payload.rule_key || skuCode || productName, - `rule-${Date.now()}`, - ), + ruleKey, provider: String(payload.provider || '').trim(), platform: String(payload.platform || '').trim(), shopId: String(payload.shopId || payload.shop_id || '').trim(), diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index fd44bf73..516d7e20 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -36,6 +36,7 @@ import { getWorkerUserById, getWorkerUserByPhone, getWorkerUserByUsername, + getWorkerUserByDisplayName, getWorkOrderById, getWorkOrderByOrderItemId, getWorkOrderShare, @@ -248,6 +249,13 @@ export async function registerWorker(payload: JsonObject = {}) { }) } + if (await getWorkerUserByDisplayName(displayName)) { + throw createHttpError('该昵称已被使用,请更换昵称', { + statusCode: 409, + errorCode: 'worker_display_name_taken', + }) + } + const defaults = await ensureWorkerPlatformDefaults() const now = nowIso() const created = await createWorkerUser({ @@ -327,7 +335,10 @@ export async function loginWorker(username: unknown, password: unknown) { }) } - const worker = await getWorkerUserByUsername(normalizedUsername) + const worker = + (await getWorkerUserByUsername(normalizedUsername)) || + (await getWorkerUserByPhone(normalizedUsername)) || + (await getWorkerUserByDisplayName(normalizedUsername)) if (!worker || !verifyWorkerPassword(normalizedPassword, worker.password_hash)) { throw createHttpError('账号或密码错误', { statusCode: 401, diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index 1598d9d2..33168169 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -102,6 +102,7 @@ export default function ProductRulesPanel() { }) async function saveRule(values: { + ruleId?: number ruleKey: string provider?: string platform?: string @@ -124,7 +125,10 @@ export default function ProductRulesPanel() { autoCreate?: boolean }) { try { - await saveAdminWorkProductRule(values) + await saveAdminWorkProductRule({ + ...values, + ruleId: editingRule?.ruleId, + }) message.success(editingRule ? '物品规则已更新' : '物品规则已创建') resetRuleForm() await Promise.all([ @@ -346,7 +350,23 @@ export default function ProductRulesPanel() { { + const key = String(value || '').trim() + if ( + !editingRule && + (rulesQuery.data?.data.items || []).some( + (rule) => rule.ruleKey === key, + ) + ) { + return Promise.reject(new Error('规则标识已存在,请更换')) + } + return Promise.resolve() + }, + }, + ]} > diff --git a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx index 87b8cefa..1027bf76 100644 --- a/apps/frontend/src/pages/worker/WorkerLoginPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerLoginPage.tsx @@ -167,15 +167,19 @@ export default function WorkerLoginPage() { <>

欢迎回来

-

使用账号或手机号登录,进入抢单大厅

+

使用手机号、昵称或账号登录,进入抢单大厅

- } autoComplete="username" /> + } + autoComplete="username" + placeholder="手机号 / 昵称 / 账号" + /> } /> diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index a7e78b21..a3bfa3dd 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -87,6 +87,7 @@ export function fetchAdminWorkProductRules(params?: Record) { } export function saveAdminWorkProductRule(payload: { + ruleId?: number ruleKey: string provider?: string platform?: string