/** * 任务中心 Prisma 行 → 共享 DTO 映射(A6-BE-04) * * - 列表 / 详情显式 select(DB-10:列表不返回 payload 等大 JSONB); * - 金额 `estimated_cost` 为 Decimal(14,4),出参统一转 number(元,2 位小数展示由前端处理); * - DB 存英文 code,未知 code 视为脏数据直接抛错(由全局过滤器转 500,不静默降级成错误状态)。 */ import { type MediaTaskDetail, type MediaTaskItem, type MediaTaskSummary, TASK_ITEM_STATUSES, TASK_STATUSES, TASK_TYPES, type TaskItemStatus, type TaskStatus, type TaskType, } from '@lingxi/shared'; import type { Prisma } from '@prisma/client'; /** 摘要 select:列表与详情共用(详情再追加 payload / 取消 / 错误字段) */ export const MEDIA_TASK_SUMMARY_SELECT = { id: true, type: true, status: true, title: true, customerId: true, customerName: true, itemTotal: true, itemSucceeded: true, itemFailed: true, itemSkipped: true, progress: true, estimatedItems: true, estimatedCost: true, estimatedSeconds: true, createdBy: true, createdByName: true, createdAt: true, startedAt: true, finishedAt: true, } satisfies Prisma.MediaTaskSelect; export type MediaTaskSummaryRow = Prisma.MediaTaskGetPayload<{ select: typeof MEDIA_TASK_SUMMARY_SELECT; }>; export const MEDIA_TASK_DETAIL_SELECT = { ...MEDIA_TASK_SUMMARY_SELECT, payloadJson: true, cancelRequestedAt: true, lastErrorCode: true, lastErrorMessage: true, } satisfies Prisma.MediaTaskSelect; export type MediaTaskDetailRow = Prisma.MediaTaskGetPayload<{ select: typeof MEDIA_TASK_DETAIL_SELECT; }>; /** 任务项 select:列表 / 详情只给状态与失败原因,不含产物引用 */ export const MEDIA_ITEM_SELECT = { id: true, seq: true, status: true, failureCode: true, failureMessage: true, attempts: true, startedAt: true, finishedAt: true, } satisfies Prisma.MediaItemSelect; export type MediaItemRow = Prisma.MediaItemGetPayload<{ select: typeof MEDIA_ITEM_SELECT }>; /** DB 字符串 → 任务类型(未知视为脏数据) */ export function parseTaskType(value: string): TaskType { const found = TASK_TYPES.find((type) => type === value); if (found === undefined) throw new Error(`任务类型非法:${value}`); return found; } /** DB 字符串 → 任务状态(未知视为脏数据) */ export function parseTaskStatus(value: string): TaskStatus { const found = TASK_STATUSES.find((status) => status === value); if (found === undefined) throw new Error(`任务状态非法:${value}`); return found; } /** DB 字符串 → 任务项状态(未知视为脏数据) */ export function parseTaskItemStatus(value: string): TaskItemStatus { const found = TASK_ITEM_STATUSES.find((status) => status === value); if (found === undefined) throw new Error(`任务项状态非法:${value}`); return found; } function optionalIso(value: Date | null): string | undefined { return value === null ? undefined : value.toISOString(); } /** JsonB → 普通对象(只接受 object;异常结构回退空对象,详情不因此 500) */ export function toJsonObject(value: Prisma.JsonValue): Record { if (typeof value === 'object' && value !== null && !Array.isArray(value)) { return value; } return {}; } export function toMediaTaskSummary(row: MediaTaskSummaryRow): MediaTaskSummary { const startedAt = optionalIso(row.startedAt); const finishedAt = optionalIso(row.finishedAt); return { id: row.id, type: parseTaskType(row.type), status: parseTaskStatus(row.status), title: row.title, customerId: row.customerId, customerName: row.customerName, itemTotal: row.itemTotal, itemSucceeded: row.itemSucceeded, itemFailed: row.itemFailed, itemSkipped: row.itemSkipped, progress: row.progress, estimatedItems: row.estimatedItems, estimatedCost: Number(row.estimatedCost), estimatedSeconds: row.estimatedSeconds, createdBy: row.createdBy, createdByName: row.createdByName, createdAt: row.createdAt.toISOString(), ...(startedAt === undefined ? {} : { startedAt }), ...(finishedAt === undefined ? {} : { finishedAt }), }; } export function toMediaTaskItem(row: MediaItemRow): MediaTaskItem { const startedAt = optionalIso(row.startedAt); const finishedAt = optionalIso(row.finishedAt); return { id: row.id, seq: row.seq, status: parseTaskItemStatus(row.status), ...(row.failureCode === null ? {} : { failureCode: row.failureCode }), ...(row.failureMessage === null ? {} : { failureMessage: row.failureMessage }), attempts: row.attempts, ...(startedAt === undefined ? {} : { startedAt }), ...(finishedAt === undefined ? {} : { finishedAt }), }; } export function toMediaTaskDetail( row: MediaTaskDetailRow, items: MediaItemRow[], itemsNextCursor: string | null, ): MediaTaskDetail { const cancelRequestedAt = optionalIso(row.cancelRequestedAt); return { ...toMediaTaskSummary(row), payload: toJsonObject(row.payloadJson), ...(cancelRequestedAt === undefined ? {} : { cancelRequestedAt }), ...(row.lastErrorCode === null ? {} : { lastErrorCode: row.lastErrorCode }), ...(row.lastErrorMessage === null ? {} : { lastErrorMessage: row.lastErrorMessage }), items: items.map((item) => toMediaTaskItem(item)), itemsNextCursor, }; }