diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 2126cb0c..1e0f56c1 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -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' diff --git a/apps/backend/src/services/worker-platform/index.ts b/apps/backend/src/services/worker-platform/index.ts index 1aeb82ba..63a28920 100644 --- a/apps/backend/src/services/worker-platform/index.ts +++ b/apps/backend/src/services/worker-platform/index.ts @@ -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' diff --git a/apps/backend/src/services/worker-platform/sync-worker-orders-service.ts b/apps/backend/src/services/worker-platform/sync-worker-orders-service.ts new file mode 100644 index 00000000..16372578 --- /dev/null +++ b/apps/backend/src/services/worker-platform/sync-worker-orders-service.ts @@ -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, + } +}