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

@@ -10,7 +10,7 @@
### 原始问题
<EFBFBD><EFBFBD>銝𠹺<EFBFBD><EFBFBD><EFBFBD>xcel<EFBFBD><EFBFBD>辣銵典仍<EFBFBD><EFBFBD><EFBFBD><EFBFBD>摮㛖泵嚗<EFBFBD><EFBFBD>渲恣蝞堒<EFBFBD><EFBFBD><EFBFBD>憭梯揖嚗?
用户上传的Excel文件表头包含特殊字符导致计算列功能失败
**示例表头**:
- `体重kg`
@@ -30,84 +30,84 @@
| 方案 | 描述 | 优点 | 缺点 | 评分 |
|------|------|------|------|------|
| **<EFBFBD><EFBFBD>A** | <EFBFBD><EFBFBD>雿輻鍂摨誩噡撘閧鍂嚗Ếol_0, col_1嚗?| <20><><EFBFBD><EFBFBD>摰匧<E691B0> | <20><EFBFBD>雿㯄<E99BBF>撌殷<E6928C>銝滨凒閫?| 潃鐥<E6BD83> |
| **<EFBFBD><EFBFBD>B** | <EFBFBD><EFBFBD>雿輻鍂<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Python韐蠘提<EFBFBD>踵揢 | <20><EFBFBD>雿㯄<E99BBF>憟踝<E6869F><E8B89D><EFBFBD><EFBFBD>臬虾<E887AC>?| <20><>摰䂿緵<E482BF>踵揢<E8B8B5><EFBFBD> | 潃鐥<E6BD83>潃鐥<E6BD83>潃?|
| **<EFBFBD><EFBFBD>C** | <EFBFBD>滨垢<EFBFBD>踵揢<EFBFBD><EFBFBD> | <20><EFBFBD>蝵𤑳<E89DB5>隡㰘<E99AA1> | 颲寧<E9A2B2><EFBFBD><E99C82><EFBFBD>圈𠗕嚗䔶<E59A97><E494B6><EFBFBD> | 潃鐥<E6BD83>潃?|
| **方案A** | 用户使用序号引用col_0, col_1 | 技术最安全 | 用户体验差,不直观 | ⭐⭐ |
| **方案B** | 用户使用原列名Python负责替换 | 用户体验好,技术可靠 | 需实现替换逻辑 | ⭐⭐⭐⭐⭐ |
| **方案C** | 前端替换列名 | 减少网络传输 | 边界识别困难,不可靠 | ⭐⭐⭐ |
**<EFBFBD><EFBFBD><EFBFBD><EFBFBD>㗇𥋘**: **<EFBFBD><EFBFBD>B** <20>?
**最终选择**: **方案B**
---
## <EFBFBD><EFBFBD>儭?<3F><EFBFBD>霈曇恣
## 🏗️ 架构设计
### <EFBFBD>唳旿瘚?
### 数据流
```
<EFBFBD><EFBFBD>颲枏<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>嚗?
<EFBFBD>?
<EFBFBD>滨垢嚗帋<EFBFBD><EFBFBD><EFBFBD>kg嚗?/ (頨恍<E9A0A8>嚗Ếm嚗?100)**2
<EFBFBD>?
<EFBFBD>𡒊垢嚗朞繮<EFBFBD>?columnMapping
<EFBFBD>?
用户输入公式(原列名)
前端体重kg / (身高cm/100)**2
后端:获取 columnMapping
传递给Python: {
formula: "雿㯄<EFBFBD>ɑg嚗?/ (頨恍<E9A0A8>嚗Ếm嚗?100)**2",
formula: "体重kg / (身高cm/100)**2",
column_mapping: [
{"originalName": "雿㯄<EFBFBD>ɑg嚗?, "safeName": "col_0"},
{"originalName": "頨恍<EFBFBD>嚗Ếm嚗?, "safeName": "col_1"}
{"originalName": "体重kg", "safeName": "col_0"},
{"originalName": "身高cm", "safeName": "col_1"}
]
}
<EFBFBD>?
Python替换: col_0 / (col_1/100)**2
<EFBFBD>?
<EFBFBD><EFBFBD>霈∠<EFBFBD> <20>?
执行计算 ✅
```
### 职责划分
| <EFBFBD>漣 | <20>諹提 | <20>喲睸<E596B2>?|
| 层级 | 职责 | 关键点 |
|------|------|--------|
| **<EFBFBD>滨垢** | UI鈭支<EFBFBD><EFBFBD><EFBFBD><EFBFBD>格𤣰<EFBFBD>?| <20><EFBFBD><E586BD><EFBFBD><E8A781><EFBFBD><E8ABB9><EFBFBD><E4BA99><EFBFBD> |
| **前端** | UI交互、数据收集 | 用户看到和输入原列名 |
| **后端** | 获取columnMapping、传递给Python | 从Session获取映射 |
| **Python** | <EFBFBD><EFBFBD><EFBFBD>踵揢<EFBFBD><EFBFBD><EFBFBD>撘𤩺<EFBFBD>銵?| <20>厰鵭摨行<E691A8>摨譌<E691A8><E8AD8C>移蝖格𤜯<E6A0BC>?|
| **Python** | 列名替换、公式执行 | 按长度排序、精确替换 |
---
## 💻 实施细节
### 1. <EFBFBD>滨垢嚗㇃omputeDialog.tsx嚗?
### 1. 前端ComputeDialog.tsx
**靽脲<EFBFBD>銝滚<EFBFBD>** - 撌脩<EFBFBD>雿輻鍂<EFBFBD><EFBFBD><EFBFBD>齿䲮撘?
**保持不变** - 已经使用原列名方式
```typescript
// <EFBFBD><EFBFBD><EFBFBD>孵稬<EFBFBD><EFBFBD><EFBFBD><EFBFBD>倌嚗峕<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>獢?
// 用户点击列名标签,插入到公式框
<Tag onClick={() => setFormula(formula + col.name)}>
{col.name} {/* <EFBFBD>曄內<EFBFBD><EFBFBD><EFBFBD><EFBFBD>雿㯄<EFBFBD>ɑg嚗?*/}
{col.name} {/* 显示原列名体重kg */}
</Tag>
// 提交时直接传递原公式
onApply({
newColumnName: "BMI",
formula: "雿㯄<EFBFBD>ɑg嚗?/ (頨恍<E9A0A8>嚗Ếm嚗?100)**2", // <EFBFBD><EFBFBD><EFBFBD>?
formula: "体重kg / (身高cm/100)**2", // 原列名
});
```
### 2. <EFBFBD>𡒊垢嚗㇋uickActionController.ts嚗?
### 2. 后端QuickActionController.ts
**修改**: 获取session并传递columnMapping
```typescript
// <EFBFBD><EFBFBD>session<EFBFBD><EFBFBD><EFBFBD>olumnMapping嚗?
// 获取session包含columnMapping
session = await sessionService.getSession(sessionId);
// 传递给QuickActionService
executeResult = await quickActionService.executeCompute(
fullData,
params,
session.columnMapping // <EFBFBD>?隡𣳇<E99AA1><EFBFBD>撠?
session.columnMapping // ✅ 传递映射
);
```
### 3. <EFBFBD>𡒊垢嚗㇋uickActionService.ts嚗?
### 3. 后端QuickActionService.ts
**修改**: 接收并传递columnMapping给Python
@@ -115,20 +115,20 @@ executeResult = await quickActionService.executeCompute(
async executeCompute(
data: any[],
params: ComputeParams,
columnMapping?: any[] // <EFBFBD>?<3F><EFBFBD><E595A3><EFBFBD>
columnMapping?: any[] // ✅ 新增参数
): Promise<OperationResult> {
const response = await axios.post(`${PYTHON_SERVICE_URL}/api/operations/compute`, {
data,
new_column_name: params.newColumnName,
formula: params.formula,
column_mapping: columnMapping || [], // <EFBFBD>?隡𣳇<E99AA1><EFBFBD>撠?
column_mapping: columnMapping || [], // ✅ 传递映射
});
return response.data;
}
```
### 4. Python嚗éain.py嚗?
### 4. Pythonmain.py
**修改**: 更新请求模型
@@ -137,7 +137,7 @@ class ComputeRequest(BaseModel):
data: List[Dict[str, Any]]
new_column_name: str
formula: str
column_mapping: List[Dict[str, str]] = [] # <EFBFBD>?<3F><EFBFBD>摮埈挾
column_mapping: List[Dict[str, str]] = [] # ✅ 新增字段
@app.post("/api/operations/compute")
async def operation_compute(request: ComputeRequest):
@@ -145,11 +145,11 @@ async def operation_compute(request: ComputeRequest):
df,
request.new_column_name,
request.formula,
request.column_mapping # <EFBFBD>?隡𣳇<E99AA1><EFBFBD>撠?
request.column_mapping # ✅ 传递映射
)
```
### 5. Python嚗Ếompute.py嚗?
### 5. Pythoncompute.py
**核心实现**: 列名替换逻辑
@@ -159,19 +159,19 @@ def replace_column_names_in_formula(
column_mapping: List[Dict[str, str]]
) -> str:
"""
<EFBFBD>?<3F><EFBFBD>蝞埈<E89D9E>嚗𡁜虾<F0A1819C><EFBFBD><E588A0><EFBFBD><E5A092>踵揢
✅ 核心算法:可靠的列名替换
"""
safe_formula = formula
# 关键1按列名长度倒序排序
# <EFBFBD><EFBFBD>摮𣂷葡<EFBFBD><EFBFBD>嚗𡁜<EFBFBD><EFBFBD>踵揢"擃䁅<E69383><E48185><EFBFBD><E8AEA0>?嚗<><E59A97><EFBFBD>踵揢"擃䁅<E69383><E48185>?
# 避免子串问题:先替换"高血压病史",再替换"高血压"
sorted_mapping = sorted(
column_mapping,
key=lambda x: len(x['originalName']),
reverse=True
)
# <EFBFBD>喲睸2嚗𡁻<EFBFBD>𣂷葵蝎曄<EFBFBD>踵揢嚗<EFBFBD><EFBFBD>雿輻鍂甇<EFBFBD><EFBFBD>嚗?
# 关键2逐个精确替换不使用正则
for item in sorted_mapping:
original = item['originalName']
safe = item['safeName']
@@ -188,7 +188,7 @@ def compute_column(
column_mapping: Optional[List[Dict[str, str]]] = None
) -> pd.DataFrame:
"""
<EFBFBD>?<3F><EFBFBD>B嚗䥪ython韐蠘提<E8A098>踵揢
✅ 方案BPython负责替换
"""
# 1. 替换列名
if column_mapping:
@@ -202,7 +202,7 @@ def compute_column(
env[item['safeName']] = df[item['originalName']]
env.update(ALLOWED_FUNCTIONS)
# 3. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>蝚阡<EFBFBD><EFBFBD><EFBFBD>嚗?
# 3. 执行(不需要字符验证!)
result = eval(safe_formula, {"__builtins__": {}}, env)
return df.assign(**{new_column_name: result})
@@ -210,64 +210,64 @@ def compute_column(
---
## <EFBFBD>?閫<><E996AB><EFBFBD><EFBFBD>䔮憸?
## ✅ 解决的问题
### 1. <EFBFBD><EFBFBD>摮㛖泵<EFBFBD><EFBFBD> <20>?
### 1. 特殊字符问题 ✅
- **问题**: `体重kg` 包含中文括号
- **<EFBFBD><EFBFBD>**: Python雿輻鍂摰匧<EFBFBD><EFBFBD><EFBFBD> `col_0`嚗䔶<EFBFBD><EFBFBD>㛖鸌畾𠰴<EFBFBD>蝚血蔣<EFBFBD>?
- **解决**: Python使用安全列名 `col_0`,不受特殊字符影响
### 2. 摮𣂷葡<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>?
- **<EFBFBD><EFBFBD>**: "擃䁅<EFBFBD><EFBFBD>? <20>?"擃䁅<E69383><E48185><EFBFBD><E8AEA0>? <20><EFBFBD>霂舀𤜯<E88880>?
- **<EFBFBD><EFBFBD>**: <EFBFBD>厰鵭摨血<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>踵揢<EFBFBD><EFBFBD><EFBFBD>?
### 2. 子串包含问题 ✅
- **问题**: "高血压" 和 "高血压病史" 可能误替换
- **解决**: 按长度倒序排序,先替换长列名
### 3. 颲寧<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>?
### 3. 边界识别问题 ✅
- **问题**: 正则`\b`对中文字符不可靠
- **<EFBFBD><EFBFBD>**: 雿輻鍂Python摮㛖泵銝深replace`嚗𣬚<EFBFBD><EFBFBD>訫虾<EFBFBD>?
- **解决**: 使用Python字符串`replace`,简单可靠
### 4. 摮㛖泵<EFBFBD><EFBFBD><EFBFBD>閖䔮憸?<3F>?
### 4. 字符白名单问题 ✅
- **问题**: 需要枚举所有允许的字符
- **<EFBFBD><EFBFBD>**: 銝漤<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Python<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?
- **解决**: 不需要验证Python只处理安全列名
---
## 🧪 测试用例
### 瘚贝<EFBFBD>1嚗𡁜抅<EFBFBD><EFBFBD><EFBFBD>?
### 测试1基本功能
```python
column_mapping = [
{"originalName": "雿㯄<EFBFBD>ɑg嚗?, "safeName": "col_0"},
{"originalName": "頨恍<EFBFBD>嚗Ếm嚗?, "safeName": "col_1"}
{"originalName": "体重kg", "safeName": "col_0"},
{"originalName": "身高cm", "safeName": "col_1"}
]
formula = "雿㯄<EFBFBD>ɑg嚗?/ (頨恍<E9A0A8>嚗Ếm嚗?100)**2"
# <EFBFBD><EFBFBD>: col_0 / (col_1/100)**2 <EFBFBD>?
formula = "体重kg / (身高cm/100)**2"
# 预期: col_0 / (col_1/100)**2
```
### 瘚贝<EFBFBD>2嚗𡁜<EFBFBD>銝脣<EFBFBD><EFBFBD>?
### 测试2子串包含
```python
column_mapping = [
{"originalName": "擃䁅<EFBFBD><EFBFBD>?, "safeName": "col_0"},
{"originalName": "擃䁅<EFBFBD><EFBFBD><EFBFBD><EFBFBD>?, "safeName": "col_1"}
{"originalName": "高血压", "safeName": "col_0"},
{"originalName": "高血压病史", "safeName": "col_1"}
]
formula = "擃䁅<EFBFBD><EFBFBD><EFBFBD><EFBFBD>?+ 擃䁅<E69383><E48185>?
formula = "高血压病史 + 高血压"
# 预期: col_1 + col_0 ✅(因为按长度排序)
```
### 瘚贝<EFBFBD>3嚗𡁜<EFBFBD><EFBFBD><EFBFBD>鸌畾𠰴<EFBFBD>蝚?
### 测试3复杂特殊字符
```python
column_mapping = [
{"originalName": "1.擃䁅<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>=0嚗峕<E59A97>=1嚗䔶<E59A97><E494B6>仿<EFBFBD>=2嚗?, "safeName": "col_0"}
{"originalName": "1.高血压病(无=0有=1不知道=2", "safeName": "col_0"}
]
formula = "1.擃䁅<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>=0嚗峕<E59A97>=1嚗䔶<E59A97><E494B6>仿<EFBFBD>=2嚗?* 2"
# <EFBFBD><EFBFBD>: col_0 * 2 <EFBFBD>?
formula = "1.高血压病(无=0有=1不知道=2 * 2"
# 预期: col_0 * 2
```
### 瘚贝<EFBFBD>4嚗𡁜<EFBFBD>憟埈𡠺<EFBFBD>?
### 测试4嵌套括号
```python
column_mapping = [
{"originalName": "FMA<EFBFBD><EFBFBD>嚗?-100嚗?, "safeName": "col_0"}
{"originalName": "FMA总分0-100", "safeName": "col_0"}
]
formula = "FMA<EFBFBD><EFBFBD>嚗?-100嚗?/ 100"
# <EFBFBD><EFBFBD>: col_0 / 100 <EFBFBD>?
formula = "FMA总分0-100 / 100"
# 预期: col_0 / 100
```
---
@@ -276,41 +276,41 @@ formula = "FMA总分
| 指标 | 影响 | 说明 |
|------|------|------|
| **蝵𤑳<EFBFBD>隡㰘<EFBFBD>** | +5KB | columnMapping蝥?KB嚗?00<30><EFBFBD> |
| **网络传输** | +5KB | columnMapping约5KB100列 |
| **替换时间** | <1ms | 字符串替换非常快 |
| **<EFBFBD><EFBFBD><EFBFBD><EFBFBD>** | <EFBFBD>臬蕭<EFBFBD>?| <20><EFBFBD><E8B1A2>唳旿憭<E697BF><E686AD><EFBFBD>園𡢿嚗<F0A1A2BF><E59A97>蝥改<E89DA5><E694B9>臬蕭<E887AC>?|
| **总体性能** | 可忽略 | 相比数据处理时间(秒级)可忽略 |
---
## 🎯 优势总结
### <EFBFBD><EFBFBD>雿㯄<EFBFBD> 潃鐥<E6BD83>潃鐥<E6BD83>潃?
- <EFBFBD>?<3F><EFBFBD><E586BD><EFBFBD><E8A781><EFBFBD><E8ABB9><EFBFBD><E4BA99><EFBFBD>
- <EFBFBD>?<3F><EFBFBD><E7A08D><EFBFBD><E6B8B2>𤘪<EFBFBD>
- <EFBFBD>?<3F><>蟮霈啣<E99C88><EFBFBD>
### 用户体验 ⭐⭐⭐⭐⭐
- ✅ 用户看到和输入原列名
- ✅ 公式直观易懂
- ✅ 历史记录清晰
### <EFBFBD><EFBFBD><EFBFBD>臬虾<EFBFBD><EFBFBD>?潃鐥<E6BD83>潃鐥<E6BD83>潃?
- <EFBFBD>?銝滢<E98A9D>韏𡝗迤<F0A19D97>躰器<E8BAB0><EFBFBD><E8ABB9>?
- <EFBFBD>?<3F>厰鵭摨行<E691A8>摨誯<E691A8><E8AAAF><EFBFBD>銝脤䔮憸?
- <EFBFBD>?Python摮㛖泵銝脫<EFBFBD>雿𦦵<EFBFBD><EFBFBD>訫虾<EFBFBD>?
### 技术可靠性 ⭐⭐⭐⭐⭐
- ✅ 不依赖正则边界识别
- ✅ 按长度排序避免子串问题
- Python字符串操作简单可靠
### <EFBFBD>舐輕<EFBFBD><EFBFBD>?潃鐥<E6BD83>潃鐥<E6BD83>潃?
- <EFBFBD>?<3F>諹提皜<E68F90>苊嚗<E88B8A><E59A97>蝡狹I<E78BB9><49>ython<6F><EFBFBD>嚗?
- <EFBFBD>?<3F><EFBFBD><EFBFBD><E99D9A><EFBFBD><EFBFBD>枏㫲<E69E8F>踵揢<E8B8B5><EFBFBD>嚗?
- <EFBFBD>?<3F>芣䔉銝滢<E98A9D><E6BBA2>齿<EFBFBD>摮㛖泵<E39B96><EFBFBD>
### 可维护性 ⭐⭐⭐⭐⭐
- ✅ 职责清晰前端UI、Python逻辑
- ✅ 易于调试(可打印替换日志)
- ✅ 未来不会再有字符问题
---
## 📝 后续工作
### 撌脣<EFBFBD><EFBFBD>?<3F>?
- [x] <EFBFBD>滨垢靽脲<EFBFBD>雿輻鍂<EFBFBD><EFBFBD><EFBFBD>?
### 已完成 ✅
- [x] 前端保持使用原列名
- [x] 后端传递columnMapping
- [x] Python实现替换逻辑
- [x] 移除字符验证
- [x] 更新Pivot操作
### <EFBFBD><EFBFBD>霂?<3F>?
### 待测试 ⏳
- [ ] 用户实际测试
- [ ] 边界情况验证
- [ ] 性能测试
@@ -324,7 +324,7 @@ formula = "FMA总分
## 🔗 相关文件
### 靽格㺿<EFBFBD><EFBFBD><EFBFBD>隞?
### 修改的文件
1. `backend/src/modules/dc/tool-c/controllers/QuickActionController.ts`
2. `backend/src/modules/dc/tool-c/services/QuickActionService.ts`
3. `extraction_service/main.py`
@@ -337,15 +337,14 @@ formula = "FMA总分
---
## <EFBFBD>?<3F><EFBFBD>
## ✨ 总结
方案B成功实现了
1. **用户体验优秀** - 使用原列名,直观易懂
2. **<EFBFBD><EFBFBD><EFBFBD>臬虾<EFBFBD>?* - Python<6F>踵揢嚗𣬚<E59A97><F0A3AC9A>訫虾<E8A8AB>?
3. **敶餃<EFBFBD><EFBFBD><EFBFBD>** - 銝滚<EFBFBD><EFBFBD>厩鸌畾𠰴<EFBFBD>蝚阡䔮憸?
**銝衤<E98A9D>甇?*: 蝑匧<E89D91><E58CA7><EFBFBD>瘚贝<E7989A>撉諹<E69289> <20>?
2. **技术可靠** - Python替换简单可控
3. **彻底解决** - 不再有特殊字符问题
**下一步**: 等待用户测试验证 ✅