新增接单平台第一期闭环

This commit is contained in:
yml2213
2026-07-25 13:58:53 +08:00
parent 06decc51ff
commit f6d3f00f21
27 changed files with 3987 additions and 0 deletions
@@ -0,0 +1,974 @@
import crypto from 'node:crypto'
import { runtimeConfig } from '../../config/runtime.js'
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
import {
acceptWorkOrderAndSettle,
addWorkerWalletCredit,
countWorkerActiveOrders,
createWorkOrder,
createWorkOrderEvent,
createWorkerUser,
findPendingMaterialWorkOrderByPlatformOrderId,
getWorkCategoryByKey,
getWorkOrderById,
getWorkerLevelById,
getWorkerLevelByKey,
getWorkerUserById,
getWorkerUserByUsername,
grabWorkOrder,
listWorkCategories,
listWorkOrders,
listWorkerLevels,
listWorkerUsers,
updateWorkOrder,
updateWorkerUser,
upsertWorkCategory,
upsertWorkerLevel,
type WorkOrderRow,
type WorkerLevelRow,
type WorkerUserRow,
} from '../../repositories/worker-platform-repo.js'
import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js'
import { randomId } from '../../utils/random.js'
import { nowIso, addHours } from '../../utils/time.js'
import {
normalizePage,
normalizePageSize,
safeParseJson,
} from '../admin/admin-query-utils.js'
export type WorkerSession = {
sessionId: string
workerId: number
username: string
status: string
expiresAt: string
sessionVersion: number
}
const DEFAULT_LEVEL_KEY = 'regular'
const DEFAULT_CATEGORY_KEY = 'default'
const DEFAULT_DEPOSIT_THRESHOLD_AMOUNT = 20_000
export async function ensureWorkerPlatformDefaults() {
const now = nowIso()
const level = await upsertWorkerLevel({
levelKey: DEFAULT_LEVEL_KEY,
name: '普通接单员',
sortOrder: 100,
status: 'active',
permissionJson: JSON.stringify({
depositFreeAmount: 0,
maxActiveOrders: 3,
}),
now,
})
const category = await upsertWorkCategory({
categoryKey: DEFAULT_CATEGORY_KEY,
name: '默认分类',
sortOrder: 100,
status: 'active',
now,
})
return { level, category }
}
export async function registerWorker(payload: JsonObject = {}) {
ensureWorkerAuthConfigured()
const username = normalizeUsername(payload.username)
const password = normalizePassword(payload.password)
const displayName = String(payload.displayName || payload.nickname || username).trim()
const phone = String(payload.phone || '').trim()
validateWorkerUsername(username)
validateWorkerPassword(password)
if (await getWorkerUserByUsername(username)) {
throw createHttpError('接单账号已存在', {
statusCode: 409,
errorCode: 'worker_user_exists',
})
}
const defaults = await ensureWorkerPlatformDefaults()
const now = nowIso()
const created = await createWorkerUser({
username,
passwordHash: hashWorkerPassword(password),
displayName,
phone,
levelId: defaults.level?.id || null,
status: 'pending_review',
now,
})
if (!created) {
throw createHttpError('接单账号注册失败', {
statusCode: 500,
errorCode: 'worker_register_failed',
})
}
return {
worker: mapWorkerUser(created),
reviewRequired: true,
}
}
export async function loginWorker(username: unknown, password: unknown) {
ensureWorkerAuthConfigured()
const normalizedUsername = normalizeUsername(username)
const normalizedPassword = normalizePassword(password)
if (!normalizedUsername || !normalizedPassword) {
throw createHttpError('缺少接单账号或密码', {
statusCode: 400,
errorCode: 'worker_credentials_required',
})
}
const worker = await getWorkerUserByUsername(normalizedUsername)
if (!worker || !verifyWorkerPassword(normalizedPassword, worker.password_hash)) {
throw createHttpError('账号或密码错误', {
statusCode: 401,
errorCode: 'worker_login_failed',
})
}
if (worker.status === 'disabled') {
throw createHttpError('接单账号已停用', {
statusCode: 403,
errorCode: 'worker_disabled',
})
}
return createWorkerSession(worker)
}
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 [encodedPayload, signature] = normalizedToken.split('.')
if (!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 === 'disabled') {
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',
})
}
return {
sessionId: String(payload.sid || '').trim(),
workerId: Number(worker.id),
username: worker.username,
status: worker.status,
expiresAt,
sessionVersion: currentSessionVersion,
}
}
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 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)
return {
worker: mapWorkerUser(worker),
permissions: resolveWorkerPermissions(worker),
}
}
export async function listWorkerHallOrders(query: JsonObject = {}, session: WorkerSession) {
requireActiveWorkerSession(session)
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const { items, total } = await listWorkOrders({
page,
pageSize,
status: WORK_ORDER_STATUS.OPEN,
keyword: String(query.keyword || '').trim(),
})
const worker = await getRequiredWorker(session.workerId)
const permissions = resolveWorkerPermissions(worker)
return {
items: items.map((item) => mapWorkOrderForWorker(item, permissions)),
pagination: { page, pageSize, total },
}
}
export async function grabWorkerHallOrder(workOrderId: number | string, session: WorkerSession) {
requireActiveWorkerSession(session)
const worker = await getRequiredWorker(session.workerId)
const permissions = resolveWorkerPermissions(worker)
const activeCount = await countWorkerActiveOrders(worker.id)
if (activeCount >= permissions.maxActiveOrders) {
throw createHttpError('已达到当前等级最大同时接单量', {
statusCode: 409,
errorCode: 'worker_active_order_limit',
})
}
const workOrder = await getRequiredWorkOrder(workOrderId)
if (workOrder.status !== WORK_ORDER_STATUS.OPEN || workOrder.assigned_worker_id) {
throw createHttpError('该订单已不可抢', {
statusCode: 409,
errorCode: 'work_order_not_open',
})
}
const freezeAmount = resolveFreezeDepositAmount(workOrder, permissions)
if (Number(worker.available_amount || 0) < freezeAmount) {
throw createHttpError('余额不足,无法冻结所需押金', {
statusCode: 409,
errorCode: 'worker_deposit_insufficient',
})
}
const grabbed = await grabWorkOrder({
workOrderId: Number(workOrder.id),
workerId: Number(worker.id),
depositAmount: freezeAmount,
now: nowIso(),
})
if (!grabbed) {
throw createHttpError('抢单失败,订单可能已被抢走', {
statusCode: 409,
errorCode: 'work_order_grab_conflict',
})
}
return { order: mapWorkOrderForWorker(grabbed, permissions) }
}
export async function listWorkerMyOrders(query: JsonObject = {}, session: WorkerSession) {
requireActiveWorkerSession(session)
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const worker = await getRequiredWorker(session.workerId)
const { items, total } = await listWorkOrders({
page,
pageSize,
status: String(query.status || '').trim(),
keyword: String(query.keyword || '').trim(),
workerId: worker.id,
})
const permissions = resolveWorkerPermissions(worker)
return {
items: items.map((item) => mapWorkOrderForWorker(item, permissions)),
pagination: { page, pageSize, total },
}
}
export async function submitWorkerOrderAcceptance(
workOrderId: number | string,
payload: JsonObject = {},
session: WorkerSession,
) {
requireActiveWorkerSession(session)
const workOrder = await getRequiredWorkOrder(workOrderId)
if (Number(workOrder.assigned_worker_id || 0) !== Number(session.workerId)) {
throw createHttpError('只能提交自己的订单', {
statusCode: 403,
errorCode: 'work_order_owner_required',
})
}
if (![WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PROBLEM].includes(workOrder.status as never)) {
throw createHttpError('当前状态不能提交验收', {
statusCode: 409,
errorCode: 'work_order_submit_status_invalid',
})
}
const now = nowIso()
const acceptance = {
note: String(payload.note || '').trim(),
imageUrls: normalizeStringArray(payload.imageUrls),
submittedAt: now,
}
const updated = await updateWorkOrder(workOrder.id, {
status: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
acceptance_json: acceptance,
submitted_at: now,
updated_at: now,
})
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'worker',
actorId: String(session.workerId),
eventType: 'acceptance_submitted',
fromStatus: workOrder.status,
toStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
payloadJson: JSON.stringify(acceptance),
now,
})
return { order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(await getRequiredWorker(session.workerId))) }
}
export async function collectLookupWorkOrder(payload: JsonObject = {}) {
const orderNo = String(payload.orderNo || payload.platformOrderId || '').trim()
if (!orderNo) {
throw createHttpError('请填写订单号', {
statusCode: 400,
errorCode: 'collect_order_no_required',
})
}
const workOrder = await findPendingMaterialWorkOrderByPlatformOrderId(orderNo)
if (!workOrder) {
throw createHttpError('未找到待完善订单,请确认订单号是否正确', {
statusCode: 404,
errorCode: 'collect_order_not_found',
})
}
return {
order: mapWorkOrderPublic(workOrder),
fields: resolveRequirementFields(workOrder),
}
}
export async function collectSubmitWorkOrder(payload: JsonObject = {}) {
const orderNo = String(payload.orderNo || payload.platformOrderId || '').trim()
if (!orderNo) {
throw createHttpError('请填写订单号', {
statusCode: 400,
errorCode: 'collect_order_no_required',
})
}
const workOrder = await findPendingMaterialWorkOrderByPlatformOrderId(orderNo)
if (!workOrder) {
throw createHttpError('未找到待完善订单,请确认订单号是否正确', {
statusCode: 404,
errorCode: 'collect_order_not_found',
})
}
const fields = resolveRequirementFields(workOrder)
const submittedFields = normalizeSubmittedFields(payload.fields || payload.material || payload)
const complete = fields.every((field) => !field.required || String(submittedFields[field.key] || '').trim())
const now = nowIso()
const material = {
...safeParseJson(workOrder.material_json),
collect: {
fields: submittedFields,
submittedAt: now,
complete,
},
}
const nextStatus = complete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL
const updated = await updateWorkOrder(workOrder.id, {
status: nextStatus,
material_json: material,
updated_at: now,
})
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'collect',
actorId: orderNo,
eventType: 'material_submitted',
fromStatus: workOrder.status,
toStatus: nextStatus,
payloadJson: JSON.stringify({ fields: submittedFields, complete }),
now,
})
return {
order: mapWorkOrderPublic(updated || workOrder),
complete,
}
}
export async function listAdminWorkerLevels() {
await ensureWorkerPlatformDefaults()
return { items: (await listWorkerLevels()).map(mapWorkerLevel) }
}
export async function saveAdminWorkerLevel(payload: JsonObject = {}) {
const now = nowIso()
const levelKey = String(payload.levelKey || payload.level_key || '').trim() || DEFAULT_LEVEL_KEY
const name = String(payload.name || '').trim() || '普通接单员'
const permissions = {
depositFreeAmount: normalizeAmountFen(payload.depositFreeAmount ?? payload.depositFreeAmountYuan, 0),
maxActiveOrders: normalizePositiveInteger(payload.maxActiveOrders, 3),
}
const level = await upsertWorkerLevel({
levelKey,
name,
sortOrder: normalizeInteger(payload.sortOrder, 100),
status: String(payload.status || 'active').trim() === 'disabled' ? 'disabled' : 'active',
permissionJson: JSON.stringify(permissions),
now,
})
return { level: mapWorkerLevel(level) }
}
export async function listAdminWorkerUsers(query: JsonObject = {}) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const { items, total } = await listWorkerUsers({
page,
pageSize,
status: String(query.status || '').trim(),
keyword: String(query.keyword || '').trim(),
})
return {
items: items.map(mapWorkerUser),
pagination: { page, pageSize, total },
}
}
export async function reviewAdminWorkerUser(workerId: number | string, payload: JsonObject = {}) {
const worker = await getRequiredWorker(workerId)
const status = normalizeReviewStatus(payload.status)
const levelId = normalizeOptionalId(payload.levelId) || worker.level_id || (await ensureWorkerPlatformDefaults()).level?.id || null
const now = nowIso()
const updated = await updateWorkerUser(worker.id, {
status,
level_id: levelId,
review_note: String(payload.reviewNote || payload.note || '').trim(),
session_version: normalizeSessionVersion(worker.session_version) + 1,
reviewed_at: now,
updated_at: now,
})
return { worker: mapWorkerUser(updated || worker) }
}
export async function creditAdminWorkerWallet(workerId: number | string, payload: JsonObject = {}) {
await getRequiredWorker(workerId)
const amount = normalizeAmountFen(payload.amount ?? payload.amountYuan, 0)
if (amount <= 0) {
throw createHttpError('充值金额必须大于 0', {
statusCode: 400,
errorCode: 'worker_credit_amount_invalid',
})
}
const wallet = await addWorkerWalletCredit({
workerId: Number(workerId),
amount,
note: String(payload.note || '后台人工充值').trim(),
payloadJson: JSON.stringify({ source: 'admin_manual_credit' }),
now: nowIso(),
})
return { wallet: mapWallet(wallet) }
}
export async function listAdminWorkOrders(query: JsonObject = {}) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
const { items, total } = await listWorkOrders({
page,
pageSize,
status: String(query.status || '').trim(),
keyword: String(query.keyword || '').trim(),
})
return {
items: items.map(mapWorkOrderAdmin),
pagination: { page, pageSize, total },
}
}
export async function createAdminMockWorkOrder(payload: JsonObject = {}) {
const defaults = await ensureWorkerPlatformDefaults()
const now = nowIso()
const rewardAmount = normalizeAmountFen(payload.rewardAmount ?? payload.rewardAmountYuan, 2500)
const depositThresholdAmount = normalizeAmountFen(
payload.depositThresholdAmount ?? payload.depositThresholdAmountYuan,
DEFAULT_DEPOSIT_THRESHOLD_AMOUNT,
)
const requiredDepositAmount = payload.requiredDepositAmount === undefined
? (rewardAmount > depositThresholdAmount ? rewardAmount : 0)
: normalizeAmountFen(payload.requiredDepositAmount, 0)
const materialComplete = payload.materialComplete === true || payload.materialComplete === 'true'
const fields = normalizeRequirementFields(payload.fields)
const platformOrderId = String(payload.platformOrderId || payload.orderNo || `MOCKWORK${Date.now()}`).trim()
const productName = String(payload.productName || '指挥官秘钥1个').trim()
const material = materialComplete
? {
collect: {
complete: true,
submittedAt: now,
fields: fields.reduce<Record<string, string>>((result, field) => {
result[field.key] = String(field.mockValue || `${field.label}测试值`)
return result
}, {}),
},
}
: {}
const workOrder = await createWorkOrder({
workOrderNo: randomId('WO'),
platformOrderId,
productName,
categoryId: defaults.category?.id || null,
status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL,
rewardAmount,
requiredDepositAmount,
depositThresholdAmount,
materialJson: JSON.stringify(material),
requirementJson: JSON.stringify({ fields }),
now,
})
if (!workOrder) {
throw createHttpError('接单工单创建失败', {
statusCode: 500,
errorCode: 'work_order_create_failed',
})
}
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'admin',
actorId: 'dev_mock',
eventType: 'mock_created',
fromStatus: '',
toStatus: workOrder.status,
payloadJson: JSON.stringify({ platformOrderId, productName, rewardAmount }),
now,
})
return { order: mapWorkOrderAdmin(workOrder) }
}
export async function publishAdminWorkOrder(workOrderId: number | string) {
const workOrder = await getRequiredWorkOrder(workOrderId)
if (workOrder.status !== WORK_ORDER_STATUS.UNASSIGNED) {
throw createHttpError('只有未分配订单可以发布到大厅', {
statusCode: 409,
errorCode: 'work_order_publish_status_invalid',
})
}
const now = nowIso()
const updated = await updateWorkOrder(workOrder.id, {
status: WORK_ORDER_STATUS.OPEN,
published_at: now,
updated_at: now,
})
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'admin',
actorId: '',
eventType: 'published',
fromStatus: workOrder.status,
toStatus: WORK_ORDER_STATUS.OPEN,
now,
})
return { order: mapWorkOrderAdmin(updated || workOrder) }
}
export async function markAdminWorkOrderProblem(workOrderId: number | string, payload: JsonObject = {}) {
const workOrder = await getRequiredWorkOrder(workOrderId)
if (![WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PENDING_ACCEPTANCE].includes(workOrder.status as never)) {
throw createHttpError('当前状态不能标记问题单', {
statusCode: 409,
errorCode: 'work_order_problem_status_invalid',
})
}
const now = nowIso()
const note = String(payload.note || payload.problemNote || '').trim()
const updated = await updateWorkOrder(workOrder.id, {
status: WORK_ORDER_STATUS.PROBLEM,
problem_note: note,
updated_at: now,
})
await createWorkOrderEvent({
workOrderId: workOrder.id,
actorType: 'admin',
actorId: '',
eventType: 'marked_problem',
fromStatus: workOrder.status,
toStatus: WORK_ORDER_STATUS.PROBLEM,
payloadJson: JSON.stringify({ note }),
now,
})
return { order: mapWorkOrderAdmin(updated || workOrder) }
}
export async function acceptAdminWorkOrder(workOrderId: number | string) {
const workOrder = await getRequiredWorkOrder(workOrderId)
if (workOrder.status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
throw createHttpError('只有待验收订单可以通过验收', {
statusCode: 409,
errorCode: 'work_order_accept_status_invalid',
})
}
const now = nowIso()
const updated = await acceptWorkOrderAndSettle({ workOrderId: workOrder.id, now })
if (!updated) {
throw createHttpError('验收失败,订单状态可能已变化', {
statusCode: 409,
errorCode: 'work_order_accept_conflict',
})
}
return { order: mapWorkOrderAdmin(updated || workOrder) }
}
export async function getAdminWorkerPlatformSummary() {
const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
listWorkOrders({ page: 1, pageSize: 1, status: WORK_ORDER_STATUS.PENDING_MATERIAL }),
listWorkOrders({ page: 1, pageSize: 1, status: WORK_ORDER_STATUS.OPEN }),
listWorkOrders({ page: 1, pageSize: 1, status: WORK_ORDER_STATUS.IN_PROGRESS }),
])
return {
pendingWorkerCount: pendingWorkers.total,
pendingMaterialCount: pendingMaterial.total,
openOrderCount: openOrders.total,
inProgressOrderCount: inProgressOrders.total,
}
}
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')
return {
token: `${encodedPayload}.${signWorkerPayload(encodedPayload)}`,
expiresAt,
worker: mapWorkerUser(worker),
}
}
function ensureWorkerAuthConfigured() {
if (String(runtimeConfig.admin?.sessionSecret || '').trim()) {
return
}
throw createHttpError('接单登录态签名密钥未配置,请设置 ADMIN_SESSION_SECRET', {
statusCode: 503,
errorCode: 'worker_auth_not_configured',
})
}
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}`
}
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)
}
function signWorkerPayload(encodedPayload: string): string {
return crypto
.createHmac('sha256', String(runtimeConfig.admin?.sessionSecret || ''))
.update(`worker:${encodedPayload}`)
.digest('base64url')
}
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)
}
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
}
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
}
function resolveWorkerPermissions(worker: WorkerUserRow) {
const permission = safeParseJson(worker.level_permission_json)
return {
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 3),
}
}
function resolveFreezeDepositAmount(workOrder: WorkOrderRow, permissions: { depositFreeAmount: number }) {
return Math.max(0, Number(workOrder.required_deposit_amount || 0) - Number(permissions.depositFreeAmount || 0))
}
function mapWorkerLevel(level: WorkerLevelRow | null | undefined) {
if (!level) return null
const permission = safeParseJson(level.permission_json)
return {
levelId: Number(level.id),
levelKey: level.level_key,
name: level.name,
sortOrder: Number(level.sort_order || 100),
status: level.status,
depositFreeAmount: normalizeInteger(permission.depositFreeAmount, 0),
maxActiveOrders: normalizePositiveInteger(permission.maxActiveOrders, 3),
createdAt: level.created_at,
updatedAt: level.updated_at,
}
}
function mapWorkerUser(worker: WorkerUserRow) {
return {
workerId: Number(worker.id),
username: worker.username,
displayName: worker.display_name || worker.username,
phone: worker.phone,
status: worker.status,
reviewNote: worker.review_note || '',
level: worker.level_id
? {
levelId: Number(worker.level_id),
levelKey: worker.level_key || '',
name: worker.level_name || '',
permissions: resolveWorkerPermissions(worker),
}
: null,
wallet: {
availableAmount: Number(worker.available_amount || 0),
frozenDepositAmount: Number(worker.frozen_deposit_amount || 0),
},
createdAt: worker.created_at,
updatedAt: worker.updated_at,
reviewedAt: worker.reviewed_at,
}
}
function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
return {
workOrderId: Number(workOrder.id),
workOrderNo: workOrder.work_order_no,
platformOrderId: workOrder.platform_order_id,
productName: workOrder.product_name,
categoryName: workOrder.category_name || '',
status: workOrder.status,
rewardAmount: Number(workOrder.reward_amount || 0),
requiredDepositAmount: Number(workOrder.required_deposit_amount || 0),
depositThresholdAmount: Number(workOrder.deposit_threshold_amount || DEFAULT_DEPOSIT_THRESHOLD_AMOUNT),
material: safeParseJson(workOrder.material_json),
requirement: safeParseJson(workOrder.requirement_json),
acceptance: safeParseJson(workOrder.acceptance_json),
problemNote: workOrder.problem_note || '',
worker: workOrder.assigned_worker_id
? {
workerId: Number(workOrder.assigned_worker_id),
username: workOrder.worker_username || '',
displayName: workOrder.worker_display_name || '',
}
: null,
publishedAt: workOrder.published_at,
assignedAt: workOrder.assigned_at,
submittedAt: workOrder.submitted_at,
acceptedAt: workOrder.accepted_at,
createdAt: workOrder.created_at,
updatedAt: workOrder.updated_at,
}
}
function mapWorkOrderForWorker(workOrder: WorkOrderRow, permissions: { depositFreeAmount: number }) {
return {
...mapWorkOrderAdmin(workOrder),
freezeDepositAmount: resolveFreezeDepositAmount(workOrder, permissions),
}
}
function mapWorkOrderPublic(workOrder: WorkOrderRow) {
return {
workOrderNo: workOrder.work_order_no,
platformOrderId: workOrder.platform_order_id,
productName: workOrder.product_name,
status: workOrder.status,
}
}
function mapWallet(wallet: Awaited<ReturnType<typeof addWorkerWalletCredit>>) {
return {
workerId: Number(wallet?.worker_id || 0),
availableAmount: Number(wallet?.available_amount || 0),
frozenDepositAmount: Number(wallet?.frozen_deposit_amount || 0),
totalCreditedAmount: Number(wallet?.total_credited_amount || 0),
totalSettledAmount: Number(wallet?.total_settled_amount || 0),
}
}
function resolveRequirementFields(workOrder: WorkOrderRow) {
return normalizeRequirementFields(safeParseJson(workOrder.requirement_json).fields)
}
function normalizeRequirementFields(value: unknown) {
const rawFields = Array.isArray(value) ? value : []
const fields = rawFields
.map((item) => {
const source = item && typeof item === 'object' && !Array.isArray(item) ? item as JsonObject : {}
const key = String(source.key || '').trim()
if (!key) return null
return {
key,
label: String(source.label || key).trim(),
required: source.required !== false,
mockValue: String(source.mockValue || '').trim(),
}
})
.filter((item): item is { key: string; label: string; required: boolean; mockValue: string } => Boolean(item))
return fields.length > 0
? fields
: [
{ key: 'gameAccount', label: '游戏账号', required: true, mockValue: 'test_account' },
{ key: 'serverName', label: '区服', required: true, mockValue: '安卓一区' },
{ key: 'roleName', label: '角色名', required: true, mockValue: '测试角色' },
]
}
function normalizeSubmittedFields(value: unknown) {
const source = value && typeof value === 'object' && !Array.isArray(value) ? value as JsonObject : {}
return Object.fromEntries(
Object.entries(source)
.filter(([key]) => !['orderNo', 'platformOrderId', 'fields', 'material'].includes(key))
.map(([key, currentValue]) => [key, String(currentValue || '').trim()]),
)
}
function normalizeStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return []
return value.map((item) => String(item || '').trim()).filter(Boolean)
}
function normalizeReviewStatus(value: unknown) {
const status = String(value || '').trim()
if (['active', 'rejected', 'disabled', 'pending_review'].includes(status)) return status
return 'pending_review'
}
function normalizeUsername(value: unknown): string {
return String(value || '').trim().toLowerCase()
}
function normalizePassword(value: unknown): string {
return String(value || '').trim()
}
function validateWorkerUsername(username: string) {
if (!/^[a-zA-Z0-9._-]{3,32}$/.test(username)) {
throw createHttpError('接单账号格式无效,需为 3-32 位字母数字或 ._-', {
statusCode: 400,
errorCode: 'worker_username_invalid',
})
}
}
function validateWorkerPassword(password: string) {
if (password.length < 6) {
throw createHttpError('接单密码至少 6 位', {
statusCode: 400,
errorCode: 'worker_password_invalid',
})
}
}
function normalizeSessionVersion(value: unknown): number {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1
}
function normalizeOptionalId(value: unknown): number | null {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : null
}
function normalizeInteger(value: unknown, fallback: number): number {
const parsed = Number(value)
return Number.isInteger(parsed) ? parsed : fallback
}
function normalizePositiveInteger(value: unknown, fallback: number): number {
const parsed = Number(value)
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
}
function normalizeAmountFen(value: unknown, fallback: number): number {
const text = String(value ?? '').trim()
if (!text) return fallback
const parsed = Number(text)
if (!Number.isFinite(parsed) || parsed < 0) return fallback
return Math.round(parsed * 100)
}