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
340 lines
10 KiB
Markdown
340 lines
10 KiB
Markdown
# 工具C - NA处理功能开发总结
|
||
|
||
## 📋 概述
|
||
|
||
**目标**:在4个核心功能中添加对NA(空值/缺失值)的显式处理,让用户能够明确看到并处理缺失值。
|
||
|
||
**NA显示名称**:`空值/NA`(中英文结合)
|
||
|
||
---
|
||
|
||
## ✅ 已完成:Python后端(100%)
|
||
|
||
### 1. recode.py - 数值映射 ✅
|
||
|
||
**新增参数**:
|
||
- `na_handling`: 'keep' | 'map' | 'drop'
|
||
- `keep`: 保持为NA(默认)
|
||
- `map`: 映射为指定值
|
||
- `drop`: 删除包含NA的行
|
||
- `na_value`: NA映射值(当na_handling='map'时使用)
|
||
|
||
**实现逻辑**:
|
||
```python
|
||
if original_na_count > 0:
|
||
na_mask = result[column].isna()
|
||
|
||
if na_handling == 'keep':
|
||
# 保持为NA(已经是NA,无需操作)
|
||
print(f'📊 NA处理:保持为NA({original_na_count}个)')
|
||
|
||
elif na_handling == 'map':
|
||
# 映射为指定值
|
||
result.loc[na_mask, target_column] = na_value
|
||
print(f'📊 NA处理:映射为 {na_value}({original_na_count}个)')
|
||
|
||
elif na_handling == 'drop':
|
||
# 删除包含NA的行
|
||
result = result[~na_mask].copy()
|
||
```
|
||
|
||
### 2. filter.py - 高级筛选 ✅
|
||
|
||
**已支持**:`is_null` 和 `not_null` 运算符
|
||
|
||
无需修改,原有代码已经支持!
|
||
|
||
```python
|
||
elif operator == 'is_null':
|
||
mask = df[column].isna()
|
||
elif operator == 'not_null':
|
||
mask = df[column].notna()
|
||
```
|
||
|
||
### 3. binning.py - 生成分类变量 ✅
|
||
|
||
**新增参数**:
|
||
- `na_handling`: 'keep' | 'label' | 'assign'
|
||
- `keep`: 保持为NA(默认)
|
||
- `label`: 标记为指定标签(如"缺失")
|
||
- `assign`: 分配到指定组
|
||
- `na_label`: NA标签(当na_handling='label'时使用)
|
||
- `na_assign_to`: NA分配到的组索引(当na_handling='assign'时使用)
|
||
|
||
**实现逻辑**:
|
||
```python
|
||
if original_na_count > 0:
|
||
na_mask = result[column].isna()
|
||
|
||
if na_handling == 'keep':
|
||
# 保持为NA
|
||
print(f'📊 NA处理:保持为NA({original_na_count}个)')
|
||
|
||
elif na_handling == 'label':
|
||
# 标记为指定标签
|
||
label_to_use = na_label if na_label else '空值/NA'
|
||
result.loc[na_mask, new_column_name] = label_to_use
|
||
print(f'📊 NA处理:标记为 "{label_to_use}"({original_na_count}个)')
|
||
|
||
elif na_handling == 'assign':
|
||
# 分配到指定组
|
||
if labels and na_assign_to is not None:
|
||
result.loc[na_mask, new_column_name] = labels[na_assign_to]
|
||
```
|
||
|
||
### 4. conditional.py - 条件生成列 ✅
|
||
|
||
**新增支持**:`is_null` 和 `not_null` 运算符
|
||
|
||
```python
|
||
elif operator == 'is_null': # ✨ 新增:为空
|
||
mask = result[column].isna()
|
||
elif operator == 'not_null': # ✨ 新增:不为空
|
||
mask = result[column].notna()
|
||
```
|
||
|
||
### 5. main.py - API请求模型 ✅
|
||
|
||
**RecodeRequest**:
|
||
```python
|
||
na_handling: str = 'keep'
|
||
na_value: Any = None
|
||
```
|
||
|
||
**BinningRequest**:
|
||
```python
|
||
na_handling: str = 'keep'
|
||
na_label: str = None
|
||
na_assign_to: int = None
|
||
```
|
||
|
||
**FilterRequest 和 ConditionalRequest**:
|
||
无需修改,已支持
|
||
|
||
---
|
||
|
||
## 🔄 待完成:Node.js后端
|
||
|
||
### QuickActionService.ts
|
||
|
||
**需要更新的接口**:
|
||
|
||
1. **RecodeParams**:
|
||
```typescript
|
||
interface RecodeParams {
|
||
column: string;
|
||
mapping: Record<string, any>;
|
||
createNewColumn?: boolean;
|
||
newColumnName?: string;
|
||
naHandling?: 'keep' | 'map' | 'drop'; // ✨ 新增
|
||
naValue?: any; // ✨ 新增
|
||
}
|
||
```
|
||
|
||
2. **BinningParams**:
|
||
```typescript
|
||
interface BinningParams {
|
||
column: string;
|
||
method: 'custom' | 'equal_width' | 'equal_freq';
|
||
newColumnName: string;
|
||
bins?: number[];
|
||
labels?: string[];
|
||
numBins?: number;
|
||
naHandling?: 'keep' | 'label' | 'assign'; // ✨ 新增
|
||
naLabel?: string; // ✨ 新增
|
||
naAssignTo?: number; // ✨ 新增
|
||
}
|
||
```
|
||
|
||
**API调用**(自动传递所有参数,无需特殊处理)
|
||
|
||
---
|
||
|
||
## 🎨 待完成:前端UI
|
||
|
||
### 1. RecodeDialog.tsx - 数值映射
|
||
|
||
**UI设计**:
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ 数值映射 [X] │
|
||
├─────────────────────────────────────┤
|
||
│ 选择列:[婚姻状况▼] │
|
||
│ │
|
||
│ 唯一值映射: │
|
||
│ ┌──────────────────────────────┐ │
|
||
│ │ 原始值 → 新值 │ │
|
||
│ │ 已婚 → [1 ] │ │
|
||
│ │ 未婚 → [0 ] │ │
|
||
│ │ 空值/NA → [▼] │ ⭐│
|
||
│ │ ├─ 保持为NA(默认) │ │
|
||
│ │ ├─ 映射为:[____] │ │
|
||
│ │ └─ 删除该行 │ │
|
||
│ └──────────────────────────────┘ │
|
||
│ │
|
||
│ ℹ️ 当前有125个空值(15.6%) │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
**实现要点**:
|
||
1. 调用`/api/v1/dc/tool-c/sessions/:id/unique-values`时,检测是否有NA
|
||
2. 如果有NA,显示"空值/NA"特殊行
|
||
3. 提供3种选择:保持NA / 映射为指定值 / 删除行
|
||
|
||
### 2. FilterDialog.tsx - 高级筛选
|
||
|
||
**UI设计**:
|
||
```
|
||
条件:
|
||
[婚姻状况▼] [运算符▼]
|
||
• 等于
|
||
• 不等于
|
||
• 为空 ← ✨ 新增
|
||
• 不为空 ← ✨ 新增
|
||
• ...
|
||
```
|
||
|
||
**实现要点**:
|
||
1. 在运算符下拉菜单中添加"为空"和"不为空"选项
|
||
2. 当选择这两个运算符时,隐藏"值"输入框(不需要输入值)
|
||
|
||
### 3. BinningDialog.tsx - 生成分类变量
|
||
|
||
**UI设计**:
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ 生成分类变量 [X] │
|
||
├─────────────────────────────────────┤
|
||
│ 原始列:[年龄▼] │
|
||
│ ...分组规则... │
|
||
│ │
|
||
│ ⚠️ 空值处理: │ ⭐
|
||
│ ⚪ 保持为空(默认) │
|
||
│ ⚪ 标记为:[缺失___] │
|
||
│ ⚪ 分配到组:[第1组▼] │
|
||
│ │
|
||
│ ℹ️ 当前有25个空值(3.1%) │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
**实现要点**:
|
||
1. 添加Radio Group for NA处理方式
|
||
2. 根据选择显示相应的输入框
|
||
3. 传递`naHandling`、`naLabel`、`naAssignTo`参数
|
||
|
||
### 4. ConditionalDialog.tsx - 条件生成列
|
||
|
||
**UI设计**:
|
||
```
|
||
规则1:
|
||
如果 [婚姻状况▼] [运算符▼]
|
||
• 等于
|
||
• 不等于
|
||
• 为空 ← ✨ 新增
|
||
• 不为空 ← ✨ 新增
|
||
• ...
|
||
则填充:[低风险 ]
|
||
```
|
||
|
||
**实现要点**:
|
||
1. 与FilterDialog类似,在运算符下拉菜单中添加"为空"和"不为空"
|
||
2. 这两个运算符不需要输入值
|
||
|
||
---
|
||
|
||
## 🧪 测试用例
|
||
|
||
### 测试数据准备
|
||
|
||
```csv
|
||
ID,婚姻状况,年龄,收缩压
|
||
1,已婚,45,120
|
||
2,未婚,35,130
|
||
3,,50, # ← NA
|
||
4,离异,60,
|
||
5,,NA,140
|
||
```
|
||
|
||
### 测试场景
|
||
|
||
| 编号 | 功能 | 测试场景 | 预期结果 |
|
||
|------|------|----------|----------|
|
||
| TC-1 | 数值映射 - 保持NA | 婚姻状况:已婚=1,未婚=0,NA=保持 | NA行的新列为NA ✅ |
|
||
| TC-2 | 数值映射 - 映射NA | 婚姻状况:已婚=1,未婚=0,NA=映射为9 | NA行的新列为9 ✅ |
|
||
| TC-3 | 数值映射 - 删除NA | 婚姻状况:已婚=1,未婚=0,NA=删除 | NA行被删除,总行数减少 ✅ |
|
||
| TC-4 | 高级筛选 - 为空 | 筛选"婚姻状况"为空 | 只保留NA行 ✅ |
|
||
| TC-5 | 高级筛选 - 不为空 | 筛选"婚姻状况"不为空 | 只保留非NA行 ✅ |
|
||
| TC-6 | 生成分类变量 - 保持NA | 年龄分组,NA保持 | NA行的新列为NA ✅ |
|
||
| TC-7 | 生成分类变量 - 标记NA | 年龄分组,NA标记为"缺失" | NA行的新列为"缺失" ✅ |
|
||
| TC-8 | 生成分类变量 - 分配NA | 年龄分组,NA分配到第1组 | NA行的新列为第1组标签 ✅ |
|
||
| TC-9 | 条件生成列 - 为空 | 如果婚姻状况为空,则"未知" | NA行的新列为"未知" ✅ |
|
||
| TC-10 | 条件生成列 - 不为空 | 如果婚姻状况不为空,则"已知" | 非NA行的新列为"已知" ✅ |
|
||
|
||
---
|
||
|
||
## 📊 开发进度
|
||
|
||
| 阶段 | 状态 | 备注 |
|
||
|------|------|------|
|
||
| Python后端 - recode.py | ✅ 100% | 已完成 |
|
||
| Python后端 - filter.py | ✅ 100% | 已支持(无需修改) |
|
||
| Python后端 - binning.py | ✅ 100% | 已完成 |
|
||
| Python后端 - conditional.py | ✅ 100% | 已完成 |
|
||
| Python后端 - main.py | ✅ 100% | 已完成 |
|
||
| Node.js后端 | ✅ 100% | 已完成(参数传递) |
|
||
| 前端 - RecodeDialog | ✅ 100% | 已完成(NA处理下拉菜单) |
|
||
| 前端 - FilterDialog | ✅ 100% | 已完成(已支持is_null/not_null) |
|
||
| 前端 - BinningDialog | ✅ 100% | 已完成(NA处理Radio Group) |
|
||
| 前端 - ConditionalDialog | ✅ 100% | 已完成(添加is_null/not_null) |
|
||
| 测试 | ⏳ 待测试 | 等待用户测试验证 |
|
||
|
||
---
|
||
|
||
## 🎯 下一步行动
|
||
|
||
1. **Node.js后端**(预计15分钟)
|
||
- 更新RecodeParams接口
|
||
- 更新BinningParams接口
|
||
- (FilterParams和ConditionalParams无需修改)
|
||
|
||
2. **前端UI**(预计2小时)
|
||
- RecodeDialog:添加NA处理下拉菜单(45分钟)
|
||
- FilterDialog:添加"为空"/"不为空"运算符(15分钟)
|
||
- BinningDialog:添加NA处理Radio Group(30分钟)
|
||
- ConditionalDialog:添加"为空"/"不为空"运算符(30分钟)
|
||
|
||
3. **测试**(预计30分钟)
|
||
- 执行10个测试用例
|
||
- 修复发现的问题
|
||
|
||
**总计剩余时间:约3小时**
|
||
|
||
---
|
||
|
||
## 📝 技术要点
|
||
|
||
### Python端
|
||
- 使用`df[column].isna()`检测NA
|
||
- 使用`df.loc[mask, col] = value`填充NA
|
||
- 使用`df[~mask]`删除NA行
|
||
- 统计并打印NA处理信息
|
||
|
||
### 前端端
|
||
- 在获取unique values时检测NA
|
||
- 使用`<空值/NA>`作为显示名称
|
||
- 根据用户选择构造请求参数
|
||
- 显示NA统计信息(如"当前有125个空值")
|
||
|
||
### 验收标准
|
||
- ✅ 用户能明确看到NA的存在
|
||
- ✅ 用户能选择如何处理NA
|
||
- ✅ 处理后的结果符合用户选择
|
||
- ✅ 所有功能的NA处理方式清晰统一
|
||
|
||
---
|
||
|
||
**文档创建时间**:2025-12-09
|
||
**Python后端开发状态**:✅ 已完成
|
||
**剩余工作**:Node.js后端 + 前端UI + 测试
|
||
|