修复接单平台并发、分页与补资料

This commit is contained in:
yml2213
2026-07-28 10:15:44 +08:00
parent a2587eda01
commit f0ba0fe17f
22 changed files with 1096 additions and 145 deletions
@@ -0,0 +1,85 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { WorkOrderRow } from '../../repositories/worker-platform-repo.js'
import { resolveCollectSubmitTargetWorkOrder } from './worker-platform-service.js'
function buildWorkOrderRow(
overrides: Partial<WorkOrderRow> = {},
): WorkOrderRow {
return {
id: 1,
work_order_no: 'WO0001',
order_id: 101,
order_item_id: 201,
task_id: null,
platform_order_id: 'ORDER-001',
product_name: '测试商品',
category_id: null,
status: 'pending_material',
reward_amount: 2500,
required_deposit_amount: 0,
deposit_threshold_amount: 20000,
assigned_worker_id: null,
material_json: '{}',
requirement_json: '{"fields":[]}',
acceptance_json: '{}',
problem_note: '',
published_at: null,
assigned_at: null,
submitted_at: null,
accepted_at: null,
created_at: '2026-07-28T00:00:00.000Z',
updated_at: '2026-07-28T00:00:00.000Z',
...overrides,
}
}
test('resolveCollectSubmitTargetWorkOrder returns the only pending work order when no id is provided', () => {
const workOrder = buildWorkOrderRow()
const selected = resolveCollectSubmitTargetWorkOrder('ORDER-001', [workOrder], '')
assert.equal(selected.id, workOrder.id)
})
test('resolveCollectSubmitTargetWorkOrder requires explicit work order id when multiple orders are pending', () => {
const workOrders = [
buildWorkOrderRow({ id: 1, work_order_no: 'WO0001' }),
buildWorkOrderRow({ id: 2, work_order_no: 'WO0002', order_item_id: 202 }),
]
assert.throws(
() => resolveCollectSubmitTargetWorkOrder('ORDER-001', workOrders, ''),
(error: unknown) =>
Boolean(
error &&
typeof error === 'object' &&
(error as { statusCode?: number }).statusCode === 409 &&
(error as { errorCode?: string }).errorCode === 'collect_work_order_required',
),
)
})
test('resolveCollectSubmitTargetWorkOrder returns the matching work order when workOrderId is provided', () => {
const workOrders = [
buildWorkOrderRow({ id: 1, work_order_no: 'WO0001' }),
buildWorkOrderRow({ id: 2, work_order_no: 'WO0002', order_item_id: 202 }),
]
const selected = resolveCollectSubmitTargetWorkOrder('ORDER-001', workOrders, '2')
assert.equal(selected.id, 2)
})
test('resolveCollectSubmitTargetWorkOrder rejects unknown workOrderId', () => {
const workOrders = [buildWorkOrderRow({ id: 1, work_order_no: 'WO0001' })]
assert.throws(
() => resolveCollectSubmitTargetWorkOrder('ORDER-001', workOrders, 999),
(error: unknown) =>
Boolean(
error &&
typeof error === 'object' &&
(error as { statusCode?: number }).statusCode === 404 &&
(error as { errorCode?: string }).errorCode === 'collect_work_order_not_found',
),
)
})
@@ -11,21 +11,19 @@ import {
createWorkOrder,
createWorkOrderEvent,
createWorkerUser,
findPendingMaterialWorkOrderByPlatformOrderId,
getWorkCategoryByKey,
incrementWorkerSessionVersion,
getWorkOrderById,
getWorkOrderByOrderItemId,
getWorkerLevelById,
getWorkerLevelByKey,
getWorkerUserById,
getWorkerUserByUsername,
listAllWorkCategories,
listPendingMaterialWorkOrdersByPlatformOrderId,
grabWorkOrder,
listWorkCategories,
listWorkOrders,
listWorkProductRules,
listWorkerLevels,
listWorkerUsers,
resolveProblemWorkOrder,
updateWorkOrder,
updateWorkerUser,
upsertWorkCategory,
@@ -36,6 +34,8 @@ import {
type WorkProductRuleRow,
type WorkerLevelRow,
type WorkerUserRow,
type GrabWorkOrderFailureReason,
type ProblemWorkOrderResolutionAction,
} from '../../repositories/worker-platform-repo.js'
import type { JsonObject } from '../../types/json.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
@@ -43,6 +43,11 @@ 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'
import {
normalizeStoredFileUrl,
refreshStoredFileUrl,
refreshUploadedFileUrls,
} from '../file-storage/file-storage-service.js'
export type WorkerSession = {
sessionId: string
@@ -56,12 +61,19 @@ export type WorkerSession = {
const DEFAULT_LEVEL_KEY = 'regular'
const DEFAULT_CATEGORY_KEY = 'default'
const DEFAULT_DEPOSIT_THRESHOLD_AMOUNT = 20_000
const IGNORABLE_WORKER_AUTH_ERROR_CODES = new Set([
'worker_auth_required',
'worker_auth_invalid',
'worker_auth_expired',
'worker_auth_user_invalid',
'worker_auth_stale',
])
export async function ensureWorkerPlatformDefaults() {
const now = nowIso()
const level = await upsertWorkerLevel({
levelKey: DEFAULT_LEVEL_KEY,
name: '普通接单员',
name: '普通打手',
sortOrder: 100,
status: 'active',
permissionJson: JSON.stringify({
@@ -152,6 +164,27 @@ export async function loginWorker(username: unknown, password: unknown) {
return createWorkerSession(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)
const revokedWorker = await incrementWorkerSessionVersion(session.workerId, nowIso())
return {
success: true,
revoked: Boolean(revokedWorker),
}
} 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()
@@ -301,16 +334,14 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
workOrderId: Number(workOrder.id),
workerId: Number(worker.id),
depositAmount: freezeAmount,
maxActiveOrders: permissions.maxActiveOrders,
now: nowIso(),
})
if (!grabbed) {
throw createHttpError('抢单失败,订单可能已被抢走', {
statusCode: 409,
errorCode: 'work_order_grab_conflict',
})
if (!grabbed.order) {
throwGrabWorkOrderFailure(grabbed.failureReason)
}
return { order: mapWorkOrderForWorker(grabbed, permissions) }
return { order: mapWorkOrderForWorker(grabbed.order, permissions) }
}
export async function listWorkerMyOrders(query: JsonObject = {}, session: WorkerSession) {
@@ -404,16 +435,25 @@ export async function collectLookupWorkOrder(payload: JsonObject = {}) {
errorCode: 'collect_order_no_required',
})
}
const workOrder = await findPendingMaterialWorkOrderByPlatformOrderId(orderNo)
if (!workOrder) {
const workOrders = await listPendingMaterialWorkOrdersByPlatformOrderId(orderNo)
if (workOrders.length === 0) {
throw createHttpError('未找到待完善订单,请确认订单号是否正确', {
statusCode: 404,
errorCode: 'collect_order_not_found',
})
}
const [primaryOrder] = workOrders
if (!primaryOrder) {
throw createHttpError('未找到待完善订单,请确认订单号是否正确', {
statusCode: 404,
errorCode: 'collect_order_not_found',
})
}
return {
order: mapWorkOrderPublic(workOrder),
fields: resolveRequirementFields(workOrder),
order: mapWorkOrderPublic(primaryOrder),
fields: resolveRequirementFields(primaryOrder),
orders: workOrders.map(mapCollectLookupOrder),
pendingCount: workOrders.length,
}
}
@@ -425,13 +465,18 @@ export async function collectSubmitWorkOrder(payload: JsonObject = {}) {
errorCode: 'collect_order_no_required',
})
}
const workOrder = await findPendingMaterialWorkOrderByPlatformOrderId(orderNo)
if (!workOrder) {
const workOrders = await listPendingMaterialWorkOrdersByPlatformOrderId(orderNo)
if (workOrders.length === 0) {
throw createHttpError('未找到待完善订单,请确认订单号是否正确', {
statusCode: 404,
errorCode: 'collect_order_not_found',
})
}
const workOrder = resolveCollectSubmitTargetWorkOrder(
orderNo,
workOrders,
payload.workOrderId || payload.work_order_id,
)
const fields = resolveRequirementFields(workOrder)
const submittedFields = normalizeSubmittedFields(payload.fields || payload.material || payload)
@@ -499,7 +544,7 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) {
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 name = String(payload.name || '').trim() || '普通打手'
const permissions = {
depositFreeAmount: normalizeAmountFen(
payload.depositFreeAmount ?? payload.depositFreeAmountYuan,
@@ -844,6 +889,42 @@ export async function markAdminWorkOrderProblem(
return { order: mapWorkOrderAdmin(updated || workOrder) }
}
export async function resolveAdminProblemWorkOrder(
workOrderId: number | string,
payload: JsonObject = {},
) {
const workOrder = await getRequiredWorkOrder(workOrderId)
if (workOrder.status !== WORK_ORDER_STATUS.PROBLEM) {
throw createHttpError('只有问题单可以执行处置', {
statusCode: 409,
errorCode: 'work_order_problem_resolution_status_invalid',
})
}
const action = normalizeProblemResolutionAction(payload.action)
const note = String(payload.note || '').trim()
if (!note) {
throw createHttpError('请填写处置备注', {
statusCode: 400,
errorCode: 'work_order_problem_resolution_note_required',
})
}
const updated = await resolveProblemWorkOrder({
workOrderId: workOrder.id,
action,
note,
now: nowIso(),
})
if (!updated) {
throw createHttpError('问题单处置失败,订单状态可能已变化', {
statusCode: 409,
errorCode: 'work_order_problem_resolution_conflict',
})
}
return { order: mapWorkOrderAdmin(updated) }
}
export async function acceptAdminWorkOrder(workOrderId: number | string) {
const workOrder = await getRequiredWorkOrder(workOrderId)
if (workOrder.status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
@@ -989,6 +1070,41 @@ function createWorkerSession(worker: WorkerUserRow) {
}
}
export function resolveCollectSubmitTargetWorkOrder(
orderNo: string,
workOrders: WorkOrderRow[],
requestedWorkOrderId: unknown,
): WorkOrderRow {
const workOrderId = normalizeOptionalId(requestedWorkOrderId)
if (workOrderId) {
const matched = workOrders.find((item) => Number(item.id) === workOrderId)
if (matched) {
return matched
}
throw createHttpError('待完善订单不存在或已更新,请刷新后重试', {
statusCode: 404,
errorCode: 'collect_work_order_not_found',
context: { orderNo, workOrderId },
})
}
if (workOrders.length === 1) {
const [singleWorkOrder] = workOrders
if (singleWorkOrder) {
return singleWorkOrder
}
}
throw createHttpError('该订单存在多个待完善商品,请逐个提交资料', {
statusCode: 409,
errorCode: 'collect_work_order_required',
context: {
orderNo,
pendingCount: workOrders.length,
},
})
}
function ensureWorkerAuthConfigured() {
if (String(runtimeConfig.admin?.sessionSecret || '').trim()) {
return
@@ -1197,7 +1313,7 @@ function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
),
material: safeParseJson(workOrder.material_json),
requirement: safeParseJson(workOrder.requirement_json),
acceptance: safeParseJson(workOrder.acceptance_json),
acceptance: mapAcceptanceForResponse(workOrder.acceptance_json),
problemNote: workOrder.problem_note || '',
worker: workOrder.assigned_worker_id
? {
@@ -1219,14 +1335,17 @@ function mapWorkOrderForWorker(
workOrder: WorkOrderRow,
permissions: { depositFreeAmount: number },
) {
const mapped = mapWorkOrderAdmin(workOrder)
const { requiredDepositAmount, depositThresholdAmount, worker: _worker, ...visibleOrder } = mapped
return {
...mapWorkOrderAdmin(workOrder),
...visibleOrder,
freezeDepositAmount: resolveFreezeDepositAmount(workOrder, permissions),
}
}
function mapWorkOrderPublic(workOrder: WorkOrderRow) {
return {
workOrderId: Number(workOrder.id),
workOrderNo: workOrder.work_order_no,
platformOrderId: workOrder.platform_order_id,
productName: workOrder.product_name,
@@ -1234,6 +1353,14 @@ function mapWorkOrderPublic(workOrder: WorkOrderRow) {
}
}
function mapCollectLookupOrder(workOrder: WorkOrderRow) {
return {
...mapWorkOrderPublic(workOrder),
fields: resolveRequirementFields(workOrder),
values: resolveCollectSubmittedFieldValues(workOrder),
}
}
function mapWallet(wallet: Awaited<ReturnType<typeof addWorkerWalletCredit>>) {
return {
workerId: Number(wallet?.worker_id || 0),
@@ -1406,9 +1533,12 @@ function normalizeUploadedFiles(value: unknown) {
if (!objectKey && !url) return null
return {
objectKey,
url,
thumbnailUrl: String(source.thumbnailUrl || source.thumbnail_url || url).trim(),
mediumUrl: String(source.mediumUrl || source.medium_url || url).trim(),
url: normalizeStoredFileUrl(url, objectKey),
thumbnailUrl: normalizeStoredFileUrl(
source.thumbnailUrl || source.thumbnail_url || url,
objectKey,
),
mediumUrl: normalizeStoredFileUrl(source.mediumUrl || source.medium_url || url, objectKey),
filename: String(source.filename || '').trim(),
contentType: String(source.contentType || source.content_type || '').trim(),
size: normalizeInteger(source.size, 0),
@@ -1429,12 +1559,66 @@ function normalizeUploadedFiles(value: unknown) {
)
}
function mapAcceptanceForResponse(value: unknown) {
const acceptance = safeParseJson(value)
const files = normalizeUploadedFiles(acceptance.files).map((file) =>
refreshUploadedFileUrls(file),
)
const imageUrls = normalizeStringArray(acceptance.imageUrls).map((url) =>
refreshStoredFileUrl(url),
)
return {
...acceptance,
files,
imageUrls,
}
}
function resolveCollectSubmittedFieldValues(workOrder: WorkOrderRow) {
const material = safeParseJson(workOrder.material_json)
const collect =
material.collect && typeof material.collect === 'object' && !Array.isArray(material.collect)
? safeParseJson(material.collect)
: {}
return normalizeSubmittedFields(collect.fields)
}
function normalizeReviewStatus(value: unknown) {
const status = String(value || '').trim()
if (['active', 'rejected', 'disabled', 'pending_review'].includes(status)) return status
return 'pending_review'
}
function normalizeProblemResolutionAction(value: unknown): ProblemWorkOrderResolutionAction {
const action = String(value || '').trim()
if (['return_to_worker', 'reopen', 'cancel_release', 'cancel_deduct'].includes(action)) {
return action as ProblemWorkOrderResolutionAction
}
throw createHttpError('问题单处置方式不正确', {
statusCode: 400,
errorCode: 'work_order_problem_resolution_action_invalid',
})
}
function throwGrabWorkOrderFailure(failureReason: GrabWorkOrderFailureReason | null): never {
if (failureReason === 'worker_active_order_limit') {
throw createHttpError('已达到当前等级最大同时接单量', {
statusCode: 409,
errorCode: 'worker_active_order_limit',
})
}
if (failureReason === 'worker_deposit_insufficient') {
throw createHttpError('余额不足,无法冻结所需押金', {
statusCode: 409,
errorCode: 'worker_deposit_insufficient',
})
}
throw createHttpError('抢单失败,订单可能已被抢走', {
statusCode: 409,
errorCode: 'work_order_grab_conflict',
})
}
function normalizeEnabledStatus(value: unknown) {
return String(value || 'active').trim() === 'disabled' ? 'disabled' : 'active'
}
@@ -1520,3 +1704,9 @@ function normalizeAmountFen(value: unknown, fallback: number): number {
if (!Number.isFinite(parsed) || parsed < 0) return fallback
return Math.round(parsed * 100)
}
function isIgnorableWorkerAuthError(error: unknown) {
return IGNORABLE_WORKER_AUTH_ERROR_CODES.has(
String((error as { errorCode?: string })?.errorCode || '').trim(),
)
}