修复接单注册审核与拼单状态校验
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE worker_sms_codes
|
||||
ADD COLUMN IF NOT EXISTS failed_attempt_count INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN worker_sms_codes.failed_attempt_count IS '当前验证码已失败的校验次数,达到上限后验证码锁定';
|
||||
@@ -7,6 +7,7 @@ export type WorkerSmsCodeRow = {
|
||||
code_hash: string
|
||||
expires_at: string
|
||||
consumed_at: string | null
|
||||
failed_attempt_count: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -61,6 +62,29 @@ export async function consumeWorkerSmsCode(
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/** 记录错误验证码尝试,达到上限后立即锁定该验证码。 */
|
||||
export async function recordWorkerSmsCodeFailure(
|
||||
codeId: number,
|
||||
now: string,
|
||||
maxAttempts: number,
|
||||
): Promise<WorkerSmsCodeRow | null> {
|
||||
const result = await query<WorkerSmsCodeRow>(
|
||||
`
|
||||
UPDATE worker_sms_codes
|
||||
SET
|
||||
failed_attempt_count = COALESCE(failed_attempt_count, 0) + 1,
|
||||
consumed_at = CASE
|
||||
WHEN COALESCE(failed_attempt_count, 0) + 1 >= $3 THEN $2
|
||||
ELSE consumed_at
|
||||
END
|
||||
WHERE id = $1 AND consumed_at IS NULL
|
||||
RETURNING *
|
||||
`,
|
||||
[codeId, now, Math.max(1, Math.floor(maxAttempts))],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function countWorkerSmsCodesOnDay(
|
||||
phone: string,
|
||||
dayStartIso: string,
|
||||
|
||||
@@ -184,7 +184,7 @@ export async function getWorkOrderShare(
|
||||
): Promise<WorkOrderShareRow | null> {
|
||||
const result = await query<WorkOrderShareRow>(
|
||||
`${WORK_ORDER_SHARE_SELECT}
|
||||
WHERE wos.work_order_id = $1 AND wos.worker_id = $2 LIMIT 1`,
|
||||
WHERE wos.work_order_id = $1 AND wos.worker_id = $2 AND wos.status != 'cancelled' LIMIT 1`,
|
||||
[Number(workOrderId), Number(workerId)],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
@@ -2394,7 +2394,10 @@ export async function acceptWorkOrderShareAndSettle(input: {
|
||||
`UPDATE work_order_shares SET status = 'accepted', accepted_at = $1, updated_at = $1 WHERE id = $2`,
|
||||
[input.now, input.shareId],
|
||||
)
|
||||
const progressResult = await client.query<{ joined_quantity: number; unfinished_count: number }>(
|
||||
const progressResult = await client.query<{
|
||||
joined_quantity: number
|
||||
unfinished_count: number
|
||||
}>(
|
||||
`SELECT COALESCE(SUM(quantity), 0)::int AS joined_quantity,
|
||||
COUNT(*) FILTER (WHERE status != 'accepted' AND status != 'cancelled')::int AS unfinished_count
|
||||
FROM work_order_shares WHERE work_order_id = $1 AND status != 'cancelled'`,
|
||||
@@ -3197,7 +3200,9 @@ function buildWorkOrderWhere({
|
||||
`
|
||||
if (normalizedStatuses.length > 0) {
|
||||
params.push(normalizedStatuses)
|
||||
filters.push(`(${useOperationalStatus ? adminOperationalStatusExpression() : 'wo.status'} = ANY($${params.length}::text[]) OR (${vipEvidencePendingWhere}))`)
|
||||
filters.push(
|
||||
`(${useOperationalStatus ? adminOperationalStatusExpression() : 'wo.status'} = ANY($${params.length}::text[]) OR (${vipEvidencePendingWhere}))`,
|
||||
)
|
||||
} else {
|
||||
filters.push(`(${vipEvidencePendingWhere})`)
|
||||
}
|
||||
@@ -3210,7 +3215,9 @@ function buildWorkOrderWhere({
|
||||
`${workerOrderStatusExpression(`$${params.length}`)} = ANY(${statusesPlaceholder}::text[])`,
|
||||
)
|
||||
} else {
|
||||
filters.push(`${useOperationalStatus ? adminOperationalStatusExpression() : 'wo.status'} = ANY(${statusesPlaceholder}::text[])`)
|
||||
filters.push(
|
||||
`${useOperationalStatus ? adminOperationalStatusExpression() : 'wo.status'} = ANY(${statusesPlaceholder}::text[])`,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (workOrderId) {
|
||||
|
||||
@@ -70,7 +70,7 @@ router.post(
|
||||
key: getBodyFieldRateLimitKey('phone'),
|
||||
}),
|
||||
createRouteHandler((req) => registerWorker(req.body || {}), {
|
||||
successMessage: '注册成功,请使用手机号登录',
|
||||
successMessage: '注册成功,请等待后台审核',
|
||||
errorMessage: '接单账号注册失败',
|
||||
scope: '[worker/auth/register]',
|
||||
}),
|
||||
|
||||
@@ -134,8 +134,10 @@ export async function verifyAdminSessionToken(token: unknown): Promise<AdminSess
|
||||
})
|
||||
}
|
||||
|
||||
const [encodedPayload, signature] = normalizedToken.split('.')
|
||||
if (!encodedPayload || !signature) {
|
||||
const parts = normalizedToken.split('.')
|
||||
const encodedPayload = parts[0]
|
||||
const signature = parts[1]
|
||||
if (parts.length !== 2 || !encodedPayload || !signature) {
|
||||
throw createHttpError('后台登录态无效', {
|
||||
statusCode: 401,
|
||||
errorCode: 'admin_auth_invalid',
|
||||
|
||||
@@ -2057,7 +2057,11 @@ export async function acceptAdminWorkOrders(payload: JsonObject = {}, actorName
|
||||
const workOrders = await Promise.all(
|
||||
workOrderIds.map((workOrderId) => getRequiredWorkOrder(workOrderId)),
|
||||
)
|
||||
if (workOrders.some((workOrder) => workOrder.status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE)) {
|
||||
if (
|
||||
workOrders.some(
|
||||
(workOrder) => mapWorkOrderAdmin(workOrder).status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||
)
|
||||
) {
|
||||
throw createHttpError('选中的订单包含非待验收状态,请刷新后重试', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_batch_accept_status_invalid',
|
||||
@@ -2204,7 +2208,12 @@ export async function getAdminWorkerPlatformSummary() {
|
||||
statuses: [WORK_ORDER_STATUS.OPEN],
|
||||
excludeFilledSharing: true,
|
||||
}),
|
||||
listWorkOrders({ page: 1, pageSize: 1, statuses: [WORK_ORDER_STATUS.IN_PROGRESS], useOperationalStatus: true }),
|
||||
listWorkOrders({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
statuses: [WORK_ORDER_STATUS.IN_PROGRESS],
|
||||
useOperationalStatus: true,
|
||||
}),
|
||||
])
|
||||
return {
|
||||
pendingWorkerCount: pendingWorkers.total,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getWorkCategoryByKey,
|
||||
getWorkerLevelByKey,
|
||||
getLatestWorkerSmsCode,
|
||||
recordWorkerSmsCodeFailure,
|
||||
getWorkerUserByInviteCode,
|
||||
getWorkerUserById,
|
||||
getWorkerWithdrawalAccount,
|
||||
@@ -141,6 +142,7 @@ import {
|
||||
|
||||
const WORKER_DAILY_WITHDRAW_LIMIT = 3
|
||||
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
||||
const WORKER_SMS_MAX_VERIFY_ATTEMPTS = 5
|
||||
|
||||
export type WorkerSession = {
|
||||
sessionId: string
|
||||
@@ -198,7 +200,11 @@ export async function sendWorkerSmsCode(payload: JsonObject = {}) {
|
||||
}
|
||||
|
||||
const latest = await getLatestWorkerSmsCode(phone, 'register')
|
||||
if (latest && new Date(latest.created_at).getTime() > Date.now() - cooldownMs) {
|
||||
if (
|
||||
latest &&
|
||||
!latest.consumed_at &&
|
||||
new Date(latest.created_at).getTime() > Date.now() - cooldownMs
|
||||
) {
|
||||
throw createHttpError('验证码发送过于频繁,请稍后再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_cooldown',
|
||||
@@ -262,13 +268,24 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
latest.code_hash === hashSmsCode(code),
|
||||
)
|
||||
if (!codeValid) {
|
||||
if (latest && !latest.consumed_at) {
|
||||
const failed = await recordWorkerSmsCodeFailure(
|
||||
latest.id,
|
||||
nowIso(),
|
||||
WORKER_SMS_MAX_VERIFY_ATTEMPTS,
|
||||
)
|
||||
if (failed && failed.consumed_at) {
|
||||
throw createHttpError('验证码错误次数过多,请重新获取验证码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_sms_code_locked',
|
||||
})
|
||||
}
|
||||
}
|
||||
throw createHttpError('短信验证码错误或已过期', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_sms_code_invalid',
|
||||
})
|
||||
}
|
||||
await consumeWorkerSmsCode(latest!.id, nowIso())
|
||||
|
||||
if (await getWorkerUserByUsername(phone)) {
|
||||
throw createHttpError('该手机号已注册,请直接登录', {
|
||||
statusCode: 409,
|
||||
@@ -311,7 +328,7 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
displayName,
|
||||
phone,
|
||||
levelId: defaults.level?.id || null,
|
||||
status: 'active',
|
||||
status: 'pending_review',
|
||||
inviterId: inviter ? Number(inviter.id) : null,
|
||||
inviteCode: await generateInviteCode(now),
|
||||
now,
|
||||
@@ -324,9 +341,12 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
})
|
||||
}
|
||||
|
||||
// 账号创建成功后再消费验证码,避免邀请码、昵称或数据库异常导致验证码提前失效。
|
||||
await consumeWorkerSmsCode(latest!.id, nowIso())
|
||||
|
||||
return {
|
||||
worker: mapWorkerUser(created),
|
||||
reviewRequired: false,
|
||||
reviewRequired: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,8 +554,15 @@ export async function verifyWorkerSessionToken(token: unknown): Promise<WorkerSe
|
||||
})
|
||||
}
|
||||
|
||||
const [encodedPayload, signature] = normalizedToken.split('.')
|
||||
if (!encodedPayload || !signature || !safeCompare(signature, signWorkerPayload(encodedPayload))) {
|
||||
const parts = normalizedToken.split('.')
|
||||
const encodedPayload = parts[0]
|
||||
const signature = parts[1]
|
||||
if (
|
||||
parts.length !== 2 ||
|
||||
!encodedPayload ||
|
||||
!signature ||
|
||||
!safeCompare(signature, signWorkerPayload(encodedPayload))
|
||||
) {
|
||||
throw createHttpError('接单登录态无效', {
|
||||
statusCode: 401,
|
||||
errorCode: 'worker_auth_invalid',
|
||||
|
||||
@@ -155,8 +155,10 @@ export default function WorkerLoginPage() {
|
||||
}) {
|
||||
setLoading(true)
|
||||
try {
|
||||
await registerWorker(values)
|
||||
message.success('注册成功,请使用手机号登录')
|
||||
const response = await registerWorker(values)
|
||||
message.success(
|
||||
response.data.reviewRequired ? '注册成功,请等待后台审核' : '注册成功,请使用手机号登录',
|
||||
)
|
||||
registerForm.resetFields()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '注册失败')
|
||||
|
||||
Reference in New Issue
Block a user