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

@@ -2,44 +2,44 @@
**日期**: 2025-11-21
**任务**: 审核工作台UX优化
**状�*: �已完�
**状态**: ✅ 已完成
---
## 📋 优化内容
### 1. 进度显示优化 �
### 1. 进度显示优化 ⭐
#### 问题
- 进度条从0%直接跳到100%
- çœä¸<EFBFBD>到中间过ç¨?
- 用户体验ä¸<EFBFBD>å<EFBFBD>好,等待时没有å<EFBFBD><EFBFBD>é¦?
- 看不到中间过程
- 用户体验不友好,等待时没有反馈
#### 原因分析
1. **å‰<EFBFBD>端轮询间隔太长**ï¼?ç§?æ¬?
2. **å<EFBFBD>Žç«¯æ´æ°é¢çއä½?*:æ¯<C3A6>10æ<30>¡æ´æ°ä¸€æ¬?
1. **前端轮询间隔太长**2秒/次
2. **后端更新频率低**每10条更新一次
对于å°é‡<EFBFBD>æ‡çŒ®ï¼?-20篇),æ¯<C3A6>10æ<30>¡æ´æ°æ„<C3A6>å³ç<C2B3>€å‡ ä¹Žçœä¸<C3A4>到中间过ç¨ã€?
对于少量文献5-20篇每10条更新意味着几乎看不到中间过程。
#### 解决方案
**前端优化** (`useScreeningTask.ts`):
```typescript
// 修改�
pollingInterval = 2000 // 2ç§?
// 修改前
pollingInterval = 2000 // 2
// 修改å<EFBFBD>?
pollingInterval = 1000 // 1ç§ï¼Œæ´å<EFBFBD>Šæ—?
// 修改后
pollingInterval = 1000 // 1秒,更及时
```
**后端优化** (`screeningService.ts`):
```typescript
// 修改å‰<EFBFBD>:æ¯?0æ<30>¡æ´æ°ä¸€æ¬?
// 修改前每10条更新一次
if (processedCount % 10 === 0 || processedCount === literatures.length) {
await prisma.aslScreeningTask.update({ ... });
}
// 修改å<EFBFBD>Žï¼šæ¯?æ<>¡æ´æ°ä¸€æ¬?
// 修改后每1条更新一次
await prisma.aslScreeningTask.update({
where: { id: taskId },
data: {
@@ -51,19 +51,19 @@ await prisma.aslScreeningTask.update({
});
```
**效果**�
- âœ?æ¯<C3A6>处ç<E2809E>†å®Œ1篇æ‡çŒ®ï¼Œç«å<E280B9>³æ´æ°æ•°æ<C2B0>®åº?
- âœ?å‰<C3A5>端æ¯?秒轮询一æ¬?
- �用户能看到平滑的进度增长
**效果**
- ✅ 每处理完1篇文献立即更新数据库
- ✅ 前端每1秒轮询一次
- ✅ 用户能看到平滑的进度增长
---
### 2. 添加模åžå¤„ç<EFBFBD>†æ•°é‡<EFBFBD>显示 â­?
### 2. 添加模型处理数量显示 ⭐
#### 需�
在è¿åº¦æ<EFBFBD>¡ä¸æ¹æ˜¾ç¤ºï¼?
- DeepSeek 处ç<EFBFBD>†äº†å‡ ç¯?
- Qwen-Max 处ç<EFBFBD>†äº†å‡ ç¯?
#### 需求
在进度条下方显示:
- DeepSeek 处理了几篇
- Qwen-Max 处理了几篇
#### 实现
@@ -72,82 +72,82 @@ await prisma.aslScreeningTask.update({
{task && (
<>
<div className="text-sm text-gray-500 mt-2">
å·²å¤ç<EFBFBD>? {task.processedItems} / {task.totalItems} ç¯?·
: {task.processedItems} / {task.totalItems} ·
: {task.successItems} ·
: {task.conflictItems} ·
: {task.failedItems}
</div>
<div className="text-xs text-gray-400 mt-1">
<Tag color="blue" className="text-xs">DeepSeek-V3</Tag>
å·²å¤ç<EFBFBD>?{task.processedItems} ç¯?·
{task.processedItems} ·
<Tag color="purple" className="text-xs">Qwen-Max</Tag>
å·²å¤ç<EFBFBD>?{task.processedItems} ç¯?
{task.processedItems}
</div>
</>
)}
```
**显示效果**�
**显示效果**
```
已处ç<EFBFBD>? 3 / 5 ç¯?· æˆ<C3A6>功: 3 · 冲çª<EFBFBD>: 1 · 失败: 0
[DeepSeek-V3] 已处ç<EFBFBD>?3 ç¯?· [Qwen-Max] 已处ç<E2809E>?3 ç¯?
已处理: 3 / 5 篇 · 成功: 3 · 冲突: 1 · 失败: 0
[DeepSeek-V3] 已处理 3 篇 · [Qwen-Max] 已处理 3 篇
```
**说明**�
**说明**
- 双模型是并行处理,所以两个模型的处理数量始终相同
- 使用ä¸<EFBFBD>å<EFBFBD>Œé¢œè‰²çš„Tag区分模åžï¼ˆè“<EFBFBD>è‰?紫色ï¼?
- 使用不同颜色的Tag区分模型蓝色/紫色)
---
### 3. ä¿®å¤<EFBFBD>列表显示顺åº<EFBFBD> â­?
### 3. 修复列表显示顺序 ⭐
#### 问题
- Excel顺序a、b、c、d
- 设置与å<EFBFBD>¯åŠ¨é¢„è§ˆï¼šaã€<EFBFBD>bã€<EFBFBD>cã€<EFBFBD>d âœ?
- 审核工作å<EFBFBD>°æ˜¾ç¤ºï¼šdã€<EFBFBD>cã€<EFBFBD>bã€<EFBFBD>a â<>?**å<><C3A5>了ï¼?*
- 设置与启动预览a、b、c、d ✅
- 审核工作台显示d、c、b、a ❌ **反了!**
#### 原因
å<EFBFBD>Žç«¯æŸ¥è¯¢ä½¿ç”¨äº?`orderBy: { createdAt: 'desc' }`(é™<EFBFBD>åº<EFBFBD>),导致最æ°åˆå»ºçš„æŽåœ¨å‰<EFBFBD>é<EFBFBD>¢ã€?
后端查询使用了 `orderBy: { createdAt: 'desc' }`(降序),导致最新创建的排在前面。
由于文献是按Excel顺序依次导入的
```
a(最早创建) â†?b â†?c â†?d(最晚åˆå»ºï¼‰
a(最早创建) → b → c → d最晚创建
```
降序排列后:
```
d(最晚åˆå»ºï¼ŒæŽç¬¬1ï¼?â†?c â†?b â†?a(最早åˆå»ºï¼ŒæŽæœ€å<E282AC>Žï¼‰
d最晚创建排第1 → c → b → a最早创建排最后
```
#### 解决方案
**后端** (`screeningController.ts`):
```typescript
// 修改�
// 修改前
orderBy: [
{ conflictStatus: 'desc' },
{ createdAt: 'desc' }, // â<EFBFBD>?é™<C3A9>åº<C3A5>,最æ°çš„在å‰<C3A5>
{ createdAt: 'desc' }, // ❌ 降序,最新的在前
]
// 修改å<EFBFBD>?
// 修改后
orderBy: [
{ conflictStatus: 'desc' }, // 保持冲突的排前面
{ createdAt: 'asc' }, // âœ?å<>‡åº<C3A5>,ä¿<C3A4>æŒ<C3A6>Excel原å§é¡ºåº<C3A5>
{ createdAt: 'asc' }, // ✅ 升序保持Excel原始顺序
]
```
**æŽåº<EFBFBD>逻è¾**ï¼?
1. **优先çº?**:冲çª<C3A7>状æ€<C3A6>(conflict > noneï¼?
**排序逻辑**
1. **优先级1**:冲突状态(conflict > none
- 有冲突的文献排在前面
- 方便用户优先处理冲突
2. **优先çº?**:åˆå»ºæ—¶é—´ï¼ˆå<CB86>‡åº<C3A5>ï¼?
2. **优先级2**:创建时间(升序)
- 保持Excel原始顺序
- 符合用户预期
**效果**�
**效果**
```
审核工作å<EFBFBD>°æ˜¾ç¤ºï¼šaã€<EFBFBD>bã€<EFBFBD>cã€<EFBFBD>d âœ?
ï¼ˆå¦æžœc有冲çª<EFBFBD>:cã€<EFBFBD>aã€<EFBFBD>bã€<EFBFBD>dï¼?
审核工作台显示a、b、c、d ✅
如果c有冲突c、a、b、d
```
---
@@ -156,86 +156,86 @@ orderBy: [
### 进度显示
| æ¹é<EFBFBD>¢ | 优化å‰?| 优åŒå<E28093>?|
| 方面 | 优化前 | 优化后 |
|-----|-------|--------|
| 轮询间隔 | 2�| 1�|
| å<EFBFBD>Žç«¯æ´æ° | æ¯?0æ<30>?| æ¯?æ<>?|
| 用户体验 | 0% �等待 �100% | 0% �20% �40% �60% �80% �100% |
| 模åžä¿¡æ<EFBFBD>¯ | æ—?| 显示DeepSeekå’ŒQwen处ç<EFBFBD>†æ•?|
| 轮询间隔 | 2秒 | 1秒 |
| 后端更新 | 每10条 | 每1条 |
| 用户体验 | 0% → 等待 → 100% | 0% 20% 40% 60% 80% 100% |
| 模型信息 | 无 | 显示DeepSeekQwen处理数 |
### 列表顺序
| 场景 | 优化å‰?| 优åŒå<E28093>?|
| 场景 | 优化前 | 优化后 |
|-----|-------|--------|
| Excel顺序 | a, b, c, d | a, b, c, d |
| 预览顺序 | a, b, c, d | a, b, c, d |
| 审核工作å<EFBFBD>?| d, c, b, a â<EFBFBD>?| a, b, c, d âœ?|
| 审核工作台 | d, c, b, a | a, b, c, d |
---
## 🔧 修改文件清单
### 前端
1. �`frontend-v2/src/modules/asl/hooks/useScreeningTask.ts`
- 轮询间隔���1�
1. `frontend-v2/src/modules/asl/hooks/useScreeningTask.ts`
- 轮询间隔2秒 → 1秒
2. �`frontend-v2/src/modules/asl/pages/ScreeningWorkbench.tsx`
2. `frontend-v2/src/modules/asl/pages/ScreeningWorkbench.tsx`
- 添加模型处理数量显示
### 后端
3. �`backend/src/modules/asl/services/screeningService.ts`
- è¿åº¦æ´æ°ï¼šæ¯<EFBFBD>10æ<EFBFBD>?â†?æ¯?æ<>?
3. `backend/src/modules/asl/services/screeningService.ts`
- 进度更新每10条 → 每1条
4. �`backend/src/modules/asl/controllers/screeningController.ts`
- æŽåº<EFBFBD>:`createdAt: 'desc'` â†?`createdAt: 'asc'`
4. `backend/src/modules/asl/controllers/screeningController.ts`
- 排序:`createdAt: 'desc'` `createdAt: 'asc'`
---
## 🧪 测试验证
### 测试场景
1. 上传5篇æ‡çŒ?
1. 上传5篇文献
2. 点击"开始AI初筛"
3. è§å¯Ÿå®¡æ ¸å·¥ä½œå<EFBFBD>?
3. 观察审核工作台
### 预期效果
#### 1. 进度显示
```
初始: 0%
10ç§å<EFBFBD>Ž: 20% â†?âœ?能çœåˆ°è¿åº¦ï¼<C3AF>
10秒后: 20% ← ✅ 能看到进度!
20秒后: 40%
30秒后: 60%
40秒后: 80%
50秒后: 100%
底部显示:
已处ç<EFBFBD>? 3 / 5 ç¯?· æˆ<C3A6>功: 3 · 冲çª<EFBFBD>: 1 · 失败: 0
[DeepSeek-V3] 已处ç<EFBFBD>?3 ç¯?· [Qwen-Max] 已处ç<E2809E>?3 ç¯?
已处理: 3 / 5 篇 · 成功: 3 · 冲突: 1 · 失败: 0
[DeepSeek-V3] 已处理 3 篇 · [Qwen-Max] 已处理 3 篇
```
#### 2. 列表顺序
```
Excel: 文献A, 文献B, 文献C, 文献D, 文献E
审核工作å<EFBFBD>? æ‡çŒ®A, æ‡çŒ®B, æ‡çŒ®C, æ‡çŒ®D, æ‡çŒ®E âœ?
审核工作台: 文献A, 文献B, 文献C, 文献D, 文献E ✅
如果文献C有冲突
审核工作å<EFBFBD>? æ‡çŒ®C, æ‡çŒ®A, æ‡çŒ®B, æ‡çŒ®D, æ‡çŒ®E âœ?
审核工作台: 文献C, 文献A, 文献B, 文献D, 文献E ✅
```
---
## 💡 技术细�
## 💡 技术细节
### 为什么æ¯<EFBFBD><EFBFBD>¡å°±æ´æ°ï¼?
**æ<EFBFBD>ƒè¡¡**ï¼?
- **优点**:实时å<C2B6><C3A5>馈,用户体验å¥?
### 为什么每1条就更新
**权衡**
- **优点**:实时反馈,用户体验好
- **缺点**:数据库写入频繁
- **评估**:对于å°é‡<C3A9>æ‡çŒ®ï¼ˆ5-200篇),数æ<C2B0>®åº“åŽåŠå<E280BA>¯æŽ¥å<C2A5>?
- **评估**对于少量文献5-200篇数据库压力可接受
**妿žœæ‡çŒ®æ•°é‡<EFBFBD>很大**ï¼?000+篇),å<C592>¯ä»¥ä¼˜åŒä¸ºï¼?
**如果文献数量很大**1000+篇),可以优化为:
```typescript
// 动æ€<EFBFBD>è°ƒæ•´æ´æ°é¢çŽ?
// 动态调整更新频率
const updateInterval = literatures.length > 500 ? 10 : 1;
if (processedCount % updateInterval === 0 || processedCount === literatures.length) {
await prisma.aslScreeningTask.update({ ... });
@@ -243,15 +243,15 @@ if (processedCount % updateInterval === 0 || processedCount === literatures.leng
```
### 为什么轮询间隔是1秒
**æ<EFBFBD>ƒè¡¡**ï¼?
- **优点**:å<C5A1>Šæ—¶æ´æ°ï¼Œå»¶è¿Ÿå°?
**权衡**
- **优点**:及时更新,延迟小
- **缺点**API调用频繁
- **评估**�
- **评估**
- 每次API调用耗时 < 100ms
- 筛选过程持续时间1-30分钟
- API调用次数ï¼?0-1800次(å<CB86>¯æŽ¥å<C2A5>—)
- API调用次数60-1800次可接受
**妿žœéœ€è¦<EFBFBD>优åŒ?*,å<C592>¯ä»¥ä½¿ç”?WebSocket 实时推é€<C3A9>:
**如果需要优化**,可以使用 WebSocket 实时推送:
```typescript
// 未来优化方案
socket.on('screening-progress', (data) => {
@@ -261,7 +261,7 @@ socket.on('screening-progress', (data) => {
---
## ðŸ“<EFBFBD> 关于æµ<C3A6>览器警å?
## 📝 关于浏览器警告
### 警告信息
```
@@ -270,18 +270,18 @@ socket.on('screening-progress', (data) => {
```
### 说明
- 这是Chrome性能æ<EFBFBD><EFBFBD>示,ä¸<EFBFBD>是错è¯?
- 这是Chrome性能提示不是错误
- 表示某个setTimeout处理函数执行时间较长
- 通常由React大量DOM更新引起
### 是否需要优化?
**短期**:ä¸<C3A4>需è¦?
- ä¸<EFBFBD>å½±å“<EFBFBD>功èƒ?
**短期**:不需要
- 不影响功能
- 用户体验正常
- 处ç<EFBFBD>†æ—¶é—´åœ¨å<EFBFBD>¯æŽ¥å<EFBFBD>—范å´å†…(< 300msï¼?
- 处理时间在可接受范围内(< 300ms
**长期**:å<C5A1>¯ä»¥ä¼˜åŒ?
1. 使用 `React.memo` å‡<EFBFBD>å°é‡<EFBFBD>渲æŸ?
**长期**:可以优化
1. 使用 `React.memo` 减少重渲染
2. 使用虚拟列表(如果文献很多)
3. 优化大型组件的渲染逻辑
@@ -291,8 +291,8 @@ socket.on('screening-progress', (data) => {
### 短期(可选)
1. 添加"暂停"按钮(暂停筛选任务)
2. 添加"估计剩余时间"(基于已处ç<E2809E>†é€Ÿåº¦ï¼?
3. 显示当å‰<EFBFBD>正在处ç<EFBFBD>†çš„æ‡çŒ®æ ‡é¢?
2. 添加"估计剩余时间"(基于已处理速度)
3. 显示当前正在处理的文献标题
### 中期
1. 使用WebSocket替代轮询实时推送
@@ -300,26 +300,26 @@ socket.on('screening-progress', (data) => {
3. 支持任务取消
### 长期
1. 分布å¼<EFBFBD>处ç<EFBFBD>†ï¼ˆå¤šä¸ªworker并行ï¼?
1. 分布式处理多个worker并行
2. 断点续传(任务中断后可恢复)
3. 性能监控和分�
3. 性能监控和分析
---
## 📊 性能数据
### 优åŒå‰<EFBFBD>å<EFBFBD>Žå¯¹æ¯”ï¼?篇文献)
### 优化前后对比5篇文献
| 指标 | 优化å‰?| 优åŒå<E28093>?| 改善 |
| 指标 | 优化前 | 优化后 | 改善 |
|-----|-------|--------|-----|
| è¿åº¦å<EFBFBD>¯è§<EFBFBD>æ€?| 0% â†?100% | 0â†?0â†?0â†?0â†?0â†?00% | âœ?5å€<C3A5>æ<EFBFBD><C3A6>å<EFBFBD>?|
| å<EFBFBD><EFBFBD>馈延迟 | ~20ç§?| ~1ç§?| âœ?20å€<C3A5>æ<EFBFBD><C3A6>å<EFBFBD>?|
| 列表顺åº<EFBFBD> | å<><C3A5>å<EFBFBD> | 正确 | âœ?ä¿®å¤<C3A5> |
| ä¿¡æ<EFBFBD>¯å®Œæ•´æ€?| 基本 | 详细(å<CB86>«æ¨¡åžæ•°ï¼‰ | âœ?æ<><C3A6>å<EFBFBD> |
| 进度可见性 | 0% 100% | 0→20→40→60→80→100% | ✅ 5倍提升 |
| 反馈延迟 | ~20| ~1秒 | ✅ 20倍提升 |
| 列表顺序 | 反向 | 正确 | ✅ 修复 |
| 信息完整性 | 基本 | 详细(含模型数) | ✅ 提升 |
---
**报告�*: AI Assistant
**报告人**: AI Assistant
**日期**: 2025-11-21
**版本**: v1.0.0