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
13 KiB
13 KiB
数据库迁移状态说明
文档版本: v1.0
创建日期: 2025-11-23
维护者: ASL开发团队
文档目的: 记录ASL模块数据库迁移状态,为未来开发人员提供清晰的上下文
📋 当前数据库状态总览
✅ ASL模块(asl_schema)- 完全正确
| 表名 | 状态 | 用途 | 记录数 |
|---|---|---|---|
literatures |
✅ 已更新 | 文献基础信息(含全文字段) | - |
screening_projects |
✅ 正常 | 筛选项目 | - |
screening_tasks |
✅ 正常 | 标题摘要初筛任务 | - |
screening_results |
✅ 正常 | 标题摘要初筛结果 | - |
fulltext_screening_tasks |
✅ 新建 | 全文复筛任务 | 0 |
fulltext_screening_results |
✅ 新建 | 全文复筛结果 | 0 |
核心结论:
- ✅ ASL模块所有数据完全位于
asl_schema - ✅ 没有数据泄漏到
publicschema - ✅ Schema隔离策略执行正确
- ✅ 代码访问路径正确(
prisma.aslLiterature,prisma.aslScreeningProject等)
🔴 Public Schema历史遗留问题(与ASL无关)
问题描述
在项目早期开发中,部分模块的表被错误地创建在 public schema 中,违反了Schema隔离策略:
| 错误表名 | 应在Schema | 当前状态 |
|---|---|---|
public.users |
platform_schema |
⚠️ 重复存在 |
public.projects |
aia_schema |
⚠️ 重复存在 |
public.conversations |
aia_schema |
⚠️ 重复存在 |
public.messages |
aia_schema |
⚠️ 重复存在 |
public.knowledge_bases |
pkb_schema |
⚠️ 重复存在 |
public.documents |
pkb_schema |
⚠️ 重复存在 |
public.batch_tasks |
pkb_schema |
⚠️ 重复存在 |
public.batch_results |
pkb_schema |
⚠️ 重复存在 |
数据对比(2025-11-23快照):
platform_schema.users: 3条记录
public.users: 2条记录
aia_schema.projects: 2条记录
public.projects: 2条记录
pkb_schema.knowledge_bases: 2条记录
public.knowledge_bases: 2条记录
影响范围:
- 🟢 不影响ASL模块(ASL完全隔离在asl_schema)
- ⚠️ 影响AIA模块(AI助手)
- ⚠️ 影响PKB模块(知识库)
- ⚠️ 影响Platform模块(用户系统)
责任归属:
- 🔵 ASL团队:无责任,数据管理完全正确
- 🟡 其他模块团队:需自行清理public schema数据
🛠️ 2025-11-23迁移操作记录
迁移目标
为全文复筛功能(Day 4开发)添加数据库支持:
- 修改
literatures表(添加全文相关字段) - 创建
fulltext_screening_tasks表 - 创建
fulltext_screening_results表
迁移策略选择
❌ 方案A:Prisma Migrate(被拒绝)
npx prisma migrate dev --name add_fulltext_screening
拒绝原因:
- Prisma会尝试删除
publicschema中的重复表 - 可能影响其他模块的数据
- 违反"管好自己"的原则
✅ 方案B:手动SQL脚本(已采用)
# 创建手动迁移脚本
backend/prisma/migrations/manual_fulltext_screening.sql
# 执行迁移(仅操作asl_schema)
Get-Content manual_fulltext_screening.sql | docker exec -i ai-clinical-postgres psql ...
优势:
- ✅ 只操作
asl_schema,不动其他schema - ✅ 不删除任何
public数据 - ✅ 安全、可控、可审计
- ✅ 符合"管好自己"原则
迁移内容详情
1. 修改 literatures 表
新增字段(13个):
文献生命周期:
stage TEXT DEFAULT 'imported'- 阶段标记(imported → title_screened → fulltext_pending → fulltext_screened)
PDF管理:
has_pdf BOOLEAN DEFAULT false- 是否有PDFpdf_storage_type TEXT- 存储类型(oss/dify/local)pdf_storage_ref TEXT- 存储引用(key或ID)pdf_status TEXT DEFAULT 'pending'- 状态(pending/extracting/completed/failed)pdf_uploaded_at TIMESTAMP(3)- 上传时间
全文管理(云原生):
full_text_storage_type TEXT- 存储类型(oss/dify)full_text_storage_ref TEXT- 存储引用full_text_url TEXT- 访问URL
全文元数据:
full_text_format TEXT- 格式(markdown/plaintext)full_text_source TEXT- 提取方式(nougat/pymupdf)full_text_token_count INTEGER- Token数量full_text_extracted_at TIMESTAMP(3)- 提取时间
新增索引:
idx_literatures_stageidx_literatures_has_pdfidx_literatures_pdf_status
2. 创建 fulltext_screening_tasks 表
任务管理表,字段包括:
- 基础信息:
id,project_id - 模型配置:
model_a,model_b,prompt_version - 进度跟踪:
total_count,processed_count,success_count,failed_count,degraded_count - 成本统计:
total_tokens,total_cost - 状态管理:
status,started_at,completed_at,estimated_end_at - 错误记录:
error_message,error_stack
索引:
idx_fulltext_tasks_project_ididx_fulltext_tasks_statusidx_fulltext_tasks_created_at
外键约束:
project_id→screening_projects(id)ON DELETE CASCADE
3. 创建 fulltext_screening_results 表
结果存储表(12字段模板),字段包括:
- 关联信息:
task_id,project_id,literature_id - Model A结果:
model_a_name,model_a_fields(JSONB),model_a_tokens,model_a_cost等 - Model B结果:
model_b_name,model_b_fields(JSONB),model_b_tokens,model_b_cost等 - 验证结果:
medical_logic_issues(JSONB),evidence_chain_issues(JSONB) - 冲突检测:
is_conflict,conflict_severity,conflict_fields,review_priority - 人工复核:
final_decision,final_decision_by,exclusion_reason,review_notes - 处理状态:
processing_status,is_degraded,degraded_model - 可追溯性:
raw_output_a(JSONB),raw_output_b(JSONB),prompt_version
索引:
idx_fulltext_results_task_ididx_fulltext_results_project_ididx_fulltext_results_literature_ididx_fulltext_results_is_conflictidx_fulltext_results_final_decisionidx_fulltext_results_review_priority
唯一约束:
unique_project_literature_fulltext (project_id, literature_id)
外键约束:
task_id→fulltext_screening_tasks(id)ON DELETE CASCADEproject_id→screening_projects(id)ON DELETE CASCADEliterature_id→literatures(id)ON DELETE CASCADE
迁移结果验证
-- 验证表创建
\dt asl_schema.*
-- 结果:6个表
-- ✅ literatures (已更新)
-- ✅ screening_projects
-- ✅ screening_tasks
-- ✅ screening_results
-- ✅ fulltext_screening_tasks (新建)
-- ✅ fulltext_screening_results (新建)
-- 验证新字段
\d asl_schema.literatures
-- 结果:
-- ✅ stage
-- ✅ has_pdf
-- ✅ full_text_storage_type
-- ✅ full_text_storage_ref
-- ✅ full_text_url
-- ✅ full_text_format
-- ... 等13个新字段
Prisma Client生成:
cd backend
npx prisma generate
# 结果:✅ 生成成功
# 代码可访问:
# - prisma.aslLiterature
# - prisma.aslFulltextScreeningTask
# - prisma.aslFulltextScreeningResult
📐 Schema隔离策略执行情况
设计原则(来自系统架构文档)
各模块数据逻辑隔离:
├── admin_schema (系统管理)
├── platform_schema (用户系统)
├── aia_schema (AI助手)
├── asl_schema (AI智能文献) ✅ 执行正确
├── pkb_schema (知识库)
├── rvw_schema (审阅协作)
├── st_schema (统计分析)
├── dc_schema (数据采集)
├── ssa_schema (样本量分析)
└── common_schema (公共数据)
ASL模块执行情况 ✅
| 检查项 | 状态 | 说明 |
|---|---|---|
| Schema命名 | ✅ 正确 | asl_schema |
| 所有表都在正确Schema | ✅ 正确 | 6个表全部在 asl_schema |
| 没有表在public | ✅ 正确 | 无泄漏 |
| Prisma Model映射正确 | ✅ 正确 | @@schema("asl_schema") |
| 代码访问路径正确 | ✅ 正确 | prisma.aslXxx |
| 外键约束内部化 | ✅ 正确 | 所有FK指向同schema表 |
代码示例(正确访问方式):
// ✅ 正确:通过Prisma Client访问asl_schema
const project = await prisma.aslScreeningProject.findUnique({
where: { id: projectId },
});
const literatures = await prisma.aslLiterature.findMany({
where: { projectId },
});
const task = await prisma.aslFulltextScreeningTask.create({
data: { ... },
});
// ❌ 错误:直接SQL访问public(不会发生,因为表不在public)
await prisma.$queryRaw`SELECT * FROM public.literatures`;
🔮 未来迁移策略
对于ASL模块
推荐策略:继续使用手动SQL脚本
原因:
- ✅ Public schema的历史遗留问题短期无法解决
- ✅ 手动脚本更安全、可控
- ✅ 避免意外影响其他模块
- ✅ 便于代码审查和审计
操作流程:
# 1. 修改 Prisma Schema
# backend/prisma/schema.prisma
# 2. 编写手动SQL脚本
# backend/prisma/migrations/manual_xxx.sql
# 3. 执行脚本(只操作asl_schema)
Get-Content manual_xxx.sql | docker exec -i ai-clinical-postgres psql ...
# 4. 验证结果
docker exec ai-clinical-postgres psql ... -c "\dt asl_schema.*"
# 5. 生成Prisma Client
npx prisma generate
# 6. 提交Git
git add .
git commit -m "feat(asl): add xxx tables for xxx feature"
SQL脚本模板:
-- 只操作asl_schema,不影响其他schema
ALTER TABLE asl_schema.xxx ADD COLUMN IF NOT EXISTS ...;
CREATE TABLE IF NOT EXISTS asl_schema.xxx (...);
CREATE INDEX IF NOT EXISTS idx_xxx ON asl_schema.xxx(...);
对于其他模块
问题所有者:各模块开发团队
建议操作(由各模块团队自行决定):
- 检查
publicschema中是否有本模块的表 - 对比数据差异(
publicvs 正确schema) - 决策是否需要数据迁移或清理
- 执行清理操作(风险自负)
ASL团队立场:
- 🔵 不主动清理其他模块的public表
- 🔵 不对其他模块数据安全负责
- 🔵 专注于asl_schema的质量和稳定性
📊 数据完整性验证
ASL模块数据关系图
asl_schema.screening_projects (项目)
↓ 1:N
asl_schema.literatures (文献)
↓ 1:1 ↓ 1:1
asl_schema.screening_results asl_schema.fulltext_screening_results
(标题摘要初筛结果) (全文复筛结果)
↑ N:1 ↑ N:1
asl_schema.screening_tasks asl_schema.fulltext_screening_tasks
(标题摘要初筛任务) (全文复筛任务)
外键约束验证
-- 验证所有外键都指向asl_schema内部
SELECT
tc.constraint_name,
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'asl_schema'
ORDER BY tc.table_name;
-- 预期结果:
-- ✅ 所有FK的 foreign_table_name 都在 asl_schema 中
-- ✅ 没有跨schema引用
🎯 关键结论
✅ ASL模块:完全健康
- Schema隔离:100%正确,所有表都在
asl_schema - 数据管理:无数据泄漏到
public - 代码规范:所有访问路径正确
- 迁移策略:手动SQL脚本,安全可控
⚠️ 系统级问题:Public Schema污染
- 问题性质:历史遗留,与ASL无关
- 影响范围:AIA、PKB、Platform模块
- 解决责任:各模块团队自行处理
- ASL策略:不动public,管好自己
📋 开发人员指南
如果你是ASL模块开发者:
- ✅ 继续保持当前的Schema隔离实践
- ✅ 使用手动SQL脚本进行数据库迁移
- ✅ 所有表都创建在
asl_schema - ✅ 不要尝试清理
publicschema
如果你是其他模块开发者:
- 🟡 检查自己模块的Schema隔离状况
- 🟡 决定是否需要清理
public中的重复表 - 🟡 参考ASL的迁移策略(手动SQL)
- 🟡 不要依赖ASL团队清理public
📚 相关文档
文档维护:
- 数据库结构变更时更新
- 发现新问题时记录
- 定期审查Schema隔离状况
最后更新:2025-11-23
更新人:ASL开发团队
下次审查:下次数据库迁移时