拆分打手平台通用模块
This commit is contained in:
@@ -98,12 +98,8 @@ import {
|
||||
resolveWorkerPermissions,
|
||||
resolveWorkOrderOperationalStatus,
|
||||
} from './mappers.js'
|
||||
import {
|
||||
ensureWorkerPlatformDefaults,
|
||||
getRequiredWorkOrder,
|
||||
getRequiredWorker,
|
||||
normalizeWorkOrderTimeoutPolicy,
|
||||
} from './worker-service.js'
|
||||
import { ensureWorkerPlatformDefaults, normalizeWorkOrderTimeoutPolicy } from './worker-service.js'
|
||||
import { getRequiredWorkOrder, getRequiredWorker } from './worker-session-context-service.js'
|
||||
import { resolveSharingQuantity } from './admin-worker-catalog-service.js'
|
||||
import {
|
||||
getKuaishouIndustrySourceConfig,
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
normalizeProofFiles,
|
||||
normalizeWithdrawChannel,
|
||||
} from './mappers.js'
|
||||
import { getRequiredWorker } from './worker-service.js'
|
||||
import { getRequiredWorker } from './worker-session-context-service.js'
|
||||
import { getWorkerFinanceConfig, saveWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||
|
||||
function mapAdminWorkerWithdrawalAccount(account: WorkerWithdrawalAccountRow) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
normalizeProofFiles,
|
||||
normalizeUploadedFiles,
|
||||
} from './mappers.js'
|
||||
import { requireActiveWorkerSession, type WorkerSession } from './worker-service.js'
|
||||
import { requireActiveWorkerSession, type WorkerSession } from './worker-session-context-service.js'
|
||||
|
||||
export async function listAdminAfterSalesCases(query: JsonObject = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './mappers.js'
|
||||
export * from './worker-session-context-service.js'
|
||||
export * from './worker-service.js'
|
||||
export * from './after-sales-service.js'
|
||||
export * from './admin-service.js'
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import {
|
||||
addWorkerWalletCredit,
|
||||
@@ -21,13 +18,45 @@ import {
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { nowIso, addHours } from '../../utils/time.js'
|
||||
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import {
|
||||
normalizeStoredFileUrl,
|
||||
refreshStoredFileUrl,
|
||||
refreshUploadedFileUrls,
|
||||
} from '../file-storage/file-storage-service.js'
|
||||
import { createWorkerSessionToken } from './worker-auth-utils.js'
|
||||
import {
|
||||
normalizeBoolean,
|
||||
normalizeInteger,
|
||||
normalizePositiveInteger,
|
||||
normalizeSessionVersion,
|
||||
} from './worker-normalizers.js'
|
||||
import {
|
||||
canWorkerAutoAcceptWholeOrder,
|
||||
resolveFreezeDepositAmount,
|
||||
resolveVisibleDelaySeconds,
|
||||
resolveWorkerPermissions,
|
||||
} from './worker-permission-utils.js'
|
||||
|
||||
export {
|
||||
ensureWorkerAuthConfigured,
|
||||
hashWorkerPassword,
|
||||
safeCompare,
|
||||
signWorkerPayload,
|
||||
verifyWorkerPassword,
|
||||
} from './worker-auth-utils.js'
|
||||
export {
|
||||
normalizeBoolean,
|
||||
normalizeInteger,
|
||||
normalizePositiveInteger,
|
||||
normalizeSessionVersion,
|
||||
} from './worker-normalizers.js'
|
||||
export {
|
||||
canWorkerAutoAcceptWholeOrder,
|
||||
resolveFreezeDepositAmount,
|
||||
resolveVisibleDelaySeconds,
|
||||
resolveWorkerPermissions,
|
||||
} from './worker-permission-utils.js'
|
||||
|
||||
export const DEFAULT_LEVEL_KEY = 'vip1'
|
||||
export const DEFAULT_LEVEL_NAME = 'VIP1'
|
||||
@@ -38,115 +67,13 @@ export const INVITE_CODE_RETRY_LIMIT = 5
|
||||
export const WORKER_CANCEL_LIMIT_PER_WINDOW = 5
|
||||
export const WORKER_CANCEL_LIMIT_WINDOW_MS = 30 * 24 * 60 * 60 * 1000
|
||||
export function createWorkerSession(worker: WorkerUserRow) {
|
||||
const issuedAt = nowIso()
|
||||
const expiresAt = addHours(issuedAt, Number(runtimeConfig.admin?.sessionTtlHours || 12))
|
||||
const payload = {
|
||||
typ: 'worker',
|
||||
sid: crypto.randomBytes(12).toString('hex'),
|
||||
uid: Number(worker.id),
|
||||
usr: worker.username,
|
||||
status: worker.status,
|
||||
ver: normalizeSessionVersion(worker.session_version),
|
||||
iat: issuedAt,
|
||||
exp: expiresAt,
|
||||
}
|
||||
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||||
const session = createWorkerSessionToken(worker)
|
||||
return {
|
||||
sessionId: payload.sid,
|
||||
token: `${encodedPayload}.${signWorkerPayload(encodedPayload)}`,
|
||||
expiresAt,
|
||||
...session,
|
||||
worker: mapWorkerUser(worker),
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureWorkerAuthConfigured() {
|
||||
if (String(runtimeConfig.admin?.sessionSecret || '').trim()) {
|
||||
return
|
||||
}
|
||||
|
||||
throw createHttpError('接单登录态签名密钥未配置,请设置 ADMIN_SESSION_SECRET', {
|
||||
statusCode: 503,
|
||||
errorCode: 'worker_auth_not_configured',
|
||||
})
|
||||
}
|
||||
|
||||
export function hashWorkerPassword(password: string): string {
|
||||
const salt = crypto.randomBytes(16).toString('hex')
|
||||
const derived = crypto.scryptSync(password, salt, 64).toString('hex')
|
||||
return `scrypt$${salt}$${derived}`
|
||||
}
|
||||
|
||||
export function verifyWorkerPassword(password: string, storedHash: string): boolean {
|
||||
const [algorithm, salt, expectedHash] = String(storedHash || '').split('$')
|
||||
if (algorithm !== 'scrypt' || !salt || !expectedHash) return false
|
||||
return safeCompare(crypto.scryptSync(password, salt, 64).toString('hex'), expectedHash)
|
||||
}
|
||||
|
||||
export function signWorkerPayload(encodedPayload: string): string {
|
||||
return crypto
|
||||
.createHmac('sha256', String(runtimeConfig.admin?.sessionSecret || ''))
|
||||
.update(`worker:${encodedPayload}`)
|
||||
.digest('base64url')
|
||||
}
|
||||
|
||||
export function safeCompare(input: unknown, expected: unknown): boolean {
|
||||
const left = Buffer.from(String(input || ''), 'utf8')
|
||||
const right = Buffer.from(String(expected || ''), 'utf8')
|
||||
if (left.length !== right.length) return false
|
||||
return crypto.timingSafeEqual(left, right)
|
||||
}
|
||||
|
||||
export function resolveWorkerPermissions(worker: WorkerUserRow) {
|
||||
const permission = safeParseJson(worker.level_permission_json)
|
||||
return {
|
||||
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||
autoAcceptWithoutEvidence: normalizeBoolean(permission.autoAcceptWithoutEvidence, false),
|
||||
visibleDelaySeconds: resolveVisibleDelaySeconds(
|
||||
worker.level_key || '',
|
||||
permission.visibleDelaySeconds,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 整单接单人与拼单参与者使用不同验收路径。
|
||||
* 母工单保留拼单配置时,后台仍可在无人参与前整单指派,不能因此失去 VIP 免审核资格。
|
||||
*/
|
||||
export function canWorkerAutoAcceptWholeOrder(input: {
|
||||
worker: WorkerUserRow
|
||||
hasPersonalSharingShare: boolean
|
||||
}): boolean {
|
||||
return (
|
||||
!input.hasPersonalSharingShare &&
|
||||
resolveWorkerPermissions(input.worker).autoAcceptWithoutEvidence
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveVisibleDelaySeconds(levelKey: string, configured?: unknown): number {
|
||||
const configuredValue = normalizeInteger(configured, -1)
|
||||
if (configuredValue >= 0) return configuredValue
|
||||
const defaultDelays: Record<string, number> = {
|
||||
vip1: 20,
|
||||
vip2: 15,
|
||||
vip3: 10,
|
||||
vip4: 5,
|
||||
vip5: 0,
|
||||
}
|
||||
return defaultDelays[String(levelKey || '').toLowerCase()] ?? 0
|
||||
}
|
||||
|
||||
export function resolveFreezeDepositAmount(
|
||||
workOrder: WorkOrderRow,
|
||||
permissions: { depositFreeAmount: number },
|
||||
) {
|
||||
return Math.max(
|
||||
0,
|
||||
Number(workOrder.required_deposit_amount || 0) - Number(permissions.depositFreeAmount || 0),
|
||||
)
|
||||
}
|
||||
|
||||
export function mapWorkerLevel(level: WorkerLevelRow | null | undefined) {
|
||||
if (!level) return null
|
||||
const permission = safeParseJson(level.permission_json)
|
||||
@@ -1637,17 +1564,6 @@ export function normalizeMatchType(value: unknown) {
|
||||
return 'contains'
|
||||
}
|
||||
|
||||
export function normalizeBoolean(value: unknown, fallback: boolean) {
|
||||
if (typeof value === 'boolean') return value
|
||||
const text = String(value ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!text) return fallback
|
||||
if (['true', '1', 'yes', 'enabled', 'active', 'on'].includes(text)) return true
|
||||
if (['false', '0', 'no', 'disabled', 'off'].includes(text)) return false
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function normalizeSlugKey(value: unknown, fallback: string) {
|
||||
const key = String(value || '')
|
||||
.trim()
|
||||
@@ -1689,26 +1605,11 @@ export function isWorkerPasswordStrong(password: string): boolean {
|
||||
return password.length >= 8 && /[a-zA-Z]/.test(password) && /\d/.test(password)
|
||||
}
|
||||
|
||||
export function normalizeSessionVersion(value: unknown): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
||||
}
|
||||
|
||||
export function normalizeOptionalId(value: unknown): number | null {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : null
|
||||
}
|
||||
|
||||
export function normalizeInteger(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
export function normalizePositiveInteger(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
|
||||
export function normalizeAmountFen(value: unknown, fallback: number): number {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) return fallback
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import type { WorkerUserRow } from '../../repositories/worker-platform/index.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { addHours, nowIso } from '../../utils/time.js'
|
||||
import { normalizeSessionVersion } from './worker-normalizers.js'
|
||||
|
||||
export function createWorkerSessionToken(worker: WorkerUserRow) {
|
||||
const issuedAt = nowIso()
|
||||
const expiresAt = addHours(issuedAt, Number(runtimeConfig.admin?.sessionTtlHours || 12))
|
||||
const payload = {
|
||||
typ: 'worker',
|
||||
sid: crypto.randomBytes(12).toString('hex'),
|
||||
uid: Number(worker.id),
|
||||
usr: worker.username,
|
||||
status: worker.status,
|
||||
ver: normalizeSessionVersion(worker.session_version),
|
||||
iat: issuedAt,
|
||||
exp: expiresAt,
|
||||
}
|
||||
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
||||
return {
|
||||
sessionId: payload.sid,
|
||||
token: `${encodedPayload}.${signWorkerPayload(encodedPayload)}`,
|
||||
expiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureWorkerAuthConfigured() {
|
||||
if (String(runtimeConfig.admin?.sessionSecret || '').trim()) return
|
||||
|
||||
throw createHttpError('接单登录态签名密钥未配置,请设置 ADMIN_SESSION_SECRET', {
|
||||
statusCode: 503,
|
||||
errorCode: 'worker_auth_not_configured',
|
||||
})
|
||||
}
|
||||
|
||||
export function hashWorkerPassword(password: string): string {
|
||||
const salt = crypto.randomBytes(16).toString('hex')
|
||||
const derived = crypto.scryptSync(password, salt, 64).toString('hex')
|
||||
return `scrypt$${salt}$${derived}`
|
||||
}
|
||||
|
||||
export function verifyWorkerPassword(password: string, storedHash: string): boolean {
|
||||
const [algorithm, salt, expectedHash] = String(storedHash || '').split('$')
|
||||
if (algorithm !== 'scrypt' || !salt || !expectedHash) return false
|
||||
return safeCompare(crypto.scryptSync(password, salt, 64).toString('hex'), expectedHash)
|
||||
}
|
||||
|
||||
export function signWorkerPayload(encodedPayload: string): string {
|
||||
return crypto
|
||||
.createHmac('sha256', String(runtimeConfig.admin?.sessionSecret || ''))
|
||||
.update(`worker:${encodedPayload}`)
|
||||
.digest('base64url')
|
||||
}
|
||||
|
||||
export function safeCompare(input: unknown, expected: unknown): boolean {
|
||||
const left = Buffer.from(String(input || ''), 'utf8')
|
||||
const right = Buffer.from(String(expected || ''), 'utf8')
|
||||
if (left.length !== right.length) return false
|
||||
return crypto.timingSafeEqual(left, right)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export function normalizeBoolean(value: unknown, fallback: boolean) {
|
||||
if (typeof value === 'boolean') return value
|
||||
const text = String(value ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!text) return fallback
|
||||
if (['true', '1', 'yes', 'enabled', 'active', 'on'].includes(text)) return true
|
||||
if (['false', '0', 'no', 'disabled', 'off'].includes(text)) return false
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function normalizeSessionVersion(value: unknown): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
|
||||
}
|
||||
|
||||
export function normalizeInteger(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
export function normalizePositiveInteger(value: unknown, fallback: number): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { WorkOrderRow, WorkerUserRow } from '../../repositories/worker-platform/index.js'
|
||||
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import {
|
||||
normalizeBoolean,
|
||||
normalizeInteger,
|
||||
normalizePositiveInteger,
|
||||
} from './worker-normalizers.js'
|
||||
|
||||
export function resolveWorkerPermissions(worker: WorkerUserRow) {
|
||||
const permission = safeParseJson(worker.level_permission_json)
|
||||
return {
|
||||
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
|
||||
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 1),
|
||||
upgradeThreshold: normalizeInteger(permission.upgradeThreshold, 0),
|
||||
autoAcceptWithoutEvidence: normalizeBoolean(permission.autoAcceptWithoutEvidence, false),
|
||||
visibleDelaySeconds: resolveVisibleDelaySeconds(
|
||||
worker.level_key || '',
|
||||
permission.visibleDelaySeconds,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/** 整单接单与拼单参与走不同验收路径,拼单配置不能影响整单的 VIP 免审核资格。 */
|
||||
export function canWorkerAutoAcceptWholeOrder(input: {
|
||||
worker: WorkerUserRow
|
||||
hasPersonalSharingShare: boolean
|
||||
}): boolean {
|
||||
return (
|
||||
!input.hasPersonalSharingShare &&
|
||||
resolveWorkerPermissions(input.worker).autoAcceptWithoutEvidence
|
||||
)
|
||||
}
|
||||
|
||||
export function resolveVisibleDelaySeconds(levelKey: string, configured?: unknown): number {
|
||||
const configuredValue = normalizeInteger(configured, -1)
|
||||
if (configuredValue >= 0) return configuredValue
|
||||
const defaultDelays: Record<string, number> = {
|
||||
vip1: 20,
|
||||
vip2: 15,
|
||||
vip3: 10,
|
||||
vip4: 5,
|
||||
vip5: 0,
|
||||
}
|
||||
return defaultDelays[String(levelKey || '').toLowerCase()] ?? 0
|
||||
}
|
||||
|
||||
export function resolveFreezeDepositAmount(
|
||||
workOrder: WorkOrderRow,
|
||||
permissions: { depositFreeAmount: number },
|
||||
) {
|
||||
return Math.max(
|
||||
0,
|
||||
Number(workOrder.required_deposit_amount || 0) - Number(permissions.depositFreeAmount || 0),
|
||||
)
|
||||
}
|
||||
@@ -87,6 +87,12 @@ 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'
|
||||
import {
|
||||
getRequiredWorkOrder,
|
||||
getRequiredWorker,
|
||||
requireActiveWorkerSession,
|
||||
type WorkerSession,
|
||||
} from './worker-session-context-service.js'
|
||||
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
|
||||
import {
|
||||
createWorkerAcceptanceAdminNotification,
|
||||
@@ -158,14 +164,12 @@ const WORKER_DAILY_WITHDRAW_LIMIT = 3
|
||||
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
||||
const WORKER_SMS_MAX_VERIFY_ATTEMPTS = 5
|
||||
|
||||
export type WorkerSession = {
|
||||
sessionId: string
|
||||
workerId: number
|
||||
username: string
|
||||
status: string
|
||||
expiresAt: string
|
||||
sessionVersion: number
|
||||
}
|
||||
export {
|
||||
getRequiredWorkOrder,
|
||||
getRequiredWorker,
|
||||
requireActiveWorkerSession,
|
||||
} from './worker-session-context-service.js'
|
||||
export type { WorkerSession } from './worker-session-context-service.js'
|
||||
|
||||
const WORKER_MAX_DEVICES = 3
|
||||
|
||||
@@ -943,17 +947,6 @@ export async function getWorkerSessionSummary(token: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
export function requireActiveWorkerSession(session: WorkerSession | null | undefined) {
|
||||
if (session?.status === 'active') {
|
||||
return
|
||||
}
|
||||
|
||||
throw createHttpError('接单账号尚未通过审核', {
|
||||
statusCode: 403,
|
||||
errorCode: 'worker_not_active',
|
||||
})
|
||||
}
|
||||
|
||||
export async function getWorkerProfile(session: WorkerSession) {
|
||||
const worker = await getRequiredWorker(session.workerId)
|
||||
const [
|
||||
@@ -2979,25 +2972,3 @@ export function resolveCollectSubmitTargetWorkOrder(
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function getRequiredWorker(workerId: number | string): Promise<WorkerUserRow> {
|
||||
const worker = await getWorkerUserById(workerId)
|
||||
if (!worker) {
|
||||
throw createHttpError('接单账号不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_not_found',
|
||||
})
|
||||
}
|
||||
return worker
|
||||
}
|
||||
|
||||
export async function getRequiredWorkOrder(workOrderId: number | string): Promise<WorkOrderRow> {
|
||||
const workOrder = await getWorkOrderById(workOrderId)
|
||||
if (!workOrder) {
|
||||
throw createHttpError('接单订单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_order_not_found',
|
||||
})
|
||||
}
|
||||
return workOrder
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
getWorkOrderById,
|
||||
getWorkerUserById,
|
||||
type WorkOrderRow,
|
||||
type WorkerUserRow,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
|
||||
export type WorkerSession = {
|
||||
sessionId: string
|
||||
workerId: number
|
||||
username: string
|
||||
status: string
|
||||
expiresAt: string
|
||||
sessionVersion: number
|
||||
}
|
||||
|
||||
export function requireActiveWorkerSession(session: WorkerSession | null | undefined) {
|
||||
if (session?.status === 'active') return
|
||||
|
||||
throw createHttpError('接单账号尚未通过审核', {
|
||||
statusCode: 403,
|
||||
errorCode: 'worker_not_active',
|
||||
})
|
||||
}
|
||||
|
||||
export async function getRequiredWorker(workerId: number | string): Promise<WorkerUserRow> {
|
||||
const worker = await getWorkerUserById(workerId)
|
||||
if (!worker) {
|
||||
throw createHttpError('接单账号不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'worker_not_found',
|
||||
})
|
||||
}
|
||||
return worker
|
||||
}
|
||||
|
||||
export async function getRequiredWorkOrder(workOrderId: number | string): Promise<WorkOrderRow> {
|
||||
const workOrder = await getWorkOrderById(workOrderId)
|
||||
if (!workOrder) {
|
||||
throw createHttpError('接单订单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_order_not_found',
|
||||
})
|
||||
}
|
||||
return workOrder
|
||||
}
|
||||
Reference in New Issue
Block a user