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
This commit is contained in:
2026-01-16 13:42:10 +08:00
parent 98d862dbd4
commit 66255368b7
560 changed files with 70424 additions and 52353 deletions

View File

@@ -1,52 +1,65 @@
# 标题æ˜è¦<EFBFBD>åˆ<EFBFBD>ç­æ¨¡å<EFBFBD>— - 详细开å<E282AC>计åˆï¼ˆMVP阶段ï¼?
> **文档版本�* V3.0
> **创建日期�* 2025-11-16
> **å¼€å<EFBFBD>卿œŸï¼š** 4 å‘?
> **负责团队ï¼?* ASL å¼€å<E282AC>组
# 标题摘要初筛模块 - 详细开发计划MVP阶段
> **文档版本:** V3.0
> **创建日期:** 2025-11-16
> **开发周期:** 4 周
> **负责团队:** ASL 开发组
> **最后更新:** 2025-11-16
> **â­?é‡<C3A9>è¦<C3A8>:基于真实架构(Frontend-v2 + Backend增é‡<EFBFBD>æ¼”è¿ + asl_schemaï¼?*
> **⭐ 重要:基于真实架构(Frontend-v2 + Backend增量演进 + asl_schema**
---
## 📋 模块概述
标题æ˜è¦<EFBFBD>åˆ<EFBFBD>ç­æ˜?ASL 模å<C2A1>—的第一个核心功能,也是 MVP 阶段的唯一交付功能ã€?
标题摘要初筛是 ASL 模块的第一个核心功能,也是 MVP 阶段的唯一交付功能。
### 功能范围
1. **设置与å<EFBFBD>¯åŠ¨è§†å?*:PICO 标准展示ã€<C3A3>Excel æ‡çŒ®å¯¼å…¥ã€<C3A3>å<EFBFBD>¯åЍç­é€‰ä»»åŠ?2. **审核工作å<C593>°è§†å?*:å<C5A1>Œæ¨¡åžåˆ¤æ­å¯¹æ¯”ã€<C3A3>冲çª<C3A7>标记ã€<C3A3>人工å¤<C3A5>æ ?3. **åˆ<C3A5>ç­ç»“果视å¾**:统计æ¦è§ˆã€<C3A3>PRISMA æŽé™¤æ€»ç»“ã€<C3A3>结果导å‡?
1. **设置与启动视图**PICO 标准展示、Excel 文献导入、启动筛选任务
2. **审核工作台视图**:双模型判断对比、冲突标记、人工复核
3. **初筛结果视图**统计概览、PRISMA 排除总结、结果导出
### 技术栈
| 层级 | 技�| 说明 |
| 层级 | 技术 | 说明 |
|------|------|------|
| **前端** | React 19 + TypeScript + Ant Design 5 + xlsx | Frontend-v2架构 |
| **后端** | Node.js + Fastify + TypeScript + Prisma | Backend/modules/asl/ |
| **LLM** | DeepSeek-V3 + Qwen3-72B | 复用 common/llm/adapters/ |
| **æ•°æ<EFBFBD>®åº?* | PostgreSQL 15 (asl_schema) | Schema隔离 |
| **数据库** | PostgreSQL 15 (asl_schema) | Schema隔离 |
---
## ðŸ<EFBFBD>—ï¸?æž¶æž„å‰<C3A5>æ<EFBFBD><C3A6>(已完æˆ<C3A6>ï¼?
### âœ?Frontend-v2 架构(Week 2 Day 6-7 完æˆ<C3A6>ï¼?```
## 🏗️ 架构前提(已完成)
### ✅ Frontend-v2 架构Week 2 Day 6-7 完成)
```
frontend-v2/src/
├── framework/layout/
� ├── MainLayout.tsx # �顶部导航布局
â”? └── TopNavigation.tsx # âœ?6个模å<C2A1>—导èˆ?├── framework/modules/
â”? ├── moduleRegistry.ts # âœ?模å<C2A1>—注册中心
â”? └── types.ts # âœ?ModuleDefinition接å<C2A5>£
│ ├── MainLayout.tsx # ✅ 顶部导航布局
│ └── TopNavigation.tsx # ✅ 6个模块导航
├── framework/modules/
│ ├── moduleRegistry.ts # ✅ 模块注册中心
│ └── types.ts # ✅ ModuleDefinition接口
└── modules/asl/
└── index.tsx # 🚧 å<> ä½<C3A4>页é<C2B5>¢ï¼ˆå¾…æ¿æ<C2BF>¢ï¼?```
└── index.tsx # 🚧 占位页面(待替换)
```
### âœ?Backend 架构(Week 2 Day 8-9 完æˆ<EFBFBD>ï¼?```
### Backend 架构(Week 2 Day 8-9 完成)
```
backend/src/
├── common/llm/adapters/ # âœ?LLMFactoryå<EFBFBD>¯å¤<EFBFBD>ç”?├── common/utils/jsonParser.js # âœ?JSONè§£æž<C3A6>å<EFBFBD>¯å¤<C3A5>ç”?└── modules/
├── common/llm/adapters/ # LLMFactory可复用
├── common/utils/jsonParser.js # ✅ JSON解析可复用
└── modules/
└── asl/ # 🚧 空目录(待创建)
```
### âœ?Database Schema(Week 1 完æˆ<EFBFBD>ï¼?```prisma
### Database SchemaWeek 1 完成)
```prisma
// backend/prisma/schema.prisma
datasource db {
schemas = [
"asl_schema", # �已预留,待定义表结构
"asl_schema", # ✅ 已预留,待定义表结构
// ...其他9个Schema
]
}
@@ -54,58 +67,70 @@ datasource db {
---
## 🌥ï¸?äºåŽŸç”Ÿå¼€å<E282AC>注æ„<C3A6>äºé¡¹ï¼ˆ2025-11-16 新增ï¼?
> **â­?é‡<C3A9>è¦<C3A8>æ´æ°**:本模å<C2A1>—å¼€å<E282AC>需é<E282AC>µå¾ªé˜¿é‡Œäº?Serverless 部署架构è¦<C3A8>æ±
> **详细规范**:[äºåŽŸç”Ÿå¼€å<E282AC>规范](../../../04-å¼€å<E282AC>è§„èŒ?08-äºåŽŸç”Ÿå¼€å<E282AC>è§„èŒ?md)
> **部署指å<E280A1>—**:[äºåŽŸç”Ÿéƒ¨ç½²æž¶æž„æŒ‡å<E280A1>—](../../../09-架构实施/03-äºåŽŸç”Ÿéƒ¨ç½²æž¶æž„æŒ‡å<E280A1>?md)
## 🌥️ 云原生开发注意事项(2025-11-16 新增)
> **⭐ 重要更新**:本模块开发需遵循阿里云 Serverless 部署架构要求
> **详细规范**[云原生开发规范](../../../04-开发规范/08-云原生开发规范.md)
> **部署指南**[云原生部署架构指南](../../../09-架构实施/03-云原生部署架构指南.md)
### 🎯 本地开发 + 云端部署双兼容策略
### 🎯 本地开å<E282AC>?+ äºç«¯éƒ¨ç½²å<C2B2>Œå…¼å®¹ç­ç•?
| 环境 | 存储方式 | 配置 | 说明 |
|------|---------|------|------|
| **本地开å<EFBFBD>?* | LocalAdapter | `STORAGE_TYPE=local` | 文件存储åˆ?`./uploads/` |
| **本地开发** | LocalAdapter | `STORAGE_TYPE=local` | 文件存储到 `./uploads/` |
| **生产环境** | OSSAdapter | `STORAGE_TYPE=oss` | 文件存储到阿里云 OSS |
**核心原则**ï¼?- âœ?**Excel导入**:内存解æž<C3A6>(`xlsx.read(buffer)`),ä¸<C3A4>è<EFBFBD>½ç?- âœ?**PDF上传**(V1.0):使用 `StorageFactory`,本åœ?OSS自动切æ<E280A1>¢
- âœ?**异步任务**:LLMç­é€‰ä»»åŠ¡å¿…é¡»å¼æ­¥å¤„ç<E2809E>†ï¼ˆ> 10ç§ä»»åŠ¡ï¼‰
- âœ?**环境å<C692>˜é‡<C3A9>**:所有é…<C3A9>置从 `.env` 读å<C2BB>
- âœ?**æ•°æ<C2B0>®åº“连接池**:使用全局 `prisma` 实ä¾ï¼Œä¸<C3A4>æ°å»ºè¿žæŽ¥
**核心原则**
-**Excel导入**:内存解析(`xlsx.read(buffer)`),不落盘
-**PDF上传**V1.0):使用 `StorageFactory`,本地/OSS自动切换
-**异步任务**LLM筛选任务必须异步处理> 10秒任务
-**环境变量**:所有配置从 `.env` 读取
-**数据库连接池**:使用全局 `prisma` 实例,不新建连接
### ❌ 禁止的做法
### â<>?ç¦<C3A7>止的å<E2809E>šæ³?
| 禁止操作 | 正确做法 | 原因 |
|---------|---------|------|
| `fs.writeFileSync('./temp.xlsx')` | `xlsx.read(buffer)` 内存解析 | Serverless容器重启丢失文件 |
| `new PrismaClient()` æ¯<C3A6>次æ°å»ºè¿žæŽ¥ | 使用全局 `prisma` 实例 | é<>¿å…<C3A5>连接数暴å¢?|
| 硬编ç ?`apiKey = 'sk-xxx'` | `process.env.LLM_API_KEY` | é…<EFBFBD>置管ç<EFBFBD>†æ··ä¹± |
| å<EFBFBD>Œæ­¥å¤„ç<EFBFBD>†1000æ<EFBFBD>¡æ‡çŒ®ç­é€?| 异步任务 + 进度轮询 | 超过30ç§è¶…æ—¶é™<C3A9>åˆ?|
| `new PrismaClient()` 每次新建连接 | 使用全局 `prisma` 实例 | 避免连接数暴增 |
| 硬编码 `apiKey = 'sk-xxx'` | `process.env.LLM_API_KEY` | 配置管理混乱 |
| 同步处理1000条文献筛选 | 异步任务 + 进度轮询 | 超过30秒超时限制 |
### ✅ MVP阶段开发检查清单
在提交代码前,请确认:
### âœ?MVP阶段开å<E282AC>检查清å<E280A6>?
在æ<EFBFBD><EFBFBD>交代ç <EFBFBD>å‰<EFBFBD>,请确认ï¼?
- [ ] Excel导入是否使用内存解析`xlsx.read(buffer)`
- [ ] 是否使用全局 `prisma` 实例(`import { prisma } from '@/config/database'`
- [ ] 是否所有配置都从环境变量读取?
- [ ] LLM筛选任务是否异步处理`POST /screening/start` 立即返回taskId
- [ ] 是å<C2AF>¦é¢„ç•™äº?OSS 字段(`pdfUrl`, `pdfOssKey`, `pdfFileSize`)?
- [ ] 是否预留了 OSS 字段(`pdfUrl`, `pdfOssKey`, `pdfFileSize`
- [ ] 是否使用存储抽象层(`StorageFactory.create()`
**预留字段说明**ï¼?- MVP阶段仅å<E280A6>šæ ‡é¢˜æ˜è¦<C3A8>ç­é€‰ï¼Œä¸<C3A4>处ç<E2809E>†PDF
**预留字段说明**
- MVP阶段仅做标题摘要筛选不处理PDF
- V1.0阶段实现全文PDF筛选时使用预留的OSS字段
---
## 📅 åå¨å¼€å<E282AC>计åˆ?
## 📅 四周开发计划
```
Week 1: æ•°æ<EFBFBD>®åº“Schema + å<>Žç«¯API框架 + 存储抽象å±?Week 2: LLMç­é€‰æ ¸å¿?+ 弿­¥æ‰¹å¤„ç<E2809E>†é€»è¾
Week 3: å‰<EFBFBD>端模å<EFBFBD>—å¼€å<EFBFBD>?+ 审核工作å<C593>°ï¼ˆå†…存解æž<C3A6>Excelï¼?Week 4: 结果展示 + 导出 + 醿ˆ<C3A6>æµè¯•
Week 1: 数据库Schema + 后端API框架 + 存储抽象层
Week 2: LLM筛选核心 + 异步批处理逻辑
Week 3: 前端模块开发 + 审核工作台内存解析Excel
Week 4: 结果展示 + 导出 + 集成测试
```
---
## 🗓ï¸?Week 1: æ•°æ<EFBFBD>®åº“Schema与å<EFBFBD>Žç«¯API框架
## 🗓️ Week 1: 数据库Schema与后端API框架
### Day 1: Prisma Schema 设计
#### 任务1: 设计 asl_schema 表结�
**�`backend/prisma/schema.prisma` 中添加:**
#### 任务1: 设计 asl_schema 表结构
**在 `backend/prisma/schema.prisma` 中添加:**
```prisma
// ==================== ASL 筛选项目表 ====================
@@ -119,12 +144,15 @@ model AslScreeningProject {
// PICO标准
picoCriteria Json @map("pico_criteria") // { population, intervention, comparison, outcome, studyDesign }
// 筛选标� inclusionCriteria String @map("inclusion_criteria") @db.Text
// 筛选标准
inclusionCriteria String @map("inclusion_criteria") @db.Text
exclusionCriteria String @map("exclusion_criteria") @db.Text
// 状� status String @default("draft") // draft, screening, completed
// 状态
status String @default("draft") // draft, screening, completed
// ç­é€‰é…<EFBFBD>ç½? screeningConfig Json? @map("screening_config") // { models: ["deepseek", "qwen"], temperature: 0 }
// 筛选配置
screeningConfig Json? @map("screening_config") // { models: ["deepseek", "qwen"], temperature: 0 }
// 关联
literatures AslLiterature[]
@@ -140,7 +168,7 @@ model AslScreeningProject {
@@index([status])
}
// ==================== ASL æ‡çŒ®æ<EFBFBD>¡ç®è¡?====================
// ==================== ASL 文献条目表 ====================
model AslLiterature {
id String @id @default(uuid())
projectId String @map("project_id")
@@ -155,7 +183,8 @@ model AslLiterature {
publicationYear Int? @map("publication_year")
doi String?
// äºåŽŸç”Ÿå­˜å¨å­—段(V1.0 阶段使用,MVP阶段预留ï¼? pdfUrl String? @map("pdf_url") // PDF访问URL
// 云原生存储字段V1.0 阶段使用MVP阶段预留
pdfUrl String? @map("pdf_url") // PDF访问URL
pdfOssKey String? @map("pdf_oss_key") // OSS存储Key用于删除
pdfFileSize Int? @map("pdf_file_size") // 文件大小(字节)
@@ -212,19 +241,23 @@ model AslScreeningResult {
qwenSEvidence String? @map("qwen_s_evidence") @db.Text
qwenReason String? @map("qwen_reason") @db.Text
// 冲çª<EFBFBD>状æ€? conflictStatus String @default("none") @map("conflict_status") // "none" | "conflict" | "resolved"
// 冲突状态
conflictStatus String @default("none") @map("conflict_status") // "none" | "conflict" | "resolved"
conflictFields Json? @map("conflict_fields") // ["P", "I", "conclusion"]
// 最终决� finalDecision String? @map("final_decision") // "include" | "exclude" | "pending"
// 最终决策
finalDecision String? @map("final_decision") // "include" | "exclude" | "pending"
finalDecisionBy String? @map("final_decision_by") // userId
finalDecisionAt DateTime? @map("final_decision_at")
exclusionReason String? @map("exclusion_reason") @db.Text
// AI处ç<EFBFBD>†çжæ€? aiProcessingStatus String @default("pending") @map("ai_processing_status") // "pending" | "processing" | "completed" | "failed"
// AI处理状态
aiProcessingStatus String @default("pending") @map("ai_processing_status") // "pending" | "processing" | "completed" | "failed"
aiProcessedAt DateTime? @map("ai_processed_at")
aiErrorMessage String? @map("ai_error_message") @db.Text
// å<EFBFBD>¯è¿½æº¯ä¿¡æ<EFBFBD>? promptVersion String @default("v1.0.0") @map("prompt_version")
// 可追溯信息
promptVersion String @default("v1.0.0") @map("prompt_version")
rawOutput Json? @map("raw_output") // 原始LLM输出备份
createdAt DateTime @default(now()) @map("created_at")
@@ -272,48 +305,54 @@ model AslScreeningTask {
@@index([status])
}
// ==================== 用户表关è<EFBFBD>”(添加到User模åžï¼?===================
// �platform_schema �User 模型中添加:
// ==================== 用户表关联添加到User模型====================
// platform_schema User 模型中添加:
// aslProjects AslScreeningProject[] @relation("AslProjects")
```
**执行è¿<EFBFBD>ç§»ï¼?*
**执行迁移:**
```bash
cd backend
npx prisma migrate dev --name add_asl_screening_tables
npx prisma generate
```
**验收标准**ï¼?- âœ?æ•°æ<C2B0>®åº“表åˆå»ºæˆ<C3A6>功ï¼?张表ï¼?- âœ?Prisma Client 生æˆ<C3A6>æˆ<C3A6>功
- âœ?å<>¯æŸ¥è¯?asl_schema è¡?
**验收标准**
- ✅ 数据库表创建成功4张表
- ✅ Prisma Client 生成成功
- ✅ 可查询 asl_schema 表
---
### Day 2: 后端目录结构创建
> **â­?å‰<C3A5>ç½®æ<C2AE>¡ä»¶ï¼?025-11-17 æ›´æ–°ï¼?*:平å<C2B3>°åŸºç¡€è®¾æ½å·²å®Œæˆ<C3A6>实æ?âœ?
> **完æˆ<C3A6>状æ€?*ï¼?个核心模å<C2A1>—,100%测试通过
> **⭐ 前置条件2025-11-17 更新)**:平台基础设施已完成实施 ✅
> **完成状态**8个核心模块100%测试通过
> **完成报告**[平台基础设施实施完成报告](../../../08-项目管理/03-每周计划/2025-11-17-平台基础设施实施完成报告.md)
> **使用指南**[backend/src/common/README.md](../../../../backend/src/common/README.md)
#### å¹³å<EFBFBD>°å·²æ<EFBFBD><EFBFBD>ä¾çš„8个核心模å<EFBFBD>—(无需ASL模å<EFBFBD>—实现ï¼?
#### 平台已提供的8个核心模块无需ASL模块实现
**平台基础设施路径**`backend/src/common/`
| # | 模块 | 使用方式 | 功能说明 |
|---|------|---------|---------|
| 1 | **å­˜å¨æœ<EFBFBD>务** | `import { storage } from '@/common/storage'` | 文件上传下载(本åœ?OSS切æ<E280A1>¢ï¼?|
| 1 | **存储服务** | `import { storage } from '@/common/storage'` | 文件上传下载(本地/OSS切换 |
| 2 | **日志系统** | `import { logger } from '@/common/logging'` | 结构化JSON日志 |
| 3 | **缓存服务** | `import { cache } from '@/common/cache'` | 内存/Redis缓存 |
| 4 | **异步任务** | `import { jobQueue } from '@/common/jobs'` | 长时间任务处ç<E2809E>?|
| 5 | **å<EFBFBD>¥åº·æ£€æŸ?* | `import { registerHealthRoutes } from '@/common/health'` | SAEå<EFBFBD>¥åº·æ£€æŸ?|
| 6 | **监控指标** | `import { Metrics } from '@/common/monitoring'` | 性能监控和告�|
| 4 | **异步任务** | `import { jobQueue } from '@/common/jobs'` | 长时间任务处理 |
| 5 | **健康检查** | `import { registerHealthRoutes } from '@/common/health'` | SAE健康检查 |
| 6 | **监控指标** | `import { Metrics } from '@/common/monitoring'` | 性能监控和告警 |
| 7 | **数据库连接池** | `import { prisma } from '@/config/database'` | 全局Prisma实例 |
| 8 | **环境配置** | `import { env } from '@/config/env'` | 统一配置管理 |
**å­˜å¨æœ<EFBFBD>务使用示ä¾**ï¼?```typescript
**存储服务使用示例**
```typescript
// ASL模块直接使用一行代码
import { storage } from '@/common/storage'
// 上传æ‡ä»¶ï¼ˆä¸<EFBFBD>关心本地还是OSSï¼?const url = await storage.upload('asl/literature/123.pdf', pdfBuffer)
// 上传文件不关心本地还是OSS
const url = await storage.upload('asl/literature/123.pdf', pdfBuffer)
// 下载文件
const buffer = await storage.download('asl/literature/123.pdf')
@@ -322,18 +361,26 @@ const buffer = await storage.download('asl/literature/123.pdf')
await storage.delete('asl/literature/123.pdf')
```
**支æŒ<EFBFBD>的部署环å¢?*ï¼?- âœ?本地开å<E282AC>:LocalAdapter(æ‡ä»¶å­˜å¨åˆ° `./uploads/`ï¼?- âœ?äºç«¯SaaS:OSSAdapter(æ‡ä»¶å­˜å¨åˆ°é˜¿é‡ŒäºOSSï¼?- âœ?ç§<C3A7>有åŒéƒ¨ç½²ï¼šLocalAdapter(æ‡ä»¶å­˜å¨åˆ°æœ<C3A6>务器)
- âœ?å<>•机版:LocalAdapter(æ‡ä»¶å­˜å¨åˆ°ç”¨æˆ·æœ¬åœ°ï¼?
**环境切æ<E280A1>¢**:修改一个环境å<C692>˜é‡<C3A9>å<EFBFBD>³å<C2B3>?```bash
# 本地开å<E282AC>?STORAGE_TYPE=local
**支持的部署环境**
- ✅ 本地开发LocalAdapter文件存储到 `./uploads/`
- ✅ 云端SaaSOSSAdapter文件存储到阿里云OSS
- ✅ 私有化部署LocalAdapter文件存储到服务器
- ✅ 单机版LocalAdapter文件存储到用户本地
**环境切换**:修改一个环境变量即可
```bash
# 本地开发
STORAGE_TYPE=local
# 生产环境
STORAGE_TYPE=oss
```
**核心优势**ï¼?- âœ?ASL模å<C2A1>—无需关心基础设æ½å®žçŽ°ç»†èŠ
- âœ?代ç <C3A7>é¶æ”¹åŠ¨åˆ‡æ<E280A1>¢çŽ¯å¢ƒï¼ˆæœ¬åœ° â†?云端ï¼?- âœ?所有业务模å<C2A1>—(AIA/PKB/DC等)å¤<C3A5>用å<C2A8>Œä¸€å¥—基础设æ½
- âœ?统一维护ã€<C3A3>统一å<E282AC>‡çº§ã€<C3A3>ç»Ÿä¸€çæŽ§
**核心优势**
- ✅ ASL模块无需关心基础设施实现细节
- ✅ 代码零改动切换环境(本地 ↔ 云端)
- ✅ 所有业务模块AIA/PKB/DC等复用同一套基础设施
- ✅ 统一维护、统一升级、统一监控
---
@@ -355,7 +402,7 @@ touch types/screening.types.ts
#### 任务2: 创建路由文件
**`backend/src/modules/asl/routes/index.ts`ï¼?*
**`backend/src/modules/asl/routes/index.ts`**
```typescript
import { FastifyInstance } from 'fastify'
import * as projectController from '../controllers/projectController.js'
@@ -366,8 +413,9 @@ import * as screeningController from '../controllers/screeningController.js'
* ASL 模块路由注册
*
* @description
* - 注册åˆ?/api/v1/asl å‰<EFBFBD>ç¼€
* - å<EFBFBD>è€?legacy/routes/ 的风æ ? *
* - 注册到 /api/v1/asl 前缀
* - 参考 legacy/routes/ 的风格
*
* @version Week 3 Day 2
*/
export async function aslRoutes(fastify: FastifyInstance) {
@@ -382,7 +430,8 @@ export async function aslRoutes(fastify: FastifyInstance) {
fastify.post('/projects/:projectId/literatures/import', literatureController.importLiteratures)
fastify.get('/projects/:projectId/literatures', literatureController.listLiteratures)
// ç­é€‰ç®¡ç<EFBFBD>? fastify.post('/projects/:projectId/screening/start', screeningController.startScreening)
// 筛选管理
fastify.post('/projects/:projectId/screening/start', screeningController.startScreening)
fastify.get('/projects/:projectId/screening/results', screeningController.getScreeningResults)
fastify.put('/screening/results/:resultId', screeningController.updateScreeningResult)
fastify.post('/screening/results/batch-update', screeningController.batchUpdateResults)
@@ -391,19 +440,21 @@ export async function aslRoutes(fastify: FastifyInstance) {
}
```
**验收标准**�- �目录结构清晰
- âœ?路由æ‡ä»¶åˆå»ºå®Œæˆ<C3A6>
- âœ?**å<>¯æ­£å¸¸ä½¿ç”¨å¹³å<C2B3>°æœ<C3A6>åŠ?*ï¼? - âœ?`import { storage } from '@/common/storage'` å<>¯ç”¨
- âœ?`import { logger } from '@/common/logging'` å<>¯ç”¨
- âœ?`import { prisma } from '@/config/database'` å<>¯ç”¨
- âœ?`import { jobQueue } from '@/common/jobs'` å<>¯ç”¨
- âœ?`import { cache } from '@/common/cache'` å<>¯ç”¨
**验收标准**
- ✅ 目录结构清晰
- ✅ 路由文件创建完成
-**可正常使用平台服务**
-`import { storage } from '@/common/storage'` 可用
-`import { logger } from '@/common/logging'` 可用
-`import { prisma } from '@/config/database'` 可用
-`import { jobQueue } from '@/common/jobs'` 可用
-`import { cache } from '@/common/cache'` 可用
---
### Day 3: �index.ts 中注册ASL路由
### Day 3: index.ts 中注册ASL路由
**`backend/src/index.ts`(修改)�*
**`backend/src/index.ts`(修改):**
```typescript
// ============================================
// 【新架构】ASL 模块 - Week 3 新增
@@ -415,19 +466,22 @@ import { aslRoutes } from './modules/asl/routes/index.js';
// 注册 ASL 模块路由
await fastify.register(aslRoutes, { prefix: '/api/v1/asl' });
console.log('�ASL 路由已注册到 /api/v1/asl/*');
console.log('✅ ASL 路由已注册到 /api/v1/asl/*');
```
**验收标准**ï¼?- âœ?å<>Žç«¯å<C2AF>¯åЍæˆ<C3A6>功
- âœ?访问 `http://localhost:3001/api/v1/asl/projects` 返回 200(å<CB86>³ä½¿æ˜¯ç©ºåˆ—表)
**验收标准**
- ✅ 后端启动成功
- ✅ 访问 `http://localhost:3001/api/v1/asl/projects` 返回 200即使是空列表
---
## 🗓�Week 2: LLM筛选核�
### Day 4-5: LLMç­é€‰æœ<C3A6>务实çŽ?
## 🗓️ Week 2: LLM筛选核心
### Day 4-5: LLM筛选服务实现
#### 任务1: 定义 JSON Schema
**`backend/src/modules/asl/schemas/screening.schema.ts`ï¼?*
**`backend/src/modules/asl/schemas/screening.schema.ts`**
```typescript
export const screeningOutputSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
@@ -487,10 +541,12 @@ export const screeningOutputSchema = {
};
```
#### 任务2: åˆå»ºæ<C2BA><C3A6>示è¯<C3A8>模æ<C2A1>?
**`backend/prompts/asl/screening/v1.0.0-basic.txt`ï¼?*
#### 任务2: 创建提示词模板
**`backend/prompts/asl/screening/v1.0.0-basic.txt`**
```
你是一ä½<EFBFBD>医学æ‡çŒ®ç­é€‰ä¸“å®¶ã€è¯·æ ¹æ<EFBFBD>®ä»¥ä¸ PICO 标准判æ­è¿™ç¯‡æ‡çŒ®æ˜¯å<C2AF>¦åº”该纳入系统评价ã€?
你是一位医学文献筛选专家。请根据以下 PICO 标准判断这篇文献是否应该纳入系统评价。
# PICO 标准
- **Population (研究对象)**: {{population}}
- **Intervention (干预措施)**: {{intervention}}
@@ -504,16 +560,18 @@ export const screeningOutputSchema = {
# 排除标准
{{exclusionCriteria}}
# 待筛选文�**标题**: {{title}}
# 待筛选文献
**标题**: {{title}}
**摘要**: {{abstract}}
# 输出要求
请严格按照以ä¸?JSON Schema 输出结果,输出纯JSON(ä¸<C3A4>è¦<C3A8>包å<E280A6>«ä»»ä½•其仿‡å­—)ï¼?
请严格按照以下 JSON Schema 输出结果输出纯JSON不要包含任何其他文字
{
"decision": "include/exclude/uncertain",
"reason": "判æ­ç<EFBFBD>†ç”±ï¼?0-500字)",
"reason": "判断理由10-500字",
"confidence": 0.95,
"pico": {
"population": "match/partial/mismatch",
@@ -531,13 +589,16 @@ export const screeningOutputSchema = {
}
# 注意事项
1. decision å<EFBFBD>ªèƒ½æ˜?"include"(纳入)ã€?exclude"(排除)æˆ?"uncertain"(ä¸<C3A4>确定ï¼?2. reason 必须具体说明判æ­ä¾<C3A4>æ<EFBFBD>®
3. confidence ä¸?0-1 之间的数å€?4. pico 字段é€<C3A9>项评估匹é…<C3A9>ç¨åº¦
1. decision 只能是 "include"(纳入)、"exclude"(排除)或 "uncertain"(不确定)
2. reason 必须具体说明判断依据
3. confidence 为 0-1 之间的数值
4. pico 字段逐项评估匹配程度
5. evidences 字段提取原文中的关键短语作为证据
```
#### 任务3: 实现 LLM ç­é€‰æœ<C3A6>åŠ?
**`backend/src/modules/asl/services/llmScreeningService.ts`ï¼?*
#### 任务3: 实现 LLM 筛选服务
**`backend/src/modules/asl/services/llmScreeningService.ts`**
```typescript
import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js';
import { parseJSON } from '../../../common/utils/jsonParser.js';
@@ -554,18 +615,23 @@ const ajv = new Ajv();
const validateSchema = ajv.compile(screeningOutputSchema);
/**
* LLM ç­é€‰æœ<EFBFBD>åŠ? *
* LLM 筛选服务
*
* @description
* - 复用 common/llm/adapters/LLMFactory.ts
* - å<EFBFBD>Œæ¨¡åžå¹¶è¡Œè°ƒç”¨ï¼ˆDeepSeek + Qwenï¼? * - JSON Schema 验è¯<C3A8>
* - 冲çª<EFBFBD>检æµ? *
* - 双模型并行调用DeepSeek + Qwen
* - JSON Schema 验证
* - 冲突检测
*
* @version Week 3 Day 4-5
*/
class LLMScreeningService {
/**
* å<EFBFBD>Œæ¨¡åžå¹¶è¡Œç­é€? */
* 双模型并行筛选
*/
async dualModelScreening(literature: any, protocol: any) {
// 构建æ<EFBFBD><EFBFBD>示è¯? const prompt = this.buildPrompt(literature, protocol);
// 构建提示词
const prompt = this.buildPrompt(literature, protocol);
// 并行调用两个模型
const [resultA, resultB] = await Promise.all([
@@ -577,7 +643,8 @@ class LLMScreeningService {
const decisionA = await this.parseModelOutput(resultA.content, 'deepseek');
const decisionB = await this.parseModelOutput(resultB.content, 'qwen');
// 一致性判� const { consensus, conflictFields } = this.compareDecisions(decisionA, decisionB);
// 一致性判断
const { consensus, conflictFields } = this.compareDecisions(decisionA, decisionB);
// 自动分流
const needReview = this.shouldReview(consensus, decisionA, decisionB);
@@ -593,7 +660,8 @@ class LLMScreeningService {
}
/**
* 调用LLM模åžï¼ˆå¤<EFBFBD>用common/llmï¼? */
* 调用LLM模型复用common/llm
*/
private async callModel(modelName: string, prompt: string) {
const llm = LLMFactory.createLLM(modelName);
@@ -601,16 +669,19 @@ class LLMScreeningService {
messages: [
{ role: 'user', content: prompt }
],
temperature: 0, // 确定性输� max_tokens: 1000
temperature: 0, // 确定性输出
max_tokens: 1000
});
return response;
}
/**
* 构建æ<EFBFBD><EFBFBD>示è¯? */
* 构建提示词
*/
private buildPrompt(literature: any, protocol: any): string {
// 读å<EFBFBD>æ<EFBFBD><EFBFBD>示è¯<EFBFBD>模æ<EFBFBD>? const templatePath = path.resolve(__dirname, '../../../../prompts/asl/screening/v1.0.0-basic.txt');
// 读取提示词模板
const templatePath = path.resolve(__dirname, '../../../../prompts/asl/screening/v1.0.0-basic.txt');
let template = fs.readFileSync(templatePath, 'utf-8');
// 替换变量
@@ -631,12 +702,13 @@ class LLMScreeningService {
* 解析模型输出
*/
private async parseModelOutput(content: string, modelName: string) {
// 使用JSONè§£æž<EFBFBD>器(å¤<EFBFBD>用common/utilsï¼? const parsed = parseJSON(content);
// 使用JSON解析器复用common/utils
const parsed = parseJSON(content);
// JSON Schema 验证
const valid = validateSchema(parsed);
if (!valid) {
console.error('JSON Schema验è¯<C3A8>失败ï¼?, validateSchema.errors);
console.error('JSON Schema验证失败', validateSchema.errors);
throw new Error(`模型${modelName}输出格式不符合Schema`);
}
@@ -652,15 +724,18 @@ class LLMScreeningService {
}
/**
* 对比两个模型的决� */
* 对比两个模型的决策
*/
private compareDecisions(decisionA: any, decisionB: any) {
const conflicts: string[] = [];
// 比较最终决� if (decisionA.decision !== decisionB.decision) {
// 比较最终决策
if (decisionA.decision !== decisionB.decision) {
conflicts.push('decision');
}
// 比较PICOå<EFBFBD>„ç»´åº? if (decisionA.pico.population !== decisionB.pico.population) conflicts.push('P');
// 比较PICO各维度
if (decisionA.pico.population !== decisionB.pico.population) conflicts.push('P');
if (decisionA.pico.intervention !== decisionB.pico.intervention) conflicts.push('I');
if (decisionA.pico.comparison !== decisionB.pico.comparison) conflicts.push('C');
if (decisionA.pico.outcome !== decisionB.pico.outcome) conflicts.push('O');
@@ -674,24 +749,27 @@ class LLMScreeningService {
* 自动分流规则
*/
private shouldReview(consensus: string, decisionA: any, decisionB: any): boolean {
// 规则1:冲çª?â†?å¿…é¡»å¤<C3A5>æ ¸
// 规则1冲突 → 必须复核
if (consensus === 'conflict') {
return true;
}
// 规则2:低置信åº?â†?需è¦<C3A8>å¤<C3A5>æ ? const avgConfidence = (decisionA.confidence + decisionB.confidence) / 2;
// 规则2低置信度 → 需要复核
const avgConfidence = (decisionA.confidence + decisionB.confidence) / 2;
if (avgConfidence < 0.7) {
return true;
}
// 规则3:高置信�+ 一��自动通过
// 规则3高置信度 + 一致 → 自动通过
return false;
}
/**
* 批é‡<EFBFBD>ç­é€? */
* 批量筛选
*/
async batchScreening(literatures: any[], protocol: any, progressCallback?: (progress: number) => void) {
const batchSize = 15; // æ¯<EFBFBD>批15ç¯? const results = [];
const batchSize = 15; // 每批15篇
const results = [];
for (let i = 0; i < literatures.length; i += batchSize) {
const batch = literatures.slice(i, i + batchSize);
@@ -703,7 +781,8 @@ class LLMScreeningService {
results.push(...batchResults);
// 推é€<EFBFBD>è¿åº? const progress = Math.round(((i + batch.length) / literatures.length) * 100);
// 推送进度
const progress = Math.round(((i + batch.length) / literatures.length) * 100);
progressCallback?.(progress);
}
@@ -714,16 +793,20 @@ class LLMScreeningService {
export const llmScreeningService = new LLMScreeningService();
```
**验收标准**ï¼?- âœ?LLMå<4D>Œæ¨¡åžè°ƒç”¨æˆ<C3A6>åŠ?- âœ?JSON Schema验è¯<C3A8>通过çŽ?> 95%
- âœ?冲çª<C3A7>检æµå‡†ç¡?
**验收标准**
- ✅ LLM双模型调用成功
- ✅ JSON Schema验证通过率 > 95%
- ✅ 冲突检测准确
---
## 🗓ï¸?Week 3: å‰<EFBFBD>端模å<EFBFBD>—å¼€å<EFBFBD>?
## 🗓️ Week 3: 前端模块开发
### Day 6-7: 前端模块结构创建
#### 任务1: 更新模块定义
**`frontend-v2/src/modules/asl/index.tsx`(修改)�*
**`frontend-v2/src/modules/asl/index.tsx`(修改):**
```typescript
import { lazy } from 'react'
import { ModuleDefinition } from '@/framework/modules/types'
@@ -744,7 +827,7 @@ const ASLModule: ModuleDefinition = {
path: '/literature',
icon: FileSearchOutlined,
component: lazy(() => import('./routes')),
placeholder: false, // �改为 false
placeholder: false, // ✅ 改为 false
requiredVersion: 'advanced',
description: 'AI驱动的文献筛选和分析系统',
}
@@ -767,7 +850,7 @@ touch api/index.ts
#### 任务3: 实现路由配置
**`frontend-v2/src/modules/asl/routes.tsx`ï¼?*
**`frontend-v2/src/modules/asl/routes.tsx`**
```typescript
import { lazy } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
@@ -782,7 +865,9 @@ const ScreeningResults = lazy(() => import('./pages/ScreeningResults'))
*
* @description
* - /literature - 项目列表
* - /literature/project/:id/settings - 设置与å<EFBFBD>¯åŠ? * - /literature/project/:id/workbench - 审核工作å<C593>? * - /literature/project/:id/results - åˆ<C3A5>ç­ç»“æžœ
* - /literature/project/:id/settings - 设置与启动
* - /literature/project/:id/workbench - 审核工作台
* - /literature/project/:id/results - 初筛结果
*
* @version Week 3 Day 6
*/
@@ -800,7 +885,9 @@ export default function ASLRoutes() {
}
```
**验收标准**ï¼?- âœ?顶部导航显示"AI智能æ‡çŒ®"(ä¸<C3A4>å†<C3A5>是å<C2AF> ä½<C3A4>ï¼?- âœ?ç¹å‡»å<C2BB>Žè¿å…¥é¡¹ç®åˆ—表页(å<CB86>³ä½¿æ˜¯ç©ºåˆ—表)
**验收标准**
- ✅ 顶部导航显示"AI智能文献"(不再是占位)
- ✅ 点击后进入项目列表页(即使是空列表)
---
@@ -808,29 +895,39 @@ export default function ASLRoutes() {
(由于篇幅限制,核心实现代码请参考任务分解文档)
**验收标准**�- �Excel上传功能正常
- âœ?审核工作å<C593>°å<C2B0>¯å±•示ç­é€‰ç»“æž?- âœ?å<>Œè§†å¾æ¨¡æ€<C3A6>框å<E280A0>¯å¼¹å‡?
**验收标准**
- ✅ Excel上传功能正常
- ✅ 审核工作台可展示筛选结果
- ✅ 双视图模态框可弹出
---
## 🗓ï¸?Week 4: 醿ˆ<EFBFBD>æµè¯•与验æ”?
### Day 11-14: 端到端测�
(详细æµè¯•计åˆè§<EFBFBD>ä»»åŠ¡åˆ†è§£æ‡æ¡£ï¼?
**验收标准**ï¼?- âœ?完整æµ<C3A6>ç¨ï¼šä¸Šä¼?â†?ç­›é€?â†?å¤<C3A5>æ ¸ â†?导出
- �准确��85%
- âœ?性能达标ï¼?00ç¯?< 10分éŸï¼?
## 🗓️ Week 4: 集成测试与验收
### Day 11-14: 端到端测试
(详细测试计划见任务分解文档)
**验收标准**
- ✅ 完整流程:上传 → 筛选 → 复核 → 导出
- ✅ 准确率 ≥ 85%
- ✅ 性能达标100篇 < 10分钟
---
## 📚 相关文档
- [开发里程碑](./01-开发里程碑.md)
- [任务分解Todo List](./03-任务分解.md)
- [è´¨é‡<EFBFBD>ä¿<EFBFBD>éšœç­ç•¥](../02-技术设è®?06-è´¨é‡<C3A9>ä¿<C3A4>障与å<C5BD>¯è¿½æº¯ç­ç•¥.md)
- [技术选型](../02-技术设è®?07-æ‡çŒ®å¤„ç<E2809E>†æŠ€æœ¯é€‰åž.md)
- [API设计规范](../02-技术设�02-API设计规范.md)
- [质量保障策略](../02-技术设计/06-质量保障与可追溯策略.md)
- [技术选型](../02-技术设计/07-文献处理技术选型.md)
- [API设计规范](../02-技术设计/02-API设计规范.md)
- [前后端模块化架构设计-V2](../../../00-系统总体设计/前后端模块化架构设计-V2.md)
---
**更新日志**ï¼?- 2025-11-18: V3.1 æ´æ°ï¼Œè¡¥å……å¹³å<C2B3>°åŸºç¡€è®¾æ½å®Œæˆ<C3A6>状æ€<C3A6>(¸ªæ ¸å¿ƒæ¨¡å<C2A1>—)
- 2025-11-16: V3.0 é‡<C3A9>写,基于真实架构(Frontend-v2 + Backend + asl_schemaï¼?- 2025-11-16: V2.0 é‡<C3A9>写,详细到æ¯<C3A6>天的任务åŒä»£ç <C3A7>示ä¾
- 2025-10-29: V1.0 åˆå»ºï¼Œåˆ<EFBFBD>å§ç‰ˆæœ?
**更新日志**
- 2025-11-18: V3.1 更新补充平台基础设施完成状态8个核心模块
- 2025-11-16: V3.0 重写基于真实架构Frontend-v2 + Backend + asl_schema
- 2025-11-16: V2.0 重写,详细到每天的任务和代码示例
- 2025-10-29: V1.0 创建,初始版本