From b2eaf3b6c7bdfe433ce9f2115886c7af2a8d1455 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 18 Aug 2026 18:16:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8E=A5=E5=8D=95=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E5=AE=A1=E6=A0=B8=E4=B8=8E=E6=8B=BC=E5=8D=95=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../039_worker_sms_verify_attempts.sql | 4 ++ .../worker-platform/sms-code-repo.ts | 24 +++++++++++ .../worker-platform/work-order-repo.ts | 17 +++++--- apps/backend/src/routes/worker.ts | 2 +- .../src/services/admin/admin-auth-service.ts | 6 ++- .../services/worker-platform/admin-service.ts | 13 +++++- .../worker-platform/worker-service.ts | 41 +++++++++++++++---- .../src/pages/worker/WorkerLoginPage.tsx | 6 ++- 8 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 apps/backend/src/db/migrations/039_worker_sms_verify_attempts.sql diff --git a/apps/backend/src/db/migrations/039_worker_sms_verify_attempts.sql b/apps/backend/src/db/migrations/039_worker_sms_verify_attempts.sql new file mode 100644 index 00000000..042cfc4a --- /dev/null +++ b/apps/backend/src/db/migrations/039_worker_sms_verify_attempts.sql @@ -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 '当前验证码已失败的校验次数,达到上限后验证码锁定'; diff --git a/apps/backend/src/repositories/worker-platform/sms-code-repo.ts b/apps/backend/src/repositories/worker-platform/sms-code-repo.ts index 6cc0e680..43e63962 100644 --- a/apps/backend/src/repositories/worker-platform/sms-code-repo.ts +++ b/apps/backend/src/repositories/worker-platform/sms-code-repo.ts @@ -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 { + const result = await query( + ` + 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, diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index 48a23b29..998c0c06 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -184,7 +184,7 @@ export async function getWorkOrderShare( ): Promise { const result = await query( `${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) { diff --git a/apps/backend/src/routes/worker.ts b/apps/backend/src/routes/worker.ts index d17f514c..5d18e401 100644 --- a/apps/backend/src/routes/worker.ts +++ b/apps/backend/src/routes/worker.ts @@ -70,7 +70,7 @@ router.post( key: getBodyFieldRateLimitKey('phone'), }), createRouteHandler((req) => registerWorker(req.body || {}), { - successMessage: '注册成功,请使用手机号登录', + successMessage: '注册成功,请等待后台审核', errorMessage: '接单账号注册失败', scope: '[worker/auth/register]', }), diff --git a/apps/backend/src/services/admin/admin-auth-service.ts b/apps/backend/src/services/admin/admin-auth-service.ts index 74dbf4b2..431180b1 100644 --- a/apps/backend/src/services/admin/admin-auth-service.ts +++ b/apps/backend/src/services/admin/admin-auth-service.ts @@ -134,8 +134,10 @@ export async function verifyAdminSessionToken(token: unknown): Promise 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, diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 46870d14..861fbbd4 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -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