280 lines
9.1 KiB
TypeScript
280 lines
9.1 KiB
TypeScript
import { getTaskById } from '../../repositories/task-repo.js'
|
|
import { createHttpError } from '../../utils/http.js'
|
|
import { safeParseJson } from './admin-query-utils.js'
|
|
import {
|
|
TASK_STATUS,
|
|
isTaskFulfillmentCompletedStatus,
|
|
normalizeTaskStatus,
|
|
} from '../../domain/task-status.js'
|
|
import {
|
|
createAdminViewerContext,
|
|
getTaskPrimaryClaimTokenId,
|
|
} from './admin-read-shared-helpers.js'
|
|
|
|
import type {
|
|
AdminOrderFulfillmentProgress,
|
|
AdminTaskListItem,
|
|
} from '../../types/admin/read-models.js'
|
|
import type { AdminTaskActionPayload } from '../../types/admin/write-models.js'
|
|
import type {
|
|
TaskEventRow,
|
|
TaskRow,
|
|
} from '../../types/repository/rows.js'
|
|
import type { AdminViewerContext } from './admin-read-shared-helpers.js'
|
|
|
|
type JsonRecord = Record<string, any>
|
|
|
|
type TaskFulfillmentState = {
|
|
resourceStatus: string
|
|
customerStatus: string
|
|
}
|
|
|
|
export function mapAdminTaskSummary(
|
|
task: TaskRow,
|
|
): JsonRecord {
|
|
const fulfillment = buildTaskFulfillmentState(task)
|
|
|
|
return {
|
|
taskId: task.id,
|
|
taskNo: task.task_no,
|
|
deliveryStatus: task.delivery_status || '',
|
|
status: task.task_status,
|
|
resourceStatus: fulfillment.resourceStatus,
|
|
customerStatus: fulfillment.customerStatus,
|
|
loginType: task.login_type,
|
|
runtimeSessionId: task.runtime_session_id,
|
|
claimedAt: task.claimed_at,
|
|
roleConfirmedAt: task.role_confirmed_at,
|
|
redeemedAt: task.redeemed_at,
|
|
lastError: task.last_error,
|
|
retryCount: getTaskRetryCount(task),
|
|
createdAt: task.created_at,
|
|
updatedAt: task.updated_at,
|
|
}
|
|
}
|
|
|
|
export function mapTaskActionPayload(task: any): AdminTaskActionPayload {
|
|
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,
|
|
primaryClaimTokenId,
|
|
lastError: task.last_error,
|
|
updatedAt: task.updated_at,
|
|
}
|
|
}
|
|
|
|
export async function getRequiredTask(taskId: number | string): Promise<TaskRow> {
|
|
const task = await getTaskById(Number(taskId))
|
|
|
|
if (!task) {
|
|
throw createHttpError('任务不存在', {
|
|
statusCode: 404,
|
|
errorCode: 'admin_task_not_found',
|
|
})
|
|
}
|
|
|
|
return task
|
|
}
|
|
|
|
export function mapAdminTaskListItem(
|
|
task: TaskRow,
|
|
viewerContext: AdminViewerContext = createAdminViewerContext(),
|
|
): AdminTaskListItem {
|
|
const fulfillment = buildTaskFulfillmentState(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 || '',
|
|
resourceStatus: fulfillment.resourceStatus,
|
|
customerStatus: fulfillment.customerStatus,
|
|
loginType: task.login_type,
|
|
roleName: resolveTaskDisplayRoleName(task),
|
|
roleId: resolveTaskDisplayRoleId(task),
|
|
runtimeSessionId: task.runtime_session_id,
|
|
claimedAt: task.claimed_at,
|
|
roleConfirmedAt: task.role_confirmed_at,
|
|
redeemedAt: task.redeemed_at,
|
|
retryCount: getTaskRetryCount(task),
|
|
lastError: task.last_error,
|
|
createdAt: task.created_at,
|
|
updatedAt: task.updated_at,
|
|
claimToken: viewerContext.canViewSensitiveTaskData ? (task.primary_claim_token || task.claim_token || '') : '',
|
|
screenshotPath: viewerContext.role === 'support' ? '' : (task.screenshot_path || ''),
|
|
}
|
|
}
|
|
|
|
export function mapAdminTaskEvent(event: TaskEventRow): JsonRecord {
|
|
const payload = safeParseJson(event.payload_json)
|
|
|
|
return {
|
|
eventId: event.id,
|
|
eventType: String(event.event_type || '').trim(),
|
|
payload: normalizeRecord(payload),
|
|
createdAt: event.created_at,
|
|
}
|
|
}
|
|
|
|
export function buildOrderFulfillmentProgress(
|
|
tasks: TaskRow[] | null | undefined,
|
|
): AdminOrderFulfillmentProgress {
|
|
const normalizedTasks = Array.isArray(tasks) ? tasks : []
|
|
const totalTaskCount = normalizedTasks.length
|
|
const taskFulfillments = normalizedTasks.map((task) => buildTaskFulfillmentState(task))
|
|
const preparedTaskCount = normalizedTasks.filter((task) => isTaskResourcePrepared(task)).length
|
|
const completedTaskCount = normalizedTasks.filter((task) => isTaskFulfillmentCompleted(task)).length
|
|
|
|
let resourceStatus = 'pending_prepare'
|
|
let customerStatus = 'not_started'
|
|
|
|
if (totalTaskCount === 0) {
|
|
return {
|
|
totalTaskCount,
|
|
preparedTaskCount,
|
|
completedTaskCount,
|
|
resourceStatus,
|
|
customerStatus,
|
|
}
|
|
}
|
|
|
|
if (normalizedTasks.every((task) => isTaskFulfillmentCompleted(task))) {
|
|
resourceStatus = 'resource_ready'
|
|
customerStatus = 'customer_completed'
|
|
} else if (taskFulfillments.some((item) => ['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus))) {
|
|
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
|
|
customerStatus = 'customer_processing'
|
|
} else if (taskFulfillments.some((item) => item.customerStatus === 'waiting_customer')) {
|
|
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
|
|
customerStatus = 'waiting_customer'
|
|
} else if (taskFulfillments.some((item) => item.customerStatus === 'customer_exception')) {
|
|
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
|
|
customerStatus = 'customer_exception'
|
|
} else if (preparedTaskCount > 0) {
|
|
resourceStatus = 'resource_ready'
|
|
} else if (taskFulfillments.some((item) => ['manual_review', 'retry_pending'].includes(item.resourceStatus))) {
|
|
resourceStatus = 'resource_exception'
|
|
}
|
|
|
|
return {
|
|
totalTaskCount,
|
|
preparedTaskCount,
|
|
completedTaskCount,
|
|
resourceStatus,
|
|
customerStatus,
|
|
}
|
|
}
|
|
|
|
function buildTaskFulfillmentState(task: TaskRow | null | undefined): TaskFulfillmentState {
|
|
const normalizedStatus = normalizeTaskStatus(task?.task_status)
|
|
|
|
if (!normalizedStatus) {
|
|
return {
|
|
resourceStatus: 'pending_prepare',
|
|
customerStatus: 'not_started',
|
|
}
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.PENDING_PAYMENT) {
|
|
return { resourceStatus: 'pending_payment', customerStatus: 'not_started' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.PAID) {
|
|
return { resourceStatus: 'pending_prepare', customerStatus: 'not_started' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.PENDING_BINDING_PREPARE) {
|
|
return { resourceStatus: 'pending_prepare', customerStatus: 'not_started' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.WAITING_BINDING) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'waiting_customer' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.DISPATCHED_PENDING_RETURN) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'customer_completed' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.COMPLETED) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'customer_completed' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.MANUAL_REVIEW) {
|
|
return { resourceStatus: 'manual_review', customerStatus: 'customer_exception' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.RETRY_PENDING) {
|
|
return { resourceStatus: 'retry_pending', customerStatus: 'customer_exception' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.CLOSED) {
|
|
return { resourceStatus: 'closed', customerStatus: 'closed' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.EXPIRED) {
|
|
return { resourceStatus: 'expired', customerStatus: 'expired' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.LINK_GENERATED) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'waiting_customer' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.CLAIMED) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'link_opened' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.ROLE_CONFIRMED) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'customer_confirmed' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.REDEEMING) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'customer_processing' }
|
|
}
|
|
|
|
if (normalizedStatus === TASK_STATUS.REDEEMED) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'customer_completed' }
|
|
}
|
|
|
|
if (isTaskResourcePrepared(task)) {
|
|
return { resourceStatus: 'resource_ready', customerStatus: 'waiting_customer' }
|
|
}
|
|
|
|
return { resourceStatus: 'pending_prepare', customerStatus: 'not_started' }
|
|
}
|
|
|
|
function isTaskResourcePrepared(task: TaskRow | null | undefined): boolean {
|
|
return Boolean(task && getTaskPrimaryClaimTokenId(task))
|
|
}
|
|
|
|
function isTaskFulfillmentCompleted(task: TaskRow | null | undefined): boolean {
|
|
return isTaskFulfillmentCompletedStatus(task?.task_status)
|
|
}
|
|
|
|
function normalizeRecord(value: unknown): JsonRecord {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
|
}
|
|
|
|
function getTaskRetryCount(task: TaskRow | null | undefined): number {
|
|
return Number(task?.attempt_count || 0)
|
|
}
|
|
|
|
function resolveTaskDisplayRoleName(task: TaskRow): string {
|
|
return String(task.kuaishou_role_name || task.role_name || '').trim()
|
|
}
|
|
|
|
function resolveTaskDisplayRoleId(task: TaskRow): string {
|
|
return String(task.kuaishou_role_id || task.role_id || '').trim()
|
|
}
|