收敛后台工单服务入口
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 {
|
||||
listAdminWorkerUsers,
|
||||
resetAdminWorkerPassword,
|
||||
reviewAdminWorkerUser,
|
||||
} from './admin-worker-user-service.js'
|
||||
|
||||
acceptAdminWorkOrder,
|
||||
acceptAdminWorkOrderShare,
|
||||
acceptAdminWorkOrders,
|
||||
cancelAdminWorkOrderShare,
|
||||
} from './admin-work-order-acceptance-service.js'
|
||||
export { assignAdminWorkOrderToWorker } from './admin-work-order-assignment-service.js'
|
||||
|
||||
export { getAdminWorkOrderEvents, listAdminWorkOrders } from './admin-work-order-query-service.js'
|
||||
|
||||
export {
|
||||
listAdminWorkerCancelRequests,
|
||||
reviewAdminWorkerCancelRequest,
|
||||
} from './admin-worker-cancel-service.js'
|
||||
|
||||
cancelAdminWorkOrder,
|
||||
unassignAdminWorkOrder,
|
||||
} from './admin-work-order-cancellation-service.js'
|
||||
export { deductAdminWorkOrderPendingDeposit } from './admin-work-order-deposit-service.js'
|
||||
|
||||
export { getAdminWorkOrderEvents, listAdminWorkOrders } from './admin-work-order-query-service.js'
|
||||
export {
|
||||
getAdminWorkOrderSharing,
|
||||
updateAdminWorkOrderSharing,
|
||||
} 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 {
|
||||
deleteAdminWorkOrder,
|
||||
markAdminWorkOrderProblem,
|
||||
@@ -95,448 +26,26 @@ export {
|
||||
unpublishAdminWorkOrder,
|
||||
updateAdminWorkOrder,
|
||||
} from './admin-work-order-management-service.js'
|
||||
|
||||
export async function acceptAdminWorkOrder(workOrderId: number | string, actorName = '') {
|
||||
let workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (!mapWorkOrderAdmin(workOrder).capabilities.canAccept) {
|
||||
throw createHttpError('只有待验收订单可以通过验收', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_accept_status_invalid',
|
||||
})
|
||||
}
|
||||
// 历史拼单订单可能仍保留 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 {
|
||||
createAdminMockWorkOrder,
|
||||
resolveAdminMockWorkOrderInput,
|
||||
} from './admin-mock-work-order-service.js'
|
||||
export {
|
||||
resolveAdminMaterialRewardAmount,
|
||||
submitAdminWorkOrderMaterial,
|
||||
} from './admin-work-order-material-service.js'
|
||||
export {
|
||||
getAdminWorkerHallConfig,
|
||||
getAdminWorkerPlatformSummary,
|
||||
saveAdminWorkerHallConfig,
|
||||
} from './admin-worker-platform-summary-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 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,
|
||||
}
|
||||
}
|
||||
export {
|
||||
listAdminWorkerCancelRequests,
|
||||
reviewAdminWorkerCancelRequest,
|
||||
} from './admin-worker-cancel-service.js'
|
||||
export {
|
||||
listAdminWorkerUsers,
|
||||
resetAdminWorkerPassword,
|
||||
reviewAdminWorkerUser,
|
||||
} from './admin-worker-user-service.js'
|
||||
export { syncWorkerOrdersForSourceOrder } from './sync-worker-orders-service.js'
|
||||
|
||||
@@ -49,3 +49,4 @@ export {
|
||||
export * from './worker-product-match-config-service.js'
|
||||
export * from './worker-announcement-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