// @ts-check import { buildClaimUrl } from '../claim/claim-service.js' import { getClaimTokenById } from '../../repositories/claim-token-repo.js' import { getInventoryItemById, listInventoryItems, listInventorySkuSuggestions, } from '../../repositories/inventory-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 { listTaskInventoryBindingsByTaskId } from '../../repositories/task-inventory-binding-repo.js' import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js' import { getWebhookEventById, listWebhookEvents, listWebhookEventsByOrderId } from '../../repositories/webhook-event-repo.js' import { createHttpError } from '../../utils/http.js' import { formatFenToAmount, normalizeFen } from '../../utils/money.js' import { getTencentBrowserSessionReviewScreenshotPath } from '../session/session.js' import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js' import { canViewerCloseTask, canRegenerateClaimLinkForViewer, canViewerConfirmAssistedRole, canViewerRedeemAssistedTask, createAdminViewerContext, getTaskPrimaryClaimTokenId, getTaskPrimaryInventoryItemId, isAssistedClaimTask, isManualDispatchTask, mapKuaishouCloudFulfillmentContext, mapManualDispatchContext, mapRedeemResolutionContext, parseTaskContext, parseTaskState, resolveAdminTaskScreenshotUrl, resolveDisplayShopName, resolveOrderItemDeliveryMode, resolveOrderItemTitle, } from './admin-read-shared-helpers.js' import { mapAdminInventoryListItem } from './admin-inventory-read-helpers.js' import { mapAdminOrderListItem, summarizeOrderItems } from './admin-order-read-helpers.js' import { mapAdminWebhookEvent } from './admin-webhook-read-helpers.js' import { buildOrderAgisoAutoDeliverySummary, buildOrderBindingSummary, createTaskBindingSummaryFromBindings, getRequiredTask, getTaskBindingSummary, getTaskBindingSummaryMap, mapAdminTaskEvent, mapAdminTaskInventoryBinding, mapAdminTaskListItem, mapAdminTaskSummary, } from './admin-task-read-helpers.js' /** @typedef {import('../../types/admin-read-models.js').AdminInventoryListResponse} AdminInventoryListResponse */ /** @typedef {import('../../types/admin-read-models.js').AdminInventorySkuSuggestionResponse} AdminInventorySkuSuggestionResponse */ /** @typedef {import('../../types/admin-read-models.js').AdminOrderListResponse} AdminOrderListResponse */ /** @typedef {import('../../types/admin-read-models.js').AdminTaskListResponse} AdminTaskListResponse */ /** @typedef {import('../../types/admin-read-models.js').AdminWebhookEventListResponse} AdminWebhookEventListResponse */ /** @typedef {import('../../types/admin-read-inputs.js').AdminEntityIdInput} AdminEntityIdInput */ /** @typedef {import('../../types/admin-read-inputs.js').AdminInventoryListQueryInput} AdminInventoryListQueryInput */ /** @typedef {import('../../types/admin-read-inputs.js').AdminInventorySkuSuggestionQueryInput} AdminInventorySkuSuggestionQueryInput */ /** @typedef {import('../../types/admin-read-inputs.js').AdminOrderListQueryInput} AdminOrderListQueryInput */ /** @typedef {import('../../types/admin-read-inputs.js').AdminTaskListQueryInput} AdminTaskListQueryInput */ /** @typedef {import('../../types/admin-read-inputs.js').AdminViewerSessionInput} AdminViewerSessionInput */ /** @typedef {import('../../types/admin-read-inputs.js').AdminWebhookEventListQueryInput} AdminWebhookEventListQueryInput */ /** @returns {Promise} */ /** @param {AdminOrderListQueryInput} [query] */ export async function getAdminOrders(query = /** @type {AdminOrderListQueryInput} */ ({})) { 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 }, } } /** @param {AdminEntityIdInput} orderId */ export async function getAdminOrderDetail(orderId) { const order = await getOrderById(Number(orderId)) if (!order) { throw createHttpError('订单不存在', { statusCode: 404, errorCode: 'admin_order_not_found', }) } const [items, tasks, webhookEvents] = await Promise.all([ listOrderItemsByOrderId(order.id), listTasksByOrderId(order.id), listWebhookEventsByOrderId(order.id), ]) const taskBindingSummaryMap = await getTaskBindingSummaryMap(tasks.map((task) => task.id)) const itemSummary = summarizeOrderItems(items) return { order: { orderId: order.id, provider: order.provider || 'agiso', 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 || '{}')), bindingSummary: buildOrderBindingSummary(tasks, taskBindingSummaryMap), agisoAutoDelivery: buildOrderAgisoAutoDeliverySummary(order, 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, getTaskBindingSummary(taskBindingSummaryMap, task.id))), webhookEvents: webhookEvents.map((event) => ({ eventId: event.id, provider: event.provider || 'agiso', platform: event.platform, shopId: event.shop_id || '', shopName: resolveDisplayShopName(event.provider, event.shop_id, event.shop_name), eventType: event.event_type, eventKey: event.event_key, signatureValid: Boolean(event.signature_valid), processed: Boolean(event.processed), processError: event.process_error, createdAt: event.created_at, })), } } /** @returns {Promise} */ /** @param {AdminTaskListQueryInput} [query] */ /** @param {AdminViewerSessionInput | null} [session] */ export async function getAdminTasks( query = /** @type {AdminTaskListQueryInput} */ ({}), session = null, ) { 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 taskBindingSummaryMap = await getTaskBindingSummaryMap(items.map((item) => item.id)) const viewerContext = createAdminViewerContext(session) return { items: items.map((item) => mapAdminTaskListItem( item, getTaskBindingSummary(taskBindingSummaryMap, item.id), viewerContext, )), pagination: { page, pageSize, total }, } } /** @param {AdminEntityIdInput} taskId */ /** @param {AdminViewerSessionInput | null} [session] */ export async function getAdminTaskDetail(taskId, session = null) { const task = await getTaskById(Number(taskId)) if (!task) { throw createHttpError('任务不存在', { statusCode: 404, errorCode: 'admin_task_not_found', }) } const [order, claimToken, inventoryItem, taskEvents, inventoryBindings] = await Promise.all([ getOrderById(task.order_id), getTaskPrimaryClaimTokenId(task) ? getClaimTokenById(getTaskPrimaryClaimTokenId(task)) : Promise.resolve(null), getTaskPrimaryInventoryItemId(task) ? getInventoryItemById(getTaskPrimaryInventoryItemId(task)) : Promise.resolve(null), listTaskEventsByTaskId(task.id), listTaskInventoryBindingsByTaskId(task.id), ]) 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 taskBindingSummary = createTaskBindingSummaryFromBindings(inventoryBindings) const viewerContext = createAdminViewerContext(session) const mappedInventoryBindings = inventoryBindings.map((binding) => mapAdminTaskInventoryBinding(binding, task, viewerContext)) const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : '' const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext) const kuaishouCloudFulfillment = mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment) return { task: mapAdminTaskListItem({ ...task, sku_code: orderItem?.sku_code || '', sku_name: orderItem?.sku_name || '', inventory_display_value: inventoryItem?.display_value || '', claim_token: claimToken?.token || '', }, taskBindingSummary, viewerContext), order: order ? { orderId: order.id, provider: order.provider || 'agiso', 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, inventory: inventoryItem ? { inventoryItemId: inventoryItem.id, skuCode: inventoryItem.sku_code, batchNo: inventoryItem.batch_no, credentialType: inventoryItem.credential_type || 'tencent_code', displayValue: viewerContext.canViewSensitiveTaskData ? inventoryItem.display_value : '', status: inventoryItem.status, } : null, inventoryBindings: mappedInventoryBindings, artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {}, screenshotUrl, review: { required: isAssistedClaimTask(task), screenshotCapturedAt: String(taskState.reviewCapturedAt || '').trim() || null, roleId: String(taskState.reviewRoleId || '').trim() || '', roleName: String(taskState.reviewRoleName || '').trim() || '', }, redeemResolution: mapRedeemResolutionContext(taskContext.redeemResolution), kuaishouCloudFulfillment, manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext), events: taskEvents.map(mapAdminTaskEvent), operations: { canRetry: viewerContext.canManageTaskLifecycle && !isManualDispatchTask(task) && ['retry_pending', 'manual_review', 'waiting_inventory'].includes(task.task_status), canReleaseInventory: mappedInventoryBindings.some((binding) => binding.canRelease), canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext), canClose: canViewerCloseTask(task, viewerContext), canMarkManualReview: viewerContext.canManageTaskLifecycle && !['redeemed', 'closed', 'manual_review'].includes(task.task_status), canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && !['redeemed', 'closed'].includes(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' && ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()), canRefreshKuaishouCloudRoleInfo: viewerContext.canOperateAssistedTask && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0, canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && ['waiting_binding'].includes(String(task.task_status || '').trim()), canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle && String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' && ['dispatched_pending_return'].includes(String(task.task_status || '').trim()), canSupportConfirmRole: canViewerConfirmAssistedRole(task, viewerContext), canSupportRedeem: canViewerRedeemAssistedTask(task, viewerContext), canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData, }, } } /** @param {AdminEntityIdInput} taskId */ /** @param {AdminViewerSessionInput | null} [session] */ export async function getAdminTaskScreenshotPath(taskId, session = null) { const task = await getRequiredTask(taskId) if (task.screenshot_path) { return task.screenshot_path } if (task.browser_session_id) { return getTencentBrowserSessionReviewScreenshotPath(task.browser_session_id) } throw createHttpError('当前任务还没有可查看截图', { statusCode: 404, errorCode: 'admin_task_screenshot_not_found', }) } /** @returns {Promise} */ /** @param {AdminInventoryListQueryInput} [query] */ /** @param {AdminViewerSessionInput | null} [session] */ export async function getAdminInventoryItems(query = /** @type {AdminInventoryListQueryInput} */ ({}), session = null) { const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) const viewerContext = createAdminViewerContext(session) const { items, total } = await listInventoryItems({ page, pageSize, skuCode: String(query.skuCode || '').trim(), credentialType: String(query.credentialType || '').trim(), status: String(query.status || '').trim(), batchNo: String(query.batchNo || '').trim(), inventoryGroupCode: String(query.inventoryGroupCode || '').trim(), allowedInventoryGroupCodes: viewerContext.allowedInventoryGroupCodes, }) return { items: await Promise.all(items.map((item) => mapAdminInventoryListItem(item))), pagination: { page, pageSize, total }, } } /** @returns {Promise} */ /** @param {AdminInventorySkuSuggestionQueryInput} [query] */ export async function getAdminInventorySkuSuggestions( query = /** @type {AdminInventorySkuSuggestionQueryInput} */ ({}), session = null, ) { const viewerContext = createAdminViewerContext(session) const credentialType = String(query.credentialType || '').trim() const keyword = String(query.keyword || '').trim() const inventoryGroupCode = String(query.inventoryGroupCode || '').trim() const requestedLimit = Number(query.limit) const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? Math.min(100, Math.floor(requestedLimit)) : 50 const items = await listInventorySkuSuggestions({ credentialType, keyword, limit, inventoryGroupCode, allowedInventoryGroupCodes: viewerContext.allowedInventoryGroupCodes, }) return { items: items.map((item) => ({ skuCode: String(item.sku_code || '').trim(), credentialType: String(item.credential_type || '').trim() || 'tencent_code', inventoryGroupCode: String(item.inventory_group_code || '').trim(), totalCount: Number(item.total_count || 0), availableCount: Number(item.available_count || 0), latestUpdatedAt: item.latest_updated_at || null, })), } } /** @returns {Promise} */ /** @param {AdminWebhookEventListQueryInput} [query] */ export async function getAdminWebhookEvents(query = /** @type {AdminWebhookEventListQueryInput} */ ({})) { const page = normalizePage(query.page) const pageSize = normalizePageSize(query.pageSize) const { items, total } = await listWebhookEvents({ page, pageSize, provider: String(query.provider || '').trim(), platform: String(query.platform || '').trim(), platformOrderId: String(query.platformOrderId || '').trim(), processed: String(query.processed || '').trim(), visibility: String(query.visibility || '').trim() || 'important', relatedOrderId: String(query.relatedOrderId || '').trim(), dateFrom: normalizeDateQuery(query.dateFrom), dateTo: normalizeDateQuery(query.dateTo, true), }) return { items: await Promise.all(items.map((item) => mapAdminWebhookEvent(item))), pagination: { page, pageSize, total }, } } /** @param {AdminEntityIdInput} eventId */ export async function getAdminWebhookEventDetail(eventId) { const event = await getWebhookEventById(Number(eventId)) if (!event) { throw createHttpError('Webhook 事件不存在', { statusCode: 404, errorCode: 'admin_webhook_not_found', }) } return mapAdminWebhookEvent(event, { includeRaw: true }) }