# 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<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 '其他原因';
}
```

#### 路由注册
```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<ApiResponse<ProjectStatistics>> {
  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<string, number>;
  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<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：后端统计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  
**文档状态**：✅ 已确认，可开始开发  
**开始时间**：待定


