拆分打手短信服务
This commit is contained in:
@@ -5,6 +5,7 @@ export * from './worker-auth-policy.js'
|
||||
export * from './worker-session-view.js'
|
||||
export * from './worker-session-auth-service.js'
|
||||
export * from './worker-credentials-service.js'
|
||||
export * from './worker-sms-service.js'
|
||||
export * from './worker-platform-defaults.js'
|
||||
export * from './work-order-timeout-policy.js'
|
||||
export * from './after-sales-service.js'
|
||||
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
countWorkerTimeoutEvents,
|
||||
countWorkerWithdrawRequestsOnDay,
|
||||
countWorkOrderPendingSharingSubmissions,
|
||||
countWorkerSmsCodesOnDay,
|
||||
consumeWorkerSmsCode,
|
||||
createWorkerSmsCode,
|
||||
createWorkOrderEvent,
|
||||
createWorkerFinanceRequest,
|
||||
createWorkerUser,
|
||||
@@ -79,7 +77,6 @@ import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { addHours, nowIso } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { getSmsProvider } from '../sms/index.js'
|
||||
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||
import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
|
||||
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
||||
@@ -156,6 +153,13 @@ import {
|
||||
normalizeWorkerDeviceType,
|
||||
} from './worker-auth-policy.js'
|
||||
import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js'
|
||||
import {
|
||||
hashSmsCode,
|
||||
normalizeWorkerPhone,
|
||||
sendWorkerLoginSmsCode,
|
||||
sendWorkerPasswordResetSmsCode,
|
||||
sendWorkerSmsCode,
|
||||
} from './worker-sms-service.js'
|
||||
import { changeWorkerPassword } from './worker-credentials-service.js'
|
||||
import {
|
||||
deleteWorkerSession,
|
||||
@@ -185,6 +189,11 @@ export {
|
||||
} from './work-order-timeout-policy.js'
|
||||
export type { WorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
||||
export { changeWorkerPassword } from './worker-credentials-service.js'
|
||||
export {
|
||||
sendWorkerLoginSmsCode,
|
||||
sendWorkerPasswordResetSmsCode,
|
||||
sendWorkerSmsCode,
|
||||
} from './worker-sms-service.js'
|
||||
export {
|
||||
deleteWorkerSession,
|
||||
getWorkerSessionSummary,
|
||||
@@ -195,153 +204,6 @@ export {
|
||||
|
||||
const WORKER_MAX_DEVICES = 3
|
||||
|
||||
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 &&
|
||||
!latest.consumed_at &&
|
||||
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 sendWorkerPasswordResetSmsCode(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: 404,
|
||||
errorCode: 'worker_phone_not_registered',
|
||||
})
|
||||
}
|
||||
|
||||
const latest = await getLatestWorkerSmsCode(phone, 'password_reset')
|
||||
if (
|
||||
latest &&
|
||||
!latest.consumed_at &&
|
||||
new Date(latest.created_at).getTime() > Date.now() - cooldownMs
|
||||
) {
|
||||
throw createHttpError('验证码发送过于频繁,请稍后再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_cooldown',
|
||||
})
|
||||
}
|
||||
|
||||
const dayRange = resolveChinaDayRange()
|
||||
if ((await countWorkerSmsCodesOnDay(phone, dayRange.start, dayRange.end)) >= dailyLimit) {
|
||||
throw createHttpError('今日验证码发送次数已达上限,请明天再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_daily_limit',
|
||||
})
|
||||
}
|
||||
|
||||
const code = generateSmsCode()
|
||||
await getSmsProvider().sendCode(phone, code)
|
||||
await createWorkerSmsCode({
|
||||
phone,
|
||||
purpose: 'password_reset',
|
||||
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 sendWorkerLoginSmsCode(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: 404,
|
||||
errorCode: 'worker_phone_not_registered',
|
||||
})
|
||||
}
|
||||
const latest = await getLatestWorkerSmsCode(phone, 'login')
|
||||
if (
|
||||
latest &&
|
||||
!latest.consumed_at &&
|
||||
new Date(latest.created_at).getTime() > Date.now() - cooldownMs
|
||||
) {
|
||||
throw createHttpError('验证码发送过于频繁,请稍后再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_cooldown',
|
||||
})
|
||||
}
|
||||
const dayRange = resolveChinaDayRange()
|
||||
if ((await countWorkerSmsCodesOnDay(phone, dayRange.start, dayRange.end)) >= dailyLimit) {
|
||||
throw createHttpError('今日验证码发送次数已达上限,请明天再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_daily_limit',
|
||||
})
|
||||
}
|
||||
const code = generateSmsCode()
|
||||
await getSmsProvider().sendCode(phone, code)
|
||||
await createWorkerSmsCode({
|
||||
phone,
|
||||
purpose: 'login',
|
||||
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 resetWorkerPassword(payload: JsonObject = {}) {
|
||||
const phone = normalizeWorkerPhone(payload.phone)
|
||||
const code = String(payload.code || payload.smsCode || '').trim()
|
||||
@@ -519,32 +381,6 @@ export async function registerWorker(payload: JsonObject = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
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++) {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import {
|
||||
countWorkerSmsCodesOnDay,
|
||||
createWorkerSmsCode,
|
||||
getLatestWorkerSmsCode,
|
||||
getWorkerUserByPhone,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { getSmsProvider } from '../sms/index.js'
|
||||
|
||||
type WorkerSmsPurpose = 'register' | 'password_reset' | 'login'
|
||||
|
||||
export async function sendWorkerSmsCode(payload: JsonObject = {}) {
|
||||
return sendWorkerSmsCodeForPurpose(payload.phone, 'register')
|
||||
}
|
||||
|
||||
export async function sendWorkerPasswordResetSmsCode(payload: JsonObject = {}) {
|
||||
return sendWorkerSmsCodeForPurpose(payload.phone, 'password_reset')
|
||||
}
|
||||
|
||||
export async function sendWorkerLoginSmsCode(payload: JsonObject = {}) {
|
||||
return sendWorkerSmsCodeForPurpose(payload.phone, 'login')
|
||||
}
|
||||
|
||||
async function sendWorkerSmsCodeForPurpose(phoneValue: unknown, purpose: WorkerSmsPurpose) {
|
||||
const phone = normalizeWorkerPhone(phoneValue)
|
||||
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))
|
||||
const worker = await getWorkerUserByPhone(phone)
|
||||
|
||||
if (purpose === 'register' && worker) {
|
||||
throw createHttpError('该手机号已注册,请直接登录', {
|
||||
statusCode: 409,
|
||||
errorCode: 'worker_phone_registered',
|
||||
})
|
||||
}
|
||||
if (purpose !== 'register' && !worker) {
|
||||
throw createHttpError('该手机号未注册', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_phone_not_registered',
|
||||
})
|
||||
}
|
||||
|
||||
const latest = await getLatestWorkerSmsCode(phone, purpose)
|
||||
if (
|
||||
latest &&
|
||||
!latest.consumed_at &&
|
||||
new Date(latest.created_at).getTime() > Date.now() - cooldownMs
|
||||
) {
|
||||
throw createHttpError('验证码发送过于频繁,请稍后再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_cooldown',
|
||||
})
|
||||
}
|
||||
|
||||
const dayRange = resolveChinaDayRange()
|
||||
if ((await countWorkerSmsCodesOnDay(phone, dayRange.start, dayRange.end)) >= dailyLimit) {
|
||||
throw createHttpError('今日验证码发送次数已达上限,请明天再试', {
|
||||
statusCode: 429,
|
||||
errorCode: 'worker_sms_daily_limit',
|
||||
})
|
||||
}
|
||||
|
||||
const code = generateSmsCode()
|
||||
await getSmsProvider().sendCode(phone, code)
|
||||
await createWorkerSmsCode({
|
||||
phone,
|
||||
purpose,
|
||||
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 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
|
||||
}
|
||||
|
||||
export function hashSmsCode(code: string): string {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(String(code || ''))
|
||||
.digest('hex')
|
||||
}
|
||||
|
||||
function generateSmsCode(): string {
|
||||
let code = ''
|
||||
for (let index = 0; index < 6; index += 1) {
|
||||
code += String(crypto.randomInt(10))
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
function resolveChinaDayRange(): { start: string; end: string } {
|
||||
const now = new Date()
|
||||
const chinaNow = new Date(now.getTime() + 8 * 60 * 60 * 1000)
|
||||
const year = chinaNow.getUTCFullYear()
|
||||
const month = chinaNow.getUTCMonth()
|
||||
const day = chinaNow.getUTCDate()
|
||||
const start = new Date(Date.UTC(year, month, day) - 8 * 60 * 60 * 1000)
|
||||
const end = new Date(Date.UTC(year, month, day + 1) - 8 * 60 * 60 * 1000)
|
||||
return { start: start.toISOString(), end: end.toISOString() }
|
||||
}
|
||||
Reference in New Issue
Block a user