拆分打手短信服务
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-view.js'
|
||||||
export * from './worker-session-auth-service.js'
|
export * from './worker-session-auth-service.js'
|
||||||
export * from './worker-credentials-service.js'
|
export * from './worker-credentials-service.js'
|
||||||
|
export * from './worker-sms-service.js'
|
||||||
export * from './worker-platform-defaults.js'
|
export * from './worker-platform-defaults.js'
|
||||||
export * from './work-order-timeout-policy.js'
|
export * from './work-order-timeout-policy.js'
|
||||||
export * from './after-sales-service.js'
|
export * from './after-sales-service.js'
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import {
|
|||||||
countWorkerTimeoutEvents,
|
countWorkerTimeoutEvents,
|
||||||
countWorkerWithdrawRequestsOnDay,
|
countWorkerWithdrawRequestsOnDay,
|
||||||
countWorkOrderPendingSharingSubmissions,
|
countWorkOrderPendingSharingSubmissions,
|
||||||
countWorkerSmsCodesOnDay,
|
|
||||||
consumeWorkerSmsCode,
|
consumeWorkerSmsCode,
|
||||||
createWorkerSmsCode,
|
|
||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
createWorkerFinanceRequest,
|
createWorkerFinanceRequest,
|
||||||
createWorkerUser,
|
createWorkerUser,
|
||||||
@@ -79,7 +77,6 @@ import type { JsonObject } from '../../types/json.js'
|
|||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { addHours, nowIso } from '../../utils/time.js'
|
import { addHours, nowIso } from '../../utils/time.js'
|
||||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.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 { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||||
import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
|
import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
|
||||||
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
||||||
@@ -156,6 +153,13 @@ import {
|
|||||||
normalizeWorkerDeviceType,
|
normalizeWorkerDeviceType,
|
||||||
} from './worker-auth-policy.js'
|
} from './worker-auth-policy.js'
|
||||||
import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.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 { changeWorkerPassword } from './worker-credentials-service.js'
|
||||||
import {
|
import {
|
||||||
deleteWorkerSession,
|
deleteWorkerSession,
|
||||||
@@ -185,6 +189,11 @@ export {
|
|||||||
} from './work-order-timeout-policy.js'
|
} from './work-order-timeout-policy.js'
|
||||||
export type { WorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
export type { WorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
||||||
export { changeWorkerPassword } from './worker-credentials-service.js'
|
export { changeWorkerPassword } from './worker-credentials-service.js'
|
||||||
|
export {
|
||||||
|
sendWorkerLoginSmsCode,
|
||||||
|
sendWorkerPasswordResetSmsCode,
|
||||||
|
sendWorkerSmsCode,
|
||||||
|
} from './worker-sms-service.js'
|
||||||
export {
|
export {
|
||||||
deleteWorkerSession,
|
deleteWorkerSession,
|
||||||
getWorkerSessionSummary,
|
getWorkerSessionSummary,
|
||||||
@@ -195,153 +204,6 @@ export {
|
|||||||
|
|
||||||
const WORKER_MAX_DEVICES = 3
|
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 = {}) {
|
export async function resetWorkerPassword(payload: JsonObject = {}) {
|
||||||
const phone = normalizeWorkerPhone(payload.phone)
|
const phone = normalizeWorkerPhone(payload.phone)
|
||||||
const code = String(payload.code || payload.smsCode || '').trim()
|
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> {
|
async function generateInviteCode(_now: string): Promise<string> {
|
||||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||||
for (let attempt = 0; attempt < INVITE_CODE_RETRY_LIMIT; attempt++) {
|
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