/** 店铺数据任务列表模型(任务 110):解析按店铺分组的任务列表与结果文件行,纯逻辑。 */ import { unwrap } from '../../api/envelope.ts' import { SHOP_DATA_DEFAULT_PAGE_SIZE } from './shop-data-filter.ts' import { normalizeTaskStatus, type ResultId, type TaskId, type TaskStatus } from './task-model.ts' export interface ShopDataResultRow { taskId?: TaskId taskNo: string resultId: ResultId username: string shopName: string shopId: string groupName: string status: TaskStatus success?: boolean | null error: string countryCodes: string[] filename: string fileUrl: string fileReady: boolean fileStatus: string fileError: string fileSize?: number rowCount?: number createdAt?: string updatedAt?: string finishedAt?: string } export interface ShopDataTaskGroup { shopName: string shopId: string groupName: string latestCreatedAt?: string results: ShopDataResultRow[] } export interface ShopDataTaskPageResult { items: ShopDataTaskGroup[] total: number page: number pageSize: number } export function emptyShopDataTaskPage(): ShopDataTaskPageResult { return { items: [], total: 0, page: 1, pageSize: SHOP_DATA_DEFAULT_PAGE_SIZE } } function text(value: unknown): string { return typeof value === 'string' ? value.trim() : '' } function positiveNumber(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined } /** 解析单条结果文件行;缺有效 result_id 视为无效。 */ export function toShopDataResultRow(raw: unknown): ShopDataResultRow | null { if (!raw || typeof raw !== 'object') return null const r = raw as Record const rawResultId = r.result_id const resultId = ( typeof rawResultId === 'number' && Number.isFinite(rawResultId) && rawResultId >= 1 ? String(Math.floor(rawResultId)) : text(rawResultId) ) as ResultId if (!resultId) return null const codesRaw = r.country_codes ?? r.countryCodes const countryCodes = Array.isArray(codesRaw) ? (codesRaw as unknown[]).map((code) => text(code)).filter(Boolean) : [] const row: ShopDataResultRow = { resultId, taskNo: text(r.task_no ?? r.taskNo), username: text(r.username), shopName: text(r.shop_name ?? r.shopName), shopId: text(r.shop_id ?? r.shopId), groupName: text(r.group_name ?? r.groupName), status: normalizeTaskStatus(r.status), error: text(r.error), countryCodes, filename: text(r.output_filename ?? r.result_filename ?? r.filename), fileUrl: text(r.result_file_url ?? r.file_url ?? r.fileUrl), fileReady: r.file_ready === true || r.fileReady === true, fileStatus: text(r.file_status ?? r.fileStatus), fileError: text(r.file_error ?? r.fileError), } const taskId = text(r.task_id) if (taskId) row.taskId = taskId as TaskId if (typeof r.success === 'boolean' || r.success === null) row.success = r.success as boolean | null const fileSize = positiveNumber(r.file_size ?? r.fileSize) if (fileSize !== undefined) row.fileSize = fileSize const rowCount = positiveNumber(r.row_count ?? r.rowCount) if (rowCount !== undefined) row.rowCount = rowCount const createdAt = text(r.created_at ?? r.createdAt) if (createdAt) row.createdAt = createdAt const updatedAt = text(r.updated_at ?? r.updatedAt) if (updatedAt) row.updatedAt = updatedAt const finishedAt = text(r.finished_at ?? r.finishedAt) if (finishedAt) row.finishedAt = finishedAt return row } /** 解析单店铺任务组;results 默认空数组。 */ export function toShopDataTaskGroup(raw: unknown): ShopDataTaskGroup | null { if (!raw || typeof raw !== 'object') return null const r = raw as Record const group: ShopDataTaskGroup = { shopName: text(r.shop_name ?? r.shopName) || '未命名', shopId: text(r.shop_id ?? r.shopId), groupName: text(r.group_name ?? r.groupName), results: Array.isArray(r.results) ? r.results.map((rawRow) => toShopDataResultRow(rawRow)).filter((row): row is ShopDataResultRow => row !== null) : [], } const latest = text(r.latest_created_at ?? r.latestCreatedAt) if (latest) group.latestCreatedAt = latest return group } /** 归一化店铺数据任务分页负载为前端结果;缺省字段回默认。 */ export function parseShopDataTaskPage(payload: unknown): ShopDataTaskPageResult { const out = emptyShopDataTaskPage() const core = unwrap(payload) if (!core || typeof core !== 'object') return out const record = core as Record if (Array.isArray(record.items)) { out.items = record.items .map((raw) => toShopDataTaskGroup(raw)) .filter((group): group is ShopDataTaskGroup => group !== null) } if (typeof record.total === 'number') out.total = Math.floor(record.total) if (typeof record.page === 'number' && record.page >= 1) out.page = Math.floor(record.page) const rawSize = record.page_size ?? record.pageSize if (typeof rawSize === 'number' && rawSize >= 1) out.pageSize = Math.floor(rawSize) return out }