拆分后台工单管理服务
This commit is contained in:
@@ -13,18 +13,13 @@ import {
|
|||||||
createWorkOrder,
|
createWorkOrder,
|
||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
deductPendingDepositUnfreeze,
|
deductPendingDepositUnfreeze,
|
||||||
deleteWorkOrder,
|
|
||||||
getWorkerUserById,
|
getWorkerUserById,
|
||||||
getWorkOrderByOrderItemId,
|
getWorkOrderByOrderItemId,
|
||||||
listWorkOrders,
|
listWorkOrders,
|
||||||
listWorkProductRules,
|
listWorkProductRules,
|
||||||
listWorkProductRuleMappings,
|
listWorkProductRuleMappings,
|
||||||
listWorkerUsers,
|
listWorkerUsers,
|
||||||
listWorkOrderShares,
|
|
||||||
resolveProblemWorkOrder,
|
|
||||||
reopenCancelledWorkOrder,
|
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
updateWorkOrderBasic,
|
|
||||||
type WorkOrderRow,
|
type WorkOrderRow,
|
||||||
} 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'
|
||||||
@@ -45,10 +40,7 @@ import {
|
|||||||
publishWorkerWalletRealtimeChange,
|
publishWorkerWalletRealtimeChange,
|
||||||
publishWorkOrderRealtimeChange,
|
publishWorkOrderRealtimeChange,
|
||||||
} from '../realtime/realtime-event-service.js'
|
} from '../realtime/realtime-event-service.js'
|
||||||
import {
|
import { consumeIndustryVouchersBeforeWorkOrderAssign } from './publish-voucher-service.js'
|
||||||
consumeIndustryVouchersBeforeWorkOrderAssign,
|
|
||||||
consumeIndustryVouchersBeforeWorkOrderPublish,
|
|
||||||
} from './publish-voucher-service.js'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
mapWorkOrderAdmin,
|
mapWorkOrderAdmin,
|
||||||
@@ -56,14 +48,11 @@ import {
|
|||||||
normalizeAmountFen,
|
normalizeAmountFen,
|
||||||
normalizeInteger,
|
normalizeInteger,
|
||||||
normalizeOptionalId,
|
normalizeOptionalId,
|
||||||
normalizePositiveInteger,
|
|
||||||
normalizeProblemResolutionAction,
|
|
||||||
normalizeRequirementFields,
|
normalizeRequirementFields,
|
||||||
normalizeRequirementFieldsFromPayload,
|
normalizePositiveInteger,
|
||||||
resolveFreezeDepositAmount,
|
resolveFreezeDepositAmount,
|
||||||
resolveMatchingProductRuleDecision,
|
resolveMatchingProductRuleDecision,
|
||||||
resolveKuaishouWorkProductMatchContext,
|
resolveKuaishouWorkProductMatchContext,
|
||||||
resolveRequirementFields,
|
|
||||||
resolveSkuNameQuantity,
|
resolveSkuNameQuantity,
|
||||||
resolveWorkOrderRulePricing,
|
resolveWorkOrderRulePricing,
|
||||||
resolveWorkOrderSharingEnabled,
|
resolveWorkOrderSharingEnabled,
|
||||||
@@ -290,375 +279,16 @@ export {
|
|||||||
resolveAdminMockWorkOrderInput,
|
resolveAdminMockWorkOrderInput,
|
||||||
} from './admin-mock-work-order-service.js'
|
} from './admin-mock-work-order-service.js'
|
||||||
|
|
||||||
export async function updateAdminWorkOrder(
|
export {
|
||||||
workOrderId: number | string,
|
deleteAdminWorkOrder,
|
||||||
payload: JsonObject = {},
|
markAdminWorkOrderProblem,
|
||||||
options: { canEditRequirementFields?: boolean; actorName?: string } = {},
|
pinAdminWorkOrder,
|
||||||
) {
|
publishAdminWorkOrder,
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
reopenAdminCancelledWorkOrder,
|
||||||
if (!mapWorkOrderAdmin(workOrder).capabilities.canEdit) {
|
resolveAdminProblemWorkOrder,
|
||||||
throw createHttpError('只有待完善、未分配或待抢单的工单可以编辑', {
|
unpublishAdminWorkOrder,
|
||||||
statusCode: 409,
|
updateAdminWorkOrder,
|
||||||
errorCode: 'work_order_edit_status_invalid',
|
} from './admin-work-order-management-service.js'
|
||||||
})
|
|
||||||
}
|
|
||||||
if (workOrder.assigned_worker_id) {
|
|
||||||
throw createHttpError('工单已被打手接走,不能编辑', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_edit_assigned_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields =
|
|
||||||
options.canEditRequirementFields === false
|
|
||||||
? resolveRequirementFields(workOrder)
|
|
||||||
: normalizeRequirementFieldsFromPayload(payload)
|
|
||||||
const rewardAmount =
|
|
||||||
payload.rewardAmount === undefined
|
|
||||||
? workOrder.reward_amount
|
|
||||||
: normalizeAmountFen(payload.rewardAmount, workOrder.reward_amount)
|
|
||||||
const requiredDepositAmount =
|
|
||||||
payload.requiredDepositAmount === undefined
|
|
||||||
? workOrder.required_deposit_amount
|
|
||||||
: normalizeAmountFen(payload.requiredDepositAmount, workOrder.required_deposit_amount)
|
|
||||||
const depositThresholdAmount =
|
|
||||||
payload.depositThresholdAmount === undefined
|
|
||||||
? workOrder.deposit_threshold_amount
|
|
||||||
: normalizeAmountFen(payload.depositThresholdAmount, workOrder.deposit_threshold_amount)
|
|
||||||
const categoryId =
|
|
||||||
payload.categoryId === undefined || payload.categoryId === null || payload.categoryId === ''
|
|
||||||
? workOrder.category_id
|
|
||||||
: (normalizeOptionalId(payload.categoryId) ?? workOrder.category_id)
|
|
||||||
|
|
||||||
const now = nowIso()
|
|
||||||
const updated = await updateWorkOrderBasic(workOrder.id, {
|
|
||||||
productName: String(payload.productName ?? workOrder.product_name).trim(),
|
|
||||||
platformOrderId: String(payload.platformOrderId ?? workOrder.platform_order_id ?? '').trim(),
|
|
||||||
categoryId,
|
|
||||||
rewardAmount,
|
|
||||||
requiredDepositAmount,
|
|
||||||
depositThresholdAmount,
|
|
||||||
timeoutMinutes:
|
|
||||||
payload.timeoutMinutes === undefined
|
|
||||||
? workOrder.timeout_minutes
|
|
||||||
: Math.max(0, normalizeInteger(payload.timeoutMinutes, workOrder.timeout_minutes)),
|
|
||||||
timeoutPolicy: normalizeWorkOrderTimeoutPolicy(
|
|
||||||
payload.timeoutPolicy ?? workOrder.timeout_policy,
|
|
||||||
),
|
|
||||||
requirementJson: JSON.stringify({ fields }),
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: options.actorName || '',
|
|
||||||
eventType: 'edited',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: workOrder.status,
|
|
||||||
payloadJson: JSON.stringify({
|
|
||||||
productName: updated?.product_name,
|
|
||||||
rewardAmount,
|
|
||||||
requiredDepositAmount,
|
|
||||||
timeoutMinutes: updated?.timeout_minutes,
|
|
||||||
timeoutPolicy: updated?.timeout_policy,
|
|
||||||
fields,
|
|
||||||
}),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
hallChanged: workOrder.status === WORK_ORDER_STATUS.OPEN,
|
|
||||||
})
|
|
||||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteAdminWorkOrder(workOrderId: number | string) {
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
if (!mapWorkOrderAdmin(workOrder).capabilities.canDelete) {
|
|
||||||
throw createHttpError('只有待完善、未分配或已取消的工单可以删除', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_delete_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (workOrder.assigned_worker_id) {
|
|
||||||
throw createHttpError('工单已被打手接走,不能删除', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_delete_assigned_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const shares = await listWorkOrderShares(workOrder.id)
|
|
||||||
const activeShares = shares.filter((share) => share.status !== 'cancelled')
|
|
||||||
if (activeShares.length > 0) {
|
|
||||||
throw createHttpError('工单存在参与中的拼单,不能删除', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_delete_has_shares',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const deleted = await deleteWorkOrder(workOrder.id)
|
|
||||||
if (!deleted) {
|
|
||||||
throw createHttpError('工单删除失败', {
|
|
||||||
statusCode: 500,
|
|
||||||
errorCode: 'work_order_delete_failed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
hallChanged: workOrder.status === WORK_ORDER_STATUS.OPEN,
|
|
||||||
})
|
|
||||||
return { deleted: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function reopenAdminCancelledWorkOrder(workOrderId: number | string, actorName = '') {
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
if (!mapWorkOrderAdmin(workOrder).capabilities.canReopen) {
|
|
||||||
throw createHttpError('只有没有有效拼单份额的已取消订单可以重新启用', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_reopen_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { order: updated, failureReason } = await reopenCancelledWorkOrder({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
now: nowIso(),
|
|
||||||
actorName,
|
|
||||||
})
|
|
||||||
if (failureReason === 'work_order_has_active_shares') {
|
|
||||||
throw createHttpError('该订单仍有有效拼单份额,不能重新启用', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_reopen_has_active_shares',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!updated) {
|
|
||||||
throw createHttpError('订单状态已变化,无法重新启用', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_reopen_conflict',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
|
||||||
return { order: mapWorkOrderAdmin(updated) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function publishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
if (workOrder.status !== WORK_ORDER_STATUS.UNASSIGNED) {
|
|
||||||
throw createHttpError('只有未分配订单可以发布到大厅', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_publish_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (Number(workOrder.reward_amount || 0) <= 0) {
|
|
||||||
throw createHttpError('接单金额为 0,无法发布到大厅', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_publish_reward_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const voucherConsume = await consumeIndustryVouchersBeforeWorkOrderPublish(workOrder)
|
|
||||||
if (!voucherConsume.ok) {
|
|
||||||
const failedAt = nowIso()
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: actorName,
|
|
||||||
eventType: 'publish_voucher_consume_failed',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: workOrder.status,
|
|
||||||
payloadJson: JSON.stringify(voucherConsume),
|
|
||||||
now: failedAt,
|
|
||||||
})
|
|
||||||
throw createHttpError(voucherConsume.errorMessage || '电子凭证核销失败,订单未发布', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_publish_voucher_consume_failed',
|
|
||||||
context: voucherConsume,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = nowIso()
|
|
||||||
const updated = await updateWorkOrder(workOrder.id, {
|
|
||||||
status: WORK_ORDER_STATUS.OPEN,
|
|
||||||
published_at: now,
|
|
||||||
hall_queued_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
if (!updated) {
|
|
||||||
throw createHttpError(
|
|
||||||
voucherConsume.voucherCount > 0
|
|
||||||
? '电子凭证已核销,但订单发布失败,请重试发布'
|
|
||||||
: '订单发布失败,请重试',
|
|
||||||
{
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_publish_update_failed',
|
|
||||||
context: voucherConsume,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: actorName,
|
|
||||||
eventType: 'published',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: WORK_ORDER_STATUS.OPEN,
|
|
||||||
payloadJson: JSON.stringify({ voucherConsume }),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
|
||||||
const hallConfig = getWorkerHallConfig()
|
|
||||||
const hallEntry = await listWorkOrders({
|
|
||||||
page: 1,
|
|
||||||
pageSize: 1,
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
statuses: [WORK_ORDER_STATUS.OPEN],
|
|
||||||
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
|
||||||
excludeFilledSharing: true,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderAdmin(updated),
|
|
||||||
voucherConsume,
|
|
||||||
hallVisible: hallEntry.total > 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function unpublishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
if (!mapWorkOrderAdmin(workOrder).capabilities.canUnpublish) {
|
|
||||||
throw createHttpError('只有待抢单且未被接走的订单可以下架', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_unpublish_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const now = nowIso()
|
|
||||||
const updated = await updateWorkOrder(workOrder.id, {
|
|
||||||
status: WORK_ORDER_STATUS.UNASSIGNED,
|
|
||||||
published_at: null,
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: actorName,
|
|
||||||
eventType: 'unpublished',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: WORK_ORDER_STATUS.UNASSIGNED,
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
|
||||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function pinAdminWorkOrder(
|
|
||||||
workOrderId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
actorName = '',
|
|
||||||
) {
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
const pinned = payload.pinned === true
|
|
||||||
const capabilities = mapWorkOrderAdmin(workOrder).capabilities
|
|
||||||
if (pinned && (!capabilities.canPin || !mapWorkOrderAdmin(workOrder).hallAvailable)) {
|
|
||||||
throw createHttpError('只有待抢单订单可以置顶', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_pin_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const now = nowIso()
|
|
||||||
const updated = await updateWorkOrder(workOrder.id, {
|
|
||||||
pinned_at: pinned ? now : null,
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: actorName,
|
|
||||||
eventType: pinned ? 'pinned' : 'unpinned',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: workOrder.status,
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
|
||||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function markAdminWorkOrderProblem(
|
|
||||||
workOrderId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
actorName = '',
|
|
||||||
) {
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
if (!mapWorkOrderAdmin(workOrder).capabilities.canMarkProblem) {
|
|
||||||
throw createHttpError('当前状态不能标记问题单', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_problem_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const now = nowIso()
|
|
||||||
const note = String(payload.note || payload.problemNote || '').trim()
|
|
||||||
const updated = await updateWorkOrder(workOrder.id, {
|
|
||||||
status: WORK_ORDER_STATUS.PROBLEM,
|
|
||||||
problem_note: note,
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorId: actorName,
|
|
||||||
eventType: 'marked_problem',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: WORK_ORDER_STATUS.PROBLEM,
|
|
||||||
payloadJson: JSON.stringify({ note }),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
if (workOrder.status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
|
|
||||||
await resolveAdminNotificationEntity('work_order', Number(workOrder.id), 'marked_problem')
|
|
||||||
}
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerIds: await resolveWorkOrderRealtimeWorkerIds(workOrder),
|
|
||||||
})
|
|
||||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveAdminProblemWorkOrder(
|
|
||||||
workOrderId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
actorName = '',
|
|
||||||
) {
|
|
||||||
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(),
|
|
||||||
actorName,
|
|
||||||
})
|
|
||||||
if (!updated) {
|
|
||||||
throw createHttpError('问题单处置失败,订单状态可能已变化', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_problem_resolution_conflict',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerIds: await resolveWorkOrderRealtimeWorkerIds(workOrder),
|
|
||||||
})
|
|
||||||
return { order: mapWorkOrderAdmin(updated) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function acceptAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
export async function acceptAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||||
let workOrder = await getRequiredWorkOrder(workOrderId)
|
let workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
|||||||
@@ -0,0 +1,400 @@
|
|||||||
|
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||||
|
import {
|
||||||
|
createWorkOrderEvent,
|
||||||
|
deleteWorkOrder,
|
||||||
|
listWorkOrderShares,
|
||||||
|
listWorkOrders,
|
||||||
|
reopenCancelledWorkOrder,
|
||||||
|
resolveProblemWorkOrder,
|
||||||
|
updateWorkOrder,
|
||||||
|
updateWorkOrderBasic,
|
||||||
|
} 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 { resolveAdminNotificationEntity } from '../admin/admin-notification-service.js'
|
||||||
|
import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js'
|
||||||
|
import { consumeIndustryVouchersBeforeWorkOrderPublish } from './publish-voucher-service.js'
|
||||||
|
import {
|
||||||
|
mapWorkOrderAdmin,
|
||||||
|
normalizeAmountFen,
|
||||||
|
normalizeInteger,
|
||||||
|
normalizeOptionalId,
|
||||||
|
normalizeProblemResolutionAction,
|
||||||
|
normalizeRequirementFieldsFromPayload,
|
||||||
|
resolveRequirementFields,
|
||||||
|
} from './mappers.js'
|
||||||
|
import { resolveWorkOrderRealtimeWorkerIds } from './admin-work-order-realtime-service.js'
|
||||||
|
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
||||||
|
import { getRequiredWorkOrder } from './worker-session-context-service.js'
|
||||||
|
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
||||||
|
|
||||||
|
export async function updateAdminWorkOrder(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
options: { canEditRequirementFields?: boolean; actorName?: string } = {},
|
||||||
|
) {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (!mapWorkOrderAdmin(workOrder).capabilities.canEdit) {
|
||||||
|
throw createHttpError('只有待完善、未分配或待抢单的工单可以编辑', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_edit_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (workOrder.assigned_worker_id) {
|
||||||
|
throw createHttpError('工单已被打手接走,不能编辑', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_edit_assigned_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields =
|
||||||
|
options.canEditRequirementFields === false
|
||||||
|
? resolveRequirementFields(workOrder)
|
||||||
|
: normalizeRequirementFieldsFromPayload(payload)
|
||||||
|
const rewardAmount =
|
||||||
|
payload.rewardAmount === undefined
|
||||||
|
? workOrder.reward_amount
|
||||||
|
: normalizeAmountFen(payload.rewardAmount, workOrder.reward_amount)
|
||||||
|
const requiredDepositAmount =
|
||||||
|
payload.requiredDepositAmount === undefined
|
||||||
|
? workOrder.required_deposit_amount
|
||||||
|
: normalizeAmountFen(payload.requiredDepositAmount, workOrder.required_deposit_amount)
|
||||||
|
const depositThresholdAmount =
|
||||||
|
payload.depositThresholdAmount === undefined
|
||||||
|
? workOrder.deposit_threshold_amount
|
||||||
|
: normalizeAmountFen(payload.depositThresholdAmount, workOrder.deposit_threshold_amount)
|
||||||
|
const categoryId =
|
||||||
|
payload.categoryId === undefined || payload.categoryId === null || payload.categoryId === ''
|
||||||
|
? workOrder.category_id
|
||||||
|
: (normalizeOptionalId(payload.categoryId) ?? workOrder.category_id)
|
||||||
|
|
||||||
|
const now = nowIso()
|
||||||
|
const updated = await updateWorkOrderBasic(workOrder.id, {
|
||||||
|
productName: String(payload.productName ?? workOrder.product_name).trim(),
|
||||||
|
platformOrderId: String(payload.platformOrderId ?? workOrder.platform_order_id ?? '').trim(),
|
||||||
|
categoryId,
|
||||||
|
rewardAmount,
|
||||||
|
requiredDepositAmount,
|
||||||
|
depositThresholdAmount,
|
||||||
|
timeoutMinutes:
|
||||||
|
payload.timeoutMinutes === undefined
|
||||||
|
? workOrder.timeout_minutes
|
||||||
|
: Math.max(0, normalizeInteger(payload.timeoutMinutes, workOrder.timeout_minutes)),
|
||||||
|
timeoutPolicy: normalizeWorkOrderTimeoutPolicy(
|
||||||
|
payload.timeoutPolicy ?? workOrder.timeout_policy,
|
||||||
|
),
|
||||||
|
requirementJson: JSON.stringify({ fields }),
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: options.actorName || '',
|
||||||
|
eventType: 'edited',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
productName: updated?.product_name,
|
||||||
|
rewardAmount,
|
||||||
|
requiredDepositAmount,
|
||||||
|
timeoutMinutes: updated?.timeout_minutes,
|
||||||
|
timeoutPolicy: updated?.timeout_policy,
|
||||||
|
fields,
|
||||||
|
}),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
hallChanged: workOrder.status === WORK_ORDER_STATUS.OPEN,
|
||||||
|
})
|
||||||
|
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAdminWorkOrder(workOrderId: number | string) {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (!mapWorkOrderAdmin(workOrder).capabilities.canDelete) {
|
||||||
|
throw createHttpError('只有待完善、未分配或已取消的工单可以删除', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_delete_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (workOrder.assigned_worker_id) {
|
||||||
|
throw createHttpError('工单已被打手接走,不能删除', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_delete_assigned_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const shares = await listWorkOrderShares(workOrder.id)
|
||||||
|
const activeShares = shares.filter((share) => share.status !== 'cancelled')
|
||||||
|
if (activeShares.length > 0) {
|
||||||
|
throw createHttpError('工单存在参与中的拼单,不能删除', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_delete_has_shares',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const deleted = await deleteWorkOrder(workOrder.id)
|
||||||
|
if (!deleted) {
|
||||||
|
throw createHttpError('工单删除失败', {
|
||||||
|
statusCode: 500,
|
||||||
|
errorCode: 'work_order_delete_failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
hallChanged: workOrder.status === WORK_ORDER_STATUS.OPEN,
|
||||||
|
})
|
||||||
|
return { deleted: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reopenAdminCancelledWorkOrder(workOrderId: number | string, actorName = '') {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (!mapWorkOrderAdmin(workOrder).capabilities.canReopen) {
|
||||||
|
throw createHttpError('只有没有有效拼单份额的已取消订单可以重新启用', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_reopen_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { order: updated, failureReason } = await reopenCancelledWorkOrder({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
now: nowIso(),
|
||||||
|
actorName,
|
||||||
|
})
|
||||||
|
if (failureReason === 'work_order_has_active_shares') {
|
||||||
|
throw createHttpError('该订单仍有有效拼单份额,不能重新启用', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_reopen_has_active_shares',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError('订单状态已变化,无法重新启用', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_reopen_conflict',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
||||||
|
return { order: mapWorkOrderAdmin(updated) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function publishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (workOrder.status !== WORK_ORDER_STATUS.UNASSIGNED) {
|
||||||
|
throw createHttpError('只有未分配订单可以发布到大厅', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_publish_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (Number(workOrder.reward_amount || 0) <= 0) {
|
||||||
|
throw createHttpError('接单金额为 0,无法发布到大厅', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_publish_reward_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const voucherConsume = await consumeIndustryVouchersBeforeWorkOrderPublish(workOrder)
|
||||||
|
if (!voucherConsume.ok) {
|
||||||
|
const failedAt = nowIso()
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: actorName,
|
||||||
|
eventType: 'publish_voucher_consume_failed',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify(voucherConsume),
|
||||||
|
now: failedAt,
|
||||||
|
})
|
||||||
|
throw createHttpError(voucherConsume.errorMessage || '电子凭证核销失败,订单未发布', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_publish_voucher_consume_failed',
|
||||||
|
context: voucherConsume,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = nowIso()
|
||||||
|
const updated = await updateWorkOrder(workOrder.id, {
|
||||||
|
status: WORK_ORDER_STATUS.OPEN,
|
||||||
|
published_at: now,
|
||||||
|
hall_queued_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError(
|
||||||
|
voucherConsume.voucherCount > 0
|
||||||
|
? '电子凭证已核销,但订单发布失败,请重试发布'
|
||||||
|
: '订单发布失败,请重试',
|
||||||
|
{
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_publish_update_failed',
|
||||||
|
context: voucherConsume,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: actorName,
|
||||||
|
eventType: 'published',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: WORK_ORDER_STATUS.OPEN,
|
||||||
|
payloadJson: JSON.stringify({ voucherConsume }),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
||||||
|
const hallConfig = getWorkerHallConfig()
|
||||||
|
const hallEntry = await listWorkOrders({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
statuses: [WORK_ORDER_STATUS.OPEN],
|
||||||
|
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||||
|
excludeFilledSharing: true,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderAdmin(updated),
|
||||||
|
voucherConsume,
|
||||||
|
hallVisible: hallEntry.total > 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unpublishAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (!mapWorkOrderAdmin(workOrder).capabilities.canUnpublish) {
|
||||||
|
throw createHttpError('只有待抢单且未被接走的订单可以下架', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_unpublish_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const updated = await updateWorkOrder(workOrder.id, {
|
||||||
|
status: WORK_ORDER_STATUS.UNASSIGNED,
|
||||||
|
published_at: null,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: actorName,
|
||||||
|
eventType: 'unpublished',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: WORK_ORDER_STATUS.UNASSIGNED,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
||||||
|
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pinAdminWorkOrder(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
actorName = '',
|
||||||
|
) {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
const pinned = payload.pinned === true
|
||||||
|
const capabilities = mapWorkOrderAdmin(workOrder).capabilities
|
||||||
|
if (pinned && (!capabilities.canPin || !mapWorkOrderAdmin(workOrder).hallAvailable)) {
|
||||||
|
throw createHttpError('只有待抢单订单可以置顶', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_pin_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const updated = await updateWorkOrder(workOrder.id, {
|
||||||
|
pinned_at: pinned ? now : null,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: actorName,
|
||||||
|
eventType: pinned ? 'pinned' : 'unpinned',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id), hallChanged: true })
|
||||||
|
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function markAdminWorkOrderProblem(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
actorName = '',
|
||||||
|
) {
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (!mapWorkOrderAdmin(workOrder).capabilities.canMarkProblem) {
|
||||||
|
throw createHttpError('当前状态不能标记问题单', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_problem_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const note = String(payload.note || payload.problemNote || '').trim()
|
||||||
|
const updated = await updateWorkOrder(workOrder.id, {
|
||||||
|
status: WORK_ORDER_STATUS.PROBLEM,
|
||||||
|
problem_note: note,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: actorName,
|
||||||
|
eventType: 'marked_problem',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: WORK_ORDER_STATUS.PROBLEM,
|
||||||
|
payloadJson: JSON.stringify({ note }),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (workOrder.status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
|
||||||
|
await resolveAdminNotificationEntity('work_order', Number(workOrder.id), 'marked_problem')
|
||||||
|
}
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerIds: await resolveWorkOrderRealtimeWorkerIds(workOrder),
|
||||||
|
})
|
||||||
|
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveAdminProblemWorkOrder(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
actorName = '',
|
||||||
|
) {
|
||||||
|
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(),
|
||||||
|
actorName,
|
||||||
|
})
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError('问题单处置失败,订单状态可能已变化', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_problem_resolution_conflict',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerIds: await resolveWorkOrderRealtimeWorkerIds(workOrder),
|
||||||
|
})
|
||||||
|
return { order: mapWorkOrderAdmin(updated) }
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ export * from './admin-work-order-sharing-service.js'
|
|||||||
export * from './admin-work-order-realtime-service.js'
|
export * from './admin-work-order-realtime-service.js'
|
||||||
export * from './admin-work-order-material-service.js'
|
export * from './admin-work-order-material-service.js'
|
||||||
export * from './admin-mock-work-order-service.js'
|
export * from './admin-mock-work-order-service.js'
|
||||||
|
export * from './admin-work-order-management-service.js'
|
||||||
export * from './worker-product-match-config-service.js'
|
export * from './worker-product-match-config-service.js'
|
||||||
export * from './worker-announcement-config-service.js'
|
export * from './worker-announcement-config-service.js'
|
||||||
export * from './worker-hall-config-service.js'
|
export * from './worker-hall-config-service.js'
|
||||||
|
|||||||
Reference in New Issue
Block a user