import { getOrderById } from '../../../repositories/order-repo.js' import { listTasksByOrderId, updateTask } from '../../../repositories/task-repo.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js' import type { JsonObject } from '../../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByTaskId, } from '../../../repositories/kuaishou-industry-voucher-repo.js' import { resendKuaishouIndustryVoucherSendCallback } from '../../platforms/kuaishou-industry/send-code-service.js' import { consumeKuaishouIndustryVoucher } from '../../platforms/kuaishou-industry/voucher-service.js' import { attachKuaishouIndustryVoucherToTask, bindKuaishouIndustryVouchersToOrderTasks, } from '../../platforms/kuaishou-industry/voucher-binding-service.js' import { createHttpError } from '../../../utils/http.js' import { nowIso } from '../../../utils/time.js' import { TASK_STATUS } from '../../../domain/task-status.js' import { createAdminViewerContext, parseTaskContext } from '../admin-read-shared-helpers.js' import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js' import { dispatchFulfillmentTask, prepareFulfillmentBinding, rebindFulfillmentRole, refreshFulfillmentRole, returnFulfillmentNumber, } from '../../fulfillment/executors/registry.js' import { isKuaishouCloudTask, normalizeKuaishouCloudFlow, } from './kuaishou-cloud-helpers.js' import type { AdminEntityIdInput, AdminViewerSessionInput, } from '../../../types/admin/read-inputs.js' import type { AdminTaskKuaishouIndustryConsumeInput, } from '../../../types/admin/write-inputs.js' import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js' import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js' export async function prepareAdminTaskKuaishouCloudFulfillment( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getRequiredTask(taskId) const viewerContext = createAdminViewerContext(session) if (!viewerContext.canManageTaskLifecycle) { throw createHttpError('当前账号没有权限准备绑定资源', { statusCode: 403, errorCode: 'admin_task_prepare_kuaishou_cloud_forbidden', }) } if (!isKuaishouCloudTask(task)) { throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', { statusCode: 409, errorCode: 'admin_task_not_kuaishou_cloud', }) } const result = await prepareFulfillmentBinding(task, { source: 'admin_task_prepare', actor: buildAdminActionActor(session), }) if (!result?.task) { throw createHttpError('当前任务不支持准备绑定资源', { statusCode: 409, errorCode: 'admin_task_prepare_not_supported', }) } return { task: mapTaskActionPayload(result.task), claimUrl: String(result.claimUrl || ''), token: String(result.token || ''), } } export async function dispatchAdminTaskKuaishouCloudFulfillment( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getRequiredTask(taskId) const viewerContext = createAdminViewerContext(session) if (!viewerContext.canManageTaskLifecycle) { throw createHttpError('当前账号没有权限执行发货', { statusCode: 403, errorCode: 'admin_task_dispatch_kuaishou_cloud_forbidden', }) } if (!isKuaishouCloudTask(task)) { throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', { statusCode: 409, errorCode: 'admin_task_not_kuaishou_cloud', }) } const result = await dispatchFulfillmentTask(task, { source: 'admin_task_dispatch', actor: buildAdminActionActor(session), autoFinalize: false, }) if (!result?.task) { throw createHttpError('当前任务不支持 executor 发货', { statusCode: 409, errorCode: 'admin_task_dispatch_not_supported', }) } return { task: mapTaskActionPayload(result.task), } } export async function refreshAdminTaskKuaishouCloudRoleInfo( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getRequiredTask(taskId) const viewerContext = createAdminViewerContext(session) if (!viewerContext.canOperateAssistedTask) { throw createHttpError('当前账号没有权限刷新角色信息', { statusCode: 403, errorCode: 'admin_task_refresh_kuaishou_cloud_role_forbidden', }) } if (!isKuaishouCloudTask(task)) { throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', { statusCode: 409, errorCode: 'admin_task_not_kuaishou_cloud', }) } const result = await refreshFulfillmentRole(task, { source: 'admin_task_refresh_role', actor: buildAdminActionActor(session), forceProbe: true, recordEvent: true, }) if (!result?.task) { throw createHttpError('当前任务不支持刷新角色信息', { statusCode: 409, errorCode: 'admin_task_refresh_not_supported', }) } return { task: mapTaskActionPayload(result.task), } } export async function rebindAdminTaskKuaishouCloudRole( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getRequiredTask(taskId) const viewerContext = createAdminViewerContext(session) // 与刷新角色一致:客服可换绑协助处理 UID 绑错,不开放完整任务生命周期 if (!viewerContext.canOperateAssistedTask) { throw createHttpError('当前账号没有权限换绑角色', { statusCode: 403, errorCode: 'admin_task_rebind_kuaishou_cloud_forbidden', }) } const result = await rebindFulfillmentRole(task, { source: 'admin_task_rebind_role', actor: buildAdminActionActor(session), }) if (!result?.task) { throw createHttpError('当前任务不支持换绑角色', { statusCode: 409, errorCode: 'admin_task_rebind_not_supported', }) } return { task: mapTaskActionPayload(result.task), claimUrl: String(result.claimUrl || ''), token: String(result.token || ''), } } export async function returnNumberAdminTaskKuaishouCloudFulfillment( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getRequiredTask(taskId) const viewerContext = createAdminViewerContext(session) if (!viewerContext.canManageTaskLifecycle) { throw createHttpError('当前账号没有权限退还号码', { statusCode: 403, errorCode: 'admin_task_return_kuaishou_cloud_forbidden', }) } if (!isKuaishouCloudTask(task)) { throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', { statusCode: 409, errorCode: 'admin_task_not_kuaishou_cloud', }) } // admin 退号 = 清理虚拟号占位,不核销电子凭证 const result = await returnFulfillmentNumber(task, { source: 'admin_task_return_number', actor: buildAdminActionActor(session), consumeIndustryVoucher: false, }) if (!result?.task) { throw createHttpError('当前任务不支持退还号码', { statusCode: 409, errorCode: 'admin_task_return_not_supported', }) } return { task: mapTaskActionPayload(result.task), } } export async function resendAdminTaskKuaishouIndustryVoucherCode( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getRequiredTask(taskId) const now = nowIso() const viewerContext = createAdminViewerContext(session) if (!viewerContext.canOperateKuaishouIndustryVoucher) { throw createHttpError('当前账号没有权限重发电子凭证发码回调', { statusCode: 403, errorCode: 'admin_task_kuaishou_industry_resend_forbidden', }) } const voucher = await getRequiredIndustryVoucherForTask(task) const order = await getOrderById(task.order_id) const result = await resendKuaishouIndustryVoucherSendCallback({ voucherCode: voucher.voucher_code, oid: voucher.oid, preferredTotalGoodsValue: Number(order?.total_amount || 0) || 0, }) const updatedVoucher = result.voucher || voucher if (!result.success) { const message = String(result.error || '电子凭证发码回调重发失败').trim() const failedTask = await updateTask(task.id, { task_status: TASK_STATUS.MANUAL_REVIEW, result_code: 'kuaishou_industry_send_callback_resend_failed', result_message: message, last_error: message, updated_at: now, }) await createTaskEvent( task.id, 'kuaishou_industry_send_callback_resend_failed', { oid: voucher.oid, voucherCode: voucher.voucher_code, errorMessage: message, actor: buildAdminActionActor(session), }, now, ) void failedTask throw createHttpError(message, { statusCode: 409, errorCode: 'admin_task_kuaishou_industry_resend_failed', }) } const attachedTask = await attachKuaishouIndustryVoucherToTask(task, updatedVoucher, { source: 'admin_resend_kuaishou_industry_code', now, }) const resendTargetTask = attachedTask || task const refreshedTask = await updateTask(resendTargetTask.id, { result_code: 'kuaishou_industry_send_callback_resent', result_message: '电子凭证发码回调已重发成功', last_error: '', updated_at: now, }) await createTaskEvent( task.id, 'kuaishou_industry_send_callback_resent', { oid: voucher.oid, voucherCode: voucher.voucher_code, actor: buildAdminActionActor(session), }, now, ) return { task: mapTaskActionPayload(refreshedTask || resendTargetTask) } } export async function resendAdminOrderKuaishouIndustryVoucherCodes( orderId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise<{ success: boolean; resentCount: number; failedCount: number; errorMessage: string }> { const order = await getOrderById(Number(orderId)) const viewerContext = createAdminViewerContext(session) const now = nowIso() if (!viewerContext.canOperateKuaishouIndustryVoucher) { throw createHttpError('当前账号没有权限重发电子凭证发码回调', { statusCode: 403, errorCode: 'admin_order_kuaishou_industry_resend_forbidden', }) } if (!order) { throw createHttpError('订单不存在', { statusCode: 404, errorCode: 'admin_order_not_found', }) } const vouchers = await resolveIndustryVouchersForOrder(order.platform_order_id) if (vouchers.length === 0) { throw createHttpError('当前订单没有关联电子凭证,无法重发发码回调', { statusCode: 409, errorCode: 'admin_order_kuaishou_industry_voucher_missing', }) } let resentCount = 0 let failedCount = 0 let errorMessage = '' for (const voucher of vouchers) { const result = await resendKuaishouIndustryVoucherSendCallback({ voucherCode: voucher.voucher_code, oid: voucher.oid, preferredTotalGoodsValue: Number(order.total_amount || 0) || 0, }) if (result.success) { resentCount += 1 continue } failedCount += 1 errorMessage = errorMessage || String(result.error || '电子凭证发码回调重发失败').trim() } if (failedCount > 0) { throw createHttpError(errorMessage || '电子凭证发码回调重发失败', { statusCode: 409, errorCode: 'admin_order_kuaishou_industry_resend_failed', context: { resentCount, failedCount }, }) } const tasks = await listTasksByOrderId(order.id) await bindKuaishouIndustryVouchersToOrderTasks(order, tasks, { source: 'admin_order_resend_kuaishou_industry_code', now, }) return { success: failedCount === 0, resentCount, failedCount, errorMessage, } } export async function consumeAdminTaskKuaishouIndustryVoucher( taskId: AdminEntityIdInput, payload: AdminTaskKuaishouIndustryConsumeInput = {}, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getRequiredTask(taskId) const now = nowIso() const viewerContext = createAdminViewerContext(session) if (!viewerContext.canOperateKuaishouIndustryVoucher) { throw createHttpError('当前账号没有权限执行电子凭证核销', { statusCode: 403, errorCode: 'admin_task_kuaishou_industry_consume_forbidden', }) } const voucher = await getRequiredIndustryVoucherForTask(task) const consumeType = String(payload.consumeType || '').trim() const result = await consumeKuaishouIndustryVoucher(voucher, { source: 'admin_manual_consume', task, token: voucher.token, consumeTime: Date.now(), ...(consumeType ? { consumeType } : {}), ...(payload.storeName ? { storeName: payload.storeName } : {}), ...(payload.storeAddress ? { storeAddress: payload.storeAddress } : {}), ...(payload.expressCode ? { expressCode: payload.expressCode } : {}), ...(payload.expressNo ? { expressNo: payload.expressNo } : {}), }) if (!result.ok || !result.voucher) { const message = String(result.errorMessage || '电子凭证核销失败').trim() const failedTask = await updateTask(task.id, { task_status: TASK_STATUS.MANUAL_REVIEW, result_code: 'kuaishou_industry_manual_consume_failed', result_message: message, last_error: message, updated_at: now, }) await createTaskEvent( task.id, 'kuaishou_industry_manual_consume_failed', { oid: voucher.oid, voucherCode: voucher.voucher_code, errorMessage: message, actor: buildAdminActionActor(session), }, now, ) void failedTask throw createHttpError(message, { statusCode: 409, errorCode: 'admin_task_kuaishou_industry_consume_failed', }) } const attachedConsumeTask = await attachKuaishouIndustryVoucherToTask(task, result.voucher, { source: 'admin_manual_consume', now, }) const consumeTargetTask = attachedConsumeTask || task const taskContext = parseTaskContext(consumeTargetTask) const hasCloudFulfillmentContext = Boolean( taskContext.kuaishouCloudFulfillment && typeof taskContext.kuaishouCloudFulfillment === 'object' && !Array.isArray(taskContext.kuaishouCloudFulfillment), ) const flow = hasCloudFulfillmentContext ? normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) : null const nextContext = { ...taskContext, ...(flow ? { kuaishouCloudFulfillment: { ...flow, consume: { ...flow.consume, status: 'success', consumedAt: result.voucher.consumed_at || now, errorMessage: '', }, }, } : {}), } const updatedTask = await updateTask(consumeTargetTask.id, { task_status: TASK_STATUS.COMPLETED, delivery_status: 'delivered', result_code: 'kuaishou_industry_manual_consumed', result_message: '电子凭证已手动核销完成', redeemed_at: result.voucher.consumed_at || now, last_error: '', context_json: JSON.stringify(nextContext), updated_at: now, }) await createTaskEvent( task.id, 'kuaishou_industry_manual_consumed', { oid: result.voucher.oid, voucherCode: result.voucher.voucher_code, consumeSerialNum: result.voucher.consume_serial_num, actor: buildAdminActionActor(session), }, now, ) return { task: mapTaskActionPayload(updatedTask || consumeTargetTask) } } async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise { const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id) const firstVoucher = vouchers[0] if (firstVoucher) { return firstVoucher } const taskContext = parseTaskContext(task) const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher) const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim() if (voucherCode) { const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id) if (voucher) { return voucher } } const oidVouchers = await resolveIndustryVouchersForOrder(task.platform_order_id) if (voucherCode) { const matchedByCode = oidVouchers.find( (voucher) => String(voucher.voucher_code || '').trim() === voucherCode, ) if (matchedByCode) { return matchedByCode } } const unitIndex = Number(task.unit_index || 0) || 0 if (unitIndex > 0) { const matchedByUnit = oidVouchers.find( (voucher) => Number(voucher.unit_index || 0) === unitIndex, ) if (matchedByUnit) { return matchedByUnit } } if (oidVouchers.length === 1 && oidVouchers[0]) { return oidVouchers[0] } throw createHttpError('当前任务没有关联电子凭证,无法执行该操作', { statusCode: 409, errorCode: 'admin_task_kuaishou_industry_voucher_missing', }) } function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject { return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonObject : {} } async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise { const normalizedOid = String(platformOrderId || '').trim() if (!normalizedOid) { return [] } return listKuaishouIndustryVouchersByOid(normalizedOid) } function buildAdminActionActor(session: AdminViewerSessionInput | null) { return session ? { userId: Number(session.userId || 0) || 0, username: String(session.username || '').trim(), role: String(session.role || '').trim(), } : null }