拆分打手个人订单服务
This commit is contained in:
@@ -14,6 +14,7 @@ export * from './worker-order-timeout-service.js'
|
|||||||
export * from './worker-order-realtime-service.js'
|
export * from './worker-order-realtime-service.js'
|
||||||
export * from './worker-collect-service.js'
|
export * from './worker-collect-service.js'
|
||||||
export * from './worker-acceptance-service.js'
|
export * from './worker-acceptance-service.js'
|
||||||
|
export * from './worker-my-order-service.js'
|
||||||
export * from './worker-platform-defaults.js'
|
export * from './worker-platform-defaults.js'
|
||||||
export * from './work-order-timeout-policy.js'
|
export * from './work-order-timeout-policy.js'
|
||||||
export * from './after-sales-service.js'
|
export * from './after-sales-service.js'
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
import { runtimeConfig } from '../../config/runtime.js'
|
||||||
|
import {
|
||||||
|
createWorkerCancelRequest,
|
||||||
|
createWorkerOrderFeedback,
|
||||||
|
getWorkOrderShare,
|
||||||
|
getWorkerCancelRequestRisk,
|
||||||
|
getWorkerOrderOverview,
|
||||||
|
listWorkerCancelRequests,
|
||||||
|
listWorkerOrderFeedbacks,
|
||||||
|
listWorkerSharesByWorker,
|
||||||
|
listWorkerWorkOrderNotes,
|
||||||
|
listWorkerWorkOrderViews,
|
||||||
|
listWorkOrderEventsByOrderId,
|
||||||
|
listWorkOrders,
|
||||||
|
markWorkerWorkOrderViewed,
|
||||||
|
upsertWorkerWorkOrderNote,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
import { createHttpError } from '../../utils/http.js'
|
||||||
|
import { nowIso } from '../../utils/time.js'
|
||||||
|
import { normalizePage, normalizePageSize } from '../admin/admin-query-utils.js'
|
||||||
|
import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
|
||||||
|
import {
|
||||||
|
createWorkerCancelRequestAdminNotification,
|
||||||
|
createWorkerOrderFeedbackAdminNotification,
|
||||||
|
} from '../admin/admin-notification-service.js'
|
||||||
|
import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js'
|
||||||
|
import {
|
||||||
|
mapWorkOrderEvents,
|
||||||
|
mapWorkOrderForWorker,
|
||||||
|
mapWorkerCancelRequest,
|
||||||
|
mapWorkerOrderFeedback,
|
||||||
|
normalizeBoolean,
|
||||||
|
normalizeStatuses,
|
||||||
|
resolveWorkerPermissions,
|
||||||
|
} from './mappers.js'
|
||||||
|
import {
|
||||||
|
getRequiredWorkOrder,
|
||||||
|
getRequiredWorker,
|
||||||
|
requireActiveWorkerSession,
|
||||||
|
type WorkerSession,
|
||||||
|
} from './worker-session-context-service.js'
|
||||||
|
import { settleOverdueWorkOrders } from './worker-order-timeout-service.js'
|
||||||
|
|
||||||
|
export async function listWorkerMyOrders(query: JsonObject = {}, session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
const page = normalizePage(query.page)
|
||||||
|
const pageSize = normalizePageSize(query.pageSize)
|
||||||
|
const vipEvidencePending = normalizeBoolean(query.vipEvidencePending, false)
|
||||||
|
const { items, total } = await listWorkOrders({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
statuses: normalizeStatuses(query.status),
|
||||||
|
keyword: String(query.keyword || '').trim(),
|
||||||
|
workerSharingId: worker.id,
|
||||||
|
vipEvidencePending,
|
||||||
|
sort: 'worker_claimed_at_desc',
|
||||||
|
})
|
||||||
|
const permissions = resolveWorkerPermissions(worker)
|
||||||
|
const myShares = await listWorkerSharesByWorker(worker.id)
|
||||||
|
const myShareByOrderId = new Map(myShares.map((share) => [Number(share.work_order_id), share]))
|
||||||
|
const workOrderIds = items.map((item) => Number(item.id))
|
||||||
|
const [notes, views] = await Promise.all([
|
||||||
|
listWorkerWorkOrderNotes(worker.id, workOrderIds),
|
||||||
|
listWorkerWorkOrderViews(worker.id, workOrderIds),
|
||||||
|
])
|
||||||
|
const noteByOrderId = new Map(
|
||||||
|
notes.map((item) => [Number(item.work_order_id), String(item.note || '')]),
|
||||||
|
)
|
||||||
|
const viewedAtByOrderId = new Map(
|
||||||
|
views.map((item) => [Number(item.work_order_id), String(item.viewed_at || '')]),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
items: items.map((item) =>
|
||||||
|
mapWorkOrderForWorker(
|
||||||
|
item,
|
||||||
|
permissions,
|
||||||
|
myShareByOrderId.get(Number(item.id)),
|
||||||
|
noteByOrderId.get(Number(item.id)) || '',
|
||||||
|
viewedAtByOrderId.get(Number(item.id)) || null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pagination: { page, pageSize, total },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打手订单标签概览:订单数与预计或已结算金额。 */
|
||||||
|
export async function getWorkerMyOrderOverview(session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
await settleOverdueWorkOrders({ workerId: worker.id, limit: 50 })
|
||||||
|
const rows = await getWorkerOrderOverview(worker.id)
|
||||||
|
const items = rows.map((row) => ({
|
||||||
|
status: row.status,
|
||||||
|
orderCount: row.orderCount,
|
||||||
|
settlementAmount: row.settlementAmount,
|
||||||
|
}))
|
||||||
|
const vipEvidencePending = rows.reduce(
|
||||||
|
(summary, row) => ({
|
||||||
|
orderCount: summary.orderCount + row.vipEvidencePendingCount,
|
||||||
|
settlementAmount: summary.settlementAmount + row.vipEvidencePendingSettlementAmount,
|
||||||
|
}),
|
||||||
|
{ orderCount: 0, settlementAmount: 0 },
|
||||||
|
)
|
||||||
|
const all = items.reduce(
|
||||||
|
(summary, item) => ({
|
||||||
|
orderCount: summary.orderCount + item.orderCount,
|
||||||
|
settlementAmount: summary.settlementAmount + item.settlementAmount,
|
||||||
|
}),
|
||||||
|
{ orderCount: 0, settlementAmount: 0 },
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
items: [
|
||||||
|
...items,
|
||||||
|
{ status: 'vip_evidence_pending', ...vipEvidencePending },
|
||||||
|
{ status: 'all', ...all },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkerMyOrderEvents(workOrderId: number | string, session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
if (runtimeConfig.worker.orderTimelineVisible !== true) {
|
||||||
|
throw createHttpError('流转记录暂未对打手开放', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'work_order_events_worker_disabled',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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 getWorkerCancelRequestSummary(session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const [risk, requests] = await Promise.all([
|
||||||
|
getWorkerCancelRequestRisk(session.workerId, nowIso()),
|
||||||
|
listWorkerCancelRequests(session.workerId),
|
||||||
|
])
|
||||||
|
return { risk, items: requests.map(mapWorkerCancelRequest) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkerAnnouncement(session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
return getWorkerAnnouncementConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkerOrderFeedbackSummary(session: WorkerSession) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
return { items: (await listWorkerOrderFeedbacks(session.workerId)).map(mapWorkerOrderFeedback) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitWorkerOrderFeedback(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const content = String(payload.content || '').trim()
|
||||||
|
if (!content) {
|
||||||
|
throw createHttpError('请填写问题反馈内容', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_order_feedback_content_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (content.length > 500) {
|
||||||
|
throw createHttpError('问题反馈不能超过 500 个字符', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_order_feedback_content_too_long',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const result = await createWorkerOrderFeedback({
|
||||||
|
workOrderId: Number(workOrderId),
|
||||||
|
workerId: Number(session.workerId),
|
||||||
|
content,
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
if (!result.feedback) {
|
||||||
|
throw createHttpError('只能反馈自己的订单问题', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'worker_order_feedback_owner_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
await createWorkerOrderFeedbackAdminNotification({
|
||||||
|
feedbackId: Number(result.feedback.id),
|
||||||
|
workOrderId: Number(result.feedback.work_order_id),
|
||||||
|
workOrderNo: result.feedback.work_order_no || '',
|
||||||
|
productName: result.feedback.product_name || '',
|
||||||
|
workerName: worker.display_name || worker.username,
|
||||||
|
content,
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(result.feedback.work_order_id),
|
||||||
|
workerIds: [Number(session.workerId)],
|
||||||
|
})
|
||||||
|
return { feedback: mapWorkerOrderFeedback(result.feedback) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitWorkerCancelRequest(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const reason = String(payload.reason || '').trim()
|
||||||
|
if (!reason) {
|
||||||
|
throw createHttpError('请填写申请取消原因', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_cancel_request_reason_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (reason.length > 300) {
|
||||||
|
throw createHttpError('申请取消原因不能超过 300 个字符', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_cancel_request_reason_too_long',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const result = await createWorkerCancelRequest({
|
||||||
|
workOrderId: Number(workOrderId),
|
||||||
|
workerId: Number(session.workerId),
|
||||||
|
reason,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (result.failureReason === 'daily_limit_reached') {
|
||||||
|
throw createHttpError('今日最多申请取消 5 次,请明日再试', {
|
||||||
|
statusCode: 429,
|
||||||
|
errorCode: 'worker_cancel_request_daily_limit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (result.failureReason === 'request_pending') {
|
||||||
|
throw createHttpError('该订单已有待审核的取消申请', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_cancel_request_pending',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (result.failureReason === 'share_not_cancellable') {
|
||||||
|
throw createHttpError('当前拼单份额不能申请取消', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_cancel_request_share_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!result.request) {
|
||||||
|
throw createHttpError('当前订单不能申请取消', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'worker_cancel_request_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
await createWorkerCancelRequestAdminNotification({
|
||||||
|
requestId: Number(result.request.id),
|
||||||
|
workOrderId: Number(result.request.work_order_id),
|
||||||
|
platformOrderId: result.request.platform_order_id || '',
|
||||||
|
productName: result.request.product_name || '',
|
||||||
|
workerName: worker.display_name || worker.username,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
const risk = await getWorkerCancelRequestRisk(session.workerId, now)
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(result.request.work_order_id),
|
||||||
|
workerIds: [Number(session.workerId)],
|
||||||
|
})
|
||||||
|
return { request: mapWorkerCancelRequest(result.request), risk }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveWorkerOrderNote(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
const workerId = Number(session.workerId)
|
||||||
|
const note = String(payload.note || '').trim()
|
||||||
|
if (note.length > 100) {
|
||||||
|
throw createHttpError('备注不能超过 100 个字符', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_note_too_long',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const worker = await getRequiredWorker(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_note_owner_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const savedNote = await upsertWorkerWorkOrderNote({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerId,
|
||||||
|
note,
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderForWorker(
|
||||||
|
workOrder,
|
||||||
|
resolveWorkerPermissions(worker),
|
||||||
|
sharingShare,
|
||||||
|
savedNote,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 标记打手本人已查看工单,首次操作后不再展示“新”标识。 */
|
||||||
|
export async function markWorkerOrderRead(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_read_owner_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewedAt = await markWorkerWorkOrderViewed({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerId,
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerIds: [workerId],
|
||||||
|
})
|
||||||
|
return { viewedAt, isUnread: false }
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { runtimeConfig } from '../../config/runtime.js'
|
|
||||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||||
import {
|
import {
|
||||||
createWorkerCancelRequest,
|
|
||||||
createWorkerOrderFeedback,
|
|
||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
consumeWorkerSmsCode,
|
consumeWorkerSmsCode,
|
||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
@@ -13,8 +10,6 @@ import {
|
|||||||
getWorkerUserByUsername,
|
getWorkerUserByUsername,
|
||||||
getWorkerUserByDisplayName,
|
getWorkerUserByDisplayName,
|
||||||
getWorkOrderById,
|
getWorkOrderById,
|
||||||
getWorkOrderShare,
|
|
||||||
getWorkerOrderOverview,
|
|
||||||
grabWorkOrder,
|
grabWorkOrder,
|
||||||
createWorkerSessionRecord,
|
createWorkerSessionRecord,
|
||||||
getWorkerSessionBySessionId,
|
getWorkerSessionBySessionId,
|
||||||
@@ -22,24 +17,12 @@ import {
|
|||||||
revokeWorkerSession,
|
revokeWorkerSession,
|
||||||
touchWorkerSession,
|
touchWorkerSession,
|
||||||
joinWorkOrderShare,
|
joinWorkOrderShare,
|
||||||
listWorkerCancelRequests,
|
|
||||||
listWorkerOrderFeedbacks,
|
|
||||||
listWorkerSharesByWorker,
|
|
||||||
listWorkerWorkOrderNotes,
|
|
||||||
listWorkerWorkOrderViews,
|
|
||||||
listWorkOrders,
|
|
||||||
listWorkOrderEventsByOrderId,
|
|
||||||
getWorkerCancelRequestRisk,
|
|
||||||
upsertWorkerWorkOrderNote,
|
|
||||||
markWorkerWorkOrderViewed,
|
|
||||||
type WorkOrderRow,
|
type WorkOrderRow,
|
||||||
type WorkerSessionRow,
|
type WorkerSessionRow,
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
import type { JsonObject } from '../../types/json.js'
|
import type { JsonObject } from '../../types/json.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import { normalizePage, normalizePageSize } from '../admin/admin-query-utils.js'
|
|
||||||
import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
|
|
||||||
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
||||||
import {
|
import {
|
||||||
getRequiredWorkOrder,
|
getRequiredWorkOrder,
|
||||||
@@ -47,10 +30,6 @@ import {
|
|||||||
requireActiveWorkerSession,
|
requireActiveWorkerSession,
|
||||||
type WorkerSession,
|
type WorkerSession,
|
||||||
} from './worker-session-context-service.js'
|
} from './worker-session-context-service.js'
|
||||||
import {
|
|
||||||
createWorkerCancelRequestAdminNotification,
|
|
||||||
createWorkerOrderFeedbackAdminNotification,
|
|
||||||
} from '../admin/admin-notification-service.js'
|
|
||||||
import {
|
import {
|
||||||
publishWorkerWalletRealtimeChange,
|
publishWorkerWalletRealtimeChange,
|
||||||
publishWorkOrderRealtimeChange,
|
publishWorkOrderRealtimeChange,
|
||||||
@@ -58,14 +37,8 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
mapWorkOrderForWorker,
|
mapWorkOrderForWorker,
|
||||||
mapWorkOrderEvents,
|
|
||||||
mapWorkOrderShare,
|
mapWorkOrderShare,
|
||||||
mapWorkerCancelRequest,
|
|
||||||
mapWorkerOrderFeedback,
|
|
||||||
normalizeBoolean,
|
|
||||||
normalizeOptionalId,
|
|
||||||
normalizePositiveInteger,
|
normalizePositiveInteger,
|
||||||
normalizeStatuses,
|
|
||||||
resolveFreezeDepositAmount,
|
resolveFreezeDepositAmount,
|
||||||
resolveWorkerPermissions,
|
resolveWorkerPermissions,
|
||||||
throwGrabWorkOrderFailure,
|
throwGrabWorkOrderFailure,
|
||||||
@@ -99,7 +72,6 @@ import {
|
|||||||
} from './worker-session-auth-service.js'
|
} from './worker-session-auth-service.js'
|
||||||
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
||||||
import { publishWorkOrderParticipantChange } from './worker-order-realtime-service.js'
|
import { publishWorkOrderParticipantChange } from './worker-order-realtime-service.js'
|
||||||
import { settleOverdueWorkOrders } from './worker-order-timeout-service.js'
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getRequiredWorkOrder,
|
getRequiredWorkOrder,
|
||||||
@@ -255,310 +227,18 @@ export { settleDueDepositUnfreezes } from './worker-profile-service.js'
|
|||||||
|
|
||||||
export { settleOverdueWorkOrders } from './worker-order-timeout-service.js'
|
export { settleOverdueWorkOrders } from './worker-order-timeout-service.js'
|
||||||
|
|
||||||
export async function listWorkerMyOrders(query: JsonObject = {}, session: WorkerSession) {
|
export {
|
||||||
requireActiveWorkerSession(session)
|
getWorkerAnnouncement,
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
getWorkerCancelRequestSummary,
|
||||||
const page = normalizePage(query.page)
|
getWorkerMyOrderEvents,
|
||||||
const pageSize = normalizePageSize(query.pageSize)
|
getWorkerMyOrderOverview,
|
||||||
const vipEvidencePending = normalizeBoolean(query.vipEvidencePending, false)
|
getWorkerOrderFeedbackSummary,
|
||||||
const { items, total } = await listWorkOrders({
|
listWorkerMyOrders,
|
||||||
page,
|
markWorkerOrderRead,
|
||||||
pageSize,
|
saveWorkerOrderNote,
|
||||||
statuses: normalizeStatuses(query.status),
|
submitWorkerCancelRequest,
|
||||||
keyword: String(query.keyword || '').trim(),
|
submitWorkerOrderFeedback,
|
||||||
workerSharingId: worker.id,
|
} from './worker-my-order-service.js'
|
||||||
vipEvidencePending,
|
|
||||||
sort: 'worker_claimed_at_desc',
|
|
||||||
})
|
|
||||||
const permissions = resolveWorkerPermissions(worker)
|
|
||||||
const myShares = await listWorkerSharesByWorker(worker.id)
|
|
||||||
const myShareByOrderId = new Map(myShares.map((share) => [Number(share.work_order_id), share]))
|
|
||||||
const workOrderIds = items.map((item) => Number(item.id))
|
|
||||||
const [notes, views] = await Promise.all([
|
|
||||||
listWorkerWorkOrderNotes(worker.id, workOrderIds),
|
|
||||||
listWorkerWorkOrderViews(worker.id, workOrderIds),
|
|
||||||
])
|
|
||||||
const noteByOrderId = new Map(
|
|
||||||
notes.map((item) => [Number(item.work_order_id), String(item.note || '')]),
|
|
||||||
)
|
|
||||||
const viewedAtByOrderId = new Map(
|
|
||||||
views.map((item) => [Number(item.work_order_id), String(item.viewed_at || '')]),
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
items: items.map((item) =>
|
|
||||||
mapWorkOrderForWorker(
|
|
||||||
item,
|
|
||||||
permissions,
|
|
||||||
myShareByOrderId.get(Number(item.id)),
|
|
||||||
noteByOrderId.get(Number(item.id)) || '',
|
|
||||||
viewedAtByOrderId.get(Number(item.id)) || null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
pagination: { page, pageSize, total },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打手订单标签概览:订单数与预计/已结算金额。 */
|
|
||||||
export async function getWorkerMyOrderOverview(session: WorkerSession) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
|
||||||
await settleOverdueWorkOrders({ workerId: worker.id, limit: 50 })
|
|
||||||
const rows = await getWorkerOrderOverview(worker.id)
|
|
||||||
const items = rows.map((row) => ({
|
|
||||||
status: row.status,
|
|
||||||
orderCount: row.orderCount,
|
|
||||||
settlementAmount: row.settlementAmount,
|
|
||||||
}))
|
|
||||||
const vipEvidencePending = rows.reduce(
|
|
||||||
(summary, row) => ({
|
|
||||||
orderCount: summary.orderCount + row.vipEvidencePendingCount,
|
|
||||||
settlementAmount: summary.settlementAmount + row.vipEvidencePendingSettlementAmount,
|
|
||||||
}),
|
|
||||||
{ orderCount: 0, settlementAmount: 0 },
|
|
||||||
)
|
|
||||||
const all = items.reduce(
|
|
||||||
(summary, item) => ({
|
|
||||||
orderCount: summary.orderCount + item.orderCount,
|
|
||||||
settlementAmount: summary.settlementAmount + item.settlementAmount,
|
|
||||||
}),
|
|
||||||
{ orderCount: 0, settlementAmount: 0 },
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
items: [
|
|
||||||
...items,
|
|
||||||
{ status: 'vip_evidence_pending', ...vipEvidencePending },
|
|
||||||
{ status: 'all', ...all },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkerMyOrderEvents(workOrderId: number | string, session: WorkerSession) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
if (runtimeConfig.worker.orderTimelineVisible !== true) {
|
|
||||||
throw createHttpError('流转记录暂未对打手开放', {
|
|
||||||
statusCode: 403,
|
|
||||||
errorCode: 'work_order_events_worker_disabled',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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 getWorkerCancelRequestSummary(session: WorkerSession) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
const [risk, requests] = await Promise.all([
|
|
||||||
getWorkerCancelRequestRisk(session.workerId, nowIso()),
|
|
||||||
listWorkerCancelRequests(session.workerId),
|
|
||||||
])
|
|
||||||
return { risk, items: requests.map(mapWorkerCancelRequest) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkerAnnouncement(session: WorkerSession) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
return getWorkerAnnouncementConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkerOrderFeedbackSummary(session: WorkerSession) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
return { items: (await listWorkerOrderFeedbacks(session.workerId)).map(mapWorkerOrderFeedback) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function submitWorkerOrderFeedback(
|
|
||||||
workOrderId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
session: WorkerSession,
|
|
||||||
) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
const content = String(payload.content || '').trim()
|
|
||||||
if (!content) {
|
|
||||||
throw createHttpError('请填写问题反馈内容', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'worker_order_feedback_content_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (content.length > 500) {
|
|
||||||
throw createHttpError('问题反馈不能超过 500 个字符', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'worker_order_feedback_content_too_long',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const result = await createWorkerOrderFeedback({
|
|
||||||
workOrderId: Number(workOrderId),
|
|
||||||
workerId: Number(session.workerId),
|
|
||||||
content,
|
|
||||||
now: nowIso(),
|
|
||||||
})
|
|
||||||
if (!result.feedback) {
|
|
||||||
throw createHttpError('只能反馈自己的订单问题', {
|
|
||||||
statusCode: 403,
|
|
||||||
errorCode: 'worker_order_feedback_owner_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
|
||||||
await createWorkerOrderFeedbackAdminNotification({
|
|
||||||
feedbackId: Number(result.feedback.id),
|
|
||||||
workOrderId: Number(result.feedback.work_order_id),
|
|
||||||
workOrderNo: result.feedback.work_order_no || '',
|
|
||||||
productName: result.feedback.product_name || '',
|
|
||||||
workerName: worker.display_name || worker.username,
|
|
||||||
content,
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(result.feedback.work_order_id),
|
|
||||||
workerIds: [Number(session.workerId)],
|
|
||||||
})
|
|
||||||
return { feedback: mapWorkerOrderFeedback(result.feedback) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function submitWorkerCancelRequest(
|
|
||||||
workOrderId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
session: WorkerSession,
|
|
||||||
) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
const reason = String(payload.reason || '').trim()
|
|
||||||
if (!reason) {
|
|
||||||
throw createHttpError('请填写申请取消原因', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'worker_cancel_request_reason_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (reason.length > 300) {
|
|
||||||
throw createHttpError('申请取消原因不能超过 300 个字符', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'worker_cancel_request_reason_too_long',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const now = nowIso()
|
|
||||||
const result = await createWorkerCancelRequest({
|
|
||||||
workOrderId: Number(workOrderId),
|
|
||||||
workerId: Number(session.workerId),
|
|
||||||
reason,
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
if (result.failureReason === 'daily_limit_reached') {
|
|
||||||
throw createHttpError('今日最多申请取消 5 次,请明日再试', {
|
|
||||||
statusCode: 429,
|
|
||||||
errorCode: 'worker_cancel_request_daily_limit',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (result.failureReason === 'request_pending') {
|
|
||||||
throw createHttpError('该订单已有待审核的取消申请', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'worker_cancel_request_pending',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (result.failureReason === 'share_not_cancellable') {
|
|
||||||
throw createHttpError('当前拼单份额不能申请取消', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'worker_cancel_request_share_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!result.request) {
|
|
||||||
throw createHttpError('当前订单不能申请取消', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'worker_cancel_request_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
|
||||||
await createWorkerCancelRequestAdminNotification({
|
|
||||||
requestId: Number(result.request.id),
|
|
||||||
workOrderId: Number(result.request.work_order_id),
|
|
||||||
platformOrderId: result.request.platform_order_id || '',
|
|
||||||
productName: result.request.product_name || '',
|
|
||||||
workerName: worker.display_name || worker.username,
|
|
||||||
reason,
|
|
||||||
})
|
|
||||||
const risk = await getWorkerCancelRequestRisk(session.workerId, now)
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(result.request.work_order_id),
|
|
||||||
workerIds: [Number(session.workerId)],
|
|
||||||
})
|
|
||||||
return { request: mapWorkerCancelRequest(result.request), risk }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveWorkerOrderNote(
|
|
||||||
workOrderId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
session: WorkerSession,
|
|
||||||
) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
const workerId = Number(session.workerId)
|
|
||||||
const note = String(payload.note || '').trim()
|
|
||||||
if (note.length > 100) {
|
|
||||||
throw createHttpError('备注不能超过 100 个字符', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'work_order_note_too_long',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const worker = await getRequiredWorker(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_note_owner_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const savedNote = await upsertWorkerWorkOrderNote({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerId,
|
|
||||||
note,
|
|
||||||
now: nowIso(),
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderForWorker(
|
|
||||||
workOrder,
|
|
||||||
resolveWorkerPermissions(worker),
|
|
||||||
sharingShare,
|
|
||||||
savedNote,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 标记打手本人已查看工单,首次操作后不再展示“新”标识。 */
|
|
||||||
export async function markWorkerOrderRead(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_read_owner_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewedAt = await markWorkerWorkOrderViewed({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerId,
|
|
||||||
now: nowIso(),
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerIds: [workerId],
|
|
||||||
})
|
|
||||||
return { viewedAt, isUnread: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function joinWorkerSharingOrder(
|
export async function joinWorkerSharingOrder(
|
||||||
workOrderId: number | string,
|
workOrderId: number | string,
|
||||||
|
|||||||
Reference in New Issue
Block a user