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
14 KiB
14 KiB
数据库设计文档 - 工具B(病历结构化机器人)
模块: DC数据清洗整理 - 工具B
版本: V2.0 (MVP)
Schema:dc_schema
更新日期: 2025-12-03
状态: ✅ MVP完成(已验证可用,真实数据测试通过)
📋 目录
一、概述
1.1 设计目标
工具B的数据库设计旨在支持:
- ✅ 双大模型交叉验证的文本结构化
- ✅ 大规模异步任务处理(1000+条记录)
- ✅ 冲突检测与人工裁决
- ✅ 预设模板管理与复用
- ✅ 健康检查缓存优化
1.2 表关系总览
dc_schema ✅ 已创建并运行中
├── dc_health_checks [健康检查缓存] ✅ 运行正常
├── dc_templates [预设模板] ✅ 3个预设模板可用
├── dc_extraction_tasks [提取任务] ✅ 已完成多个任务
│ └── dc_extraction_items [提取记录] (1:N) ✅ 双模型结果正常保存
✅ MVP完成状态(2025-12-03):
- 所有表正常工作,已处理多个真实任务
- 3个预设模板:肺癌病理报告、糖尿病入院记录、高血压门诊病历
- 真实测试:9条病理数据提取成功,100%成功率
- 双模型结果:resultA、resultB、finalResult字段正常保存
- Token统计:totalTokens字段正常累加
- 冲突检测:conflictFields数组正常工作
- 验证脚本:
backend/scripts/check-task-progress.mjs
1.3 技术栈
- 数据库: PostgreSQL 15
- ORM: Prisma 6
- Schema隔离:
dc_schema(独立命名空间) - JSON字段: 使用JSONB类型(高性能查询)
二、Schema设计原则
2.1 Schema隔离
-- 所有表使用dc_schema命名空间
CREATE TABLE "dc_schema"."dc_health_checks" (...);
CREATE TABLE "dc_schema"."dc_extraction_tasks" (...);
优势:
- ✅ 与其他模块完全隔离(platform_schema、asl_schema等)
- ✅ 数据安全,避免误操作
- ✅ 便于模块化管理和迁移
2.2 命名规范
| 规则 | 说明 | 示例 |
|---|---|---|
| 表名前缀 | dc_ |
dc_extraction_tasks |
| 字段命名 | snake_case | user_id, source_file_key |
| 时间戳 | 统一后缀 | created_at, started_at |
| 外键 | 实体名_id | task_id, user_id |
2.3 JSONB字段使用场景
| 字段 | 类型 | 原因 |
|---|---|---|
target_fields |
JSONB | 灵活的字段配置 |
result_a/result_b |
JSONB | 动态提取结果 |
final_result |
JSONB | 最终裁决结果 |
三、数据表设计
3.1 dc_health_checks(健康检查缓存表)
用途: 缓存健康检查结果,避免重复计算
CREATE TABLE "dc_schema"."dc_health_checks" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"file_name" TEXT NOT NULL,
"column_name" TEXT NOT NULL,
-- 统计指标
"empty_rate" DOUBLE PRECISION NOT NULL,
"avg_length" DOUBLE PRECISION NOT NULL,
"total_rows" INTEGER NOT NULL,
"estimated_tokens" INTEGER NOT NULL,
-- 检查结果
"status" TEXT NOT NULL, -- 'good' | 'bad'
"message" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
);
字段说明:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
id |
TEXT | UUID主键 | uuid() |
user_id |
TEXT | 用户ID | user-123 |
file_name |
TEXT | 文件名 | 患者数据.xlsx |
column_name |
TEXT | 检查的列名 | 病历文本 |
empty_rate |
DOUBLE | 空值率 (0-1) | 0.15 (15%) |
avg_length |
DOUBLE | 平均文本长度 | 256.8 |
total_rows |
INT | 总行数 | 500 |
estimated_tokens |
INT | 预估Token数 | 150000 |
status |
TEXT | 健康状态 | good / bad |
message |
TEXT | 提示信息 | 健康度良好 |
索引:
CREATE INDEX "dc_health_checks_user_id_file_name_idx"
ON "dc_schema"."dc_health_checks"("user_id", "file_name");
业务规则:
- 空值率 > 80% →
status = 'bad' - 平均长度 < 10 →
status = 'bad' - 缓存有效期:24小时(应用层实现)
3.2 dc_templates(预设模板表)
用途: 存储疾病类型的预设提取模板
CREATE TABLE "dc_schema"."dc_templates" (
"id" TEXT NOT NULL PRIMARY KEY,
"disease_type" TEXT NOT NULL, -- 'lung_cancer', 'diabetes', 'hypertension'
"report_type" TEXT NOT NULL, -- 'pathology', 'admission', 'outpatient'
"display_name" TEXT NOT NULL, -- '肺癌病理报告'
"fields" JSONB NOT NULL, -- [{name, desc, width}]
"prompt_template" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "dc_templates_disease_type_report_type_key"
UNIQUE ("disease_type", "report_type")
);
字段说明:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
disease_type |
TEXT | 疾病类型 | lung_cancer |
report_type |
TEXT | 报告类型 | pathology |
display_name |
TEXT | 显示名称 | 肺癌病理报告 |
fields |
JSONB | 提取字段配置 | 见下方示例 |
prompt_template |
TEXT | Prompt模板 | 请从以下病理报告中提取... |
fields字段结构:
[
{
"name": "病理类型",
"desc": "如:浸润性腺癌、鳞状细胞癌",
"width": "w-40"
},
{
"name": "分化程度",
"desc": "高/中/低分化",
"width": "w-32"
}
]
唯一约束:
UNIQUE ("disease_type", "report_type")
同一疾病+报告类型组合只能有一个模板
3.3 dc_extraction_tasks(提取任务表)
用途: 管理批量提取任务,追踪进度和成本
CREATE TABLE "dc_schema"."dc_extraction_tasks" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"project_name" TEXT NOT NULL,
"source_file_key" TEXT NOT NULL, -- Storage中的路径
"text_column" TEXT NOT NULL,
-- 模板配置
"disease_type" TEXT NOT NULL,
"report_type" TEXT NOT NULL,
"target_fields" JSONB NOT NULL,
-- 双模型配置
"model_a" TEXT NOT NULL DEFAULT 'deepseek-v3',
"model_b" TEXT NOT NULL DEFAULT 'qwen3-72b',
-- 任务状态
"status" TEXT NOT NULL DEFAULT 'pending',
"total_count" INTEGER NOT NULL DEFAULT 0,
"processed_count" INTEGER NOT NULL DEFAULT 0,
"clean_count" INTEGER NOT NULL DEFAULT 0,
"conflict_count" INTEGER NOT NULL DEFAULT 0,
"failed_count" INTEGER NOT NULL DEFAULT 0,
-- 成本统计
"total_tokens" INTEGER NOT NULL DEFAULT 0,
"total_cost" DOUBLE PRECISION NOT NULL DEFAULT 0,
-- 错误信息
"error" TEXT,
-- 时间戳
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"started_at" TIMESTAMP(3),
"completed_at" TIMESTAMP(3)
);
字段说明:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
source_file_key |
TEXT | Storage路径 | uploads/user123/data.xlsx |
text_column |
TEXT | 文本列名 | 病历文本 |
target_fields |
JSONB | 提取字段 | [{name, desc}] |
status |
TEXT | 任务状态 | pending/processing/completed/failed |
total_count |
INT | 总记录数 | 500 |
processed_count |
INT | 已处理数 | 250 |
clean_count |
INT | 一致数 | 200 |
conflict_count |
INT | 冲突数 | 45 |
failed_count |
INT | 失败数 | 5 |
total_tokens |
INT | 总Token数 | 150000 |
total_cost |
DOUBLE | 总成本($) | 0.27 |
状态流转:
pending → processing → completed
→ failed
索引:
CREATE INDEX "dc_extraction_tasks_user_id_status_idx"
ON "dc_schema"."dc_extraction_tasks"("user_id", "status");
3.4 dc_extraction_items(提取记录表)
用途: 存储每条记录的双模型提取结果和冲突状态
CREATE TABLE "dc_schema"."dc_extraction_items" (
"id" TEXT NOT NULL PRIMARY KEY,
"task_id" TEXT NOT NULL,
-- 原始数据
"row_index" INTEGER NOT NULL,
"original_text" TEXT NOT NULL,
-- 双模型结果
"result_a" JSONB,
"result_b" JSONB,
-- 冲突检测
"status" TEXT NOT NULL DEFAULT 'pending',
"conflict_fields" TEXT[] DEFAULT ARRAY[]::TEXT[],
-- 最终结果
"final_result" JSONB,
-- Token统计
"tokens_a" INTEGER NOT NULL DEFAULT 0,
"tokens_b" INTEGER NOT NULL DEFAULT 0,
-- 错误信息
"error" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"resolved_at" TIMESTAMP(3),
CONSTRAINT "dc_extraction_items_task_id_fkey"
FOREIGN KEY ("task_id")
REFERENCES "dc_schema"."dc_extraction_tasks"("id")
ON DELETE CASCADE
);
字段说明:
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
row_index |
INT | Excel行号 | 5 |
original_text |
TEXT | 原始病历文本 | 患者,男,45岁... |
result_a |
JSONB | DeepSeek结果 | {"肿瘤大小": "3cm"} |
result_b |
JSONB | Qwen结果 | {"肿瘤大小": "3.0cm"} |
status |
TEXT | 处理状态 | clean/conflict/resolved/failed |
conflict_fields |
TEXT[] | 冲突字段列表 | ["肿瘤大小"] |
final_result |
JSONB | 最终裁决结果 | {"肿瘤大小": "3cm"} |
result_a/result_b结构示例:
{
"病理类型": "浸润性腺癌",
"分化程度": "中分化",
"肿瘤大小": "3cm",
"淋巴结转移": "无"
}
状态说明:
pending: 等待处理clean: 双模型结果一致conflict: 存在冲突,需人工裁决resolved: 冲突已解决failed: 提取失败
索引:
CREATE INDEX "dc_extraction_items_task_id_status_idx"
ON "dc_schema"."dc_extraction_items"("task_id", "status");
外键约束:
ON DELETE CASCADE: 删除任务时自动删除所有记录
四、索引设计
4.1 索引列表
| 表名 | 索引字段 | 类型 | 用途 |
|---|---|---|---|
dc_health_checks |
(user_id, file_name) |
复合 | 查询用户的历史检查 |
dc_templates |
(disease_type, report_type) |
唯一 | 防止重复模板 |
dc_extraction_tasks |
(user_id, status) |
复合 | 查询用户的任务列表 |
dc_extraction_items |
(task_id, status) |
复合 | 查询任务的记录列表 |
4.2 性能考虑
查询优化:
-- 高效查询:利用索引
SELECT * FROM dc_extraction_tasks
WHERE user_id = 'user123' AND status = 'processing';
-- 高效查询:利用索引
SELECT * FROM dc_extraction_items
WHERE task_id = 'task456' AND status = 'conflict';
避免全表扫描:
- ✅ 始终在WHERE子句中包含索引字段
- ✅ 使用
status字段过滤可以显著减少扫描行数
五、外键约束
5.1 级联删除
ALTER TABLE "dc_schema"."dc_extraction_items"
ADD CONSTRAINT "dc_extraction_items_task_id_fkey"
FOREIGN KEY ("task_id")
REFERENCES "dc_schema"."dc_extraction_tasks"("id")
ON DELETE CASCADE;
行为:
- 删除任务 → 自动删除所有关联的提取记录
- 保证数据一致性
5.2 无外键的表
dc_health_checks: 独立表,无外键dc_templates: 独立表,无外键dc_extraction_tasks: 无外键(user_id仅为标识,不强制关联)
原因:
- ✅ 减少跨Schema依赖
- ✅ 提高模块独立性
- ✅ 简化迁移和回滚
六、数据生命周期
6.1 数据保留策略
| 表名 | 保留时间 | 清理策略 |
|---|---|---|
dc_health_checks |
7天 | 定期清理旧记录 |
dc_templates |
永久 | 手动管理 |
dc_extraction_tasks |
90天 | 归档后删除 |
dc_extraction_items |
90天 | 随任务删除 |
6.2 归档策略
大任务归档 (> 1000条记录):
- 任务完成后,导出结果到CSV/Excel
- 上传到Storage(永久保存)
- 删除数据库记录(释放空间)
6.3 清理脚本(示例)
// 清理7天前的健康检查记录
await prisma.dCHealthCheck.deleteMany({
where: {
createdAt: {
lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
}
}
});
// 归档90天前的已完成任务
const oldTasks = await prisma.dCExtractionTask.findMany({
where: {
status: 'completed',
completedAt: {
lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
}
},
include: { items: true }
});
// 导出后删除
for (const task of oldTasks) {
await exportTaskToStorage(task);
await prisma.dCExtractionTask.delete({ where: { id: task.id } });
}
七、数据安全
7.1 PII保护
敏感字段:
original_text: 可能包含患者姓名、身份证号result_a/result_b/final_result: 可能包含结构化的敏感信息
保护措施:
- ✅ 发送LLM前自动脱敏(PIIMaskUtil)
- ✅ 数据库加密(PostgreSQL SSL)
- ✅ 定期清理历史数据
7.2 用户隔离
机制:
- 所有表包含
user_id字段 - 应用层强制过滤:
WHERE user_id = currentUserId - 永不跨用户查询
八、附录
8.1 完整Schema DDL
完整的Schema创建脚本位于:
backend/prisma/migrations/20251127_add_dc_tool_b_tables/migration.sql
8.2 Prisma模型定义
完整的Prisma模型定义位于:
backend/prisma/schema.prisma
搜索 dc_schema 查看所有模型。
8.3 变更历史
| 版本 | 日期 | 变更内容 |
|---|---|---|
| V1.0 | 2025-11-27 | 初始版本,4个表 |
文档结束 ✅