收敛后台工单服务入口
This commit is contained in:
@@ -1,90 +1,21 @@
|
|||||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
/** 后台接单平台服务兼容入口,保留旧模块导入路径。 */
|
||||||
import {
|
|
||||||
acceptWorkOrderAndSettle,
|
|
||||||
acceptWorkOrderShareAndSettle,
|
|
||||||
cancelAssignedWorkOrder,
|
|
||||||
cancelWorkOrderShare,
|
|
||||||
returnAssignedWorkOrderToHall,
|
|
||||||
unassignWorkOrder,
|
|
||||||
countWorkOrderPendingSharingSubmissions,
|
|
||||||
createWorkProductMatchLog,
|
|
||||||
createWorkOrder,
|
|
||||||
createWorkOrderEvent,
|
|
||||||
getWorkOrderByOrderItemId,
|
|
||||||
listWorkOrders,
|
|
||||||
listWorkProductRules,
|
|
||||||
listWorkProductRuleMappings,
|
|
||||||
updateWorkOrder,
|
|
||||||
type WorkOrderRow,
|
|
||||||
} from '../../repositories/worker-platform/index.js'
|
|
||||||
import type { JsonObject } from '../../types/json.js'
|
|
||||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
|
||||||
import { createHttpError } from '../../utils/http.js'
|
|
||||||
import { randomId } from '../../utils/random.js'
|
|
||||||
import { nowIso } from '../../utils/time.js'
|
|
||||||
import { safeParseJson } from '../admin/admin-query-utils.js'
|
|
||||||
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
|
||||||
import { getWorkerProductMatchConfig } from './worker-product-match-config-service.js'
|
|
||||||
import {
|
|
||||||
createWorkOrderMaterialAdminNotification,
|
|
||||||
resolveAdminNotificationEntity,
|
|
||||||
} from '../admin/admin-notification-service.js'
|
|
||||||
import {
|
|
||||||
publishWorkerWalletRealtimeChange,
|
|
||||||
publishWorkOrderRealtimeChange,
|
|
||||||
} from '../realtime/realtime-event-service.js'
|
|
||||||
|
|
||||||
import {
|
|
||||||
mapWorkOrderAdmin,
|
|
||||||
mapWorkOrderShare,
|
|
||||||
normalizeAmountFen,
|
|
||||||
normalizeInteger,
|
|
||||||
normalizeOptionalId,
|
|
||||||
normalizeRequirementFields,
|
|
||||||
normalizePositiveInteger,
|
|
||||||
resolveMatchingProductRuleDecision,
|
|
||||||
resolveKuaishouWorkProductMatchContext,
|
|
||||||
resolveSkuNameQuantity,
|
|
||||||
resolveWorkOrderRulePricing,
|
|
||||||
resolveWorkOrderSharingEnabled,
|
|
||||||
resolveWorkOrderOperationalStatus,
|
|
||||||
} from './mappers.js'
|
|
||||||
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
|
||||||
import { getRequiredWorkOrder, getRequiredWorker } from './worker-session-context-service.js'
|
|
||||||
import { resolveWorkOrderRealtimeWorkerIds } from './admin-work-order-realtime-service.js'
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
listAdminWorkerUsers,
|
acceptAdminWorkOrder,
|
||||||
resetAdminWorkerPassword,
|
acceptAdminWorkOrderShare,
|
||||||
reviewAdminWorkerUser,
|
acceptAdminWorkOrders,
|
||||||
} from './admin-worker-user-service.js'
|
cancelAdminWorkOrderShare,
|
||||||
|
} from './admin-work-order-acceptance-service.js'
|
||||||
export { assignAdminWorkOrderToWorker } from './admin-work-order-assignment-service.js'
|
export { assignAdminWorkOrderToWorker } from './admin-work-order-assignment-service.js'
|
||||||
|
|
||||||
export { getAdminWorkOrderEvents, listAdminWorkOrders } from './admin-work-order-query-service.js'
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
listAdminWorkerCancelRequests,
|
cancelAdminWorkOrder,
|
||||||
reviewAdminWorkerCancelRequest,
|
unassignAdminWorkOrder,
|
||||||
} from './admin-worker-cancel-service.js'
|
} from './admin-work-order-cancellation-service.js'
|
||||||
|
|
||||||
export { deductAdminWorkOrderPendingDeposit } from './admin-work-order-deposit-service.js'
|
export { deductAdminWorkOrderPendingDeposit } from './admin-work-order-deposit-service.js'
|
||||||
|
export { getAdminWorkOrderEvents, listAdminWorkOrders } from './admin-work-order-query-service.js'
|
||||||
export {
|
export {
|
||||||
getAdminWorkOrderSharing,
|
getAdminWorkOrderSharing,
|
||||||
updateAdminWorkOrderSharing,
|
updateAdminWorkOrderSharing,
|
||||||
} from './admin-work-order-sharing-service.js'
|
} from './admin-work-order-sharing-service.js'
|
||||||
|
|
||||||
export {
|
|
||||||
resolveAdminMaterialRewardAmount,
|
|
||||||
submitAdminWorkOrderMaterial,
|
|
||||||
} from './admin-work-order-material-service.js'
|
|
||||||
|
|
||||||
export {
|
|
||||||
createAdminMockWorkOrder,
|
|
||||||
resolveAdminMockWorkOrderInput,
|
|
||||||
} from './admin-mock-work-order-service.js'
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
deleteAdminWorkOrder,
|
deleteAdminWorkOrder,
|
||||||
markAdminWorkOrderProblem,
|
markAdminWorkOrderProblem,
|
||||||
@@ -95,448 +26,26 @@ export {
|
|||||||
unpublishAdminWorkOrder,
|
unpublishAdminWorkOrder,
|
||||||
updateAdminWorkOrder,
|
updateAdminWorkOrder,
|
||||||
} from './admin-work-order-management-service.js'
|
} from './admin-work-order-management-service.js'
|
||||||
|
export {
|
||||||
export async function acceptAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
createAdminMockWorkOrder,
|
||||||
let workOrder = await getRequiredWorkOrder(workOrderId)
|
resolveAdminMockWorkOrderInput,
|
||||||
if (!mapWorkOrderAdmin(workOrder).capabilities.canAccept) {
|
} from './admin-mock-work-order-service.js'
|
||||||
throw createHttpError('只有待验收订单可以通过验收', {
|
export {
|
||||||
statusCode: 409,
|
resolveAdminMaterialRewardAmount,
|
||||||
errorCode: 'work_order_accept_status_invalid',
|
submitAdminWorkOrderMaterial,
|
||||||
})
|
} from './admin-work-order-material-service.js'
|
||||||
}
|
|
||||||
// 历史拼单订单可能仍保留 open,验收前先补齐母工单生命周期状态。
|
|
||||||
if (workOrder.status === WORK_ORDER_STATUS.OPEN) {
|
|
||||||
const promoted = await updateWorkOrder(workOrder.id, {
|
|
||||||
status: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
|
||||||
updated_at: nowIso(),
|
|
||||||
})
|
|
||||||
if (promoted) workOrder = promoted
|
|
||||||
}
|
|
||||||
const now = nowIso()
|
|
||||||
const { order: updated, failureReason } = await acceptWorkOrderAndSettle({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
|
||||||
now,
|
|
||||||
actorType: 'admin',
|
|
||||||
actorName,
|
|
||||||
})
|
|
||||||
if (failureReason === 'sharing_pending_submissions') {
|
|
||||||
throw createHttpError('还有拼单打手未提交验收资料,暂不能统一验收', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_pending_submissions',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!updated) {
|
|
||||||
throw createHttpError('验收失败,订单状态可能已变化', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_accept_conflict',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await resolveAdminNotificationEntity('work_order', Number(workOrder.id), 'accepted')
|
|
||||||
const workerIds = await resolveWorkOrderRealtimeWorkerIds(workOrder)
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerIds,
|
|
||||||
})
|
|
||||||
for (const workerId of workerIds) {
|
|
||||||
publishWorkerWalletRealtimeChange(workerId)
|
|
||||||
}
|
|
||||||
return { order: mapWorkOrderAdmin(updated || workOrder) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function acceptAdminWorkOrderShare(
|
|
||||||
workOrderId: number | string,
|
|
||||||
shareId: number | string,
|
|
||||||
actorName = '',
|
|
||||||
) {
|
|
||||||
const result = await acceptWorkOrderShareAndSettle({
|
|
||||||
workOrderId: Number(workOrderId),
|
|
||||||
shareId: Number(shareId),
|
|
||||||
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
|
||||||
now: nowIso(),
|
|
||||||
actorType: 'admin',
|
|
||||||
actorName,
|
|
||||||
})
|
|
||||||
if (result.failureReason === 'work_order_not_accepting') {
|
|
||||||
throw createHttpError('当前母工单不能验收拼单份额', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_accept_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (result.failureReason === 'share_not_submitted' || !result.share || !result.order) {
|
|
||||||
throw createHttpError('该拼单参与者尚未提交验收资料或已被验收', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_share_accept_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const workerId = Number(result.share.worker_id || 0)
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrderId),
|
|
||||||
workerIds: workerId > 0 ? [workerId] : [],
|
|
||||||
})
|
|
||||||
if (workerId > 0) publishWorkerWalletRealtimeChange(workerId)
|
|
||||||
if (result.order.status === WORK_ORDER_STATUS.ACCEPTED) {
|
|
||||||
await resolveAdminNotificationEntity('work_order', Number(workOrderId), 'accepted')
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderAdmin(result.order),
|
|
||||||
share: mapWorkOrderShare(result.share),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cancelAdminWorkOrderShare(
|
|
||||||
workOrderId: number | string,
|
|
||||||
shareId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
actorName = '',
|
|
||||||
) {
|
|
||||||
const result = await cancelWorkOrderShare({
|
|
||||||
workOrderId: Number(workOrderId),
|
|
||||||
shareId: Number(shareId),
|
|
||||||
now: nowIso(),
|
|
||||||
reason: String(payload.reason || '').trim(),
|
|
||||||
actorName,
|
|
||||||
})
|
|
||||||
if (result.failureReason === 'work_order_not_cancellable') {
|
|
||||||
throw createHttpError('当前拼单状态不能撤销参与者', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_cancel_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (result.failureReason === 'share_not_found') {
|
|
||||||
throw createHttpError('未找到该拼单参与记录', {
|
|
||||||
statusCode: 404,
|
|
||||||
errorCode: 'work_order_sharing_share_not_found',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (result.failureReason === 'share_status_not_cancellable' || !result.share || !result.order) {
|
|
||||||
throw createHttpError('只有已参与或已提交验收的拼单份额可以撤单', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_share_cancel_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const workerId = Number(result.share.worker_id || 0)
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrderId),
|
|
||||||
workerIds: await resolveWorkOrderRealtimeWorkerIds(result.order),
|
|
||||||
hallChanged: true,
|
|
||||||
})
|
|
||||||
if (workerId > 0) publishWorkerWalletRealtimeChange(workerId)
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderAdmin(result.order),
|
|
||||||
share: mapWorkOrderShare(result.share),
|
|
||||||
releasedDepositAmount: result.releasedDepositAmount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function acceptAdminWorkOrders(payload: JsonObject = {}, actorName = '') {
|
|
||||||
const workOrderIds = [
|
|
||||||
...new Set(
|
|
||||||
(Array.isArray(payload.workOrderIds) ? payload.workOrderIds : [])
|
|
||||||
.map((item) => normalizeOptionalId(item))
|
|
||||||
.filter((item): item is number => item !== null),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
if (workOrderIds.length === 0) {
|
|
||||||
throw createHttpError('请至少选择一个待验收订单', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'work_order_batch_accept_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const workOrders = await Promise.all(
|
|
||||||
workOrderIds.map((workOrderId) => getRequiredWorkOrder(workOrderId)),
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
workOrders.some(
|
|
||||||
(workOrder) =>
|
|
||||||
resolveWorkOrderOperationalStatus(workOrder) !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw createHttpError('选中的订单包含非待验收状态,请刷新后重试', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_batch_accept_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingSharingCounts = await Promise.all(
|
|
||||||
workOrders.map((workOrder) => countWorkOrderPendingSharingSubmissions(workOrder.id)),
|
|
||||||
)
|
|
||||||
if (pendingSharingCounts.some((count) => count > 0)) {
|
|
||||||
throw createHttpError('选中的订单存在拼单打手未提交验收资料,暂不能统一验收', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_batch_accept_sharing_pending_submissions',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = []
|
|
||||||
for (const workOrder of workOrders) {
|
|
||||||
results.push(await acceptAdminWorkOrder(workOrder.id, actorName))
|
|
||||||
}
|
|
||||||
return { orders: results.map((result) => result.order) }
|
|
||||||
}
|
|
||||||
|
|
||||||
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('只有进行中且已指派打手的订单可以取消指派', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_unassign_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const now = nowIso()
|
|
||||||
const { order: updated, failureReason } = await unassignWorkOrder({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
now,
|
|
||||||
actorName,
|
|
||||||
})
|
|
||||||
if (failureReason === 'work_order_not_in_progress') {
|
|
||||||
throw createHttpError('订单状态已变化,无法取消指派', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_unassign_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (failureReason === 'work_order_no_worker') {
|
|
||||||
throw createHttpError('该订单未指派打手', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_unassign_no_worker',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!updated) {
|
|
||||||
throw createHttpError('取消指派失败,订单状态可能已变化', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_unassign_conflict',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerIds: [Number(workOrder.assigned_worker_id)],
|
|
||||||
})
|
|
||||||
publishWorkerWalletRealtimeChange(Number(workOrder.assigned_worker_id))
|
|
||||||
return { order: mapWorkOrderAdmin(updated) }
|
|
||||||
}
|
|
||||||
|
|
||||||
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('只有代练中且已接单的订单可以撤单', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_cancel_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const action = String(payload.action || '').trim()
|
|
||||||
if (!['return_to_hall', 'cancel_order'].includes(action)) {
|
|
||||||
throw createHttpError('请选择撤单处理方式', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'work_order_cancel_action_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const reason = String(payload.reason || '').trim()
|
|
||||||
if (action === 'cancel_order' && !reason) {
|
|
||||||
throw createHttpError('取消订单时请填写退款或撤单原因', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'work_order_cancel_reason_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const input = {
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
now: nowIso(),
|
|
||||||
reason,
|
|
||||||
actorName,
|
|
||||||
}
|
|
||||||
const { order: updated, failureReason } =
|
|
||||||
action === 'return_to_hall'
|
|
||||||
? await returnAssignedWorkOrderToHall(input)
|
|
||||||
: await cancelAssignedWorkOrder(input)
|
|
||||||
if (failureReason === 'work_order_not_in_progress') {
|
|
||||||
throw createHttpError('订单状态已变化,无法撤单', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_cancel_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (failureReason === 'work_order_no_worker') {
|
|
||||||
throw createHttpError('该订单尚未被打手接取', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_cancel_no_worker',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!updated) {
|
|
||||||
throw createHttpError('撤单失败,订单状态可能已变化', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_cancel_conflict',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const workerId = Number(workOrder.assigned_worker_id)
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerIds: workerId ? [workerId] : [],
|
|
||||||
hallChanged: action === 'return_to_hall',
|
|
||||||
})
|
|
||||||
if (workerId) {
|
|
||||||
publishWorkerWalletRealtimeChange(workerId)
|
|
||||||
}
|
|
||||||
return { order: mapWorkOrderAdmin(updated), action }
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getAdminWorkerHallConfig,
|
getAdminWorkerHallConfig,
|
||||||
getAdminWorkerPlatformSummary,
|
getAdminWorkerPlatformSummary,
|
||||||
saveAdminWorkerHallConfig,
|
saveAdminWorkerHallConfig,
|
||||||
} from './admin-worker-platform-summary-service.js'
|
} from './admin-worker-platform-summary-service.js'
|
||||||
|
export {
|
||||||
export async function syncWorkerOrdersForSourceOrder(
|
listAdminWorkerCancelRequests,
|
||||||
order: OrderRow,
|
reviewAdminWorkerCancelRequest,
|
||||||
orderItems: OrderItemRow[],
|
} from './admin-worker-cancel-service.js'
|
||||||
options: { source?: string; autoOnly?: boolean; sourceMetadata?: JsonObject } = {},
|
export {
|
||||||
) {
|
listAdminWorkerUsers,
|
||||||
const [rules, mappings] = await Promise.all([
|
resetAdminWorkerPassword,
|
||||||
listWorkProductRules({ enabled: true }),
|
reviewAdminWorkerUser,
|
||||||
listWorkProductRuleMappings({ enabled: true }),
|
} from './admin-worker-user-service.js'
|
||||||
])
|
export { syncWorkerOrdersForSourceOrder } from './sync-worker-orders-service.js'
|
||||||
const productMatchConfig = getWorkerProductMatchConfig()
|
|
||||||
const created: WorkOrderRow[] = []
|
|
||||||
const skipped: Array<{ orderItemId: number; reason: string }> = []
|
|
||||||
|
|
||||||
for (const item of orderItems) {
|
|
||||||
const match = resolveMatchingProductRuleDecision(order, item, rules, mappings, {
|
|
||||||
legacyFallbackEnabled: productMatchConfig.legacyFallbackEnabled,
|
|
||||||
})
|
|
||||||
const matchContext = resolveKuaishouWorkProductMatchContext(item)
|
|
||||||
if (matchContext.sellerId) {
|
|
||||||
const snapshot = safeParseJson(item.item_snapshot_json)
|
|
||||||
await createWorkProductMatchLog({
|
|
||||||
orderId: Number(order.id),
|
|
||||||
orderItemId: Number(item.id),
|
|
||||||
source: options.source || 'source_order',
|
|
||||||
sellerId: matchContext.sellerId,
|
|
||||||
relItemId: matchContext.relItemId,
|
|
||||||
itemTitle: matchContext.itemTitle,
|
|
||||||
relSkuId: matchContext.relSkuId,
|
|
||||||
skuNick: matchContext.skuNick,
|
|
||||||
matchStatus: match.reason,
|
|
||||||
ruleId: match.rule?.id || null,
|
|
||||||
mappingId: match.mappingId,
|
|
||||||
candidatesJson: JSON.stringify(match.candidates),
|
|
||||||
rawPayloadJson: JSON.stringify(
|
|
||||||
options.sourceMetadata?.kuaishouSendCodeRawPayload ||
|
|
||||||
snapshot.kuaishouSendCodeRawPayload ||
|
|
||||||
snapshot.kuaishouSendCode ||
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
now: nowIso(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!match.rule) {
|
|
||||||
skipped.push({
|
|
||||||
orderItemId: Number(item.id),
|
|
||||||
reason: match.reason === 'ambiguous' ? 'rule_match_ambiguous' : 'rule_not_matched',
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const rule = match.rule
|
|
||||||
if (options.autoOnly && !rule.auto_create) {
|
|
||||||
skipped.push({ orderItemId: Number(item.id), reason: 'auto_create_disabled' })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (await getWorkOrderByOrderItemId(item.id)) {
|
|
||||||
skipped.push({ orderItemId: Number(item.id), reason: 'already_exists' })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = normalizeRequirementFields(safeParseJson(rule.requirement_json).fields)
|
|
||||||
const skuQuantity = resolveSkuNameQuantity(item.sku_name)
|
|
||||||
const unitPriceFen = Number(rule.unit_price_fen || 0)
|
|
||||||
// 按数量计价:单价 × SKU 名称文字中的规格数量(如 指挥官秘钥15个);未启用则用固定接单金额
|
|
||||||
const { sharingTotalQuantity, sharingUnitReward, rewardAmount } = resolveWorkOrderRulePricing({
|
|
||||||
skuQuantity,
|
|
||||||
unitPriceFen,
|
|
||||||
fixedRewardAmount: Number(rule.reward_amount || 0),
|
|
||||||
sharingEnabled: rule.sharing_enabled === true,
|
|
||||||
sharingAutoFromOrder: rule.sharing_auto_from_order === true,
|
|
||||||
sharingTotalQuantity: Number(rule.sharing_total_quantity || 1),
|
|
||||||
sharingUnitReward: Number(rule.sharing_unit_reward || 0),
|
|
||||||
})
|
|
||||||
const sharingEnabled = resolveWorkOrderSharingEnabled(
|
|
||||||
rule.sharing_enabled === true,
|
|
||||||
sharingTotalQuantity,
|
|
||||||
)
|
|
||||||
const now = nowIso()
|
|
||||||
const materialComplete = fields.every((field) => !field.required)
|
|
||||||
const workOrder = await createWorkOrder({
|
|
||||||
workOrderNo: randomId('WO'),
|
|
||||||
productRuleId: Number(rule.id),
|
|
||||||
orderId: Number(order.id),
|
|
||||||
orderItemId: Number(item.id),
|
|
||||||
taskId: null,
|
|
||||||
platformOrderId: order.platform_order_id,
|
|
||||||
productName: item.sku_name || item.sku_code,
|
|
||||||
categoryId: rule.category_id,
|
|
||||||
status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL,
|
|
||||||
rewardAmount,
|
|
||||||
requiredDepositAmount: 0,
|
|
||||||
depositThresholdAmount: 0,
|
|
||||||
sharingEnabled,
|
|
||||||
sharingTotalQuantity,
|
|
||||||
sharingUnitReward,
|
|
||||||
timeoutMinutes: Number(rule.timeout_minutes || 0),
|
|
||||||
timeoutPolicy: String(rule.timeout_policy || 'reopen').trim(),
|
|
||||||
materialJson: JSON.stringify({
|
|
||||||
source: {
|
|
||||||
orderId: Number(order.id),
|
|
||||||
orderItemId: Number(item.id),
|
|
||||||
provider: order.provider,
|
|
||||||
platform: order.platform,
|
|
||||||
shopId: order.shop_id,
|
|
||||||
shopName: order.shop_name,
|
|
||||||
skuCode: item.sku_code,
|
|
||||||
skuName: item.sku_name,
|
|
||||||
skuQuantity,
|
|
||||||
quantity: Number(item.quantity || 1),
|
|
||||||
orderAmountFen: Number(order.total_amount || 0),
|
|
||||||
currency: order.currency || 'CNY',
|
|
||||||
syncSource: options.source || 'source_order',
|
|
||||||
...options.sourceMetadata,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
requirementJson: JSON.stringify({ fields }),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (workOrder) {
|
|
||||||
created.push(workOrder)
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'system',
|
|
||||||
actorId: options.source || 'source_order',
|
|
||||||
eventType: 'source_synced',
|
|
||||||
fromStatus: '',
|
|
||||||
toStatus: workOrder.status,
|
|
||||||
payloadJson: JSON.stringify({
|
|
||||||
ruleKey: rule.rule_key,
|
|
||||||
orderId: order.id,
|
|
||||||
orderItemId: item.id,
|
|
||||||
}),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
if (workOrder.status === WORK_ORDER_STATUS.PENDING_MATERIAL) {
|
|
||||||
await createWorkOrderMaterialAdminNotification({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workOrderNo: workOrder.work_order_no,
|
|
||||||
productName: workOrder.product_name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
created: created.map((workOrder) => mapWorkOrderAdmin(workOrder)),
|
|
||||||
skipped,
|
|
||||||
createdCount: created.length,
|
|
||||||
skippedCount: skipped.length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -49,3 +49,4 @@ export {
|
|||||||
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'
|
||||||
|
export * from './sync-worker-orders-service.js'
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||||
|
import {
|
||||||
|
createWorkProductMatchLog,
|
||||||
|
createWorkOrder,
|
||||||
|
createWorkOrderEvent,
|
||||||
|
getWorkOrderByOrderItemId,
|
||||||
|
listWorkProductRuleMappings,
|
||||||
|
listWorkProductRules,
|
||||||
|
type WorkOrderRow,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||||
|
import { randomId } from '../../utils/random.js'
|
||||||
|
import { nowIso } from '../../utils/time.js'
|
||||||
|
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||||
|
import { createWorkOrderMaterialAdminNotification } from '../admin/admin-notification-service.js'
|
||||||
|
import {
|
||||||
|
mapWorkOrderAdmin,
|
||||||
|
normalizeRequirementFields,
|
||||||
|
resolveKuaishouWorkProductMatchContext,
|
||||||
|
resolveMatchingProductRuleDecision,
|
||||||
|
resolveSkuNameQuantity,
|
||||||
|
resolveWorkOrderRulePricing,
|
||||||
|
resolveWorkOrderSharingEnabled,
|
||||||
|
} from './mappers.js'
|
||||||
|
import { getWorkerProductMatchConfig } from './worker-product-match-config-service.js'
|
||||||
|
|
||||||
|
export async function syncWorkerOrdersForSourceOrder(
|
||||||
|
order: OrderRow,
|
||||||
|
orderItems: OrderItemRow[],
|
||||||
|
options: { source?: string; autoOnly?: boolean; sourceMetadata?: JsonObject } = {},
|
||||||
|
) {
|
||||||
|
const [rules, mappings] = await Promise.all([
|
||||||
|
listWorkProductRules({ enabled: true }),
|
||||||
|
listWorkProductRuleMappings({ enabled: true }),
|
||||||
|
])
|
||||||
|
const productMatchConfig = getWorkerProductMatchConfig()
|
||||||
|
const created: WorkOrderRow[] = []
|
||||||
|
const skipped: Array<{ orderItemId: number; reason: string }> = []
|
||||||
|
|
||||||
|
for (const item of orderItems) {
|
||||||
|
const match = resolveMatchingProductRuleDecision(order, item, rules, mappings, {
|
||||||
|
legacyFallbackEnabled: productMatchConfig.legacyFallbackEnabled,
|
||||||
|
})
|
||||||
|
const matchContext = resolveKuaishouWorkProductMatchContext(item)
|
||||||
|
if (matchContext.sellerId) {
|
||||||
|
const snapshot = safeParseJson(item.item_snapshot_json)
|
||||||
|
await createWorkProductMatchLog({
|
||||||
|
orderId: Number(order.id),
|
||||||
|
orderItemId: Number(item.id),
|
||||||
|
source: options.source || 'source_order',
|
||||||
|
sellerId: matchContext.sellerId,
|
||||||
|
relItemId: matchContext.relItemId,
|
||||||
|
itemTitle: matchContext.itemTitle,
|
||||||
|
relSkuId: matchContext.relSkuId,
|
||||||
|
skuNick: matchContext.skuNick,
|
||||||
|
matchStatus: match.reason,
|
||||||
|
ruleId: match.rule?.id || null,
|
||||||
|
mappingId: match.mappingId,
|
||||||
|
candidatesJson: JSON.stringify(match.candidates),
|
||||||
|
rawPayloadJson: JSON.stringify(
|
||||||
|
options.sourceMetadata?.kuaishouSendCodeRawPayload ||
|
||||||
|
snapshot.kuaishouSendCodeRawPayload ||
|
||||||
|
snapshot.kuaishouSendCode ||
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!match.rule) {
|
||||||
|
skipped.push({
|
||||||
|
orderItemId: Number(item.id),
|
||||||
|
reason: match.reason === 'ambiguous' ? 'rule_match_ambiguous' : 'rule_not_matched',
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const rule = match.rule
|
||||||
|
if (options.autoOnly && !rule.auto_create) {
|
||||||
|
skipped.push({ orderItemId: Number(item.id), reason: 'auto_create_disabled' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (await getWorkOrderByOrderItemId(item.id)) {
|
||||||
|
skipped.push({ orderItemId: Number(item.id), reason: 'already_exists' })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = normalizeRequirementFields(safeParseJson(rule.requirement_json).fields)
|
||||||
|
const skuQuantity = resolveSkuNameQuantity(item.sku_name)
|
||||||
|
const { sharingTotalQuantity, sharingUnitReward, rewardAmount } = resolveWorkOrderRulePricing({
|
||||||
|
skuQuantity,
|
||||||
|
unitPriceFen: Number(rule.unit_price_fen || 0),
|
||||||
|
fixedRewardAmount: Number(rule.reward_amount || 0),
|
||||||
|
sharingEnabled: rule.sharing_enabled === true,
|
||||||
|
sharingAutoFromOrder: rule.sharing_auto_from_order === true,
|
||||||
|
sharingTotalQuantity: Number(rule.sharing_total_quantity || 1),
|
||||||
|
sharingUnitReward: Number(rule.sharing_unit_reward || 0),
|
||||||
|
})
|
||||||
|
const sharingEnabled = resolveWorkOrderSharingEnabled(
|
||||||
|
rule.sharing_enabled === true,
|
||||||
|
sharingTotalQuantity,
|
||||||
|
)
|
||||||
|
const now = nowIso()
|
||||||
|
const materialComplete = fields.every((field) => !field.required)
|
||||||
|
const workOrder = await createWorkOrder({
|
||||||
|
workOrderNo: randomId('WO'),
|
||||||
|
productRuleId: Number(rule.id),
|
||||||
|
orderId: Number(order.id),
|
||||||
|
orderItemId: Number(item.id),
|
||||||
|
taskId: null,
|
||||||
|
platformOrderId: order.platform_order_id,
|
||||||
|
productName: item.sku_name || item.sku_code,
|
||||||
|
categoryId: rule.category_id,
|
||||||
|
status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||||
|
rewardAmount,
|
||||||
|
requiredDepositAmount: 0,
|
||||||
|
depositThresholdAmount: 0,
|
||||||
|
sharingEnabled,
|
||||||
|
sharingTotalQuantity,
|
||||||
|
sharingUnitReward,
|
||||||
|
timeoutMinutes: Number(rule.timeout_minutes || 0),
|
||||||
|
timeoutPolicy: String(rule.timeout_policy || 'reopen').trim(),
|
||||||
|
materialJson: JSON.stringify({
|
||||||
|
source: {
|
||||||
|
orderId: Number(order.id),
|
||||||
|
orderItemId: Number(item.id),
|
||||||
|
provider: order.provider,
|
||||||
|
platform: order.platform,
|
||||||
|
shopId: order.shop_id,
|
||||||
|
shopName: order.shop_name,
|
||||||
|
skuCode: item.sku_code,
|
||||||
|
skuName: item.sku_name,
|
||||||
|
skuQuantity,
|
||||||
|
quantity: Number(item.quantity || 1),
|
||||||
|
orderAmountFen: Number(order.total_amount || 0),
|
||||||
|
currency: order.currency || 'CNY',
|
||||||
|
syncSource: options.source || 'source_order',
|
||||||
|
...options.sourceMetadata,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
requirementJson: JSON.stringify({ fields }),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (!workOrder) continue
|
||||||
|
|
||||||
|
created.push(workOrder)
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'system',
|
||||||
|
actorId: options.source || 'source_order',
|
||||||
|
eventType: 'source_synced',
|
||||||
|
fromStatus: '',
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
ruleKey: rule.rule_key,
|
||||||
|
orderId: order.id,
|
||||||
|
orderItemId: item.id,
|
||||||
|
}),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (workOrder.status === WORK_ORDER_STATUS.PENDING_MATERIAL) {
|
||||||
|
await createWorkOrderMaterialAdminNotification({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workOrderNo: workOrder.work_order_no,
|
||||||
|
productName: workOrder.product_name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
created: created.map((workOrder) => mapWorkOrderAdmin(workOrder)),
|
||||||
|
skipped,
|
||||||
|
createdCount: created.length,
|
||||||
|
skippedCount: skipped.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user