# Week 4:结果展示与导出 - 开发计划(云原生架构) > **文档版本:** v1.0 > **创建日期:** 2025-11-21 > **计划周期:** 2天(Day 16-17) > **架构原则:** ✅ 云原生优先 > **最后更新:** 2025-11-21 --- ## 📋 文档说明 本文档是 Week 4 功能开发的详细计划,遵循云原生开发规范,实现筛选结果的统计展示和Excel导出功能。 **核心目标**: - ✅ 统计概览(总数、纳入率、排除率、待复核) - ✅ PRISMA式排除原因统计 - ✅ 结果列表Tab切换与查看 - ✅ Excel批量导出 - ✅ 完整功能闭环(上传→筛选→复核→统计→导出) **架构原则**: - ✅ **云原生优先**:遵循[云原生开发规范](../../../04-开发规范/08-云原生开发规范.md) - ✅ **复用平台能力**:使用全局`prisma`、`logger`等 - ✅ **零文件落盘**:Excel前端生成或OSS存储 - ✅ **后端聚合计算**:统计数据后端聚合 --- ## 🎯 一、功能定位 ### 1.1 "审核工作台" vs "初筛结果" 区别 | 维度 | 审核工作台(ScreeningWorkbench) | 初筛结果(ScreeningResults) | |------|--------------------------------|---------------------------| | **定位** | 实时监控、冲突处理、人工复核 | 最终结果展示、统计分析、批量导出 | | **使用时机** | 筛选进行中 | 筛选完成后 | | **核心功能** | 进度轮询、逐条复核、冲突高亮 | 统计概览、PRISMA总结、批量导出 | | **表格形式** | 双行表格(DS + Qwen对比) | 单行表格(显示最终决策) | | **强调重点** | 工作流 | 结果汇总 | **比喻**: ``` 审核工作台 = 生产车间(正在筛选、复核) 初筛结果 = 成品仓库(已完成、统计、导出) ``` ### 1.2 用户流程 ``` 设置与启动 → 审核工作台 → 初筛结果 (配置) (实时监控) (结果汇总) ↓ ↓ ↓ 填写PICOS 逐条复核 批量导出 上传Excel 冲突处理 统计分析 ``` --- ## 🏗️ 二、技术架构(云原生) ### 2.1 核心架构决策 #### 决策1:数据获取策略 → 后端聚合API ✅ **方案A**(不采用):前端获取全量数据,前端计算 ```typescript // ❌ 不符合云原生:前端获取全量数据(可能上千条) const { data } = await aslApi.getScreeningResultsList(projectId, { pageSize: 9999 // 获取全部 }); // 前端计算统计... ``` **方案B**(采用):后端聚合API ✅ ```typescript // ✅ 符合云原生:后端聚合,减少网络传输 GET /api/v1/asl/projects/:projectId/statistics // 后端使用Prisma聚合 const stats = await prisma.aslScreeningResult.groupBy({ by: ['finalDecision', 'conflictStatus'], _count: true, where: { projectId } }); ``` **选择理由**: - ✅ 后端聚合,性能好 - ✅ 减少网络传输(从MB级降到KB级) - ✅ 可扩展性强(支持更复杂的统计) - ✅ 符合云原生"计算靠近数据"原则 --- #### 决策2:Excel导出策略 → 前端生成(MVP)✅ **方案A**(采用MVP):前端生成 ✅ ```typescript // ✅ 符合云原生:零文件落盘,完全在浏览器内存中 import * as XLSX from 'xlsx'; function exportToExcel(results) { const ws = XLSX.utils.json_to_sheet(exportData); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '筛选结果'); XLSX.writeFile(wb, 'screening-results.xlsx'); // 浏览器下载 } ``` **优点**: - ✅ **零文件落盘**(完全在浏览器内存中生成) - ✅ **无需后端存储**(不占用OSS空间) - ✅ **实时生成**(无异步等待) - ✅ **符合云原生原则**(避免Serverless文件操作) - ✅ **成本低**(不消耗后端资源) **限制**: - ⚠️ 适用数据量:<5000条 - ⚠️ 生成速度:<1000条约2-3秒 - ⚠️ 不支持复杂格式(多Sheet、图表) **方案B**(未来扩展):后端生成 + OSS存储 ⏸️ ```typescript // ⏸️ 技术债务:当数据量>5000条或需要复杂格式时 // 1. 后端生成Excel(内存中) import ExcelJS from 'exceljs'; const workbook = new ExcelJS.Workbook(); // ... 生成Excel // 2. ⭐ 上传到OSS(使用平台存储服务) import { storage } from '@/common/storage'; const buffer = await workbook.xlsx.writeBuffer(); const url = await storage.upload(`asl/exports/${Date.now()}.xlsx`, buffer); // 3. 返回OSS URL res.send({ success: true, url }); ``` **触发条件**: - 单次导出数据量 >5000条 - 需要复杂Excel格式(多Sheet、图表等) - 用户反馈前端导出卡顿 **记录位置**:[技术债务清单 - 优先级4](../../06-技术债务/技术债务清单.md) --- ### 2.2 云原生架构检查 基于[云原生开发规范](../../../04-开发规范/08-云原生开发规范.md),本开发计划遵循: | 检查项 | 要求 | 本计划实现 | 状态 | |--------|------|-----------|------| | **存储** | 使用`storage.upload()`,不用`fs.writeFile()` | Excel前端生成,零落盘 | ✅ | | **数据库** | 使用全局`prisma`实例 | 统计API使用全局`prisma` | ✅ | | **长任务** | 异步处理,不阻塞请求 | 统计API <500ms,无需异步 | ✅ | | **日志** | 使用`logger`,不用`console.log` | 后端使用`logger.info/error` | ✅ | | **配置** | 使用`process.env` | 无新增配置 | ✅ | | **计算** | 复杂计算后端完成 | 统计聚合后端完成 | ✅ | --- ## 📐 三、页面设计 ### 3.1 整体布局 ``` ┌─────────────────────────────────────────────────────────┐ │ 标题:标题摘要初筛 - 结果 │ │ 说明:筛选结果统计、PRISMA流程图、批量操作和导出 │ └─────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────┐ │ 📊 统计概览(4个卡片 + 待复核提示) │ │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ │ │ 总数 │ │ 已纳入│ │ 已排除│ │ 待复核│ │ │ │ 199 │ │ 85 │ │ 90 │ │ 24 │ │ │ │ 篇 │ │ 42.7% │ │ 45.2% │ │ 12.1% │ │ │ └───────┘ └───────┘ └───────┘ └───────┘ │ │ │ │ ⚠️ 提示:还有 24 篇文献待复核,请前往"审核工作台"处理 │ └─────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────┐ │ 📈 排除原因统计(柱状图) │ │ ┌─────────────────────────────────────────────┐ │ │ │ P不匹配(人群) ████████████████ 40篇 (44%) │ │ │ │ I不匹配(干预) ████████ 25篇 (28%) │ │ │ │ S不匹配(研究设计)████ 15篇 (17%) │ │ │ │ 其他原因 ██ 10篇 (11%) │ │ │ └─────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────┐ │ 📋 结果列表(Tabs + 单行表格 + 批量操作) │ │ ┌─────────────────────────────────────────────────┐ │ │ │ [全部 199] [已纳入 85] [已排除 90] [待复核 24] │ │ │ ├─────────────────────────────────────────────────┤ │ │ │ [导出全部] [导出当前页] [导出选中项] │ │ │ ├─────────────────────────────────────────────────┤ │ │ │ ☑ 序号 | 标题 | 最终决策 | 排除原因 | 操作 │ │ │ │ ☐ 1 | ... | 已纳入 | - | [查看] │ │ │ │ ☐ 2 | ... | 已排除 | P不匹配 | [查看] │ │ │ │ ☐ 3 | ... | 待复核 | 冲突 | [复核] │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 3.2 表格设计 **列定义**(单行表格,区别于审核工作台的双行): | 列名 | 宽度 | 说明 | |------|------|------| | 选择 | 50px | Checkbox多选 | | 序号 | 60px | 行号 | | 文献标题 | 400px | Tooltip显示全文 | | 最终决策 | 100px | Tag显示(纳入/排除/待定) | | 排除原因 | 150px | 显示具体原因 | | 置信度 | 80px | DeepSeek置信度 | | 操作 | 100px | 查看详情按钮 | **关键区别**: - **审核工作台**:双行表格,显示DS+Qwen对比,强调冲突 - **初筛结果**:**单行表格**,显示最终决策,强调结果 --- ## 🔧 四、后端开发 ### 4.1 新增统计API #### API设计 ``` GET /api/v1/asl/projects/:projectId/statistics ``` #### 请求参数 ``` 无 ``` #### 响应格式 ```json { "success": true, "data": { "total": 199, "included": 85, "excluded": 90, "pending": 24, "conflict": 24, "reviewed": 175, "exclusionReasons": { "P不匹配(人群)": 40, "I不匹配(干预)": 25, "S不匹配(研究设计)": 15, "其他原因": 10 }, "includedRate": "42.7", "excludedRate": "45.2", "pendingRate": "12.1" } } ``` #### 实现代码 ```typescript // backend/src/modules/asl/controllers/screeningController.ts /** * 获取项目筛选统计数据(云原生:后端聚合) * GET /api/v1/asl/projects/:projectId/statistics */ export async function getProjectStatistics( request: FastifyRequest<{ Params: { projectId: string } }>, reply: FastifyReply ) { try { const userId = (request as any).userId || 'asl-test-user-001'; const { projectId } = request.params; // 1. 验证项目归属 const project = await prisma.aslScreeningProject.findFirst({ where: { id: projectId, userId }, }); if (!project) { return reply.status(404).send({ error: 'Project not found' }); } // 2. ⭐ 云原生:使用Prisma聚合查询(并行) const [ total, includedCount, excludedCount, pendingCount, conflictCount, reviewedCount ] = await Promise.all([ prisma.aslScreeningResult.count({ where: { projectId } }), prisma.aslScreeningResult.count({ where: { projectId, finalDecision: 'include' } }), prisma.aslScreeningResult.count({ where: { projectId, finalDecision: 'exclude' } }), prisma.aslScreeningResult.count({ where: { projectId, finalDecision: null } }), prisma.aslScreeningResult.count({ where: { projectId, conflictStatus: 'conflict', finalDecision: null } }), prisma.aslScreeningResult.count({ where: { projectId, NOT: { finalDecision: null } } }), ]); // 3. 查询排除结果(用于统计原因) const excludedResults = await prisma.aslScreeningResult.findMany({ where: { projectId, OR: [ { finalDecision: 'exclude' }, { finalDecision: null, dsConclusion: 'exclude' } ] }, select: { exclusionReason: true, dsPJudgment: true, dsIJudgment: true, dsCJudgment: true, dsSJudgment: true, } }); // 4. 分析排除原因 const exclusionReasons: Record = {}; excludedResults.forEach(result => { const reason = result.exclusionReason || extractAutoReason(result); exclusionReasons[reason] = (exclusionReasons[reason] || 0) + 1; }); // 5. 返回统计数据 return reply.send({ success: true, data: { total, included: includedCount, excluded: excludedCount, pending: pendingCount, conflict: conflictCount, reviewed: reviewedCount, exclusionReasons, includedRate: total > 0 ? ((includedCount / total) * 100).toFixed(1) : '0.0', excludedRate: total > 0 ? ((excludedCount / total) * 100).toFixed(1) : '0.0', pendingRate: total > 0 ? ((pendingCount / total) * 100).toFixed(1) : '0.0', } }); } catch (error) { logger.error('Failed to get statistics', { error }); return reply.status(500).send({ error: 'Failed to get statistics', }); } } /** * 辅助函数:从AI判断中提取排除原因 */ function extractAutoReason(result: any): string { if (result.dsPJudgment === 'mismatch') return 'P不匹配(人群)'; if (result.dsIJudgment === 'mismatch') return 'I不匹配(干预)'; if (result.dsCJudgment === 'mismatch') return 'C不匹配(对照)'; if (result.dsSJudgment === 'mismatch') return 'S不匹配(研究设计)'; return '其他原因'; } ``` #### 路由注册 ```typescript // backend/src/modules/asl/routes/index.ts // 添加到路由注册 fastify.get( '/projects/:projectId/statistics', screeningController.getProjectStatistics ); ``` --- ## 💻 五、前端开发 ### 5.1 API客户端 ```typescript // frontend-v2/src/modules/asl/api/index.ts /** * 获取项目统计数据 */ export async function getProjectStatistics( projectId: string ): Promise> { return request(`/projects/${projectId}/statistics`); } ``` ### 5.2 类型定义 ```typescript // frontend-v2/src/modules/asl/types/index.ts /** * 项目统计数据 */ export interface ProjectStatistics { total: number; included: number; excluded: number; pending: number; conflict: number; reviewed: number; exclusionReasons: Record; includedRate: string; excludedRate: string; pendingRate: string; } ``` ### 5.3 Excel导出工具 ```typescript // frontend-v2/src/modules/asl/utils/excelExport.ts import * as XLSX from 'xlsx'; import { ScreeningResult } from '../types'; /** * 导出筛选结果到Excel(云原生:前端生成,零文件落盘) * * @param results 筛选结果数组 * @param options 导出选项 */ export function exportScreeningResults( results: ScreeningResult[], options: { filter?: 'all' | 'included' | 'excluded' | 'pending'; projectName?: string; } = {} ) { // 1. 准备导出数据 const exportData = results.map((r, idx) => ({ '序号': idx + 1, '文献标题': r.literature.title, '摘要': r.literature.abstract || '', '作者': r.literature.authors || '', '期刊': r.literature.journal || '', '发表年份': r.literature.publicationYear || '', 'PMID': r.literature.pmid || '', 'DOI': r.literature.doi || '', 'DeepSeek决策': r.dsConclusion || '', 'DeepSeek置信度': r.dsConfidence ? `${(r.dsConfidence * 100).toFixed(0)}%` : '', 'DeepSeek理由': r.dsReason || '', 'Qwen决策': r.qwenConclusion || '', 'Qwen置信度': r.qwenConfidence ? `${(r.qwenConfidence * 100).toFixed(0)}%` : '', 'Qwen理由': r.qwenReason || '', '是否冲突': r.conflictStatus === 'conflict' ? '是' : '否', '最终决策': r.finalDecision || '待定', '排除原因': r.exclusionReason || '', '复核人': r.finalDecisionBy || '', '复核时间': r.finalDecisionAt ? new Date(r.finalDecisionAt).toLocaleString('zh-CN') : '', })); // 2. ⭐ 生成Excel(完全在内存中,零文件落盘) const ws = XLSX.utils.json_to_sheet(exportData); // 设置列宽 ws['!cols'] = [ { wch: 6 }, // 序号 { wch: 50 }, // 标题 { wch: 60 }, // 摘要 { wch: 30 }, // 作者 { wch: 30 }, // 期刊 { wch: 10 }, // 年份 { wch: 12 }, // PMID { wch: 25 }, // DOI { wch: 12 }, // DS决策 { wch: 12 }, // DS置信度 { wch: 40 }, // DS理由 { wch: 12 }, // Qwen决策 { wch: 12 }, // Qwen置信度 { wch: 40 }, // Qwen理由 { wch: 10 }, // 冲突 { wch: 12 }, // 最终决策 { wch: 30 }, // 排除原因 { wch: 15 }, // 复核人 { wch: 20 }, // 复核时间 ]; const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '筛选结果'); // 3. 生成文件名 const timestamp = new Date().toISOString().slice(0, 10); const filterSuffix = options.filter && options.filter !== 'all' ? `_${options.filter}` : ''; const filename = `${options.projectName || '筛选结果'}${filterSuffix}_${timestamp}.xlsx`; // 4. ⭐ 触发浏览器下载(零文件落盘) XLSX.writeFile(wb, filename); } ``` ### 5.4 初筛结果页面 ```typescript // frontend-v2/src/modules/asl/pages/ScreeningResults.tsx import { useState } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { Card, Statistic, Row, Col, Tabs, Table, Button, Alert, Progress, message, Checkbox, Tooltip } from 'antd'; import { DownloadOutlined, CheckCircleOutlined, CloseCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'; import { useQuery } from '@tanstack/react-query'; import * as aslApi from '../api'; import { exportScreeningResults } from '../utils/excelExport'; import { ConclusionTag } from '../components/ConclusionTag'; const ScreeningResults = () => { const { projectId } = useParams<{ projectId: string }>(); const [searchParams, setSearchParams] = useSearchParams(); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const activeTab = searchParams.get('tab') || 'all'; const page = parseInt(searchParams.get('page') || '1', 10); const pageSize = 20; // 1. ⭐ 获取统计数据(云原生:后端聚合) const { data: statsData, isLoading: statsLoading } = useQuery({ queryKey: ['projectStatistics', projectId], queryFn: () => aslApi.getProjectStatistics(projectId!), enabled: !!projectId, }); const stats = statsData?.data; // 2. 获取结果列表(分页) const { data: resultsData, isLoading: resultsLoading } = useQuery({ queryKey: ['screeningResults', projectId, activeTab, page], queryFn: () => aslApi.getScreeningResultsList(projectId!, { page, pageSize, filter: activeTab, }), enabled: !!projectId, }); // 3. ⭐ 导出Excel(前端生成,云原生) const handleExport = async (filter: string = 'all') => { try { message.loading('正在生成Excel...', 0); // 获取全量数据(用于导出) const { data } = await aslApi.getScreeningResultsList(projectId!, { page: 1, pageSize: 9999, filter, }); if (data.items.length === 0) { message.warning('没有可导出的数据'); return; } // ⭐ 前端生成Excel(零文件落盘) exportScreeningResults(data.items, { filter, projectName: `项目${projectId!.slice(0, 8)}`, }); message.destroy(); message.success(`成功导出 ${data.items.length} 条记录`); } catch (error) { message.destroy(); message.error('导出失败: ' + (error as Error).message); } }; // 4. 批量导出选中项 const handleExportSelected = () => { if (selectedRowKeys.length === 0) { message.warning('请先选择要导出的记录'); return; } const selectedResults = resultsData?.data.items.filter( r => selectedRowKeys.includes(r.id) ) || []; exportScreeningResults(selectedResults, { projectName: `项目${projectId?.slice(0, 8)}_选中`, }); message.success(`成功导出 ${selectedResults.length} 条记录`); }; // 表格列定义、Tab配置等... // (完整代码见实际实现) }; export default ScreeningResults; ``` --- ## 📅 六、开发任务分解 ### Phase 1:后端统计API(Day 16上午)⏱️ 2小时 **任务**: 1. 在 `screeningController.ts` 中实现 `getProjectStatistics` 2. 使用Prisma聚合查询(并行查询优化) 3. 实现排除原因提取逻辑 `extractAutoReason` 4. 在 `routes/index.ts` 中注册路由 5. Postman测试API **验收标准**: - ✅ API返回正确统计数据 - ✅ 性能良好(<500ms) - ✅ 符合云原生原则(后端聚合) **文件清单**: - `backend/src/modules/asl/controllers/screeningController.ts` - `backend/src/modules/asl/routes/index.ts` --- ### Phase 2:前端API客户端(Day 16上午)⏱️ 30分钟 **任务**: 1. 在 `api/index.ts` 中添加 `getProjectStatistics` 2. 在 `types/index.ts` 中添加 `ProjectStatistics` 类型 **验收标准**: - ✅ API调用正常 - ✅ TypeScript类型正确 **文件清单**: - `frontend-v2/src/modules/asl/api/index.ts` - `frontend-v2/src/modules/asl/types/index.ts` --- ### Phase 3:统计概览卡片(Day 16上午)⏱️ 1.5小时 **任务**: 1. 实现统计卡片组件(4个卡片) 2. 实现"待复核"提示Alert 3. 实现PRISMA排除统计(柱状图) **验收标准**: - ✅ 统计数据正确显示 - ✅ 排除原因柱状图清晰 - ✅ "待复核"提示醒目 **文件清单**: - `frontend-v2/src/modules/asl/pages/ScreeningResults.tsx` --- ### Phase 4:结果列表Tab(Day 16下午)⏱️ 3小时 **任务**: 1. 实现Tab切换(全部/已纳入/已排除/待复核) 2. 创建单行表格(区别于审核工作台) 3. 实现Checkbox多选 4. 实现详情查看Modal(复用审核工作台的Drawer) **验收标准**: - ✅ Tab切换正常 - ✅ 表格数据正确 - ✅ 可多选行 - ✅ 可查看详情 **文件清单**: - `frontend-v2/src/modules/asl/pages/ScreeningResults.tsx` --- ### Phase 5:Excel导出(Day 17上午)⏱️ 2小时 **任务**: 1. 创建 `excelExport.ts` 工具文件 2. 实现前端导出逻辑(使用 `xlsx`) 3. 添加导出按钮(导出全部/导出当前页/导出选中项) 4. 支持过滤导出(全部/仅纳入/仅排除) **验收标准**: - ✅ 可导出Excel - ✅ 数据完整 - ✅ 零文件落盘(云原生) - ✅ 生成速度<3秒(<1000条) **文件清单**: - `frontend-v2/src/modules/asl/utils/excelExport.ts` - `frontend-v2/src/modules/asl/pages/ScreeningResults.tsx` --- ### Phase 6:集成测试与优化(Day 17下午-Day 18)⏱️ 4小时 **任务**: 1. 完整流程测试(上传→筛选→复核→查看结果→导出) 2. 异常场景测试(无数据、网络错误) 3. UI/UX优化(加载状态、错误提示) 4. 性能测试(统计API、Excel导出) 5. 云原生规范检查 **验收标准**: - ✅ 流程完整无阻塞 - ✅ 异常处理完善 - ✅ 性能达标 - ✅ 符合云原生规范 --- ## ✅ 七、验收标准 ### 7.1 功能验收 - [✅] 统计概览卡片正确显示(总数、纳入、排除、待复核) - [✅] 排除原因统计准确(柱状图) - [✅] 待复核提示醒目 - [✅] Tab切换正常(全部/已纳入/已排除/待复核) - [✅] 表格数据正确(单行表格) - [✅] Checkbox多选正常 - [✅] 可查看详情 - [✅] 可导出Excel(全部/选中) - [✅] Excel数据完整 ### 7.2 性能验收 - [✅] 统计API响应时间 <500ms - [✅] Excel导出(<1000条)<3秒 - [✅] 表格分页加载正常 ### 7.3 云原生验收 基于[云原生开发规范检查清单](../../../04-开发规范/08-云原生开发规范.md): **后端API**: - [✅] 使用全局 `prisma` 实例(不new PrismaClient) - [✅] 统计使用Prisma聚合查询(不查全量数据) - [✅] 无本地文件存储(无fs.writeFile) - [✅] 使用 `logger` 记录日志(不用console.log) - [✅] 统一错误处理 **前端实现**: - [✅] Excel前端生成(零文件落盘) - [✅] 使用 `xlsx` 库(成熟稳定) - [✅] 友好的用户提示 --- ## 📊 八、时间估算 | 阶段 | 任务 | 预计耗时 | 负责人 | |------|------|---------|--------| | Phase 1 | 后端统计API | 2小时 | 后端开发 | | Phase 2 | 前端API客户端 | 0.5小时 | 前端开发 | | Phase 3 | 统计概览 | 1.5小时 | 前端开发 | | Phase 4 | 结果列表Tab | 3小时 | 前端开发 | | Phase 5 | Excel导出 | 2小时 | 前端开发 | | Phase 6 | 集成测试 | 4小时 | 全栈开发 | | **总计** | | **13小时** | **约2天** | --- ## 🔗 九、相关文档 - [云原生开发规范](../../../04-开发规范/08-云原生开发规范.md) - 必读 - [任务分解](./03-任务分解.md) - Week 4任务清单 - [模块当前状态](../00-模块当前状态与开发指南.md) - 模块真实状态 - [技术债务清单](../06-技术债务/技术债务清单.md) - Excel后端导出方案 - [数据库设计](../02-技术设计/01-数据库设计.md) - 数据表结构 - [API设计规范](../02-技术设计/02-API设计规范.md) - API规范 --- ## 📝 十、技术债务记录 ### 债务1:Excel后端导出优化 **触发条件**: - 单次导出数据量 >5000条 - 需要复杂Excel格式(多Sheet、图表等) - 用户反馈前端导出卡顿 **解决方案**: - 后端生成Excel(使用 `ExcelJS`) - 上传到OSS(使用 `storage.upload()`) - 返回下载URL **记录位置**:[技术债务清单 - 优先级4](../06-技术债务/技术债务清单.md) **预计耗时**:1-2天 --- ## 💡 十一、开发建议 ### 对开发人员 1. **先阅读云原生规范**:[云原生开发规范](../../../04-开发规范/08-云原生开发规范.md) 2. **复用平台能力**:使用全局`prisma`、`logger` 3. **避免文件落盘**:Excel前端生成 4. **后端聚合计算**:统计数据后端完成 5. **性能优化**:Prisma聚合查询使用并行 ### 对AI助手 1. **优先云原生**:所有设计优先考虑云原生架构 2. **参考现有代码**:复用审核工作台的组件 3. **注意区别**:初筛结果是单行表格,审核工作台是双行表格 4. **测试充分**:完整流程测试 --- **文档维护者**:AI智能文献开发团队 **最后更新**:2025-11-21 **文档状态**:✅ 已确认,可开始开发 **开始时间**:待定