修复打手短信验证码发送
This commit is contained in:
@@ -14,8 +14,11 @@ import {
|
||||
countWorkCategoryUsages,
|
||||
countWorkerLevelUsages,
|
||||
countWorkOrderPendingSharingSubmissions,
|
||||
cancelWorkerWorkOrder,
|
||||
countWorkerCancellationsSince,
|
||||
countWorkerSmsCodesOnDay,
|
||||
cancelWorkerWorkOrder,
|
||||
consumeWorkerSmsCode,
|
||||
createWorkerSmsCode,
|
||||
createWorkOrder,
|
||||
createWorkOrderEvent,
|
||||
createWorkerFinanceRequest,
|
||||
@@ -28,8 +31,10 @@ import {
|
||||
getWorkCategoryByKey,
|
||||
getWorkerLevelById,
|
||||
getWorkerLevelByKey,
|
||||
getLatestWorkerSmsCode,
|
||||
getWorkerUserByInviteCode,
|
||||
getWorkerUserById,
|
||||
getWorkerUserByPhone,
|
||||
getWorkerUserByUsername,
|
||||
getWorkOrderById,
|
||||
getWorkOrderByOrderItemId,
|
||||
@@ -79,6 +84,7 @@ import { createHttpError } from '../../utils/http.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
import { nowIso, addHours } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { getSmsProvider } from '../sms/index.js'
|
||||
import {
|
||||
normalizeStoredFileUrl,
|
||||
refreshStoredFileUrl,
|
||||
@@ -131,23 +137,96 @@ export async function ensureWorkerPlatformDefaults() {
|
||||
return { level, category }
|
||||
}
|
||||
|
||||
export async function sendWorkerSmsCode(payload: JsonObject = {}) {
|
||||
const phone = normalizeWorkerPhone(payload.phone)
|
||||
const config = runtimeConfig.worker.sms
|
||||
const cooldownMs = Math.max(10, Number(config.cooldownSeconds || 60)) * 1000
|
||||
const dailyLimit = Math.max(1, Number(config.dailyLimit || 10))
|
||||
const ttlSeconds = Math.max(60, Number(config.codeTtlSeconds || 300))
|
||||
|
||||
if (await getWorkerUserByPhone(phone)) {
|
||||
throw createHttpError('该手机号已注册,请直接登录', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_phone_registered',
|
||||
})
|
||||
}
|
||||
|
||||
const latest = await getLatestWorkerSmsCode(phone, 'register')
|
||||
if (latest && new Date(latest.created_at).getTime() > Date.now() - cooldownMs) {
|
||||
throw createHttpError('验证码发送过于频繁,请稍后再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_cooldown',
|
||||
})
|
||||
}
|
||||
|
||||
const dayRange = resolveChinaDayRange()
|
||||
const todayCount = await countWorkerSmsCodesOnDay(phone, dayRange.start, dayRange.end)
|
||||
if (todayCount >= dailyLimit) {
|
||||
throw createHttpError('今日验证码发送次数已达上限,请明天再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_daily_limit',
|
||||
})
|
||||
}
|
||||
|
||||
const code = generateSmsCode()
|
||||
await getSmsProvider().sendCode(phone, code)
|
||||
await createWorkerSmsCode({
|
||||
phone,
|
||||
purpose: 'register',
|
||||
codeHash: hashSmsCode(code),
|
||||
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
|
||||
now: nowIso(),
|
||||
})
|
||||
return {
|
||||
sent: true,
|
||||
expiresInSeconds: ttlSeconds,
|
||||
provider: String(runtimeConfig.worker.sms.provider || 'mock'),
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerWorker(payload: JsonObject = {}) {
|
||||
ensureWorkerAuthConfigured()
|
||||
const username = normalizeUsername(payload.username)
|
||||
const phone = normalizeWorkerPhone(payload.phone)
|
||||
const code = String(payload.code || payload.smsCode || '').trim()
|
||||
const password = normalizePassword(payload.password)
|
||||
const displayName = String(payload.displayName || payload.nickname || username).trim()
|
||||
const phone = String(payload.phone || '').trim()
|
||||
const displayName = String(payload.displayName || payload.nickname || phone).trim()
|
||||
const inviteCode = String(payload.inviteCode || '').trim().toUpperCase()
|
||||
|
||||
validateWorkerUsername(username)
|
||||
validateWorkerPassword(password)
|
||||
if (!code) {
|
||||
throw createHttpError('请输入短信验证码', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_sms_code_required',
|
||||
})
|
||||
}
|
||||
|
||||
if (await getWorkerUserByUsername(username)) {
|
||||
throw createHttpError('接单账号已存在', {
|
||||
const latest = await getLatestWorkerSmsCode(phone, 'register')
|
||||
const codeValid = Boolean(
|
||||
latest &&
|
||||
!latest.consumed_at &&
|
||||
new Date(latest.expires_at).getTime() > Date.now() &&
|
||||
latest.code_hash === hashSmsCode(code),
|
||||
)
|
||||
if (!codeValid) {
|
||||
throw createHttpError('短信验证码错误或已过期', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_sms_code_invalid',
|
||||
})
|
||||
}
|
||||
await consumeWorkerSmsCode(latest!.id, nowIso())
|
||||
|
||||
if (await getWorkerUserByUsername(phone)) {
|
||||
throw createHttpError('该手机号已注册,请直接登录', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_user_exists',
|
||||
})
|
||||
}
|
||||
if (await getWorkerUserByPhone(phone)) {
|
||||
throw createHttpError('该手机号已注册,请直接登录', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_phone_exists',
|
||||
})
|
||||
}
|
||||
|
||||
const inviter = inviteCode ? await getWorkerUserByInviteCode(inviteCode) : null
|
||||
if (inviteCode && !inviter) {
|
||||
@@ -166,12 +245,12 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
const defaults = await ensureWorkerPlatformDefaults()
|
||||
const now = nowIso()
|
||||
const created = await createWorkerUser({
|
||||
username,
|
||||
username: phone,
|
||||
passwordHash: hashWorkerPassword(password),
|
||||
displayName,
|
||||
phone,
|
||||
levelId: defaults.level?.id || null,
|
||||
status: 'pending_review',
|
||||
status: 'active',
|
||||
inviterId: inviter ? Number(inviter.id) : null,
|
||||
inviteCode: await generateInviteCode(now),
|
||||
now,
|
||||
@@ -186,10 +265,33 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
|
||||
return {
|
||||
worker: mapWorkerUser(created),
|
||||
reviewRequired: true,
|
||||
reviewRequired: false,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWorkerPhone(value: unknown): string {
|
||||
const phone = String(value || '').trim()
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
throw createHttpError('手机号格式不正确', {
|
||||
statusCode: 400,
|
||||
errorCode: 'worker_phone_invalid',
|
||||
})
|
||||
}
|
||||
return phone
|
||||
}
|
||||
|
||||
function generateSmsCode(): string {
|
||||
let code = ''
|
||||
for (let index = 0; index < 6; index += 1) {
|
||||
code += String(crypto.randomInt(10))
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
function hashSmsCode(code: string): string {
|
||||
return crypto.createHash('sha256').update(String(code || '')).digest('hex')
|
||||
}
|
||||
|
||||
async function generateInviteCode(now: string): Promise<string> {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
for (let attempt = 0; attempt < INVITE_CODE_RETRY_LIMIT; attempt++) {
|
||||
|
||||
Reference in New Issue
Block a user