拆分打手会话服务

This commit is contained in:
yml2213
2026-08-21 16:37:16 +08:00
parent 3b7322954a
commit f0d1da4e1a
5 changed files with 265 additions and 210 deletions
@@ -2,6 +2,9 @@ export * from './mappers.js'
export * from './worker-session-context-service.js'
export * from './worker-service.js'
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-platform-defaults.js'
export * from './work-order-timeout-policy.js'
export * from './after-sales-service.js'
@@ -0,0 +1,65 @@
import {
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 {
hashWorkerPassword,
mapWorkerUser,
normalizePassword,
normalizeSessionVersion,
validateWorkerPassword,
verifyWorkerPassword,
} from './mappers.js'
import { getRequiredWorker, type WorkerSession } from './worker-session-context-service.js'
export async function changeWorkerPassword(payload: JsonObject = {}, session: WorkerSession) {
const worker = await getRequiredWorker(session.workerId)
const currentPassword = normalizePassword(payload.currentPassword || payload.oldPassword)
const nextPassword = normalizePassword(payload.newPassword || payload.password)
if (!currentPassword) {
throw createHttpError('请输入当前密码', {
statusCode: 400,
errorCode: 'worker_password_current_required',
})
}
if (!verifyWorkerPassword(currentPassword, worker.password_hash)) {
throw createHttpError('当前密码不正确', {
statusCode: 400,
errorCode: 'worker_password_current_invalid',
})
}
if (!nextPassword) {
throw createHttpError('请输入新密码', {
statusCode: 400,
errorCode: 'worker_password_new_required',
})
}
validateWorkerPassword(nextPassword)
if (currentPassword === nextPassword) {
throw createHttpError('新密码不能与当前密码一致', {
statusCode: 400,
errorCode: 'worker_password_unchanged',
})
}
const now = nowIso()
const updated = await updateWorkerPassword({
workerId: worker.id,
passwordHash: hashWorkerPassword(nextPassword),
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
now,
})
await revokeAllWorkerSessions(worker.id, now)
return {
worker: mapWorkerUser(updated || worker),
reloginRequired: true,
}
}
@@ -111,7 +111,6 @@ import {
createWorkerSession,
ensureWorkerAuthConfigured,
hashWorkerPassword,
isIgnorableWorkerAuthError,
mapCollectLookupOrder,
mapFinanceRequest,
mapWalletLedger,
@@ -145,8 +144,6 @@ import {
resolveRequirementFields,
resolveWorkerPermissions,
canWorkerAutoAcceptWholeOrder,
safeCompare,
signWorkerPayload,
throwGrabWorkOrderFailure,
validateWorkerPassword,
isWorkerPasswordStrong,
@@ -159,6 +156,14 @@ import {
normalizeWorkerDeviceType,
} from './worker-auth-policy.js'
import { ensureWorkerPlatformDefaults } from './worker-platform-defaults.js'
import { changeWorkerPassword } from './worker-credentials-service.js'
import {
deleteWorkerSession,
getWorkerSessionSummary,
getWorkerSessions,
logoutWorkerSession,
verifyWorkerSessionToken,
} from './worker-session-auth-service.js'
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
const WORKER_DAILY_WITHDRAW_LIMIT = 3
@@ -179,6 +184,14 @@ export {
WORK_ORDER_TIMEOUT_POLICIES,
} from './work-order-timeout-policy.js'
export type { WorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
export { changeWorkerPassword } from './worker-credentials-service.js'
export {
deleteWorkerSession,
getWorkerSessionSummary,
getWorkerSessions,
logoutWorkerSession,
verifyWorkerSessionToken,
} from './worker-session-auth-service.js'
const WORKER_MAX_DEVICES = 3
@@ -726,164 +739,6 @@ async function authenticateWorkerCredentials(
return worker
}
export async function logoutWorkerSession(token: unknown) {
const normalizedToken = String(token || '').trim()
if (!normalizedToken) {
return { success: true, revoked: false }
}
try {
const session = await verifyWorkerSessionToken(normalizedToken)
return {
success: true,
revoked: await revokeWorkerSession(session.workerId, session.sessionId, nowIso()),
}
} catch (error) {
if (isIgnorableWorkerAuthError(error)) {
return { success: true, revoked: false }
}
throw error
}
}
export async function verifyWorkerSessionToken(token: unknown): Promise<WorkerSession> {
ensureWorkerAuthConfigured()
const normalizedToken = String(token || '').trim()
if (!normalizedToken) {
throw createHttpError('未登录或登录已失效', {
statusCode: 401,
errorCode: 'worker_auth_required',
})
}
const parts = normalizedToken.split('.')
const encodedPayload = parts[0]
const signature = parts[1]
if (
parts.length !== 2 ||
!encodedPayload ||
!signature ||
!safeCompare(signature, signWorkerPayload(encodedPayload))
) {
throw createHttpError('接单登录态无效', {
statusCode: 401,
errorCode: 'worker_auth_invalid',
})
}
let payload: JsonObject
try {
payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'))
} catch {
throw createHttpError('接单登录态无效', {
statusCode: 401,
errorCode: 'worker_auth_invalid',
})
}
if (String(payload.typ || '') !== 'worker') {
throw createHttpError('接单登录态无效', {
statusCode: 401,
errorCode: 'worker_auth_invalid',
})
}
const expiresAt = String(payload.exp || '').trim()
if (!expiresAt || Date.parse(expiresAt) <= Date.now()) {
throw createHttpError('登录已过期,请重新登录', {
statusCode: 401,
errorCode: 'worker_auth_expired',
})
}
const workerId = Number(payload.uid || 0)
const worker = await getWorkerUserById(workerId)
if (!worker || worker.status !== 'active') {
throw createHttpError('接单账号已不可用,请重新登录', {
statusCode: 401,
errorCode: 'worker_auth_user_invalid',
})
}
const currentSessionVersion = normalizeSessionVersion(worker.session_version)
if (Number(payload.ver || 0) !== currentSessionVersion) {
throw createHttpError('登录态已失效,请重新登录', {
statusCode: 401,
errorCode: 'worker_auth_stale',
})
}
const sessionId = String(payload.sid || '').trim()
const sessionRecord = sessionId ? await getWorkerSessionBySessionId(sessionId) : null
if (
!sessionRecord ||
Number(sessionRecord.worker_id) !== Number(worker.id) ||
sessionRecord.status !== 'active' ||
Date.parse(String(sessionRecord.expires_at || '')) <= Date.now() ||
!(await touchWorkerSession(sessionId, nowIso()))
) {
throw createHttpError('登录设备已被删除,请重新登录', {
statusCode: 401,
errorCode: 'worker_auth_device_revoked',
})
}
return {
sessionId,
workerId: Number(worker.id),
username: worker.username,
status: worker.status,
expiresAt,
sessionVersion: currentSessionVersion,
}
}
export async function getWorkerSessions(session: WorkerSession) {
const sessions = await listWorkerSessions(session.workerId, nowIso())
return {
maxDevices: WORKER_MAX_DEVICES,
items: sessions.map((item) => mapWorkerSession(item, session.sessionId)),
}
}
export async function deleteWorkerSession(session: WorkerSession, targetSessionId: unknown) {
const normalizedTarget = String(targetSessionId || '').trim()
if (!normalizedTarget) {
throw createHttpError('缺少登录设备标识', {
statusCode: 400,
errorCode: 'worker_session_id_required',
})
}
return {
deleted: await revokeWorkerSession(session.workerId, normalizedTarget, nowIso()),
}
}
function mapWorkerSession(session: WorkerSessionRow, currentSessionId: string) {
return {
sessionId: session.session_id,
deviceId: session.device_id,
deviceType: session.device_type,
deviceName: session.device_name,
userAgent: session.user_agent,
ipAddress: session.ip_address,
issuedAt: session.issued_at,
lastSeenAt: session.last_seen_at,
expiresAt: session.expires_at,
current: session.session_id === currentSessionId,
}
}
export async function getWorkerSessionSummary(token: unknown) {
const session = await verifyWorkerSessionToken(token)
const worker = await getRequiredWorker(session.workerId)
return {
authenticated: true,
expiresAt: session.expiresAt,
worker: mapWorkerUser(worker),
}
}
export async function getWorkerProfile(session: WorkerSession) {
const worker = await getRequiredWorker(session.workerId)
const [
@@ -1342,55 +1197,6 @@ function maskWithdrawalAccountNo(accountNo: string) {
return `${'*'.repeat(Math.max(4, normalized.length - 4))}${normalized.slice(-4)}`
}
export async function changeWorkerPassword(payload: JsonObject = {}, session: WorkerSession) {
const worker = await getRequiredWorker(session.workerId)
const currentPassword = normalizePassword(payload.currentPassword || payload.oldPassword)
const nextPassword = normalizePassword(payload.newPassword || payload.password)
if (!currentPassword) {
throw createHttpError('请输入当前密码', {
statusCode: 400,
errorCode: 'worker_password_current_required',
})
}
if (!verifyWorkerPassword(currentPassword, worker.password_hash)) {
throw createHttpError('当前密码不正确', {
statusCode: 400,
errorCode: 'worker_password_current_invalid',
})
}
if (!nextPassword) {
throw createHttpError('请输入新密码', {
statusCode: 400,
errorCode: 'worker_password_new_required',
})
}
validateWorkerPassword(nextPassword)
if (currentPassword === nextPassword) {
throw createHttpError('新密码不能与当前密码一致', {
statusCode: 400,
errorCode: 'worker_password_unchanged',
})
}
const updated = await updateWorkerPassword({
workerId: worker.id,
passwordHash: hashWorkerPassword(nextPassword),
sessionVersion: normalizeSessionVersion(worker.session_version) + 1,
now: nowIso(),
})
await revokeAllWorkerSessions(worker.id, nowIso())
return {
worker: mapWorkerUser(updated || worker),
reloginRequired: true,
}
}
export async function listWorkerHallOrders(query: JsonObject = {}, session: WorkerSession) {
requireActiveWorkerSession(session)
const page = normalizePage(query.page)
@@ -0,0 +1,165 @@
import {
getWorkerSessionBySessionId,
getWorkerUserById,
listWorkerSessions,
revokeWorkerSession,
touchWorkerSession,
} 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,
isIgnorableWorkerAuthError,
mapWorkerUser,
normalizeSessionVersion,
safeCompare,
signWorkerPayload,
} from './mappers.js'
import { getRequiredWorker, type WorkerSession } from './worker-session-context-service.js'
import { mapWorkerSession } from './worker-session-view.js'
const WORKER_MAX_DEVICES = 3
export async function logoutWorkerSession(token: unknown) {
const normalizedToken = String(token || '').trim()
if (!normalizedToken) {
return { success: true, revoked: false }
}
try {
const session = await verifyWorkerSessionToken(normalizedToken)
return {
success: true,
revoked: await revokeWorkerSession(session.workerId, session.sessionId, nowIso()),
}
} catch (error) {
if (isIgnorableWorkerAuthError(error)) {
return { success: true, revoked: false }
}
throw error
}
}
export async function verifyWorkerSessionToken(token: unknown): Promise<WorkerSession> {
ensureWorkerAuthConfigured()
const normalizedToken = String(token || '').trim()
if (!normalizedToken) {
throw createHttpError('未登录或登录已失效', {
statusCode: 401,
errorCode: 'worker_auth_required',
})
}
const parts = normalizedToken.split('.')
const encodedPayload = parts[0]
const signature = parts[1]
if (
parts.length !== 2 ||
!encodedPayload ||
!signature ||
!safeCompare(signature, signWorkerPayload(encodedPayload))
) {
throw createHttpError('接单登录态无效', {
statusCode: 401,
errorCode: 'worker_auth_invalid',
})
}
let payload: JsonObject
try {
payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'))
} catch {
throw createHttpError('接单登录态无效', {
statusCode: 401,
errorCode: 'worker_auth_invalid',
})
}
if (String(payload.typ || '') !== 'worker') {
throw createHttpError('接单登录态无效', {
statusCode: 401,
errorCode: 'worker_auth_invalid',
})
}
const expiresAt = String(payload.exp || '').trim()
if (!expiresAt || Date.parse(expiresAt) <= Date.now()) {
throw createHttpError('登录已过期,请重新登录', {
statusCode: 401,
errorCode: 'worker_auth_expired',
})
}
const workerId = Number(payload.uid || 0)
const worker = await getWorkerUserById(workerId)
if (!worker || worker.status !== 'active') {
throw createHttpError('接单账号已不可用,请重新登录', {
statusCode: 401,
errorCode: 'worker_auth_user_invalid',
})
}
const currentSessionVersion = normalizeSessionVersion(worker.session_version)
if (Number(payload.ver || 0) !== currentSessionVersion) {
throw createHttpError('登录态已失效,请重新登录', {
statusCode: 401,
errorCode: 'worker_auth_stale',
})
}
const sessionId = String(payload.sid || '').trim()
const sessionRecord = sessionId ? await getWorkerSessionBySessionId(sessionId) : null
if (
!sessionRecord ||
Number(sessionRecord.worker_id) !== Number(worker.id) ||
sessionRecord.status !== 'active' ||
Date.parse(String(sessionRecord.expires_at || '')) <= Date.now() ||
!(await touchWorkerSession(sessionId, nowIso()))
) {
throw createHttpError('登录设备已被删除,请重新登录', {
statusCode: 401,
errorCode: 'worker_auth_device_revoked',
})
}
return {
sessionId,
workerId: Number(worker.id),
username: worker.username,
status: worker.status,
expiresAt,
sessionVersion: currentSessionVersion,
}
}
export async function getWorkerSessions(session: WorkerSession) {
const sessions = await listWorkerSessions(session.workerId, nowIso())
return {
maxDevices: WORKER_MAX_DEVICES,
items: sessions.map((item) => mapWorkerSession(item, session.sessionId)),
}
}
export async function deleteWorkerSession(session: WorkerSession, targetSessionId: unknown) {
const normalizedTarget = String(targetSessionId || '').trim()
if (!normalizedTarget) {
throw createHttpError('缺少登录设备标识', {
statusCode: 400,
errorCode: 'worker_session_id_required',
})
}
return {
deleted: await revokeWorkerSession(session.workerId, normalizedTarget, nowIso()),
}
}
export async function getWorkerSessionSummary(token: unknown) {
const session = await verifyWorkerSessionToken(token)
const worker = await getRequiredWorker(session.workerId)
return {
authenticated: true,
expiresAt: session.expiresAt,
worker: mapWorkerUser(worker),
}
}
@@ -0,0 +1,16 @@
import type { WorkerSessionRow } from '../../repositories/worker-platform/index.js'
export function mapWorkerSession(session: WorkerSessionRow, currentSessionId: string) {
return {
sessionId: session.session_id,
deviceId: session.device_id,
deviceType: session.device_type,
deviceName: session.device_name,
userAgent: session.user_agent,
ipAddress: session.ip_address,
issuedAt: session.issued_at,
lastSeenAt: session.last_seen_at,
expiresAt: session.expires_at,
current: session.session_id === currentSessionId,
}
}