feat(worker): 打手接单界面支持查看超时历史订单并增加安全免责警示
- 数据库与查询层:通过索引 idx_work_order_events_timeout_worker_id 关联打手历史 timeout 事件与取消的拼单份额,打手在退回大厅或被他人抢走后仍能在「已超时」tab 查看历史记录; - 状态与权限管控:针对超时工单将个人端状态稳定映射为 open,报酬与冻结押金置为 0,彻底禁用上传图片、提交验收、好友资料、申请取消等履约操作; - 风险防护与免责说明:列表与抽屉详情顶部增加醒目的免责声明与警示横幅(告知订单已退回大厅或被他人接手,严禁在游戏内继续赠送,造成的损失平台概不负责);锁定买家游戏资料与快捷复制; - 验证方式:通过后端全部 676 个测试、typecheck、check:sql 及前端 check(eslint、vitest/node:test、tsc、vite build)。
This commit is contained in:
@@ -421,12 +421,27 @@ export function mapWorkOrderAdmin(
|
||||
}
|
||||
}
|
||||
|
||||
export type WorkerOrderMapOptions = {
|
||||
isTimedOutForMe?: boolean
|
||||
timeoutNotice?:
|
||||
| {
|
||||
isTimedOut: boolean
|
||||
timedOutAt: string | null
|
||||
policy?: string
|
||||
warningMessage?: string
|
||||
isTakenByOther?: boolean
|
||||
}
|
||||
| null
|
||||
| undefined
|
||||
}
|
||||
|
||||
export function mapWorkOrderForWorker(
|
||||
workOrder: WorkOrderRow,
|
||||
permissions: { depositFreeAmount: number },
|
||||
myShare?: WorkOrderShareRow | null,
|
||||
workerNote = '',
|
||||
viewedAt?: string | null,
|
||||
options?: WorkerOrderMapOptions,
|
||||
) {
|
||||
const mapped = mapWorkOrderAdmin(workOrder)
|
||||
const {
|
||||
@@ -441,17 +456,27 @@ export function mapWorkOrderForWorker(
|
||||
adminNote: _adminNote,
|
||||
...visibleOrder
|
||||
} = mapped
|
||||
const personalStatus = resolveWorkerShareStatus(workOrder.status, myShare?.status)
|
||||
const isTimedOutForMe =
|
||||
options?.isTimedOutForMe === true || Boolean(options?.timeoutNotice?.isTimedOut)
|
||||
const personalStatus = isTimedOutForMe
|
||||
? WORK_ORDER_STATUS.OPEN
|
||||
: resolveWorkerShareStatus(workOrder.status, myShare?.status)
|
||||
return {
|
||||
...visibleOrder,
|
||||
productName: myShare
|
||||
? resolveWorkerShareProductName(workOrder.product_name, Number(myShare.quantity || 0))
|
||||
: visibleOrder.productName,
|
||||
status: personalStatus,
|
||||
rewardAmount: myShare ? Number(myShare.share_reward || 0) : visibleOrder.rewardAmount,
|
||||
freezeDepositAmount: myShare
|
||||
? Number(myShare.share_deposit || 0)
|
||||
: resolveFreezeDepositAmount(workOrder, permissions),
|
||||
rewardAmount: isTimedOutForMe
|
||||
? 0
|
||||
: myShare
|
||||
? Number(myShare.share_reward || 0)
|
||||
: visibleOrder.rewardAmount,
|
||||
freezeDepositAmount: isTimedOutForMe
|
||||
? 0
|
||||
: myShare
|
||||
? Number(myShare.share_deposit || 0)
|
||||
: resolveFreezeDepositAmount(workOrder, permissions),
|
||||
/** 用于大厅拼单按当前剩余份数准确计算个人免押后的押金。 */
|
||||
depositFreeAmount: Number(permissions.depositFreeAmount || 0),
|
||||
assignedAt: myShare?.created_at || visibleOrder.assignedAt,
|
||||
@@ -466,6 +491,7 @@ export function mapWorkOrderForWorker(
|
||||
workerNote,
|
||||
myShare: myShare ? mapWorkOrderShare(myShare) : null,
|
||||
...(viewedAt === undefined ? {} : { isUnread: !viewedAt }),
|
||||
...(options?.timeoutNotice ? { timeoutNotice: options.timeoutNotice } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
getWorkOrderShare,
|
||||
getWorkerCancelRequestRisk,
|
||||
getWorkerOrderOverview,
|
||||
listCancelledWorkerSharesByWorker,
|
||||
listWorkerCancelRequests,
|
||||
listWorkerOrderFeedbacks,
|
||||
listWorkerSharesByWorker,
|
||||
listWorkerTimeoutEventsByOrderIds,
|
||||
listWorkerWorkOrderNotes,
|
||||
listWorkerWorkOrderViews,
|
||||
listWorkOrderEventsByOrderId,
|
||||
@@ -88,14 +90,26 @@ export async function listWorkerMyOrders(query: JsonObject = {}, session: Worker
|
||||
const workOrderIds = items.map((item) => Number(item.id))
|
||||
const myShares = await listWorkerSharesByWorker(worker.id, workOrderIds)
|
||||
const myShareByOrderId = new Map(myShares.map((share) => [Number(share.work_order_id), share]))
|
||||
const [notes, views, pendingCancelOrderIds, pendingFeedbackOrderIds, materialEventAtByOrderId] =
|
||||
await Promise.all([
|
||||
listWorkerWorkOrderNotes(worker.id, workOrderIds),
|
||||
listWorkerWorkOrderViews(worker.id, workOrderIds),
|
||||
getPendingCancelRequestOrderIds(worker.id, workOrderIds),
|
||||
getPendingFeedbackOrderIds(worker.id, workOrderIds),
|
||||
getLatestMaterialEventAtByOrderIds(workOrderIds),
|
||||
])
|
||||
const [
|
||||
notes,
|
||||
views,
|
||||
pendingCancelOrderIds,
|
||||
pendingFeedbackOrderIds,
|
||||
materialEventAtByOrderId,
|
||||
timeoutEventsByOrderId,
|
||||
cancelledShares,
|
||||
] = await Promise.all([
|
||||
listWorkerWorkOrderNotes(worker.id, workOrderIds),
|
||||
listWorkerWorkOrderViews(worker.id, workOrderIds),
|
||||
getPendingCancelRequestOrderIds(worker.id, workOrderIds),
|
||||
getPendingFeedbackOrderIds(worker.id, workOrderIds),
|
||||
getLatestMaterialEventAtByOrderIds(workOrderIds),
|
||||
listWorkerTimeoutEventsByOrderIds(worker.id, workOrderIds),
|
||||
listCancelledWorkerSharesByWorker(worker.id, workOrderIds),
|
||||
])
|
||||
const cancelledShareByOrderId = new Map(
|
||||
cancelledShares.map((share) => [Number(share.work_order_id), share]),
|
||||
)
|
||||
const noteByOrderId = new Map(
|
||||
notes.map((item) => [Number(item.work_order_id), String(item.note || '')]),
|
||||
)
|
||||
@@ -108,24 +122,71 @@ export async function listWorkerMyOrders(query: JsonObject = {}, session: Worker
|
||||
return {
|
||||
items: items.map((item) => {
|
||||
const workOrderId = Number(item.id)
|
||||
const activeShare = myShareByOrderId.get(workOrderId)
|
||||
const cancelledShare = cancelledShareByOrderId.get(workOrderId)
|
||||
const timeoutEvent = timeoutEventsByOrderId.get(workOrderId)
|
||||
|
||||
// 判断当前打手是否仍持有该单
|
||||
const isOwnerCurrently =
|
||||
(item.assigned_worker_id != null &&
|
||||
Number(item.assigned_worker_id) === Number(worker.id)) ||
|
||||
Boolean(activeShare)
|
||||
|
||||
// 若不是当前持有人,但有该打手的超时事件或取消份额,则判定为该打手的已超时历史单
|
||||
const isTimedOutForMe =
|
||||
!isOwnerCurrently && Boolean(timeoutEvent || (cancelledShare && item.sharing_enabled))
|
||||
|
||||
let timeoutNotice:
|
||||
| {
|
||||
isTimedOut: boolean
|
||||
timedOutAt: string | null
|
||||
warningMessage: string
|
||||
isTakenByOther?: boolean
|
||||
}
|
||||
| undefined = undefined
|
||||
|
||||
if (isTimedOutForMe) {
|
||||
const isTakenByOther =
|
||||
(item.assigned_worker_id != null &&
|
||||
Number(item.assigned_worker_id) !== Number(worker.id)) ||
|
||||
['in_progress', 'submitted', 'pending_acceptance', 'accepted'].includes(item.status)
|
||||
|
||||
const warningMessage = isTakenByOther
|
||||
? '该订单已超时返回大厅并被其他打手接手,请勿继续在游戏内赠送!若私自继续赠送,造成的财产损失平台概不负责。'
|
||||
: '该订单已超时退回抢单大厅,您已失去接单资格,请勿在游戏内赠送!若私自继续赠送,造成的财产损失平台概不负责。'
|
||||
|
||||
timeoutNotice = {
|
||||
isTimedOut: true,
|
||||
timedOutAt:
|
||||
timeoutEvent?.createdAt ||
|
||||
(item.deadline_at ? String(item.deadline_at) : null) ||
|
||||
item.updated_at,
|
||||
warningMessage,
|
||||
isTakenByOther,
|
||||
}
|
||||
}
|
||||
|
||||
const materialEventAt = materialEventAtByOrderId.get(workOrderId) || ''
|
||||
const materialSeenAt = materialSeenAtByOrderId.get(workOrderId) || ''
|
||||
const materialUpdated =
|
||||
!isTimedOutForMe &&
|
||||
Boolean(materialEventAt) &&
|
||||
(!materialSeenAt ||
|
||||
new Date(materialEventAt).getTime() > new Date(materialSeenAt).getTime())
|
||||
|
||||
return {
|
||||
...mapWorkOrderForWorker(
|
||||
item,
|
||||
permissions,
|
||||
myShareByOrderId.get(workOrderId),
|
||||
activeShare || cancelledShare || null,
|
||||
noteByOrderId.get(workOrderId) || '',
|
||||
viewedAtByOrderId.get(workOrderId) || null,
|
||||
{ isTimedOutForMe, timeoutNotice },
|
||||
),
|
||||
// 问题单分组内的来源角标:平台标记 / 退单审核中 / 反馈处理中。
|
||||
problemMarked: item.status === 'problem',
|
||||
pendingCancelRequest: pendingCancelOrderIds.has(workOrderId),
|
||||
pendingFeedback: pendingFeedbackOrderIds.has(workOrderId),
|
||||
problemMarked: !isTimedOutForMe && item.status === 'problem',
|
||||
pendingCancelRequest: !isTimedOutForMe && pendingCancelOrderIds.has(workOrderId),
|
||||
pendingFeedback: !isTimedOutForMe && pendingFeedbackOrderIds.has(workOrderId),
|
||||
materialUpdated,
|
||||
}
|
||||
}),
|
||||
@@ -183,10 +244,13 @@ export async function getWorkerMyOrderEvents(workOrderId: number | string, sessi
|
||||
const workerId = Number(session.workerId)
|
||||
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
||||
if (!sharingShare && !workerOwnsWorkOrder(workOrder, workerId)) {
|
||||
throw createHttpError('只能查看自己订单的流转记录', {
|
||||
statusCode: 403,
|
||||
errorCode: 'work_order_events_owner_required',
|
||||
})
|
||||
const timeoutEvents = await listWorkerTimeoutEventsByOrderIds(workerId, [Number(workOrder.id)])
|
||||
if (!timeoutEvents.has(Number(workOrder.id))) {
|
||||
throw createHttpError('只能查看自己订单的流转记录', {
|
||||
statusCode: 403,
|
||||
errorCode: 'work_order_events_owner_required',
|
||||
})
|
||||
}
|
||||
}
|
||||
const events = await listWorkOrderEventsByOrderId(workOrderId)
|
||||
return { items: await mapWorkOrderEvents(events) }
|
||||
|
||||
Reference in New Issue
Block a user