接单工单新增流转记录(订单详情时间线)
- 后端:新增工单事件查询接口(管理端 + 打手端),事件带操作人显示名(管理员用户名 / 打手昵称 / 系统) - 补齐关键节点事件:抢单接手(grabbed)、验收通过(accepted 带操作人) - 管理端/打手端事件写入操作人(admin session username 贯通至事件) - 前端:订单详情新增「流转记录」时间线,按订单生命周期节点展示(创建/发单/接手/资料/验收/问题/取消),时间+操作人+状态流转
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
listAllWorkCategories,
|
||||
listWorkerFinanceRequests,
|
||||
listWorkOrders,
|
||||
listWorkOrderEventsByOrderId,
|
||||
listWorkProductRules,
|
||||
listWorkerLevels,
|
||||
listWorkerUsers,
|
||||
@@ -91,6 +92,7 @@ import {
|
||||
normalizeUploadedFiles,
|
||||
normalizeWorkerType,
|
||||
resolveFreezeDepositAmount,
|
||||
mapWorkOrderEvents,
|
||||
resolveMatchingProductRule,
|
||||
resolveRequirementFields,
|
||||
resolveSkuNameQuantity,
|
||||
@@ -386,6 +388,7 @@ export async function reviewAdminWorkerUser(workerId: number | string, payload:
|
||||
export async function assignAdminWorkOrderToWorker(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (!['unassigned', 'open'].includes(workOrder.status) || workOrder.assigned_worker_id) {
|
||||
@@ -430,7 +433,7 @@ export async function assignAdminWorkOrderToWorker(
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType: 'assign_voucher_consume_failed',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
@@ -452,6 +455,7 @@ export async function assignAdminWorkOrderToWorker(
|
||||
maxActiveOrders: permissions.maxActiveOrders,
|
||||
deadlineAt: resolveAssignedDeadlineAt(Number(workOrder.timeout_minutes || 0), now),
|
||||
now,
|
||||
actorName,
|
||||
})
|
||||
if (failureReason === 'work_order_not_assignable') {
|
||||
throw createHttpError(
|
||||
@@ -595,6 +599,12 @@ export async function reviewAdminWorkerFinanceRequest(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminWorkOrderEvents(workOrderId: number | string) {
|
||||
await getRequiredWorkOrder(workOrderId)
|
||||
const events = await listWorkOrderEventsByOrderId(workOrderId)
|
||||
return { items: await mapWorkOrderEvents(events) }
|
||||
}
|
||||
|
||||
export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
@@ -640,6 +650,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
export async function deductAdminWorkOrderPendingDeposit(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.ACCEPTED) {
|
||||
@@ -683,7 +694,7 @@ export async function deductAdminWorkOrderPendingDeposit(
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType: 'pending_deposit_deducted',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
@@ -705,6 +716,7 @@ export async function getAdminWorkOrderSharing(workOrderId: number | string) {
|
||||
export async function updateAdminWorkOrderSharing(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (
|
||||
@@ -751,7 +763,7 @@ export async function updateAdminWorkOrderSharing(
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType: 'sharing_config_updated',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: updated?.status || workOrder.status,
|
||||
@@ -780,6 +792,7 @@ function resolveSharingQuantity(
|
||||
export async function submitAdminWorkOrderMaterial(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (
|
||||
@@ -841,7 +854,7 @@ export async function submitAdminWorkOrderMaterial(
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType:
|
||||
workOrder.status === WORK_ORDER_STATUS.IN_PROGRESS
|
||||
? 'material_updated'
|
||||
@@ -942,7 +955,7 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) {
|
||||
export async function updateAdminWorkOrder(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
options: { canEditRequirementFields?: boolean } = {},
|
||||
options: { canEditRequirementFields?: boolean; actorName?: string } = {},
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (
|
||||
@@ -1006,7 +1019,7 @@ export async function updateAdminWorkOrder(
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: options.actorName || '',
|
||||
eventType: 'edited',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
@@ -1061,7 +1074,7 @@ export async function deleteAdminWorkOrder(workOrderId: number | string) {
|
||||
return { deleted: true }
|
||||
}
|
||||
|
||||
export async function publishAdminWorkOrder(workOrderId: number | string) {
|
||||
export async function publishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.UNASSIGNED) {
|
||||
throw createHttpError('只有未分配订单可以发布到大厅', {
|
||||
@@ -1082,7 +1095,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string) {
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType: 'publish_voucher_consume_failed',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
@@ -1117,7 +1130,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string) {
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType: 'published',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: WORK_ORDER_STATUS.OPEN,
|
||||
@@ -1127,7 +1140,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string) {
|
||||
return { order: mapWorkOrderAdmin(updated), voucherConsume }
|
||||
}
|
||||
|
||||
export async function unpublishAdminWorkOrder(workOrderId: number | string) {
|
||||
export async function unpublishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.OPEN || workOrder.assigned_worker_id) {
|
||||
throw createHttpError('只有待抢单且未被接走的订单可以下架', {
|
||||
@@ -1144,7 +1157,7 @@ export async function unpublishAdminWorkOrder(workOrderId: number | string) {
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType: 'unpublished',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: WORK_ORDER_STATUS.UNASSIGNED,
|
||||
@@ -1153,7 +1166,11 @@ export async function unpublishAdminWorkOrder(workOrderId: number | string) {
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
export async function pinAdminWorkOrder(workOrderId: number | string, payload: JsonObject = {}) {
|
||||
export async function pinAdminWorkOrder(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const pinned = payload.pinned === true
|
||||
if (pinned && workOrder.status !== WORK_ORDER_STATUS.OPEN) {
|
||||
@@ -1170,7 +1187,7 @@ export async function pinAdminWorkOrder(workOrderId: number | string, payload: J
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType: pinned ? 'pinned' : 'unpinned',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
@@ -1182,6 +1199,7 @@ export async function pinAdminWorkOrder(workOrderId: number | string, payload: J
|
||||
export async function markAdminWorkOrderProblem(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (
|
||||
@@ -1204,7 +1222,7 @@ export async function markAdminWorkOrderProblem(
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
actorId: '',
|
||||
actorId: actorName,
|
||||
eventType: 'marked_problem',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: WORK_ORDER_STATUS.PROBLEM,
|
||||
@@ -1217,6 +1235,7 @@ export async function markAdminWorkOrderProblem(
|
||||
export async function resolveAdminProblemWorkOrder(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.PROBLEM) {
|
||||
@@ -1240,6 +1259,7 @@ export async function resolveAdminProblemWorkOrder(
|
||||
action,
|
||||
note,
|
||||
now: nowIso(),
|
||||
actorName,
|
||||
})
|
||||
if (!updated) {
|
||||
throw createHttpError('问题单处置失败,订单状态可能已变化', {
|
||||
@@ -1250,7 +1270,7 @@ export async function resolveAdminProblemWorkOrder(
|
||||
return { order: mapWorkOrderAdmin(updated) }
|
||||
}
|
||||
|
||||
export async function acceptAdminWorkOrder(workOrderId: number | string) {
|
||||
export async function acceptAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
|
||||
throw createHttpError('只有待验收订单可以通过验收', {
|
||||
@@ -1263,6 +1283,8 @@ export async function acceptAdminWorkOrder(workOrderId: number | string) {
|
||||
workOrderId: workOrder.id,
|
||||
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
||||
now,
|
||||
actorType: 'admin',
|
||||
actorName,
|
||||
})
|
||||
if (failureReason === 'sharing_pending_submissions') {
|
||||
throw createHttpError('还有拼单打手未提交验收资料,暂不能统一验收', {
|
||||
@@ -1279,7 +1301,7 @@ export async function acceptAdminWorkOrder(workOrderId: number | string) {
|
||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||
}
|
||||
|
||||
export async function acceptAdminWorkOrders(payload: JsonObject = {}) {
|
||||
export async function acceptAdminWorkOrders(payload: JsonObject = {}, actorName = '') {
|
||||
const workOrderIds = [
|
||||
...new Set(
|
||||
(Array.isArray(payload.workOrderIds) ? payload.workOrderIds : [])
|
||||
@@ -1316,12 +1338,12 @@ export async function acceptAdminWorkOrders(payload: JsonObject = {}) {
|
||||
|
||||
const results = []
|
||||
for (const workOrder of workOrders) {
|
||||
results.push(await acceptAdminWorkOrder(workOrder.id))
|
||||
results.push(await acceptAdminWorkOrder(workOrder.id, actorName))
|
||||
}
|
||||
return { orders: results.map((result) => result.order) }
|
||||
}
|
||||
|
||||
export async function unassignAdminWorkOrder(workOrderId: number | string) {
|
||||
export async function unassignAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || !workOrder.assigned_worker_id) {
|
||||
throw createHttpError('只有进行中且已指派打手的订单可以取消指派', {
|
||||
@@ -1333,6 +1355,7 @@ export async function unassignAdminWorkOrder(workOrderId: number | string) {
|
||||
const { order: updated, failureReason } = await unassignWorkOrder({
|
||||
workOrderId: workOrder.id,
|
||||
now,
|
||||
actorName,
|
||||
})
|
||||
if (failureReason === 'work_order_not_in_progress') {
|
||||
throw createHttpError('订单状态已变化,无法取消指派', {
|
||||
@@ -1355,7 +1378,11 @@ export async function unassignAdminWorkOrder(workOrderId: number | string) {
|
||||
return { order: mapWorkOrderAdmin(updated) }
|
||||
}
|
||||
|
||||
export async function cancelAdminWorkOrder(workOrderId: number | string, payload: JsonObject = {}) {
|
||||
export async function cancelAdminWorkOrder(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || !workOrder.assigned_worker_id) {
|
||||
throw createHttpError('只有代练中且已接单的订单可以撤单', {
|
||||
@@ -1381,6 +1408,7 @@ export async function cancelAdminWorkOrder(workOrderId: number | string, payload
|
||||
workOrderId: workOrder.id,
|
||||
now: nowIso(),
|
||||
reason,
|
||||
actorName,
|
||||
}
|
||||
const { order: updated, failureReason } =
|
||||
action === 'return_to_hall'
|
||||
|
||||
@@ -3,6 +3,7 @@ import crypto from 'node:crypto'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import {
|
||||
addWorkerWalletCredit,
|
||||
getWorkerUserById,
|
||||
type WorkCategoryRow,
|
||||
type WorkerFinanceRequestRow,
|
||||
type WorkOrderRow,
|
||||
@@ -1028,3 +1029,78 @@ export const IGNORABLE_WORKER_AUTH_ERROR_CODES = new Set([
|
||||
'worker_auth_user_invalid',
|
||||
'worker_auth_stale',
|
||||
])
|
||||
|
||||
export type WorkOrderEventView = {
|
||||
id: number
|
||||
time: string
|
||||
actorType: string
|
||||
actorName: string
|
||||
eventType: string
|
||||
fromStatus: string
|
||||
toStatus: string
|
||||
payload: JsonObject
|
||||
}
|
||||
|
||||
/**
|
||||
* 将工单事件原始行映射为带操作人显示名的视图。
|
||||
* admin 事件的操作人来自 actor_id(管理员用户名);worker 事件按打手表解析显示名。
|
||||
*/
|
||||
export async function mapWorkOrderEvents(
|
||||
events: Array<{
|
||||
id: number
|
||||
actor_type: string
|
||||
actor_id: string
|
||||
event_type: string
|
||||
from_status: string
|
||||
to_status: string
|
||||
payload_json: string | Record<string, unknown>
|
||||
created_at: string
|
||||
}>,
|
||||
): Promise<WorkOrderEventView[]> {
|
||||
const workerIds = [
|
||||
...new Set(
|
||||
events
|
||||
.filter((event) => event.actor_type === 'worker')
|
||||
.map((event) => Number(event.actor_id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0),
|
||||
),
|
||||
]
|
||||
const workerNameById = new Map<number, string>()
|
||||
for (const workerId of workerIds) {
|
||||
const worker = await getWorkerUserById(workerId)
|
||||
if (worker) {
|
||||
workerNameById.set(workerId, worker.display_name || worker.username || `打手#${workerId}`)
|
||||
}
|
||||
}
|
||||
|
||||
return events.map((event) => ({
|
||||
id: Number(event.id),
|
||||
time: new Date(event.created_at).toISOString(),
|
||||
actorType: event.actor_type,
|
||||
actorName: resolveWorkOrderEventActorName(event, workerNameById),
|
||||
eventType: event.event_type,
|
||||
fromStatus: event.from_status,
|
||||
toStatus: event.to_status,
|
||||
payload: safeParseJson(event.payload_json),
|
||||
}))
|
||||
}
|
||||
|
||||
function resolveWorkOrderEventActorName(
|
||||
event: { actor_type: string; actor_id: string },
|
||||
workerNameById: Map<number, string>,
|
||||
) {
|
||||
const actorId = String(event.actor_id || '').trim()
|
||||
if (event.actor_type === 'worker') {
|
||||
return workerNameById.get(Number(actorId)) || (actorId ? `打手#${actorId}` : '打手')
|
||||
}
|
||||
if (event.actor_type === 'admin') {
|
||||
return actorId || '管理员'
|
||||
}
|
||||
if (event.actor_type === 'system') {
|
||||
return '系统'
|
||||
}
|
||||
if (event.actor_type === 'collect') {
|
||||
return '采集'
|
||||
}
|
||||
return actorId || '未知'
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
listWorkerWorkOrderNotes,
|
||||
listWorkCategories,
|
||||
listWorkOrders,
|
||||
listWorkOrderEventsByOrderId,
|
||||
listWorkerLevels,
|
||||
listDueDepositUnfreezes,
|
||||
listOverdueWorkOrders,
|
||||
@@ -79,6 +80,7 @@ import {
|
||||
mapWorkCategory,
|
||||
mapWorkOrderForWorker,
|
||||
mapWorkOrderPublic,
|
||||
mapWorkOrderEvents,
|
||||
mapWorkOrderShare,
|
||||
mapWorkerUser,
|
||||
normalizeAmountFen,
|
||||
@@ -903,6 +905,21 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
|
||||
throwGrabWorkOrderFailure(grabbed.failureReason)
|
||||
}
|
||||
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'worker',
|
||||
actorId: String(worker.id),
|
||||
eventType: 'grabbed',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: grabbed.order.status,
|
||||
payloadJson: JSON.stringify({
|
||||
workerId: worker.id,
|
||||
workerName: worker.display_name || worker.username,
|
||||
internal: worker.worker_type === 'internal',
|
||||
}),
|
||||
now: nowIso(),
|
||||
})
|
||||
|
||||
return { order: mapWorkOrderForWorker(grabbed.order, permissions) }
|
||||
}
|
||||
|
||||
@@ -1018,6 +1035,24 @@ export async function listWorkerMyOrders(query: JsonObject = {}, session: Worker
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorkerMyOrderEvents(workOrderId: number | string, session: WorkerSession) {
|
||||
requireActiveWorkerSession(session)
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
const workerId = Number(session.workerId)
|
||||
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
||||
const isOrderOwner =
|
||||
Number(workOrder.assigned_worker_id || 0) === workerId ||
|
||||
Number(workOrder.last_assigned_worker_id || 0) === workerId
|
||||
if (!sharingShare && !isOrderOwner) {
|
||||
throw createHttpError('只能查看自己订单的流转记录', {
|
||||
statusCode: 403,
|
||||
errorCode: 'work_order_events_owner_required',
|
||||
})
|
||||
}
|
||||
const events = await listWorkOrderEventsByOrderId(workOrderId)
|
||||
return { items: await mapWorkOrderEvents(events) }
|
||||
}
|
||||
|
||||
export async function saveWorkerOrderNote(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
@@ -1190,6 +1225,8 @@ export async function submitWorkerOrderAcceptance(
|
||||
workOrderId: workOrder.id,
|
||||
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
||||
now,
|
||||
actorType: 'worker',
|
||||
actorName: worker.display_name || worker.username || '',
|
||||
})
|
||||
if (failureReason === 'sharing_pending_submissions') {
|
||||
throw createHttpError('还有拼单打手未提交验收资料,暂不能统一验收', {
|
||||
|
||||
Reference in New Issue
Block a user