Files
AIclinicalresearch/docs/03-业务模块/DC-数据清洗整理/04-开发计划/DC模块Tool-B开发计划.md
HaHafeng 66255368b7 feat(admin): Add user management and upgrade to module permission system
Features - User Management (Phase 4.1):
- Database: Add user_modules table for fine-grained module permissions
- Database: Add 4 user permissions (view/create/edit/delete) to role_permissions
- Backend: UserService (780 lines) - CRUD with tenant isolation
- Backend: UserController + UserRoutes (648 lines) - 13 API endpoints
- Backend: Batch import users from Excel
- Frontend: UserListPage (412 lines) - list/filter/search/pagination
- Frontend: UserFormPage (341 lines) - create/edit with module config
- Frontend: UserDetailPage (393 lines) - details/tenant/module management
- Frontend: 3 modal components (592 lines) - import/assign/configure
- API: GET/POST/PUT/DELETE /api/admin/users/* endpoints

Architecture Upgrade - Module Permission System:
- Backend: Add getUserModules() method in auth.service
- Backend: Login API returns modules array in user object
- Frontend: AuthContext adds hasModule() method
- Frontend: Navigation filters modules based on user.modules
- Frontend: RouteGuard checks requiredModule instead of requiredVersion
- Frontend: Remove deprecated version-based permission system
- UX: Only show accessible modules in navigation (clean UI)
- UX: Smart redirect after login (avoid 403 for regular users)

Fixes:
- Fix UTF-8 encoding corruption in ~100 docs files
- Fix pageSize type conversion in userService (String to Number)
- Fix authUser undefined error in TopNavigation
- Fix login redirect logic with role-based access check
- Update Git commit guidelines v1.2 with UTF-8 safety rules

Database Changes:
- CREATE TABLE user_modules (user_id, tenant_id, module_code, is_enabled)
- ADD UNIQUE CONSTRAINT (user_id, tenant_id, module_code)
- INSERT 4 permissions + role assignments
- UPDATE PUBLIC tenant with 8 module subscriptions

Technical:
- Backend: 5 new files (~2400 lines)
- Frontend: 10 new files (~2500 lines)
- Docs: 1 development record + 2 status updates + 1 guideline update
- Total: ~4900 lines of code

Status: User management 100% complete, module permission system operational
2026-01-16 13:42:10 +08:00

1601 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DC模块 Tool-B 开发计划
> **文档版本:** V2.0 (MVP完成)
> **创建日期:** 2025-12-02
> **完成日期:** 2025-12-03
> **实际周期:** 2个工作日
> **状态:** ✅ MVP完成
---
## 🎉 MVP完成通告2025-12-03
**Tool B病历结构化机器人MVP版本已完成**
- ✅ 前端5步工作流完整实现~1400行
- ✅ 8个API端点全部对接并测试通过
- ✅ LLM双模型提取验证成功DeepSeek-V3 + Qwen-Max
- ✅ 真实数据测试9条病理报告提取成功
- ✅ Excel导出功能可用
- ⚠️ 4个技术债务待处理`07-技术债务/Tool-B技术债务清单.md`
**完成详情:** 参见 `06-开发记录/Tool-B-MVP完成总结-2025-12-03.md`
---
## 原始开发计划
> **目标:** 完成Tool-B病历结构化机器人的前端开发和完整功能集成
---
## 📋 一、项目当前状态调研总结
### 1.1 后端代码状态 ✅ 100%完成
#### 通用能力平台(可复用)
基于 `backend/src/common/` 平台基础设施,已完整实现:
| 能力模块 | 导入路径 | 功能说明 | 状态 |
|---------|---------|---------|------|
| **存储服务** | `@/common/storage` | 文件上传/下载Local ↔ OSS | ✅ 完成 |
| **日志系统** | `@/common/logging` | 结构化日志Winston | ✅ 完成 |
| **缓存服务** | `@/common/cache` | 缓存Memory ↔ Redis | ✅ 完成 |
| **异步任务** | `@/common/jobs` | 长时间任务队列 | ✅ 完成 |
| **LLM工厂** | `@/common/llm/adapters/LLMFactory` | 统一LLM调用DeepSeek/Qwen/GPT/Claude | ✅ 完成 |
| **数据库** | `@/config/database` | Prisma连接池 | ✅ 完成 |
**复用策略**
- ✅ Tool B后端代码已100%复用平台能力
- ✅ 前端开发应参考后端模式,复用前端共享组件
---
#### Tool B后端实现状态
**代码位置**`backend/src/modules/dc/tool-b/`
| 文件 | 功能 | 代码量 | 状态 | 复用能力 |
|-----|------|--------|------|---------|
| **services/HealthCheckService.ts** | Excel健康检查 | ~190行 | ✅ 完成 | storage, logger, cache, prisma |
| **services/TemplateService.ts** | 预设模板管理 | ~243行 | ✅ 完成 | logger, prisma |
| **services/DualModelExtractionService.ts** | 双模型提取 | ~390行 | ✅ 完成 | LLMFactory, logger, prisma |
| **services/ConflictDetectionService.ts** | 冲突检测算法 | ~215行 | ✅ 完成 | logger |
| **controllers/ExtractionController.ts** | API控制器 | ~388行 | ✅ 完成 | 全部服务 |
| **routes/index.ts** | 路由配置 | ~115行 | ✅ 完成 | - |
| **index.ts** | 模块入口 | ~117行 | ✅ 完成 | - |
**总计**约1,658行7个文件100%完成
---
#### API端点清单
**Base URL**: `/api/v1/dc/tool-b`
| 方法 | 路径 | 功能 | 请求体 | 响应 | 状态 |
|------|------|------|--------|------|------|
| **POST** | `/health-check` | Excel列健康检查 | `{fileKey, columnName}` | 健康度报告 | ✅ 完成 |
| **GET** | `/templates` | 获取预设模板列表 | - | 模板数组 | ✅ 完成 |
| **POST** | `/tasks` | 创建提取任务 | `{projectName, fileKey, textColumn, diseaseType, reportType}` | `{taskId}` | ✅ 完成 |
| **GET** | `/tasks/:taskId/progress` | 查询任务进度 | - | 进度详情 | ✅ 完成 |
| **GET** | `/tasks/:taskId/items` | 获取验证网格数据 | `?status=conflict&page=1` | 分页数据 | ✅ 完成 |
| **POST** | `/items/:itemId/resolve` | 裁决冲突 | `{resolvedData}` | 成功状态 | ✅ 完成 |
**预设模板**3个
1. 肺癌病理报告(`lung_cancer/pathology`- 5个字段
2. 糖尿病入院记录(`diabetes/admission`- 5个字段
3. 高血压门诊病历(`hypertension/outpatient`- 5个字段
---
### 1.2 数据库状态 ✅ 已验证完成2025-12-02
**Schema**: `dc_schema`(独立隔离)
| 表名 | 用途 | 字段数 | 关键字段 | 状态 | 数据量 |
|------|------|--------|---------|------|--------|
| **dc_health_checks** | 健康检查记录 | 10 | status, emptyRate, avgLength | ✅ 已创建 | 2条 |
| **dc_templates** | 预设模板 | 7 | diseaseType, reportType, fields | ✅ 已创建 | **3条** |
| **dc_extraction_tasks** | 提取任务 | 21 | status, totalCount, processedCount | ✅ 已创建 | 1条 |
| **dc_extraction_items** | 提取明细 | 15 | resultA, resultB, conflictFields | ✅ 已创建 | 4条 |
**✅ 验证结果2025-12-02**
-**dc_schema已存在**
-**4个表全部创建成功**
-**3个预设模板已初始化**
1. 肺癌病理报告 (lung_cancer/pathology)
2. 糖尿病入院记录 (diabetes/admission)
3. 高血压门诊病历 (hypertension/outpatient)
-**有测试数据可用于开发调试**
**验证脚本**
```bash
cd backend
node scripts/check-dc-tables.mjs # 执行数据库表检查脚本
```
**结论**:✅ 数据库完全准备就绪,可以开始前端开发!
---
### 1.3 前端代码状态 ❌ 0%仅Placeholder
**代码位置**`frontend-v2/src/modules/dc/`
```
frontend-v2/src/modules/dc/
├── index.tsx # ❌ 仅Placeholder14行
├── components/ # 📁 空
├── pages/ # 📁 空文件夹结构
│ ├── tool-a/ # 📁 空
│ ├── tool-b/ # 📁 空
│ └── tool-c/ # 📁 空
└── types/ # 📁 空
```
**当前内容**
```typescript
// frontend-v2/src/modules/dc/index.tsx
import Placeholder from '@/shared/components/Placeholder'
const DCModule = () => {
return (
<Placeholder
title="数据清洗模块"
description="功能规划中,将提供智能数据清洗和整理工具"
moduleName="DC - Data Cleaning"
/>
)
}
export default DCModule
```
---
### 1.4 前端架构设计 ✅ 已明确
#### 系统级架构
- **导航模式**:顶部导航
- **路由路径**`/data-cleaning`
- **技术栈**React 19 + TypeScript + Vite + Ant Design 5
#### DC模块架构方案
**选择方案A - 独立Portal页面**(推荐)
```
/data-cleaning (Portal工作台 - 总览页)
├── 快速启动区3个工具卡片
│ ├── [超级合并器] → /data-cleaning/tool-a
│ ├── [病历结构化] → /data-cleaning/tool-b
│ └── [数据编辑器] → /data-cleaning/tool-c
├── 最近任务列表(实时进度)
└── 数据资产库(文件管理)
/data-cleaning/tool-b (Tool B - 全屏5步流程)
├── Step 1: 上传与健康检查
├── Step 2: 智能模板配置
├── Step 3: 双盲提取进度
├── Step 4: 冲突验证网格 ⭐ 核心
└── Step 5: 结果导出
```
**参考模块**
- ASL模块`frontend-v2/src/modules/asl/`
- 有完整的布局、页面、组件结构
- 使用React Router嵌套路由
- 左侧导航 + 右侧内容区
---
## 🎯 二、开发计划总览
### 2.1 开发阶段划分
| 阶段 | 任务 | 预计工时 | 优先级 | 目标 |
|------|------|---------|--------|------|
| **Phase 1** | Portal工作台页面 | 4-6h | P0 | DC模块入口 |
| **Phase 2** | Tool B - Step 1&2 | 6h | P0 | 上传+配置 |
| **Phase 3** | Tool B - Step 3 | 3h | P0 | 进度监控 |
| **Phase 4** | Tool B - Step 4 | 9h | P0 | 冲突验证网格⭐ |
| **Phase 5** | Tool B - Step 5 | 3h | P0 | 结果导出 |
| **Phase 6** | 集成测试 | 4h | P1 | 端到端验证 |
**总计**约29-31小时4-5个工作日
---
### 2.2 里程碑时间表
| 里程碑 | 完成标志 | 预计完成 |
|--------|---------|---------|
| **M1: Portal上线** | 用户可访问DC模块入口 | Day 1 |
| **M2: Tool B可用** | Step1-5全部完成 | Day 4 |
| **M3: 集成测试通过** | 端到端流程测试通过 | Day 5 |
| **M4: 文档完善** | 开发文档和用户文档 | Day 6 |
---
## 📐 三、Phase 1: Portal工作台开发Day 1
### 3.1 目标
创建DC模块的入口页面适配系统顶部导航提供工具启动、任务监控和文件管理功能。
### 3.2 设计参考
- **原型文件**`docs/03-业务模块/DC-数据清洗整理/03-UI设计/智能数据清洗工作台V2.html`
- **PRD文档**`docs/03-业务模块/DC-数据清洗整理/01-需求分析/PRD智能数据清洗工作台 (The Data Cleaning Portal).md`
### 3.3 功能清单
#### 3.3.1 快速启动区3个工具卡片
**组件**`components/ToolCard.tsx`
```typescript
interface ToolCardProps {
id: 'tool-a' | 'tool-b' | 'tool-c';
title: string;
description: string;
icon: ReactNode;
color: 'blue' | 'purple' | 'emerald';
status: 'ready' | 'disabled';
onClick: () => void;
}
```
**卡片内容**
1. **Tool A - 超级合并器**
- 图标FileSpreadsheet蓝色
- 描述:"解决多源数据时间轴对齐难题"
- 状态disabled暂未开发
2. **Tool B - 病历结构化机器人** ⭐ 本次开发
- 图标Bot紫色
- 描述:"利用大模型提取非结构化文本"
- 状态ready
- 点击跳转:`/data-cleaning/tool-b`
3. **Tool C - 科研数据编辑器**
- 图标Table2绿色
- 描述:"Excel风格的在线清洗工具"
- 状态disabled暂未开发
---
#### 3.3.2 最近任务列表
**组件**`components/TaskList.tsx`
**API**`GET /api/v1/dc/tasks/recent`(需后端新增)
**数据结构**
```typescript
interface Task {
id: string;
name: string;
tool: 'tool-a' | 'tool-b' | 'tool-c';
status: 'pending' | 'processing' | 'completed' | 'failed';
progress: number; // 0-100
createdAt: string;
completedAt?: string;
}
```
**功能**
- 显示最近10条任务
- 实时轮询进度processing状态时每5秒轮询
- 智能流转按钮:
- Tool A完成 → [下载] + [去AI提取]跳Tool B
- Tool B完成 → [下载] + [去清洗]跳Tool C
- Tool C完成 → [下载]
---
#### 3.3.3 数据资产库
**组件**`components/AssetLibrary.tsx`
**API**`GET /api/v1/dc/assets`(需后端新增)
**Tab分类**
- **全部**:所有文件
- **处理结果**工具A/B/C生成的文件绿色/蓝色图标)
- **原始上传**:用户直接上传的底表(灰色图标)
**功能**
- 文件卡片显示(文件名、行数、标签、修改时间)
- 快捷操作:[下载] [去处理] [分析]
- 底部固定按钮:[+ 上传原始文件到库]
---
### 3.4 技术实现
#### 3.4.1 目录结构
```
frontend-v2/src/modules/dc/
├── pages/
│ └── Portal.tsx # ⭐ Portal主页面
├── components/
│ ├── ToolCard.tsx # 工具卡片
│ ├── TaskList.tsx # 任务列表
│ └── AssetLibrary.tsx # 数据资产库
├── hooks/
│ ├── useRecentTasks.ts # 任务列表Hook
│ └── useAssets.ts # 资产列表Hook
├── services/
│ └── portalApi.ts # Portal API封装
└── types/
└── portal.ts # Portal类型定义
```
---
#### 3.4.2 路由配置
```typescript
// frontend-v2/src/modules/dc/index.tsx
import { Routes, Route } from 'react-router-dom';
import Portal from './pages/Portal';
import ToolBModule from './pages/tool-b';
const DCModule = () => {
return (
<Routes>
<Route path="" element={<Portal />} />
<Route path="tool-b/*" element={<ToolBModule />} />
{/* 未来扩展 */}
<Route path="tool-a/*" element={<ToolAPlaceholder />} />
<Route path="tool-c/*" element={<ToolCPlaceholder />} />
</Routes>
);
};
export default DCModule;
```
---
#### 3.4.3 API对接
**需要后端新增的API**
```typescript
// GET /api/v1/dc/tasks/recent
interface GetRecentTasksResponse {
tasks: Task[];
}
// GET /api/v1/dc/assets
interface GetAssetsResponse {
assets: Asset[];
}
```
**临时方案**后端API未开发时
- 使用Mock数据
- 后续替换为真实API
---
### 3.5 验收标准
- [ ] Portal页面可访问`http://localhost:3000/data-cleaning`
- [ ] 3个工具卡片正确显示
- [ ] Tool B卡片可点击跳转其他两个显示disabled
- [ ] 任务列表显示Mock数据
- [ ] 数据资产库Tab切换正常
- [ ] 整体样式符合系统设计规范
---
## 🛠️ 四、Phase 2: Tool B - Step 1&2Day 2
### 4.1 Step 1: 文件上传与健康检查3小时
#### 4.1.1 页面设计
**组件**`pages/tool-b/Step1Upload.tsx`
**UI参考**原型V4第260-310行
**布局**
1. 文件信息卡片(显示已上传文件)
2. 列选择下拉框
3. 健康检查结果卡片(动态显示)
---
#### 4.1.2 功能实现
**1. Excel文件上传**
```typescript
// 使用Ant Design Upload组件
import { Upload } from 'antd';
// 上传到Storage
const handleUpload = async (file: File) => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/v1/storage/upload', {
method: 'POST',
body: formData
});
const { fileKey } = await response.json();
setFileKey(fileKey); // 保存fileKey用于后续步骤
};
```
**2. 列选择与健康检查**
```typescript
const [columns, setColumns] = useState<string[]>([]);
const [selectedColumn, setSelectedColumn] = useState('');
const [healthResult, setHealthResult] = useState<HealthCheckResult | null>(null);
// 自动检测列名(可选:从后端获取)
useEffect(() => {
if (fileKey) {
// TODO: 调用API获取Excel列名
// 或前端解析Excel使用xlsx库
}
}, [fileKey]);
// 健康检查
const handleHealthCheck = async (columnName: string) => {
setIsChecking(true);
try {
const response = await fetch('/api/v1/dc/tool-b/health-check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileKey, columnName })
});
const result = await response.json();
setHealthResult(result.data);
} finally {
setIsChecking(false);
}
};
```
**3. 健康度显示**
```typescript
// 根据status显示不同样式
{healthResult?.status === 'good' && (
<Alert
type="success"
icon={<CheckCircle2 />}
message="健康度优秀,适合提取"
description={
<div className="mt-2">
<span>: {healthResult.avgLength}</span>
<span className="ml-4">: {(healthResult.emptyRate * 100).toFixed(1)}%</span>
<span className="ml-4 text-purple-600 font-bold">
Token: {healthResult.estimatedTokens.toLocaleString()}
</span>
</div>
}
/>
)}
{healthResult?.status === 'bad' && (
<Alert
type="error"
icon={<AlertTriangle />}
message="警告该列不适合AI处理"
description={healthResult.message}
/>
)}
```
---
### 4.2 Step 2: 智能模板配置3小时
#### 4.2.1 页面设计
**组件**`pages/tool-b/Step2Schema.tsx`
**UI参考**原型V4第313-372行
**布局**
1. 疾病类型与报告类型选择(级联)
2. 字段列表(左侧,可编辑)
3. Prompt预览右侧代码高亮
---
#### 4.2.2 功能实现
**1. 获取模板列表**
```typescript
const [templates, setTemplates] = useState<Template[]>([]);
const [diseaseType, setDiseaseType] = useState('');
const [reportType, setReportType] = useState('');
const [fields, setFields] = useState<Field[]>([]);
useEffect(() => {
// 获取所有模板
fetch('/api/v1/dc/tool-b/templates')
.then(res => res.json())
.then(data => setTemplates(data.data.templates));
}, []);
// 当选择疾病类型和报告类型时,自动加载字段
useEffect(() => {
if (diseaseType && reportType) {
const template = templates.find(
t => t.diseaseType === diseaseType && t.reportType === reportType
);
if (template) {
setFields(template.fields);
}
}
}, [diseaseType, reportType, templates]);
```
**2. 字段编辑**
```typescript
// 添加字段
const handleAddField = () => {
setFields([...fields, {
id: `custom_${Date.now()}`,
name: '新字段',
desc: '字段描述',
width: 'w-32'
}]);
};
// 删除字段
const handleDeleteField = (id: string) => {
setFields(fields.filter(f => f.id !== id));
};
// 编辑字段
const handleEditField = (id: string, key: 'name' | 'desc', value: string) => {
setFields(fields.map(f =>
f.id === id ? { ...f, [key]: value } : f
));
};
```
**3. Prompt预览**
```typescript
// 动态生成Prompt预览
const generatePrompt = () => {
return `You are an expert in ${diseaseType.replace('_', ' ')} pathology.
Extract fields in JSON format:
{
${fields.map(f => ` "${f.name}": "string", // ${f.desc}`).join('\n')}
}
Original text:
{originalText}
Output JSON only.`;
};
// 使用代码高亮库如react-syntax-highlighter
<SyntaxHighlighter language="javascript" style={atomOneDark}>
{generatePrompt()}
</SyntaxHighlighter>
```
---
### 4.3 验收标准
**Step 1:**
- [ ] Excel上传成功获取fileKey
- [ ] 列名列表正确显示
- [ ] 健康检查API调用成功
- [ ] 健康度卡片根据结果正确显示(绿色/红色)
- [ ] Token预估数值正确
- [ ] 空值率>50%时禁止进入下一步
**Step 2:**
- [ ] 模板列表加载成功
- [ ] 疾病类型和报告类型选择框正常
- [ ] 选择模板后字段自动加载
- [ ] 字段支持添加/删除/编辑
- [ ] Prompt预览实时更新
- [ ] 代码高亮正确显示
---
## ⚙️ 五、Phase 3: Tool B - Step 3Day 3上午
### 5.1 处理进度监控3小时
#### 5.1.1 页面设计
**组件**`pages/tool-b/Step3Processing.tsx`
**UI参考**原型V4第375-400行
**布局**
1. 圆形进度环(双模型动画)
2. 进度百分比和文本
3. 日志滚动区域
---
#### 5.1.2 功能实现
**1. 创建任务**
```typescript
const handleStartExtraction = async () => {
const response = await fetch('/api/v1/dc/tool-b/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectName: 'Test Project',
sourceFileKey: fileKey,
textColumn: selectedColumn,
diseaseType,
reportType,
modelA: 'deepseek-v3',
modelB: 'qwen-max'
})
});
const { taskId } = await response.json();
setTaskId(taskId);
// 开始轮询
startPolling(taskId);
};
```
**2. 进度轮询**
```typescript
const startPolling = (taskId: string) => {
const interval = setInterval(async () => {
const response = await fetch(`/api/v1/dc/tool-b/tasks/${taskId}/progress`);
const data = await response.json();
setProgress(data.progress); // 百分比
setLogs(prevLogs => [...prevLogs, data.latestLog]); // 新增日志
if (data.status === 'completed') {
clearInterval(interval);
// 跳转到Step 4
setTimeout(() => navigate(`/data-cleaning/tool-b/verify/${taskId}`), 800);
} else if (data.status === 'failed') {
clearInterval(interval);
message.error('提取任务失败');
}
}, 5000); // 每5秒轮询
return () => clearInterval(interval);
};
```
**3. 进度动画**
```typescript
// 使用Ant Design Progress组件或自定义SVG
<Progress
type="circle"
percent={progress}
format={percent => (
<div>
<div className="text-2xl font-bold">{percent}%</div>
<div className="text-sm text-gray-500"></div>
</div>
)}
strokeColor={{
'0%': '#9333ea',
'100%': '#4f46e5'
}}
/>
// 双模型动画(两个小球跳动)
<div className="flex gap-2">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce" />
<div className="w-3 h-3 bg-orange-500 rounded-full animate-bounce delay-200" />
</div>
```
**4. 日志显示**
```typescript
<div className="h-40 overflow-y-auto bg-slate-50 p-4 rounded font-mono text-xs">
{logs.map((log, i) => (
<div key={i} className="mb-1 text-slate-600">
<span className="text-slate-400">[{log.timestamp}]</span> {log.message}
</div>
))}
<div className="animate-pulse text-purple-500">_</div>
</div>
```
---
### 5.2 验收标准
- [ ] 点击"开始提取"后成功创建任务
- [ ] 获取到taskId
- [ ] 进度轮询正常每5秒
- [ ] 进度条实时更新
- [ ] 日志滚动区域正常显示
- [ ] 任务完成后自动跳转到Step 4
- [ ] 任务失败时显示错误提示
---
## 🎯 六、Phase 4: Tool B - Step 4Day 3下午+Day 4⭐ 核心
### 6.1 冲突验证网格9小时
这是整个Tool B最复杂、最核心的页面
#### 6.1.1 页面设计
**组件**`pages/tool-b/Step4Verify.tsx`
**UI参考**原型V4第402-569行
**布局**
```
┌─────────────────────────────────────────────────┐
│ Toolbar: 统计信息 + [导出][完成] │
├─────────────────────────────────────────────────┤
│ │
│ 全景验证网格(表格) │
│ ┌────┬────────────┬────────┬────────┬──────┐ │
│ │ # │ 原文摘要 │ 字段1 │ 字段2 │ 状态 │ │
│ ├────┼────────────┼────────┼────────┼──────┤ │
│ │ 1 │ 病理诊断.. │ 冲突单元格A/B按钮│待裁决│ │
│ │ 2 │ 送检组织.. │ 绿色(一致)│ 绿色 │ 通过 │ │
│ └────┴────────────┴────────┴────────┴──────┘ │
│ │
└─────────────────────────────────────────────────┘
↓ 点击行
┌──────────────────────────────────┐
│ 侧边栏Drawer
│ ─────────────────────────────── │
│ 病历原文详情 │
│ │
│ 病理诊断:(右肺上叶)浸润性腺癌... │
│ 肿瘤大小 3.2*2.5*2.0cm... │
│ ... │
│ │
│ [关闭] │
└──────────────────────────────────┘
```
---
#### 6.1.2 技术选型
**方案选择**
**Option 1: Ant Design Table**(推荐,数据量<1000行
- 优点开箱即用API友好样式统一
- 缺点:数据量大时可能卡顿
- 适用场景:大部分场景(预计数据量<500行
**Option 2: TanStack Table**(高性能,数据量>1000行
- 优点:虚拟滚动,性能极佳
- 缺点Headless需自己实现UI
- 适用场景:处理大数据集
**决策**先用Ant Design Table如性能不佳再迁移到TanStack Table
---
#### 6.1.3 核心数据结构
```typescript
interface VerifyRow {
id: string;
rowIndex: number;
originalText: string; // 原文摘要前50字
fullText: string; // 原文全文(侧边栏显示)
results: Record<string, {
A: string; // DeepSeek结果
B: string; // Qwen结果
chosen: string | null; // 用户采纳的值null=未解决)
}>;
status: 'clean' | 'conflict' | 'resolved';
conflictFields: string[]; // 冲突字段名称列表
}
```
---
#### 6.1.4 功能实现
**1. 获取验证数据**
```typescript
const [rows, setRows] = useState<VerifyRow[]>([]);
const [pagination, setPagination] = useState({ page: 1, pageSize: 20, total: 0 });
useEffect(() => {
fetch(`/api/v1/dc/tool-b/tasks/${taskId}/items?status=conflict&page=${pagination.page}`)
.then(res => res.json())
.then(data => {
setRows(data.data.items);
setPagination({ ...pagination, total: data.data.pagination.total });
});
}, [taskId, pagination.page]);
```
**2. 表格列配置**
```typescript
const columns: ColumnsType<VerifyRow> = [
{
title: '#',
dataIndex: 'rowIndex',
width: 60,
fixed: 'left',
},
{
title: '原文摘要',
dataIndex: 'originalText',
width: 200,
render: (text, record) => (
<div className="flex items-center gap-2">
<FileText size={14} className="text-slate-300" />
<Tooltip title={text}>
<span className="truncate w-40">{text}</span>
</Tooltip>
</div>
),
},
// 动态字段列(根据模板生成)
...fields.map(field => ({
title: field.name,
dataIndex: ['results', field.name],
width: 180,
render: (cellData: { A: string; B: string; chosen: string | null }, record: VerifyRow) => {
const isConflict = cellData.A !== cellData.B && cellData.chosen === null;
if (isConflict) {
return <ConflictCell
fieldName={field.name}
valueA={cellData.A}
valueB={cellData.B}
onAdopt={(value) => handleAdopt(record.id, field.name, value)}
/>;
}
return <CleanCell value={cellData.chosen || cellData.A} />;
},
})),
{
title: '状态',
dataIndex: 'status',
width: 100,
fixed: 'right',
render: (status) => {
if (status === 'clean' || status === 'resolved') {
return <Tag color="success"></Tag>;
}
return <Tag color="warning" className="animate-pulse"></Tag>;
},
},
];
```
**3. 冲突单元格组件** ⭐ 核心
```typescript
// components/ConflictCell.tsx
interface ConflictCellProps {
fieldName: string;
valueA: string;
valueB: string;
onAdopt: (value: string) => void;
}
const ConflictCell: React.FC<ConflictCellProps> = ({ fieldName, valueA, valueB, onAdopt }) => {
return (
<div className="flex flex-col gap-1.5 bg-orange-50 p-2 rounded">
{/* 选项A - DeepSeek */}
<button
className="text-left text-xs px-2 py-1.5 rounded border border-blue-200 bg-white hover:bg-blue-50 hover:border-blue-400 transition-all flex justify-between group"
onClick={() => onAdopt(valueA)}
>
<Tooltip title={valueA}>
<span className="truncate max-w-[100px]">{valueA}</span>
</Tooltip>
<Badge className="text-[10px] text-blue-400 group-hover:text-blue-600">DS</Badge>
</button>
{/* 选项B - Qwen */}
<button
className="text-left text-xs px-2 py-1.5 rounded border border-orange-200 bg-white hover:bg-orange-50 hover:border-orange-400 transition-all flex justify-between group"
onClick={() => onAdopt(valueB)}
>
<Tooltip title={valueB}>
<span className="truncate max-w-[100px]">{valueB}</span>
</Tooltip>
<Badge className="text-[10px] text-orange-400 group-hover:text-orange-600">QW</Badge>
</button>
</div>
);
};
```
**4. 裁决逻辑**
```typescript
const handleAdopt = async (itemId: string, fieldName: string, value: string) => {
// 乐观更新UI
setRows(prevRows =>
prevRows.map(row => {
if (row.id !== itemId) return row;
const newResults = { ...row.results };
newResults[fieldName].chosen = value;
// 检查该行是否还有未解决的冲突
const hasConflict = Object.values(newResults).some(
cell => cell.chosen === null && cell.A !== cell.B
);
return {
...row,
results: newResults,
status: hasConflict ? 'conflict' : 'resolved',
};
})
);
// 调用API
try {
await fetch(`/api/v1/dc/tool-b/items/${itemId}/resolve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
resolvedData: { [fieldName]: value }
})
});
} catch (error) {
message.error('裁决失败');
// 回滚UI
// TODO: 实现回滚逻辑
}
};
```
**5. 侧边栏原文显示**
```typescript
const [selectedRowId, setSelectedRowId] = useState<string | null>(null);
// 点击行时打开侧边栏
const onRow = (record: VerifyRow) => ({
onClick: () => setSelectedRowId(record.id),
});
// Drawer组件
<Drawer
title="病历原文详情"
placement="right"
width={400}
open={!!selectedRowId}
onClose={() => setSelectedRowId(null)}
>
{(() => {
const row = rows.find(r => r.id === selectedRowId);
if (!row) return null;
return (
<div className="prose">
<p className="whitespace-pre-wrap font-serif leading-7">
{row.fullText}
</p>
<Divider />
<div className="flex flex-wrap gap-2">
{Object.keys(row.results).map(fieldName => (
<Tag
key={fieldName}
color={row.conflictFields.includes(fieldName) ? 'orange' : 'default'}
>
{fieldName}
</Tag>
))}
</div>
</div>
);
})()}
</Drawer>
```
**6. Toolbar统计**
```typescript
const conflictCount = rows.filter(r => r.status === 'conflict').length;
const resolvedCount = rows.filter(r => r.status === 'resolved').length;
const cleanCount = rows.filter(r => r.status === 'clean').length;
<div className="flex items-center gap-4 mb-4">
<div className="bg-slate-100 px-3 py-1.5 rounded">
: <strong>{rows.length}</strong>
</div>
{conflictCount > 0 ? (
<div className="bg-orange-50 px-3 py-1.5 rounded text-orange-700 animate-pulse">
<AlertTriangle size={16} className="inline mr-1" />
<strong>{conflictCount}</strong>
</div>
) : (
<div className="bg-emerald-50 px-3 py-1.5 rounded text-emerald-700">
<CheckCircle2 size={16} className="inline mr-1" />
</div>
)}
<Button onClick={handleExport}></Button>
<Button type="primary" onClick={handleComplete}></Button>
</div>
```
---
### 6.2 验收标准
**数据加载**
- [ ] 成功获取验证数据
- [ ] 表格列根据模板动态生成
- [ ] 分页功能正常
**冲突单元格**
- [ ] 冲突单元格显示A/B两个按钮
- [ ] 按钮显示正确的值和模型标识DS/QW
- [ ] 点击按钮后采纳值
- [ ] UI乐观更新立即生效
- [ ] API调用成功
**侧边栏**
- [ ] 点击行时侧边栏滑出
- [ ] 显示完整病历原文
- [ ] 显示字段标签(冲突字段高亮)
- [ ] 点击关闭按钮或外部区域关闭侧边栏
**Toolbar**
- [ ] 冲突数统计正确
- [ ] 实时更新(裁决后减少)
- [ ] 导出按钮可点击
- [ ] 完成按钮跳转到Step 5
---
## 📦 七、Phase 5: Tool B - Step 5Day 5上午
### 7.1 结果导出3小时
#### 7.1.1 页面设计
**组件**`pages/tool-b/Step5Result.tsx`
**UI参考**原型V4第572-607行
**布局**
1. 完成图标和标题
2. 统计卡片4个
3. 操作按钮(下载、流转)
---
#### 7.1.2 功能实现
**1. 统计数据获取**
```typescript
const [stats, setStats] = useState({
totalCount: 0,
cleanCount: 0,
conflictCount: 0,
failedCount: 0,
totalTokens: 0,
totalCost: 0,
});
useEffect(() => {
fetch(`/api/v1/dc/tool-b/tasks/${taskId}/progress`)
.then(res => res.json())
.then(data => setStats(data.data));
}, [taskId]);
```
**2. Excel导出**
```typescript
const handleExport = async () => {
try {
const response = await fetch(`/api/v1/dc/tool-b/tasks/${taskId}/export`, {
method: 'POST'
});
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `extraction_result_${taskId}.xlsx`;
a.click();
message.success('导出成功');
} catch (error) {
message.error('导出失败');
}
};
```
**3. 流转到工具C**
```typescript
const handleGoToToolC = () => {
// 跳转到工具C传递taskId
navigate(`/data-cleaning/tool-c?sourceTaskId=${taskId}`);
};
```
---
### 7.2 验收标准
- [ ] 统计卡片数据正确显示
- [ ] Token消耗和成本计算正确
- [ ] 下载按钮触发Excel导出
- [ ] Excel文件包含4个Sheet完整结果、无冲突、冲突项、失败项
- [ ] 流转到工具C按钮可点击提示暂未开发
- [ ] 返回Portal按钮可点击
---
## 🧪 八、Phase 6: 集成测试Day 5下午
### 8.1 端到端测试清单
#### 8.1.1 完整流程测试
**测试场景**:肺癌病理报告提取
**测试步骤**
1. [ ] 访问Portal页面`/data-cleaning`
2. [ ] 点击Tool B卡片跳转到Step 1
3. [ ] 上传测试Excel文件包含病理报告列
4. [ ] 选择"病理报告"列,触发健康检查
5. [ ] 验证健康度显示为"绿色 - 优秀"
6. [ ] 进入Step 2选择"肺癌 + 病理报告"
7. [ ] 验证字段自动加载5个字段
8. [ ] 编辑一个字段(修改描述)
9. [ ] 进入Step 3点击"开始提取"
10. [ ] 验证进度条开始更新
11. [ ] 验证日志滚动显示
12. [ ] 等待任务完成(或模拟完成)
13. [ ] 自动跳转到Step 4
14. [ ] 验证验证网格加载数据
15. [ ] 找到一个冲突单元格,点击"DS"按钮采纳
16. [ ] 验证冲突数减1
17. [ ] 点击某行,验证侧边栏显示原文
18. [ ] 点击"完成并入库"跳转到Step 5
19. [ ] 验证统计卡片数据正确
20. [ ] 点击"下载"验证Excel下载成功
---
#### 8.1.2 异常场景测试
**测试场景1健康检查失败**
- [ ] 上传Excel选择空值率>50%的列
- [ ] 验证健康度显示为"红色 - 警告"
- [ ] 验证"下一步"按钮被禁用
**测试场景2任务失败**
- [ ] 模拟API返回失败状态
- [ ] 验证错误提示显示
- [ ] 验证可以重新尝试
**测试场景3网络错误**
- [ ] 断网后点击"下一步"
- [ ] 验证错误提示显示
- [ ] 重新联网后可继续
---
### 8.2 性能测试
**测试场景**:大数据量验证网格
**测试步骤**
1. [ ] 加载500行数据
2. [ ] 验证表格滚动流畅(无卡顿)
3. [ ] 验证分页功能正常
4. [ ] 验证排序/筛选功能(如实现)
**性能指标**
- [ ] 首屏加载时间 < 2秒
- [ ] 表格滚动帧率 > 30fps
- [ ] 裁决操作响应时间 < 500ms
---
### 8.3 兼容性测试
**浏览器**
- [ ] Chrome 120+
- [ ] Edge 120+
- [ ] Firefox 120+
- [ ] Safari 17+Mac
**分辨率**
- [ ] 1920x1080常规
- [ ] 1440x900笔记本
- [ ] 2560x1440高分屏
---
## 📝 九、文档完善Day 6
### 9.1 开发文档
**需要补充的文档**
1. [ ] **前端代码结构说明**`docs/03-业务模块/DC-数据清洗整理/02-技术设计/前端架构设计.md`
2. [ ] **组件使用文档**`frontend-v2/src/modules/dc/README.md`
3. [ ] **API对接文档**补充到现有API设计文档
4. [ ] **本开发计划的完成总结**`docs/03-业务模块/DC-数据清洗整理/06-开发记录/Tool-B开发完成总结.md`
---
### 9.2 用户文档(可选)
**需要创建的文档**
1. [ ] **Tool B使用指南**`docs/03-业务模块/DC-数据清洗整理/07-用户手册/Tool-B使用指南.md`
2. [ ] **常见问题FAQ**`docs/03-业务模块/DC-数据清洗整理/07-用户手册/FAQ.md`
---
## 🎯 十、关键技术要点
### 10.1 必须遵守的规范
#### 云原生规范 ⭐ 强制
参考:`docs/04-开发规范/08-云原生开发规范.md`
**禁止**
- ❌ 使用`fs.writeFile()`等本地文件操作
- ❌ 使用全局变量缓存数据
- ❌ 硬编码配置IP、密钥等
- ❌ 同步长任务(>10秒
- ❌ 重复实现平台能力
**必须**
- ✅ 使用`storage`服务存储文件
- ✅ 使用`cache`服务缓存数据
- ✅ 使用环境变量配置
- ✅ 异步任务 + 进度轮询
- ✅ 复用平台基础设施
---
### 10.2 复用策略
#### 后端复用(参考)
```typescript
// ✅ 正确:复用平台能力
import { storage } from '@/common/storage';
import { logger } from '@/common/logging';
import { cache } from '@/common/cache';
import { LLMFactory } from '@/common/llm/adapters/LLMFactory';
```
#### 前端复用(需实现)
```typescript
// 复用ASL模块的组件和Hook
import { usePolling } from '@/shared/hooks/usePolling';
import { FileUploader } from '@/shared/components/FileUploader';
import { ProgressBar } from '@/shared/components/ProgressBar';
```
---
### 10.3 状态管理
**推荐方案**React QueryASL模块使用
```typescript
// 使用React Query管理API调用
import { useQuery, useMutation } from '@tanstack/react-query';
// 获取任务进度
const { data: progress } = useQuery({
queryKey: ['task-progress', taskId],
queryFn: () => fetch(`/api/v1/dc/tool-b/tasks/${taskId}/progress`).then(res => res.json()),
refetchInterval: 5000, // 每5秒轮询
enabled: !!taskId && status === 'processing',
});
// 裁决冲突
const resolveMutation = useMutation({
mutationFn: ({ itemId, resolvedData }) =>
fetch(`/api/v1/dc/tool-b/items/${itemId}/resolve`, {
method: 'POST',
body: JSON.stringify({ resolvedData }),
}),
onSuccess: () => {
queryClient.invalidateQueries(['task-items', taskId]);
message.success('裁决成功');
},
});
```
---
### 10.4 TypeScript类型定义
**统一类型文件**`frontend-v2/src/modules/dc/types/toolB.ts`
```typescript
// 健康检查结果
export interface HealthCheckResult {
status: 'good' | 'bad';
emptyRate: number;
avgLength: number;
totalRows: number;
estimatedTokens: number;
message: string;
}
// 模板定义
export interface Template {
id: string;
diseaseType: string;
reportType: string;
displayName: string;
fields: Field[];
}
export interface Field {
id: string;
name: string;
desc: string;
width?: string;
}
// 任务状态
export interface Task {
id: string;
projectName: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
totalCount: number;
processedCount: number;
cleanCount: number;
conflictCount: number;
failedCount: number;
totalTokens: number;
totalCost: number;
}
// 验证行
export interface VerifyRow {
id: string;
rowIndex: number;
originalText: string;
fullText: string;
results: Record<string, {
A: string;
B: string;
chosen: string | null;
}>;
status: 'clean' | 'conflict' | 'resolved';
conflictFields: string[];
}
```
---
## 📊 十一、风险评估与应对
### 11.1 技术风险
| 风险 | 概率 | 影响 | 应对措施 |
|------|------|------|---------|
| **数据库表未创建** | 中 | 高 | 立即执行`npx prisma db push`验证 |
| **后端API未测试** | 中 | 高 | 开发前先用REST Client测试6个端点 |
| **性能问题(大数据量)** | 低 | 中 | 预留TanStack Table迁移方案 |
| **Excel解析失败** | 低 | 中 | 使用xlsx库增加错误处理 |
| **LLM调用超时** | 低 | 中 | 后端已实现重试机制 |
---
### 11.2 时间风险
| 风险 | 概率 | 影响 | 应对措施 |
|------|------|------|---------|
| **Step 4开发超时** | 中 | 高 | 预留2天9小时可拆分子任务 |
| **集成测试发现问题** | 中 | 中 | 预留1天缓冲时间 |
| **原型与需求不符** | 低 | 高 | 开发前与产品确认原型 |
---
### 11.3 依赖风险
| 风险 | 概率 | 影响 | 应对措施 |
|------|------|------|---------|
| **后端API变更** | 低 | 高 | 使用TypeScript类型编译时检查 |
| **平台能力Bug** | 低 | 中 | 已完整测试,风险低 |
| **设计变更** | 低 | 中 | 组件化设计,易于修改 |
---
## 🎉 十二、预期成果
### 12.1 交付物清单
**代码**
- [ ] Portal工作台页面完整
- [ ] Tool B 5个Step页面完整
- [ ] 共享组件库TaskList、AssetLibrary、ConflictCell等
- [ ] API服务封装toolBApi.ts、portalApi.ts
- [ ] TypeScript类型定义完整
- [ ] 样式文件TailwindCSS + Ant Design
**文档**
- [ ] 前端架构设计文档
- [ ] 组件使用文档
- [ ] API对接文档
- [ ] 开发完成总结
- [ ] 用户使用指南(可选)
**测试**
- [ ] 端到端测试报告
- [ ] 性能测试报告
- [ ] 兼容性测试报告
---
### 12.2 功能完整性
**Portal工作台**
- ✅ 3个工具卡片Tool B可用
- ✅ 最近任务列表(实时轮询)
- ✅ 数据资产库Tab切换
**Tool B - 病历结构化机器人**
- ✅ Step 1: Excel上传 + 健康检查
- ✅ Step 2: 智能模板配置3个预设模板
- ✅ Step 3: 双盲提取进度监控
- ✅ Step 4: 冲突验证网格(支持裁决)
- ✅ Step 5: 结果导出Excel + 流转)
---
### 12.3 代码质量
**代码规范**
- ✅ 遵守云原生开发规范
- ✅ TypeScript类型安全
- ✅ ESLint无错误
- ✅ 代码注释完善
**性能指标**
- ✅ 首屏加载 < 2s
- ✅ 表格滚动流畅
- ✅ 裁决响应 < 500ms
- ✅ 内存占用 < 200MB
**用户体验**
- ✅ 流畅的5步流程
- ✅ 实时进度反馈
- ✅ 直观的冲突验证界面
- ✅ 友好的错误提示
---
## 🚀 十三、下一步行动
### 13.1 立即执行Day 0
**1. 验证数据库表**
```bash
cd AIclinicalresearch/backend
npx prisma db push --skip-generate
npx prisma studio # 可视化验证
```
**2. 测试后端API**
```bash
# 启动后端服务
npm run dev
# 使用REST Client测试6个API端点
# 或使用Postman/Insomnia
```
**3. Git提交计划**
```bash
# 建议每完成一个Phase就提交
git commit -m "feat(dc): Complete Portal page (Phase 1)"
git commit -m "feat(dc/tool-b): Complete Step1&2 (Phase 2)"
git commit -m "feat(dc/tool-b): Complete Step4 conflict grid (Phase 4)"
```
---
### 13.2 开发启动Day 1
**任务1创建Portal页面骨架**
```bash
cd frontend-v2/src/modules/dc
mkdir -p pages components hooks services types
# 创建Portal.tsx
touch pages/Portal.tsx
# 创建路由配置
# 修改 index.tsx
```
**任务2实现3个工具卡片**
```bash
# 创建ToolCard组件
touch components/ToolCard.tsx
```
**任务3实现任务列表使用Mock数据**
```bash
# 创建TaskList组件
touch components/TaskList.tsx
# 创建useRecentTasks Hook
touch hooks/useRecentTasks.ts
```
---
### 13.3 持续跟进
**每日检查点**
- [ ] 代码是否Git提交
- [ ] Todo列表是否更新
- [ ] 遇到的问题是否记录
- [ ] API调用是否正常
**每周检查点**
- [ ] 功能完成度是否符合预期
- [ ] 是否需要调整计划
- [ ] 是否需要额外资源
---
## 📞 十四、联系与支持
### 14.1 技术支持
**遇到问题时**
1. 查阅平台基础设施文档(`backend/src/common/README.md`
2. 查阅云原生开发规范(`docs/04-开发规范/08-云原生开发规范.md`
3. 参考ASL模块代码`frontend-v2/src/modules/asl/`
4. 查看后端API代码`backend/src/modules/dc/tool-b/`
### 14.2 代码审查
**提交前自检**
- [ ] 是否遵守云原生规范
- [ ] 是否复用平台能力
- [ ] TypeScript类型是否完整
- [ ] 是否有TODO/FIXME注释
- [ ] 是否有console.log应使用logger
---
## 🎯 十五、总结
### 核心目标
完成DC模块Tool B的前端开发实现从文件上传到结果导出的完整5步流程特别是**冲突验证网格**这一核心功能。
### 关键成功因素
1.**后端已100%完成**前端可专注UI和交互
2.**平台能力完善**,可直接复用,无需重复开发
3.**设计文档齐全**,有清晰的原型和需求
4.**参考模块成熟**ASL模块可作为参考
5.**架构方案明确**采用方案A独立Portal
### 预期收益
- **用户价值**提供AI驱动的病历结构化能力双模型交叉验证提高数据质量
- **技术价值**:完整的前端模块化架构,可复用组件库
- **商业价值**Tool B可独立销售满足医疗数据处理需求
---
**开发计划制定完成!**
**下一步**:验证数据库表 → 测试后端API → 开始Phase 1Portal页面开发
**预计完成时间**5-6个工作日
**祝开发顺利!** 🚀