修复接单注册审核与拼单状态校验

This commit is contained in:
yml2213
2026-08-18 18:16:48 +08:00
parent ee79195f47
commit b2eaf3b6c7
8 changed files with 94 additions and 19 deletions
@@ -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'`,
@@ -3195,9 +3198,11 @@ function buildWorkOrderWhere({
AND wo.acceptance_json->>'evidenceStatus' = 'pending'
AND NULLIF(wo.acceptance_json->>'evidenceDueAt', '')::timestamptz > NOW()
`
if (normalizedStatuses.length > 0) {
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) {
+1 -1
View File
@@ -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 : '注册失败')