feat: 打手支持昵称登录,物品规则标识唯一校验
- 昵称全局唯一(迁移 016 唯一索引 + 注册校验),存量空昵称自动补账号名 - 登录解析链扩展为 账号/手机号/昵称,均可加密码登录 - 登录页提示更新为「手机号 / 昵称 / 账号」,注册页提示昵称可作登录账号 - 新建物品规则时校验规则标识唯一:已存在则拒绝(409),编辑不受影响 - 前端规则表单增加规则标识重复的即时校验
This commit is contained in:
@@ -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 <> '';
|
||||||
@@ -126,6 +126,16 @@ export async function getWorkerUserByPhone(phone: string): Promise<WorkerUserRow
|
|||||||
return result.rows[0] || null
|
return result.rows[0] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getWorkerUserByDisplayName(
|
||||||
|
displayName: string,
|
||||||
|
): Promise<WorkerUserRow | null> {
|
||||||
|
const result = await query<WorkerUserRow>(
|
||||||
|
`${WORKER_USER_SELECT} WHERE wu.display_name = $1 LIMIT 1`,
|
||||||
|
[String(displayName || '').trim()],
|
||||||
|
)
|
||||||
|
return result.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
export async function getWorkerUserByInviteCode(
|
export async function getWorkerUserByInviteCode(
|
||||||
inviteCode: string,
|
inviteCode: string,
|
||||||
): Promise<WorkerUserRow | null> {
|
): Promise<WorkerUserRow | null> {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
getWorkOrderById,
|
getWorkOrderById,
|
||||||
getWorkOrderByOrderItemId,
|
getWorkOrderByOrderItemId,
|
||||||
getWorkOrderShare,
|
getWorkOrderShare,
|
||||||
|
getWorkProductRuleByKey,
|
||||||
grabWorkOrder,
|
grabWorkOrder,
|
||||||
incrementWorkerSessionVersion,
|
incrementWorkerSessionVersion,
|
||||||
joinWorkOrderShare,
|
joinWorkOrderShare,
|
||||||
@@ -260,11 +261,19 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
|||||||
: 0
|
: 0
|
||||||
: normalizeAmountFen(payload.requiredDepositAmount ?? payload.requiredDepositAmountYuan, 0)
|
: normalizeAmountFen(payload.requiredDepositAmount ?? payload.requiredDepositAmountYuan, 0)
|
||||||
const fields = normalizeRequirementFieldsFromPayload(payload)
|
const fields = normalizeRequirementFieldsFromPayload(payload)
|
||||||
const rule = await upsertWorkProductRule({
|
const ruleKey = normalizeSlugKey(
|
||||||
ruleKey: normalizeSlugKey(
|
|
||||||
payload.ruleKey || payload.rule_key || skuCode || productName,
|
payload.ruleKey || payload.rule_key || skuCode || productName,
|
||||||
`rule-${Date.now()}`,
|
`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,
|
||||||
provider: String(payload.provider || '').trim(),
|
provider: String(payload.provider || '').trim(),
|
||||||
platform: String(payload.platform || '').trim(),
|
platform: String(payload.platform || '').trim(),
|
||||||
shopId: String(payload.shopId || payload.shop_id || '').trim(),
|
shopId: String(payload.shopId || payload.shop_id || '').trim(),
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
getWorkerUserById,
|
getWorkerUserById,
|
||||||
getWorkerUserByPhone,
|
getWorkerUserByPhone,
|
||||||
getWorkerUserByUsername,
|
getWorkerUserByUsername,
|
||||||
|
getWorkerUserByDisplayName,
|
||||||
getWorkOrderById,
|
getWorkOrderById,
|
||||||
getWorkOrderByOrderItemId,
|
getWorkOrderByOrderItemId,
|
||||||
getWorkOrderShare,
|
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 defaults = await ensureWorkerPlatformDefaults()
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const created = await createWorkerUser({
|
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)) {
|
if (!worker || !verifyWorkerPassword(normalizedPassword, worker.password_hash)) {
|
||||||
throw createHttpError('账号或密码错误', {
|
throw createHttpError('账号或密码错误', {
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export default function ProductRulesPanel() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function saveRule(values: {
|
async function saveRule(values: {
|
||||||
|
ruleId?: number
|
||||||
ruleKey: string
|
ruleKey: string
|
||||||
provider?: string
|
provider?: string
|
||||||
platform?: string
|
platform?: string
|
||||||
@@ -124,7 +125,10 @@ export default function ProductRulesPanel() {
|
|||||||
autoCreate?: boolean
|
autoCreate?: boolean
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
await saveAdminWorkProductRule(values)
|
await saveAdminWorkProductRule({
|
||||||
|
...values,
|
||||||
|
ruleId: editingRule?.ruleId,
|
||||||
|
})
|
||||||
message.success(editingRule ? '物品规则已更新' : '物品规则已创建')
|
message.success(editingRule ? '物品规则已更新' : '物品规则已创建')
|
||||||
resetRuleForm()
|
resetRuleForm()
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -346,7 +350,23 @@ export default function ProductRulesPanel() {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label="规则标识"
|
label="规则标识"
|
||||||
name="ruleKey"
|
name="ruleKey"
|
||||||
rules={[{ required: true, message: '请输入规则标识' }]}
|
rules={[
|
||||||
|
{ required: true, message: '请输入规则标识' },
|
||||||
|
{
|
||||||
|
validator: (_, value) => {
|
||||||
|
const key = String(value || '').trim()
|
||||||
|
if (
|
||||||
|
!editingRule &&
|
||||||
|
(rulesQuery.data?.data.items || []).some(
|
||||||
|
(rule) => rule.ruleKey === key,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return Promise.reject(new Error('规则标识已存在,请更换'))
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="skin-training" disabled={Boolean(editingRule)} />
|
<Input placeholder="skin-training" disabled={Boolean(editingRule)} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -167,15 +167,19 @@ export default function WorkerLoginPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="auth-form-header">
|
<div className="auth-form-header">
|
||||||
<h1>欢迎回来</h1>
|
<h1>欢迎回来</h1>
|
||||||
<p>使用账号或手机号登录,进入抢单大厅</p>
|
<p>使用手机号、昵称或账号登录,进入抢单大厅</p>
|
||||||
</div>
|
</div>
|
||||||
<Form layout="vertical" onFinish={submitLogin} requiredMark={false}>
|
<Form layout="vertical" onFinish={submitLogin} requiredMark={false}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="账号 / 手机号"
|
label="手机号 / 昵称 / 账号"
|
||||||
name="username"
|
name="username"
|
||||||
rules={[{ required: true, message: '请输入账号或手机号' }]}
|
rules={[{ required: true, message: '请输入手机号、昵称或账号' }]}
|
||||||
>
|
>
|
||||||
<Input prefix={<UserOutlined />} autoComplete="username" />
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
autoComplete="username"
|
||||||
|
placeholder="手机号 / 昵称 / 账号"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="密码"
|
label="密码"
|
||||||
@@ -252,6 +256,7 @@ export default function WorkerLoginPage() {
|
|||||||
label="昵称"
|
label="昵称"
|
||||||
name="displayName"
|
name="displayName"
|
||||||
rules={[{ required: true, message: '请输入昵称' }]}
|
rules={[{ required: true, message: '请输入昵称' }]}
|
||||||
|
extra="昵称需唯一,注册后可作为登录账号使用"
|
||||||
>
|
>
|
||||||
<Input prefix={<UserOutlined />} />
|
<Input prefix={<UserOutlined />} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export function fetchAdminWorkProductRules(params?: Record<string, unknown>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function saveAdminWorkProductRule(payload: {
|
export function saveAdminWorkProductRule(payload: {
|
||||||
|
ruleId?: number
|
||||||
ruleKey: string
|
ruleKey: string
|
||||||
provider?: string
|
provider?: string
|
||||||
platform?: string
|
platform?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user