// @ts-check import { getTaskById } from '../../repositories/task-repo.js' import { listTaskInventoryBindingSummariesByTaskIds } from '../../repositories/task-inventory-binding-repo.js' import { createHttpError } from '../../utils/http.js' import { safeParseJson } from './admin-query-utils.js' import { createAdminViewerContext, getTaskPrimaryClaimTokenId, getTaskPrimaryInventoryItemId, parseTaskContext, } from './admin-read-shared-helpers.js' /** @typedef {import('../../types/admin-read-models.js').AdminTaskBindingSummary} AdminTaskBindingSummary */ /** @typedef {import('../../types/admin-read-models.js').AdminTaskListItem} AdminTaskListItem */ /** @typedef {import('../../types/repository-rows.js').TaskEventRow} TaskEventRow */ /** @typedef {import('../../types/repository-rows.js').TaskInventoryBindingRow} TaskInventoryBindingRow */ /** @typedef {import('../../types/repository-rows.js').TaskRow} TaskRow */ export function mapAdminTaskSummary(task, bindingSummary = createEmptyTaskBindingSummary()) { const binding = buildTaskBindingState(task) const taskContext = parseTaskContext(task) return { taskId: task.id, taskNo: task.task_no, deliveryStatus: task.delivery_status || '', status: task.task_status, systemBindingStatus: binding.systemBindingStatus, userBindingStatus: binding.userBindingStatus, loginType: task.login_type, browserSessionId: task.browser_session_id, claimedAt: task.claimed_at, roleConfirmedAt: task.role_confirmed_at, redeemedAt: task.redeemed_at, lastError: task.last_error, retryCount: getTaskRetryCount(task), bindingSummary, agisoAutoDelivery: mapAgisoAutoDeliveryContext(taskContext.agisoAutoDelivery), createdAt: task.created_at, updatedAt: task.updated_at, } } export function mapTaskActionPayload(task) { const inventoryItemId = getTaskPrimaryInventoryItemId(task) const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task) return { taskId: task.id, taskNo: task.task_no, status: task.task_status, deliveryStatus: task.delivery_status, resultCode: task.result_code, resultMessage: task.result_message, inventoryItemId, primaryClaimTokenId, lastError: task.last_error, updatedAt: task.updated_at, } } export async function getRequiredTask(taskId) { const task = await getTaskById(Number(taskId)) if (!task) { throw createHttpError('任务不存在', { statusCode: 404, errorCode: 'admin_task_not_found', }) } return task } /** * @param {TaskRow} task * @returns {AdminTaskListItem} */ export function mapAdminTaskListItem(task, bindingSummary = createEmptyTaskBindingSummary(), viewerContext = createAdminViewerContext()) { const binding = buildTaskBindingState(task) const taskContext = parseTaskContext(task) return { taskId: task.id, taskNo: task.task_no, platformOrderId: task.platform_order_id, skuCode: task.sku_code || '', skuName: task.sku_name || '', status: task.task_status, executorKey: task.executor_key || '', deliveryStatus: task.delivery_status || '', resultCode: task.result_code || '', resultMessage: task.result_message || '', systemBindingStatus: binding.systemBindingStatus, userBindingStatus: binding.userBindingStatus, loginType: task.login_type, roleName: task.role_name, roleId: task.role_id, browserSessionId: task.browser_session_id, claimedAt: task.claimed_at, roleConfirmedAt: task.role_confirmed_at, redeemedAt: task.redeemed_at, retryCount: getTaskRetryCount(task), bindingSummary, agisoAutoDelivery: mapAgisoAutoDeliveryContext(taskContext.agisoAutoDelivery), lastError: task.last_error, createdAt: task.created_at, updatedAt: task.updated_at, inventoryDisplayMasked: viewerContext.canViewSensitiveTaskData ? maskCode(task.primary_inventory_display_value) : '', inventoryCredentialType: String(task.primary_inventory_credential_type || '').trim(), claimToken: viewerContext.canViewSensitiveTaskData ? (task.primary_claim_token || task.claim_token || '') : '', screenshotPath: viewerContext.role === 'support' ? '' : (task.screenshot_path || ''), } } /** @param {TaskEventRow} event */ export function mapAdminTaskEvent(event) { const payload = safeParseJson(event.payload_json) return { eventId: event.id, eventType: String(event.event_type || '').trim(), payload: normalizeRecord(payload), createdAt: event.created_at, } } /** @returns {AdminTaskBindingSummary} */ export function createEmptyTaskBindingSummary() { return { totalBindingCount: 0, reservedBindingCount: 0, consumedBindingCount: 0, releasedBindingCount: 0, roleKeys: [], } } export async function getTaskBindingSummaryMap(taskIds = []) { const rows = await listTaskInventoryBindingSummariesByTaskIds(taskIds) const output = new Map() for (const row of rows) { output.set(Number(row.task_id), { totalBindingCount: Number(row.total_binding_count || 0), reservedBindingCount: Number(row.reserved_binding_count || 0), consumedBindingCount: Number(row.consumed_binding_count || 0), releasedBindingCount: Number(row.released_binding_count || 0), roleKeys: Array.isArray(row.role_keys) ? row.role_keys.map((value) => String(value || '').trim()).filter(Boolean) : [], }) } return output } export function getTaskBindingSummary(summaryMap, taskId) { return summaryMap.get(Number(taskId)) || createEmptyTaskBindingSummary() } export function createTaskBindingSummaryFromBindings(bindings = []) { const normalizedBindings = Array.isArray(bindings) ? bindings : [] const roleKeys = Array.from(new Set(normalizedBindings .map((binding) => String(binding?.role_key || '').trim()) .filter(Boolean))) return { totalBindingCount: normalizedBindings.length, reservedBindingCount: normalizedBindings.filter((binding) => String(binding?.binding_status || '') === 'reserved').length, consumedBindingCount: normalizedBindings.filter((binding) => String(binding?.binding_status || '') === 'consumed').length, releasedBindingCount: normalizedBindings.filter((binding) => String(binding?.binding_status || '') === 'released').length, roleKeys, } } export function mapAdminTaskInventoryBinding(binding, task, viewerContext = createAdminViewerContext()) { const metadata = safeParseJson(binding.metadata_json) return { bindingId: binding.id, inventoryItemId: binding.inventory_item_id, roleKey: String(binding.role_key || '').trim(), quantity: Math.max(1, Number(binding.quantity || 1)), bindingStatus: String(binding.binding_status || '').trim(), inventoryStatus: String(binding.inventory_item_status || '').trim(), skuCode: String(binding.sku_code || '').trim(), batchNo: String(binding.batch_no || '').trim(), credentialType: String(binding.credential_type || 'tencent_code').trim() || 'tencent_code', displayValue: viewerContext.canViewSensitiveTaskData ? String(binding.display_value || '').trim() : '', invalidReason: String(binding.invalid_reason || '').trim(), consumedAt: binding.consumed_at || null, releasedAt: binding.released_at || null, createdAt: binding.created_at, updatedAt: binding.updated_at, metadata: normalizeRecord(metadata), isPrimary: Number(binding.inventory_item_id || 0) === getTaskPrimaryInventoryItemId(task), canRelease: viewerContext.canManageSensitiveInventory && canReleaseTaskInventoryBinding(task, binding), } } export function buildOrderBindingSummary(tasks, taskBindingSummaryMap = new Map()) { const normalizedTasks = Array.isArray(tasks) ? tasks : [] const totalTaskCount = normalizedTasks.length const taskBindings = normalizedTasks.map((task) => buildTaskBindingState(task)) const systemBoundTaskCount = normalizedTasks.filter((task) => isTaskSystemBound(task)).length const completedBindingTaskCount = normalizedTasks.filter((task) => String(task?.task_status || '') === 'redeemed').length const bindingSummaries = normalizedTasks.map((task) => getTaskBindingSummary(taskBindingSummaryMap, task.id)) const totalBindingCount = bindingSummaries.reduce((sum, item) => sum + item.totalBindingCount, 0) const reservedBindingCount = bindingSummaries.reduce((sum, item) => sum + item.reservedBindingCount, 0) const consumedBindingCount = bindingSummaries.reduce((sum, item) => sum + item.consumedBindingCount, 0) const releasedBindingCount = bindingSummaries.reduce((sum, item) => sum + item.releasedBindingCount, 0) let systemBindingStatus = 'pending_binding' let userBindingStatus = 'not_started' if (totalTaskCount === 0) { return { totalTaskCount, systemBoundTaskCount, completedBindingTaskCount, totalBindingCount, reservedBindingCount, consumedBindingCount, releasedBindingCount, systemBindingStatus, userBindingStatus, } } if (normalizedTasks.every((task) => String(task.task_status || '') === 'redeemed')) { systemBindingStatus = 'system_bound' userBindingStatus = 'binding_completed' } else if (taskBindings.some((item) => ['binding_in_progress', 'binding_confirmed', 'link_opened'].includes(item.userBindingStatus))) { systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding' userBindingStatus = 'user_binding' } else if (taskBindings.some((item) => item.userBindingStatus === 'waiting_user_claim')) { systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding' userBindingStatus = 'waiting_user_claim' } else if (taskBindings.some((item) => item.userBindingStatus === 'binding_exception')) { systemBindingStatus = systemBoundTaskCount > 0 ? 'system_bound' : 'pending_binding' userBindingStatus = 'binding_exception' } else if (systemBoundTaskCount > 0) { systemBindingStatus = 'system_bound' } else if (taskBindings.some((item) => ['manual_review', 'retry_pending', 'waiting_inventory'].includes(item.systemBindingStatus))) { systemBindingStatus = 'binding_exception' } return { totalTaskCount, systemBoundTaskCount, completedBindingTaskCount, totalBindingCount, reservedBindingCount, consumedBindingCount, releasedBindingCount, systemBindingStatus, userBindingStatus, } } export function buildOrderAgisoAutoDeliverySummary(order, tasks = []) { if (String(order?.provider || '').trim() !== 'agiso' || String(order?.platform || '').trim() !== 'xianyu') { return null } const normalizedTasks = Array.isArray(tasks) ? tasks.filter(Boolean) : [] const totalTaskCount = normalizedTasks.length const deliveredTaskCount = normalizedTasks.filter((task) => String(task?.delivery_status || '').trim() === 'delivered').length const latest = normalizedTasks.reduce((best, task) => { const autoDelivery = mapAgisoAutoDeliveryContext(parseTaskContext(task).agisoAutoDelivery) if (!autoDelivery) { return best } const candidate = { ...autoDelivery, sourceTaskId: Number(task.id || 0) || null, sourceTaskNo: String(task.task_no || '').trim(), } const candidateTime = Date.parse(String(candidate.updatedAt || task.updated_at || '')) const bestTime = Date.parse(String(best?.updatedAt || '')) if (!best || (Number.isFinite(candidateTime) && (!Number.isFinite(bestTime) || candidateTime >= bestTime))) { return candidate } return best }, null) if (latest) { return { ...latest, totalTaskCount, deliveredTaskCount, } } return { status: totalTaskCount === 0 ? 'not_started' : deliveredTaskCount >= totalTaskCount ? 'pending' : 'waiting', trigger: '', reason: deliveredTaskCount >= totalTaskCount ? '' : 'waiting_other_tasks', platformOrderId: String(order?.platform_order_id || '').trim(), responseStatus: 0, errorMessage: '', requestId: '', updatedAt: null, sourceTaskId: null, sourceTaskNo: '', totalTaskCount, deliveredTaskCount, } } function canReleaseTaskInventoryBinding(task, binding) { if (!task || !binding) { return false } if (String(binding.binding_status || '').trim() !== 'reserved') { return false } return !['redeemed', 'expired'].includes(String(task.task_status || '').trim()) } function buildTaskBindingState(task) { const normalizedStatus = String(task?.task_status || '').trim() if (!normalizedStatus) { return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started', } } if (normalizedStatus === 'pending_payment') { return { systemBindingStatus: 'pending_payment', userBindingStatus: 'not_started' } } if (normalizedStatus === 'paid') { return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' } } if (normalizedStatus === 'waiting_inventory') { return { systemBindingStatus: 'waiting_inventory', userBindingStatus: 'not_started' } } if (normalizedStatus === 'pending_binding_prepare') { return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' } } if (normalizedStatus === 'waiting_binding') { return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' } } if (normalizedStatus === 'dispatched_pending_return') { return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' } } if (normalizedStatus === 'completed') { return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' } } if (normalizedStatus === 'manual_review') { return { systemBindingStatus: 'manual_review', userBindingStatus: 'binding_exception' } } if (normalizedStatus === 'retry_pending') { return { systemBindingStatus: 'retry_pending', userBindingStatus: 'binding_exception' } } if (normalizedStatus === 'closed') { return { systemBindingStatus: 'closed', userBindingStatus: 'closed' } } if (normalizedStatus === 'expired') { return { systemBindingStatus: 'expired', userBindingStatus: 'expired' } } if (normalizedStatus === 'link_generated') { return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' } } if (normalizedStatus === 'claimed') { return { systemBindingStatus: 'system_bound', userBindingStatus: 'link_opened' } } if (normalizedStatus === 'role_confirmed') { return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_confirmed' } } if (normalizedStatus === 'redeeming') { return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_in_progress' } } if (normalizedStatus === 'redeemed') { return { systemBindingStatus: 'system_bound', userBindingStatus: 'binding_completed' } } if (isTaskSystemBound(task)) { return { systemBindingStatus: 'system_bound', userBindingStatus: 'waiting_user_claim' } } return { systemBindingStatus: 'pending_binding', userBindingStatus: 'not_started' } } function isTaskSystemBound(task) { return Boolean(task && (getTaskPrimaryInventoryItemId(task) || getTaskPrimaryClaimTokenId(task))) } function mapAgisoAutoDeliveryContext(value) { if (!value || typeof value !== 'object') { return null } return { status: String(value.status || '').trim(), trigger: String(value.trigger || '').trim(), reason: String(value.reason || '').trim(), platformOrderId: String(value.platformOrderId || '').trim(), responseStatus: Number(value.responseStatus || 0), errorMessage: String(value.errorMessage || '').trim(), requestId: String(value.requestId || '').trim(), updatedAt: value.updatedAt || null, } } function normalizeRecord(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {} } function maskCode(value) { const text = String(value || '').trim() if (!text) { return '' } if (text.length <= 8) { return `${text.slice(0, 2)}****${text.slice(-2)}` } return `${text.slice(0, 4)}****${text.slice(-4)}` } function getTaskRetryCount(task) { return Number(task?.attempt_count || 0) }