import { buildClaimUrl } from '../claim/claim-service.js' import { getClaimTokenById } from '../../repositories/claim-token-repo.js' import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js' import { getOrderById, listOrders } from '../../repositories/order-repo.js' import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js' import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js' import { listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByTaskId, } from '../../repositories/kuaishou-industry-voucher-repo.js' import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js' import { createHttpError } from '../../utils/http.js' import { formatFenToAmount, normalizeFen } from '../../utils/money.js' import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson, } from './admin-query-utils.js' import { canCompleteManualDispatchTaskStatus, canDispatchKuaishouCloudFulfillmentStatus, canMarkManualReviewTaskStatus, canPrepareKuaishouCloudFulfillmentStatus, canRebindKuaishouCloudRoleStatus, canReturnKuaishouCloudFulfillmentStatus, canRetryTaskStatus, } from '../../domain/task-status.js' import { canViewerCloseTask, canRegenerateClaimLinkForViewer, createAdminViewerContext, getTaskPrimaryClaimTokenId, isManualDispatchTask, mapKuaishouCloudFulfillmentContext, mapKuaishouCloudTaskStateProjection, mapManualDispatchContext, mapRedeemResolutionContext, parseTaskContext, parseTaskState, resolveAdminTaskScreenshotUrl, resolveDisplayShopName, resolveOrderItemDeliveryMode, resolveOrderItemTitle, } from './admin-read-shared-helpers.js' import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js' import { buildOrderFulfillmentProgress, getRequiredTask, mapAdminTaskEvent, mapAdminTaskListItem, mapAdminTaskSummary, } from './admin-task-read-helpers.js' import type { AdminOrderListResponse, AdminTaskListResponse, } from '../../types/admin/read-models.js' import type { AdminEntityIdInput, AdminOrderListQueryInput, AdminTaskListQueryInput, AdminViewerSessionInput, } from '../../types/admin/read-inputs.js' import type { KuaishouIndustryVoucherRow, TaskRow, } from '../../types/repository/rows.js' type JsonRecord = Record export async function getAdminOrders( query: AdminOrderListQueryInput = {}, ): Promise { const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) const { items, total } = await listOrders({ page, pageSize, platformOrderId: String(query.platformOrderId || '').trim(), payStatus: String(query.payStatus || '').trim(), skuCode: String(query.skuCode || '').trim(), dateFrom: normalizeDateQuery(query.dateFrom), dateTo: normalizeDateQuery(query.dateTo, true), }) return { items: await Promise.all(items.map((item) => mapAdminOrderListItem(item))), pagination: { page, pageSize, total }, } } export async function getAdminOrderDetail( orderId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const order = await getOrderById(Number(orderId)) if (!order) { throw createHttpError('订单不存在', { statusCode: 404, errorCode: 'admin_order_not_found', }) } const [items, tasks] = await Promise.all([ listOrderItemsByOrderId(order.id), listTasksByOrderId(order.id), ]) const kuaishouIndustryVouchers = await listKuaishouIndustryVouchersByOid(order.platform_order_id) const itemSummary = summarizeOrderItems(items) const viewerContext = createAdminViewerContext(session) return { order: { orderId: order.id, provider: order.provider || '', platform: order.platform, shopId: order.shop_id || '', shopName: resolveDisplayShopName(order.provider, order.shop_id, order.shop_name), platformOrderId: order.platform_order_id, orderStatus: order.order_status, payStatus: order.pay_status, buyerId: order.buyer_id, buyerName: order.buyer_name, receiverContact: order.receiver_contact, totalAmount: formatFenToAmount(order.total_amount), totalAmountFen: normalizeFen(order.total_amount), currency: order.currency, paidAt: order.paid_at, createdAt: order.created_at, updatedAt: order.updated_at, itemSummary, rawPayload: order.raw_payload_json && typeof order.raw_payload_json === 'object' ? order.raw_payload_json : JSON.parse(String(order.raw_payload_json || '{}')), fulfillmentProgress: buildOrderFulfillmentProgress(tasks), }, items: items.map((item) => ({ orderItemId: item.id, skuCode: item.sku_code, skuName: item.sku_name, itemTitle: resolveOrderItemTitle(item), quantity: item.quantity, deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id), spec: item.spec_json && typeof item.spec_json === 'object' ? item.spec_json : JSON.parse(String(item.spec_json || '{}')), })), tasks: tasks.map((task) => mapAdminTaskSummary(task)), kuaishouIndustryVouchers: kuaishouIndustryVouchers.map(mapAdminKuaishouIndustryVoucher), operations: { canResendKuaishouIndustryVoucherCode: viewerContext.canOperateKuaishouIndustryVoucher && kuaishouIndustryVouchers.length > 0, }, } } function mapAdminKuaishouIndustryVoucher(voucher: { voucher_code?: unknown oid?: unknown unit_index?: unknown status?: unknown send_callback_status?: unknown send_callback_attempt_count?: unknown send_callback_last_error?: unknown send_callback_sent_at?: unknown consume_serial_num?: unknown consume_details_json?: unknown consumed_at?: unknown destroyed_at?: unknown }) { return { oid: String(voucher.oid || '').trim(), voucherCode: String(voucher.voucher_code || '').trim(), unitIndex: Number(voucher.unit_index || 0) || 0, status: String(voucher.status || '').trim(), sendCallbackStatus: String(voucher.send_callback_status || '').trim(), sendCallbackAttemptCount: Number(voucher.send_callback_attempt_count || 0) || 0, sendCallbackLastError: String(voucher.send_callback_last_error || '').trim(), sendCallbackSentAt: voucher.send_callback_sent_at || null, consumeSerialNum: String(voucher.consume_serial_num || '').trim(), consumeDetails: normalizeAdminJsonArray(voucher.consume_details_json), consumedAt: voucher.consumed_at || null, destroyedAt: voucher.destroyed_at || null, } } export async function getAdminTasks( query: AdminTaskListQueryInput = {}, session: AdminViewerSessionInput | null = null, ): Promise { const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) const { items, total } = await listTasks({ page, pageSize, status: String(query.status || '').trim(), platformOrderId: String(query.platformOrderId || '').trim(), taskNo: String(query.taskNo || '').trim(), skuCode: String(query.skuCode || '').trim(), roleId: String(query.roleId || '').trim(), dateFrom: normalizeDateQuery(query.dateFrom), dateTo: normalizeDateQuery(query.dateTo, true), }) const viewerContext = createAdminViewerContext(session) return { items: items.map((item) => mapAdminTaskListItem(item, viewerContext)), pagination: { page, pageSize, total }, } } export async function getAdminTaskDetail( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getTaskById(Number(taskId)) if (!task) { throw createHttpError('任务不存在', { statusCode: 404, errorCode: 'admin_task_not_found', }) } const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task) const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] = await Promise.all([ getOrderById(task.order_id), primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null), listTaskEventsByTaskId(task.id), listKuaishouIndustryVouchersByTaskId(task.id), task.platform_order_id ? listKuaishouIndustryVouchersByOid(task.platform_order_id) : Promise.resolve([]), ]) const orderItems = order ? await listOrderItemsByOrderId(order.id) : [] const orderItem = orderItems.find((item) => item.id === task.order_item_id) || null const taskContext = parseTaskContext(task) const taskState = parseTaskState(task) const viewerContext = createAdminViewerContext(session) // 统一对外领取入口为本站 claim 链接;平台 H5 仅作内部上下文 const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : '' const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext) const cloudSourceLabelMap = buildCloudSourceLabelMap() const kuaishouCloudFulfillment = mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment, { cloudSourceLabelMap, }) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap }) const firstKuaishouIndustryVoucher = resolveKuaishouIndustryVoucherForTask( task, taskContext, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers, ) const kuaishouIndustryVoucher = firstKuaishouIndustryVoucher ? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher) : mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher) const hasKuaishouIndustryVoucher = Boolean( kuaishouIndustryVoucher && String(kuaishouIndustryVoucher.voucherCode || '').trim(), ) const kuaishouIndustryVoucherStatus = String(kuaishouIndustryVoucher?.status || '').trim().toUpperCase() const kuaishouIndustryVoucherSendCallbackStatus = String(kuaishouIndustryVoucher?.sendCallbackStatus || '').trim().toLowerCase() return { task: mapAdminTaskListItem( { ...task, sku_code: orderItem?.sku_code || '', sku_name: orderItem?.sku_name || '', claim_token: claimToken?.token || '', }, viewerContext, ), order: order ? { orderId: order.id, provider: order.provider || '', platform: order.platform, shopId: order.shop_id || '', shopName: order.shop_name || '', platformOrderId: order.platform_order_id, payStatus: order.pay_status, orderStatus: order.order_status, } : null, orderItem: orderItem ? { orderItemId: orderItem.id, skuCode: orderItem.sku_code, skuName: orderItem.sku_name, quantity: orderItem.quantity, } : null, claimToken: claimToken ? { primaryClaimTokenId: claimToken.id, token: viewerContext.canViewSensitiveTaskData ? claimToken.token || '' : '', status: claimToken.status || '', expiredAt: claimToken.expired_at || '', claimUrl, } : null, artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {}, screenshotUrl, review: { required: false, screenshotCapturedAt: String(taskState.reviewCapturedAt || '').trim() || null, roleId: String(taskState.reviewRoleId || '').trim() || '', roleName: String(taskState.reviewRoleName || '').trim() || '', }, redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution), kuaishouCloudFulfillment, kuaishouIndustryVoucher, manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext), events: taskEvents.map(mapAdminTaskEvent), operations: { canRetry: viewerContext.canManageTaskLifecycle && !isManualDispatchTask(task) && canRetryTaskStatus(task.task_status), canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext), canClose: canViewerCloseTask(task, viewerContext), canMarkManualReview: viewerContext.canManageTaskLifecycle && canMarkManualReviewTaskStatus(task.task_status), canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && canCompleteManualDispatchTaskStatus(task.task_status), canPrepareKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending' && String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending' && canPrepareKuaishouCloudFulfillmentStatus(task.task_status), canRefreshKuaishouCloudRoleInfo: viewerContext.canOperateAssistedTask && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0, canRebindKuaishouCloudRole: viewerContext.canManageTaskLifecycle && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && canRebindKuaishouCloudRoleStatus(task.task_status) && String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() !== 'success' && Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0, canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && canDispatchKuaishouCloudFulfillmentStatus(task.task_status), canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && canReturnKuaishouCloudFulfillmentStatus(task.task_status), canResendKuaishouIndustryVoucherCode: viewerContext.canOperateKuaishouIndustryVoucher && hasKuaishouIndustryVoucher, canConsumeKuaishouIndustryVoucher: viewerContext.canOperateKuaishouIndustryVoucher && hasKuaishouIndustryVoucher && kuaishouIndustryVoucherStatus !== 'CONSUMED' && kuaishouIndustryVoucherSendCallbackStatus === 'success', canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData, }, } } function mapKuaishouIndustryVoucherContext(value: unknown): JsonRecord | null { if (!value || typeof value !== 'object' || Array.isArray(value)) { return null } const record = value as JsonRecord return { oid: String(record.oid || '').trim(), voucherCode: String(record.voucherCode || record.eticketId || '').trim(), unitIndex: Number(record.unitIndex || 0) || 0, status: String(record.status || '').trim(), sendCallbackStatus: String(record.sendCallbackStatus || '').trim(), sendCallbackLastError: String(record.sendCallbackLastError || '').trim(), sendCallbackAttemptCount: Number(record.sendCallbackAttemptCount || 0) || 0, sendCallbackSentAt: record.sendCallbackSentAt || null, consumeSerialNum: String(record.consumeSerialNum || '').trim(), consumeDetails: normalizeAdminJsonArray(record.consumeDetails), consumedAt: record.consumedAt || null, destroyedAt: record.destroyedAt || null, } } function normalizeAdminJsonArray(value: unknown): JsonRecord[] { if (Array.isArray(value)) { return value.filter((item): item is JsonRecord => Boolean(item && typeof item === 'object' && !Array.isArray(item)), ) } try { const parsed = JSON.parse(String(value || '[]')) return Array.isArray(parsed) ? parsed.filter((item): item is JsonRecord => Boolean(item && typeof item === 'object' && !Array.isArray(item)), ) : [] } catch { return [] } } function resolveKuaishouIndustryVoucherForTask( task: TaskRow, taskContext: JsonRecord, taskVouchers: KuaishouIndustryVoucherRow[], oidVouchers: KuaishouIndustryVoucherRow[], ): KuaishouIndustryVoucherRow | null { const linkedVoucher = taskVouchers[0] || null if (linkedVoucher) { return linkedVoucher } const voucherContext = taskContext.kuaishouIndustryVoucher && typeof taskContext.kuaishouIndustryVoucher === 'object' && !Array.isArray(taskContext.kuaishouIndustryVoucher) ? taskContext.kuaishouIndustryVoucher as JsonRecord : {} const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim() 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 } } return oidVouchers.length === 1 ? (oidVouchers[0] || null) : null } function buildCloudSourceLabelMap() { const labelMap = new Map() for (const source of listCloudtentaclesSources().sources) { const key = String(source.key || '').trim() if (!key) continue const label = String(source.label || source.username || key).trim() || key labelMap.set(key, label) } return labelMap } export async function getAdminTaskScreenshotPath( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, ): Promise { const task = await getRequiredTask(taskId) if (task.screenshot_path) { return task.screenshot_path } throw createHttpError('当前任务还没有可查看截图', { statusCode: 404, errorCode: 'admin_task_screenshot_not_found', }) }