接单工单新增流转记录(订单详情时间线)

- 后端:新增工单事件查询接口(管理端 + 打手端),事件带操作人显示名(管理员用户名 / 打手昵称 / 系统)
- 补齐关键节点事件:抢单接手(grabbed)、验收通过(accepted 带操作人)
- 管理端/打手端事件写入操作人(admin session username 贯通至事件)
- 前端:订单详情新增「流转记录」时间线,按订单生命周期节点展示(创建/发单/接手/资料/验收/问题/取消),时间+操作人+状态流转
This commit is contained in:
yml2213
2026-08-16 19:25:20 +08:00
parent 81ae04dae0
commit 950c4fd578
13 changed files with 699 additions and 92 deletions
@@ -1056,6 +1056,8 @@ export async function assignWorkOrderToWorker(input: {
maxActiveOrders: number
deadlineAt: string | null
now: string
/** 操作人显示名(管理员用户名等) */
actorName?: string
}): Promise<{
order: WorkOrderRow | null
failureReason:
@@ -1143,7 +1145,7 @@ export async function assignWorkOrderToWorker(input: {
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: '',
actorId: input.actorName || '',
eventType: 'assigned_by_admin',
fromStatus,
toStatus: 'in_progress',
@@ -1245,7 +1247,11 @@ export async function cancelWorkerWorkOrder(input: {
})
}
export async function unassignWorkOrder(input: { workOrderId: number; now: string }): Promise<{
export async function unassignWorkOrder(input: {
workOrderId: number
now: string
actorName?: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
}> {
@@ -1314,7 +1320,7 @@ export async function unassignWorkOrder(input: { workOrderId: number; now: strin
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: '',
actorId: input.actorName || '',
eventType: 'unassigned_by_admin',
fromStatus: 'in_progress',
toStatus: 'unassigned',
@@ -1333,6 +1339,7 @@ export async function returnAssignedWorkOrderToHall(input: {
workOrderId: number
now: string
reason?: string
actorName?: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
@@ -1403,7 +1410,7 @@ export async function returnAssignedWorkOrderToHall(input: {
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: '',
actorId: input.actorName || '',
eventType: 'returned_to_hall_by_admin',
fromStatus: 'in_progress',
toStatus: 'open',
@@ -1426,6 +1433,7 @@ export async function cancelAssignedWorkOrder(input: {
workOrderId: number
now: string
reason?: string
actorName?: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
@@ -1496,7 +1504,7 @@ export async function cancelAssignedWorkOrder(input: {
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: '',
actorId: input.actorName || '',
eventType: 'cancelled_by_admin',
fromStatus: 'in_progress',
toStatus: 'cancelled',
@@ -1537,6 +1545,10 @@ export async function acceptWorkOrderAndSettle(input: {
workOrderId: number
unfreezeDays: number
now: string
/** 操作人类型:admin / worker / system */
actorType?: string
/** 操作人显示名 */
actorName?: string
}): Promise<{
order: WorkOrderRow | null
failureReason: 'sharing_pending_submissions' | null
@@ -1809,8 +1821,8 @@ export async function acceptWorkOrderAndSettle(input: {
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: '',
actorType: input.actorType || 'admin',
actorId: input.actorName || '',
eventType: 'accepted',
fromStatus: 'pending_acceptance',
toStatus: 'accepted',
@@ -1834,6 +1846,7 @@ export async function resolveProblemWorkOrder(input: {
action: ProblemWorkOrderResolutionAction
note: string
now: string
actorName?: string
}): Promise<WorkOrderRow | null> {
return withTransaction(async (client) => {
const currentResult = await client.query<WorkOrderRow>(
@@ -1937,7 +1950,7 @@ export async function resolveProblemWorkOrder(input: {
await createWorkOrderEventWithClient(client, {
workOrderId: input.workOrderId,
actorType: 'admin',
actorId: '',
actorId: input.actorName || '',
eventType: `problem_${input.action}`,
fromStatus: 'problem',
toStatus: targetStatus,
@@ -2409,6 +2422,33 @@ export async function countWorkerTimeoutEvents(workerId: number | string): Promi
return counts.get(Number(workerId)) || 0
}
export type WorkOrderEventRow = {
id: number
work_order_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
}
export async function listWorkOrderEventsByOrderId(
workOrderId: number | string,
): Promise<WorkOrderEventRow[]> {
const result = await query<WorkOrderEventRow>(
`
SELECT *
FROM work_order_events
WHERE work_order_id = $1
ORDER BY id ASC
`,
[Number(workOrderId)],
)
return result.rows
}
export async function createWorkOrderEvent(input: {
workOrderId: number
actorType: string
+141 -65
View File
@@ -15,6 +15,7 @@ import {
deleteAdminWorkerLevel,
getAdminWorkerFinanceConfig,
getAdminWorkerPlatformSummary,
getAdminWorkOrderEvents,
getAdminWorkOrderSharing,
listAdminWorkerFinanceRequests,
listAdminWorkCategories,
@@ -311,7 +312,12 @@ router.post(
'/worker-platform/orders/:workOrderId/material',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler(
(req) => submitAdminWorkOrderMaterial(String(req.params.workOrderId || ''), req.body || {}),
(req) =>
submitAdminWorkOrderMaterial(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '订单资料已保存',
errorMessage: '补充订单资料失败',
@@ -333,6 +339,7 @@ router.put(
(req) =>
updateAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}, {
canEditRequirementFields: req.adminSession?.role !== 'support',
actorName: req.adminSession?.username || '',
}),
{
successMessage: '工单信息已保存',
@@ -352,7 +359,12 @@ router.post(
'/worker-platform/orders/:workOrderId/assign',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler(
(req) => assignAdminWorkOrderToWorker(String(req.params.workOrderId || ''), req.body || {}),
(req) =>
assignAdminWorkOrderToWorker(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '已指派打手',
errorMessage: '指派打手失败',
@@ -370,24 +382,36 @@ router.post(
router.post(
'/worker-platform/orders/:workOrderId/unassign',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => unassignAdminWorkOrder(String(req.params.workOrderId || '')), {
successMessage: '已取消指派',
errorMessage: '取消指派失败',
scope: '[admin/worker-platform/orders/:workOrderId/unassign]',
audit: (req, data) => ({
action: 'work_order_worker_unassigned',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
createJsonHandler(
(req) =>
unassignAdminWorkOrder(
String(req.params.workOrderId || ''),
req.adminSession?.username || '',
),
{
successMessage: '已取消指派',
errorMessage: '取消指派失败',
scope: '[admin/worker-platform/orders/:workOrderId/unassign]',
audit: (req, data) => ({
action: 'work_order_worker_unassigned',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
)
router.post(
'/worker-platform/orders/:workOrderId/cancel',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler(
(req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}),
(req) =>
cancelAdminWorkOrder(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '撤单处理完成,打手额度已恢复',
errorMessage: '撤单失败',
@@ -424,40 +448,56 @@ router.delete(
router.post(
'/worker-platform/orders/:workOrderId/publish',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => publishAdminWorkOrder(String(req.params.workOrderId || '')), {
successMessage: '订单已发布到抢单大厅',
errorMessage: '发布接单订单失败',
scope: '[admin/worker-platform/orders/:workOrderId/publish]',
audit: (req, data) => ({
action: 'work_order_published',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
createJsonHandler(
(req) =>
publishAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
{
successMessage: '订单已发布到抢单大厅',
errorMessage: '发布接单订单失败',
scope: '[admin/worker-platform/orders/:workOrderId/publish]',
audit: (req, data) => ({
action: 'work_order_published',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
)
router.post(
'/worker-platform/orders/:workOrderId/unpublish',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => unpublishAdminWorkOrder(String(req.params.workOrderId || '')), {
successMessage: '订单已撤回到未分配',
errorMessage: '下架接单订单失败',
scope: '[admin/worker-platform/orders/:workOrderId/unpublish]',
audit: (req, data) => ({
action: 'work_order_unpublished',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
createJsonHandler(
(req) =>
unpublishAdminWorkOrder(
String(req.params.workOrderId || ''),
req.adminSession?.username || '',
),
{
successMessage: '订单已撤回到未分配',
errorMessage: '下架接单订单失败',
scope: '[admin/worker-platform/orders/:workOrderId/unpublish]',
audit: (req, data) => ({
action: 'work_order_unpublished',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
)
router.post(
'/worker-platform/orders/:workOrderId/pin',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler(
(req) => pinAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}),
(req) =>
pinAdminWorkOrder(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '置顶状态已更新',
errorMessage: '更新置顶状态失败',
@@ -482,11 +522,26 @@ router.get(
}),
)
router.get(
'/worker-platform/orders/:workOrderId/events',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => getAdminWorkOrderEvents(String(req.params.workOrderId || '')), {
successMessage: 'ok',
errorMessage: '读取工单流转记录失败',
scope: '[admin/worker-platform/orders/:workOrderId/events]',
}),
)
router.post(
'/worker-platform/orders/:workOrderId/sharing',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) => updateAdminWorkOrderSharing(String(req.params.workOrderId || ''), req.body || {}),
(req) =>
updateAdminWorkOrderSharing(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '拼单配置已保存',
errorMessage: '保存拼单配置失败',
@@ -505,7 +560,12 @@ router.post(
'/worker-platform/orders/:workOrderId/problem',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler(
(req) => markAdminWorkOrderProblem(String(req.params.workOrderId || ''), req.body || {}),
(req) =>
markAdminWorkOrderProblem(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '订单已标记为问题单',
errorMessage: '标记问题单失败',
@@ -524,7 +584,12 @@ router.post(
'/worker-platform/orders/:workOrderId/problem/resolve',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) => resolveAdminProblemWorkOrder(String(req.params.workOrderId || ''), req.body || {}),
(req) =>
resolveAdminProblemWorkOrder(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '问题单已处置',
errorMessage: '处置问题单失败',
@@ -542,35 +607,42 @@ router.post(
router.post(
'/worker-platform/orders/:workOrderId/accept',
requireAdminRoles(['admin', 'operator']),
createJsonHandler((req) => acceptAdminWorkOrder(String(req.params.workOrderId || '')), {
successMessage: '订单已验收通过',
errorMessage: '验收接单订单失败',
scope: '[admin/worker-platform/orders/:workOrderId/accept]',
audit: (req, data) => ({
action: 'work_order_accepted',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
createJsonHandler(
(req) =>
acceptAdminWorkOrder(String(req.params.workOrderId || ''), req.adminSession?.username || ''),
{
successMessage: '订单已验收通过',
errorMessage: '验收接单订单失败',
scope: '[admin/worker-platform/orders/:workOrderId/accept]',
audit: (req, data) => ({
action: 'work_order_accepted',
targetType: 'work_order',
targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
)
router.post(
'/worker-platform/orders/accept-batch',
requireAdminRoles(['admin', 'operator']),
createJsonHandler((req) => acceptAdminWorkOrders(req.body || {}), {
successMessage: '选中订单已验收通过',
errorMessage: '批量验收接单订单失败',
scope: '[admin/worker-platform/orders/accept-batch]',
audit: (req, data) => ({
action: 'work_orders_batch_accepted',
targetType: 'work_order',
targetId: Array.isArray(req.body?.workOrderIds)
? req.body.workOrderIds.map((item: unknown) => String(item)).join(',')
: '',
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
createJsonHandler(
(req) => acceptAdminWorkOrders(req.body || {}, req.adminSession?.username || ''),
{
successMessage: '选中订单已验收通过',
errorMessage: '批量验收接单订单失败',
scope: '[admin/worker-platform/orders/accept-batch]',
audit: (req, data) => ({
action: 'work_orders_batch_accepted',
targetType: 'work_order',
targetId: Array.isArray(req.body?.workOrderIds)
? req.body.workOrderIds.map((item: unknown) => String(item)).join(',')
: '',
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
},
),
)
router.post(
@@ -578,7 +650,11 @@ router.post(
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
deductAdminWorkOrderPendingDeposit(String(req.params.workOrderId || ''), req.body || {}),
deductAdminWorkOrderPendingDeposit(
String(req.params.workOrderId || ''),
req.body || {},
req.adminSession?.username || '',
),
{
successMessage: '待解冻押金已扣减',
errorMessage: '扣减待解冻押金失败',
+15
View File
@@ -16,6 +16,7 @@ import {
listWorkerProfileWalletLedgers,
listWorkerHallOrders,
listWorkerMyOrders,
getWorkerMyOrderEvents,
loginWorker,
logoutWorkerSession,
registerWorker,
@@ -252,6 +253,20 @@ router.get(
}),
)
router.get(
'/orders/:workOrderId/events',
requireActiveWorker,
createRouteHandler(
(req) =>
getWorkerMyOrderEvents(String(req.params.workOrderId || ''), getRequiredWorkerSession(req)),
{
successMessage: 'ok',
errorMessage: '读取订单流转记录失败',
scope: '[worker/orders/:workOrderId/events]',
},
),
)
router.post(
'/orders/:workOrderId/note',
requireActiveWorker,
@@ -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('还有拼单打手未提交验收资料,暂不能统一验收', {