Files
AIclinicalresearch/docs/03-业务模块/ASL-AI智能文献/04-开发计划/04-Week4-结果展示与导出开发计划.md
HaHafeng beb7f7f559 feat(asl): Implement full-text screening core LLM service and validation system (Day 1-3)
Core Components:
- PDFStorageService with Dify/OSS adapters
- LLM12FieldsService with Nougat-first + dual-model + 3-layer JSON parsing
- PromptBuilder for dynamic prompt assembly
- MedicalLogicValidator with 5 rules + fault tolerance
- EvidenceChainValidator for citation integrity
- ConflictDetectionService for dual-model comparison

Prompt Engineering:
- System Prompt (6601 chars, Section-Aware strategy)
- User Prompt template (PICOS context injection)
- JSON Schema (12 fields constraints)
- Cochrane standards (not loaded in MVP)

Key Innovations:
- 3-layer JSON parsing (JSON.parse + json-repair + code block extraction)
- Promise.allSettled for dual-model fault tolerance
- safeGetFieldValue for robust field extraction
- Mixed CN/EN token calculation

Integration Tests:
- integration-test.ts (full test)
- quick-test.ts (quick test)
- cached-result-test.ts (fault tolerance test)

Documentation Updates:
- Development record (Day 2-3 summary)
- Quality assurance strategy (full-text screening)
- Development plan (progress update)
- Module status (v1.1 update)
- Technical debt (10 new items)

Test Results:
- JSON parsing success rate: 100%
- Medical logic validation: 5/5 passed
- Dual-model parallel processing: OK
- Cost per PDF: CNY 0.10

Files: 238 changed, 14383 insertions(+), 32 deletions(-)
Docs: docs/03-涓氬姟妯″潡/ASL-AI鏅鸿兘鏂囩尞/05-寮€鍙戣褰?2025-11-22_Day2-Day3_LLM鏈嶅姟涓庨獙璇佺郴缁熷紑鍙?md
2025-11-22 22:21:12 +08:00

27 KiB
Raw Blame History

Week 4结果展示与导出 - 开发计划(云原生架构)

文档版本: v1.0
创建日期: 2025-11-21
计划周期: 2天Day 16-17
架构原则: 云原生优先
最后更新: 2025-11-21


📋 文档说明

本文档是 Week 4 功能开发的详细计划遵循云原生开发规范实现筛选结果的统计展示和Excel导出功能。

核心目标

  • 统计概览(总数、纳入率、排除率、待复核)
  • PRISMA式排除原因统计
  • 结果列表Tab切换与查看
  • Excel批量导出
  • 完整功能闭环(上传→筛选→复核→统计→导出)

架构原则

  • 云原生优先:遵循云原生开发规范
  • 复用平台能力:使用全局prismalogger
  • 零文件落盘Excel前端生成或OSS存储
  • 后端聚合计算:统计数据后端聚合

🎯 一、功能定位

1.1 "审核工作台" vs "初筛结果" 区别

维度 审核工作台ScreeningWorkbench 初筛结果ScreeningResults
定位 实时监控、冲突处理、人工复核 最终结果展示、统计分析、批量导出
使用时机 筛选进行中 筛选完成后
核心功能 进度轮询、逐条复核、冲突高亮 统计概览、PRISMA总结、批量导出
表格形式 双行表格DS + Qwen对比 单行表格(显示最终决策)
强调重点 工作流 结果汇总

比喻

审核工作台 = 生产车间(正在筛选、复核)
初筛结果   = 成品仓库(已完成、统计、导出)

1.2 用户流程

设置与启动 → 审核工作台 → 初筛结果
   (配置)      (实时监控)    (结果汇总)
    ↓            ↓            ↓
  填写PICOS    逐条复核      批量导出
  上传Excel    冲突处理      统计分析

🏗️ 二、技术架构(云原生)

2.1 核心架构决策

决策1数据获取策略 → 后端聚合API

方案A(不采用):前端获取全量数据,前端计算

// ❌ 不符合云原生:前端获取全量数据(可能上千条)
const { data } = await aslApi.getScreeningResultsList(projectId, {
  pageSize: 9999  // 获取全部
});
// 前端计算统计...

方案B采用后端聚合API

// ✅ 符合云原生:后端聚合,减少网络传输
GET /api/v1/asl/projects/:projectId/statistics

// 后端使用Prisma聚合
const stats = await prisma.aslScreeningResult.groupBy({
  by: ['finalDecision', 'conflictStatus'],
  _count: true,
  where: { projectId }
});

选择理由

  • 后端聚合,性能好
  • 减少网络传输从MB级降到KB级
  • 可扩展性强(支持更复杂的统计)
  • 符合云原生"计算靠近数据"原则

决策2Excel导出策略 → 前端生成MVP

方案A采用MVP前端生成

// ✅ 符合云原生:零文件落盘,完全在浏览器内存中
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存储 ⏸️

// ⏸️ 技术债务:当数据量>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


2.2 云原生架构检查

基于云原生开发规范,本开发计划遵循:

检查项 要求 本计划实现 状态
存储 使用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

请求参数

响应格式

{
  "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"
  }
}

实现代码

// 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<string, number> = {};
    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 '其他原因';
}

路由注册

// backend/src/modules/asl/routes/index.ts

// 添加到路由注册
fastify.get(
  '/projects/:projectId/statistics',
  screeningController.getProjectStatistics
);

💻 五、前端开发

5.1 API客户端

// frontend-v2/src/modules/asl/api/index.ts

/**
 * 获取项目统计数据
 */
export async function getProjectStatistics(
  projectId: string
): Promise<ApiResponse<ProjectStatistics>> {
  return request(`/projects/${projectId}/statistics`);
}

5.2 类型定义

// 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<string, number>;
  includedRate: string;
  excludedRate: string;
  pendingRate: string;
}

5.3 Excel导出工具

// 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 初筛结果页面

// 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<string[]>([]);
  
  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后端统计APIDay 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结果列表TabDay 16下午⏱️ 3小时

任务

  1. 实现Tab切换全部/已纳入/已排除/待复核)
  2. 创建单行表格(区别于审核工作台)
  3. 实现Checkbox多选
  4. 实现详情查看Modal复用审核工作台的Drawer

验收标准

  • Tab切换正常
  • 表格数据正确
  • 可多选行
  • 可查看详情

文件清单

  • frontend-v2/src/modules/asl/pages/ScreeningResults.tsx

Phase 5Excel导出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 云原生验收

基于云原生开发规范检查清单

后端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天

🔗 九、相关文档


📝 十、技术债务记录

债务1Excel后端导出优化

触发条件

  • 单次导出数据量 >5000条
  • 需要复杂Excel格式多Sheet、图表等
  • 用户反馈前端导出卡顿

解决方案

  • 后端生成Excel使用 ExcelJS
  • 上传到OSS使用 storage.upload()
  • 返回下载URL

记录位置技术债务清单 - 优先级4

预计耗时1-2天


💡 十一、开发建议

对开发人员

  1. 先阅读云原生规范云原生开发规范
  2. 复用平台能力:使用全局prismalogger
  3. 避免文件落盘Excel前端生成
  4. 后端聚合计算:统计数据后端完成
  5. 性能优化Prisma聚合查询使用并行

对AI助手

  1. 优先云原生:所有设计优先考虑云原生架构
  2. 参考现有代码:复用审核工作台的组件
  3. 注意区别:初筛结果是单行表格,审核工作台是双行表格
  4. 测试充分:完整流程测试

文档维护者AI智能文献开发团队
最后更新2025-11-21
文档状态 已确认,可开始开发
开始时间:待定