拆分打手注册服务

This commit is contained in:
yml2213
2026-08-21 16:42:04 +08:00
parent dae32504c7
commit cc8c95bb58
3 changed files with 211 additions and 202 deletions
@@ -6,6 +6,7 @@ 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-sms-service.js'
export * from './worker-registration-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'
@@ -0,0 +1,208 @@
import crypto from 'node:crypto'
import {
consumeWorkerSmsCode,
createWorkerUser,
getLatestWorkerSmsCode,
getWorkerUserByDisplayName,
getWorkerUserByInviteCode,
getWorkerUserByPhone,
getWorkerUserByUsername,
recordWorkerSmsCodeFailure,
revokeAllWorkerSessions,
updateWorkerPassword,
} 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 {
ensureWorkerAuthConfigured,
hashWorkerPassword,
mapWorkerUser,
normalizePassword,
normalizeSessionVersion,
validateWorkerPassword,
} from './mappers.js'
import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js'
import { hashSmsCode, normalizeWorkerPhone } from './worker-sms-service.js'
const INVITE_CODE_LENGTH = 8
const INVITE_CODE_RETRY_LIMIT = 5
const WORKER_SMS_MAX_VERIFY_ATTEMPTS = 5
export async function resetWorkerPassword(payload: JsonObject = {}) {
const phone = normalizeWorkerPhone(payload.phone)
const code = String(payload.code || payload.smsCode || '').trim()
const password = normalizePassword(payload.password || payload.newPassword)
validateWorkerPassword(password)
if (!code) {
throw createHttpError('请输入短信验证码', {
statusCode: 400,
errorCode: 'worker_sms_code_required',
})
}
const worker = await getWorkerUserByPhone(phone)
if (!worker) {
throw createHttpError('该手机号未注册', {
statusCode: 404,
errorCode: 'worker_phone_not_registered',
})
}
const latest = await getLatestWorkerSmsCode(phone, 'password_reset')
const codeValid = isWorkerSmsCodeValid(latest, code)
if (!codeValid) {
await recordInvalidWorkerSmsCode(latest)
throw createHttpError('短信验证码错误或已过期', {
statusCode: 400,
errorCode: 'worker_sms_code_invalid',
})
}
if (!(await consumeWorkerSmsCode(latest!.id, nowIso()))) {
throw createHttpError('验证码已失效,请重新获取', {
statusCode: 400,
errorCode: 'worker_sms_code_invalid',
})
}
const now = nowIso()
const updated = await updateWorkerPassword({
workerId: worker.id,
passwordHash: hashWorkerPassword(password),
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
now,
})
await revokeAllWorkerSessions(worker.id, now)
return { worker: mapWorkerUser(updated || worker), reloginRequired: true }
}
export async function registerWorker(payload: JsonObject = {}) {
ensureWorkerAuthConfigured()
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 || '').trim()
const inviteCode = String(payload.inviteCode || '')
.trim()
.toUpperCase()
validateWorkerPassword(password)
if (!displayName) {
throw createHttpError('请输入昵称', {
statusCode: 400,
errorCode: 'worker_display_name_required',
})
}
if (!code) {
throw createHttpError('请输入短信验证码', {
statusCode: 400,
errorCode: 'worker_sms_code_required',
})
}
const latest = await getLatestWorkerSmsCode(phone, 'register')
if (!isWorkerSmsCodeValid(latest, code)) {
await recordInvalidWorkerSmsCode(latest)
throw createHttpError('短信验证码错误或已过期', {
statusCode: 400,
errorCode: 'worker_sms_code_invalid',
})
}
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) {
throw createHttpError('邀请码无效', {
statusCode: 400,
errorCode: 'worker_invite_code_invalid',
})
}
if (inviter && inviter.status !== 'active') {
throw createHttpError('邀请人账号不可用', {
statusCode: 400,
errorCode: 'worker_inviter_invalid',
})
}
if (await getWorkerUserByDisplayName(displayName)) {
throw createHttpError('该昵称已被使用,请更换昵称', {
statusCode: 409,
errorCode: 'worker_display_name_taken',
})
}
const defaults = await ensureWorkerPlatformDefaults()
const now = nowIso()
const created = await createWorkerUser({
username: phone,
passwordHash: hashWorkerPassword(password),
displayName,
phone,
levelId: defaults.level?.id || null,
status: 'active',
inviterId: inviter ? Number(inviter.id) : null,
inviteCode: await generateInviteCode(),
smsCodeId: latest!.id,
now,
})
if (!created) {
throw createHttpError('接单账号注册失败', {
statusCode: 500,
errorCode: 'worker_register_failed',
})
}
return { worker: mapWorkerUser(created), reviewRequired: false }
}
function isWorkerSmsCodeValid(
latest: { consumed_at: string | null; expires_at: string; code_hash: string } | null,
code: string,
) {
return Boolean(
latest &&
!latest.consumed_at &&
new Date(latest.expires_at).getTime() > Date.now() &&
latest.code_hash === hashSmsCode(code),
)
}
async function recordInvalidWorkerSmsCode(
latest: { id: number; consumed_at: string | null } | null,
) {
if (!latest || latest.consumed_at) return
const failed = await recordWorkerSmsCodeFailure(
latest.id,
nowIso(),
WORKER_SMS_MAX_VERIFY_ATTEMPTS,
)
if (failed?.consumed_at) {
throw createHttpError('验证码错误次数过多,请重新获取验证码', {
statusCode: 400,
errorCode: 'worker_sms_code_locked',
})
}
}
async function generateInviteCode(): Promise<string> {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
for (let attempt = 0; attempt < INVITE_CODE_RETRY_LIMIT; attempt += 1) {
const candidate = Array.from(
{ length: INVITE_CODE_LENGTH },
() => chars[Math.floor(crypto.randomInt(chars.length))],
).join('')
if (!(await getWorkerUserByInviteCode(candidate))) return candidate
}
throw createHttpError('邀请码生成失败,请重试', {
statusCode: 500,
errorCode: 'worker_invite_code_generation_failed',
})
}
@@ -1,5 +1,3 @@
import crypto from 'node:crypto'
import { runtimeConfig } from '../../config/runtime.js' import { runtimeConfig } from '../../config/runtime.js'
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js' import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
import { import {
@@ -15,12 +13,10 @@ import {
consumeWorkerSmsCode, consumeWorkerSmsCode,
createWorkOrderEvent, createWorkOrderEvent,
createWorkerFinanceRequest, createWorkerFinanceRequest,
createWorkerUser,
createWorkerWithdrawalAccount as createWorkerWithdrawalAccountRecord, createWorkerWithdrawalAccount as createWorkerWithdrawalAccountRecord,
getWorkerFinanceRequestSummary, getWorkerFinanceRequestSummary,
getLatestWorkerSmsCode, getLatestWorkerSmsCode,
recordWorkerSmsCodeFailure, recordWorkerSmsCodeFailure,
getWorkerUserByInviteCode,
getWorkerUserById, getWorkerUserById,
getWorkerWithdrawalAccount, getWorkerWithdrawalAccount,
getWorkerUserByPhone, getWorkerUserByPhone,
@@ -34,7 +30,6 @@ import {
getWorkerSessionBySessionId, getWorkerSessionBySessionId,
listWorkerSessions, listWorkerSessions,
revokeWorkerSession, revokeWorkerSession,
revokeAllWorkerSessions,
touchWorkerSession, touchWorkerSession,
joinWorkOrderShare, joinWorkOrderShare,
listPendingMaterialWorkOrdersByPlatformOrderId, listPendingMaterialWorkOrdersByPlatformOrderId,
@@ -67,7 +62,6 @@ import {
updateWorkOrderShareDraftAcceptance, updateWorkOrderShareDraftAcceptance,
upsertWorkerWorkOrderNote, upsertWorkerWorkOrderNote,
markWorkerWorkOrderViewed, markWorkerWorkOrderViewed,
updateWorkerPassword,
type WorkOrderRow, type WorkOrderRow,
type WorkOrderShareRow, type WorkOrderShareRow,
type WorkerSessionRow, type WorkerSessionRow,
@@ -103,8 +97,6 @@ import {
} from '../realtime/realtime-event-service.js' } from '../realtime/realtime-event-service.js'
import { import {
INVITE_CODE_LENGTH,
INVITE_CODE_RETRY_LIMIT,
createWorkerSession, createWorkerSession,
ensureWorkerAuthConfigured, ensureWorkerAuthConfigured,
hashWorkerPassword, hashWorkerPassword,
@@ -153,6 +145,7 @@ 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 { registerWorker, resetWorkerPassword } from './worker-registration-service.js'
import { import {
hashSmsCode, hashSmsCode,
normalizeWorkerPhone, normalizeWorkerPhone,
@@ -189,6 +182,7 @@ 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 { registerWorker, resetWorkerPassword } from './worker-registration-service.js'
export { export {
sendWorkerLoginSmsCode, sendWorkerLoginSmsCode,
sendWorkerPasswordResetSmsCode, sendWorkerPasswordResetSmsCode,
@@ -204,200 +198,6 @@ export {
const WORKER_MAX_DEVICES = 3 const WORKER_MAX_DEVICES = 3
export async function resetWorkerPassword(payload: JsonObject = {}) {
const phone = normalizeWorkerPhone(payload.phone)
const code = String(payload.code || payload.smsCode || '').trim()
const password = normalizePassword(payload.password || payload.newPassword)
validateWorkerPassword(password)
if (!code) {
throw createHttpError('请输入短信验证码', {
statusCode: 400,
errorCode: 'worker_sms_code_required',
})
}
const worker = await getWorkerUserByPhone(phone)
if (!worker) {
throw createHttpError('该手机号未注册', {
statusCode: 404,
errorCode: 'worker_phone_not_registered',
})
}
const latest = await getLatestWorkerSmsCode(phone, 'password_reset')
const codeValid = Boolean(
latest &&
!latest.consumed_at &&
new Date(latest.expires_at).getTime() > Date.now() &&
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?.consumed_at) {
throw createHttpError('验证码错误次数过多,请重新获取验证码', {
statusCode: 400,
errorCode: 'worker_sms_code_locked',
})
}
}
throw createHttpError('短信验证码错误或已过期', {
statusCode: 400,
errorCode: 'worker_sms_code_invalid',
})
}
const consumed = await consumeWorkerSmsCode(latest!.id, nowIso())
if (!consumed) {
throw createHttpError('验证码已失效,请重新获取', {
statusCode: 400,
errorCode: 'worker_sms_code_invalid',
})
}
const now = nowIso()
const updated = await updateWorkerPassword({
workerId: worker.id,
passwordHash: hashWorkerPassword(password),
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
now,
})
await revokeAllWorkerSessions(worker.id, now)
return { worker: mapWorkerUser(updated || worker), reloginRequired: true }
}
export async function registerWorker(payload: JsonObject = {}) {
ensureWorkerAuthConfigured()
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 || '').trim()
const inviteCode = String(payload.inviteCode || '')
.trim()
.toUpperCase()
validateWorkerPassword(password)
if (!displayName) {
throw createHttpError('请输入昵称', {
statusCode: 400,
errorCode: 'worker_display_name_required',
})
}
if (!code) {
throw createHttpError('请输入短信验证码', {
statusCode: 400,
errorCode: 'worker_sms_code_required',
})
}
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) {
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',
})
}
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) {
throw createHttpError('邀请码无效', {
statusCode: 400,
errorCode: 'worker_invite_code_invalid',
})
}
if (inviter && inviter.status !== 'active') {
throw createHttpError('邀请人账号不可用', {
statusCode: 400,
errorCode: 'worker_inviter_invalid',
})
}
if (await getWorkerUserByDisplayName(displayName)) {
throw createHttpError('该昵称已被使用,请更换昵称', {
statusCode: 409,
errorCode: 'worker_display_name_taken',
})
}
const defaults = await ensureWorkerPlatformDefaults()
const now = nowIso()
const created = await createWorkerUser({
username: phone,
passwordHash: hashWorkerPassword(password),
displayName,
phone,
levelId: defaults.level?.id || null,
// 手机号验证码已完成注册身份校验,账号直接启用,无需人工审核。
status: 'active',
inviterId: inviter ? Number(inviter.id) : null,
inviteCode: await generateInviteCode(now),
smsCodeId: latest!.id,
now,
})
if (!created) {
throw createHttpError('接单账号注册失败', {
statusCode: 500,
errorCode: 'worker_register_failed',
})
}
return {
worker: mapWorkerUser(created),
// 保留字段兼容旧版客户端,但注册流程不再需要后台审核。
reviewRequired: false,
}
}
async function generateInviteCode(_now: string): Promise<string> {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
for (let attempt = 0; attempt < INVITE_CODE_RETRY_LIMIT; attempt++) {
const candidate = Array.from(
{ length: INVITE_CODE_LENGTH },
() => chars[Math.floor(crypto.randomInt(chars.length))],
).join('')
if (!(await getWorkerUserByInviteCode(candidate))) {
return candidate
}
}
throw createHttpError('邀请码生成失败,请重试', {
statusCode: 500,
errorCode: 'worker_invite_code_generation_failed',
})
}
export async function loginWorker( export async function loginWorker(
username: unknown, username: unknown,
password: unknown, password: unknown,