351 lines
11 KiB
Markdown
351 lines
11 KiB
Markdown
# 部门数据隔离功能设计文档
|
||
|
||
## 概述
|
||
|
||
本设计文档描述了为安信平台核心业务表实现部门数据隔离功能的技术方案。该功能将通过在业务表中添加部门字段,结合数据访问拦截器和权限验证机制,确保用户只能访问和操作属于自己部门的数据。
|
||
|
||
## 架构设计
|
||
|
||
### 整体架构
|
||
|
||
```mermaid
|
||
graph TB
|
||
A[用户请求] --> B[权限拦截器]
|
||
B --> C{是否超级管理员}
|
||
C -->|是| D[允许访问所有数据]
|
||
C -->|否| E[添加部门过滤条件]
|
||
E --> F[数据访问层]
|
||
F --> G[数据库]
|
||
|
||
H[数据修改操作] --> I[部门验证器]
|
||
I --> J{数据是否属于用户部门}
|
||
J -->|是| K[执行操作]
|
||
J -->|否| L[拒绝访问]
|
||
```
|
||
|
||
### 核心组件
|
||
|
||
1. **数据库层**: 为业务表添加dept_id字段和相关索引
|
||
2. **数据访问拦截器**: 自动添加部门过滤条件
|
||
3. **权限验证器**: 验证用户对数据的操作权限
|
||
4. **部门上下文管理器**: 管理当前用户的部门信息
|
||
5. **数据迁移工具**: 为现有数据分配部门
|
||
|
||
## 组件和接口
|
||
|
||
### 1. 数据库表结构修改
|
||
|
||
#### 表结构变更
|
||
|
||
所有目标业务表都需要添加以下字段:
|
||
|
||
```sql
|
||
-- 添加部门字段
|
||
ALTER TABLE table_name ADD COLUMN dept_id BIGINT NOT NULL DEFAULT 100 COMMENT '部门ID';
|
||
|
||
-- 添加外键约束
|
||
ALTER TABLE table_name ADD CONSTRAINT fk_table_dept
|
||
FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id);
|
||
|
||
-- 添加索引
|
||
CREATE INDEX idx_table_dept_id ON table_name(dept_id);
|
||
CREATE INDEX idx_table_dept_status ON table_name(dept_id, status);
|
||
```
|
||
|
||
#### 具体表的修改脚本
|
||
|
||
**合同管理表 (dc_contract)**
|
||
```sql
|
||
ALTER TABLE dc_contract ADD COLUMN dept_id BIGINT NOT NULL DEFAULT 100 COMMENT '部门ID';
|
||
ALTER TABLE dc_contract ADD CONSTRAINT fk_contract_dept
|
||
FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id);
|
||
CREATE INDEX idx_contract_dept_id ON dc_contract(dept_id);
|
||
CREATE INDEX idx_contract_dept_status ON dc_contract(dept_id, contract_status);
|
||
```
|
||
|
||
**服务周期管理表 (dc_service_period)**
|
||
```sql
|
||
ALTER TABLE dc_service_period ADD COLUMN dept_id BIGINT NOT NULL DEFAULT 100 COMMENT '部门ID';
|
||
ALTER TABLE dc_service_period ADD CONSTRAINT fk_service_period_dept
|
||
FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id);
|
||
CREATE INDEX idx_service_period_dept_id ON dc_service_period(dept_id);
|
||
CREATE INDEX idx_service_period_dept_status ON dc_service_period(dept_id, period_status);
|
||
```
|
||
|
||
**员工表 (dc_employee_info)**
|
||
```sql
|
||
ALTER TABLE dc_employee_info ADD COLUMN dept_id BIGINT NOT NULL DEFAULT 100 COMMENT '部门ID';
|
||
ALTER TABLE dc_employee_info ADD CONSTRAINT fk_employee_dept
|
||
FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id);
|
||
CREATE INDEX idx_employee_dept_id ON dc_employee_info(dept_id);
|
||
CREATE INDEX idx_employee_dept_status ON dc_employee_info(dept_id, employee_status);
|
||
```
|
||
|
||
**员工库表 (dc_employee_library)**
|
||
```sql
|
||
ALTER TABLE dc_employee_library ADD COLUMN dept_id BIGINT NOT NULL DEFAULT 100 COMMENT '部门ID';
|
||
ALTER TABLE dc_employee_library ADD CONSTRAINT fk_employee_library_dept
|
||
FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id);
|
||
CREATE INDEX idx_employee_library_dept_id ON dc_employee_library(dept_id);
|
||
CREATE INDEX idx_employee_library_dept_status ON dc_employee_library(dept_id, library_status);
|
||
```
|
||
|
||
**债权管理表 (dc_credit)**
|
||
```sql
|
||
ALTER TABLE dc_credit ADD COLUMN dept_id BIGINT NOT NULL DEFAULT 100 COMMENT '部门ID';
|
||
ALTER TABLE dc_credit ADD CONSTRAINT fk_credit_dept
|
||
FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id);
|
||
CREATE INDEX idx_credit_dept_id ON dc_credit(dept_id);
|
||
CREATE INDEX idx_credit_dept_status ON dc_credit(dept_id, credit_status);
|
||
```
|
||
|
||
**银行管理表 (dc_bank_institution)**
|
||
```sql
|
||
ALTER TABLE dc_bank_institution ADD COLUMN dept_id BIGINT NOT NULL DEFAULT 100 COMMENT '部门ID';
|
||
ALTER TABLE dc_bank_institution ADD CONSTRAINT fk_bank_dept
|
||
FOREIGN KEY (dept_id) REFERENCES sys_dept(dept_id);
|
||
CREATE INDEX idx_bank_dept_id ON dc_bank_institution(dept_id);
|
||
CREATE INDEX idx_bank_dept_status ON dc_bank_institution(dept_id, status);
|
||
```
|
||
|
||
### 2. 数据访问拦截器
|
||
|
||
#### DepartmentDataInterceptor
|
||
|
||
```java
|
||
@Component
|
||
public class DepartmentDataInterceptor implements Interceptor {
|
||
|
||
@Override
|
||
public Object intercept(Invocation invocation) throws Throwable {
|
||
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
|
||
Object parameter = invocation.getArgs()[1];
|
||
|
||
// 检查是否需要添加部门过滤
|
||
if (needDepartmentFilter(mappedStatement)) {
|
||
addDepartmentFilter(parameter);
|
||
}
|
||
|
||
return invocation.proceed();
|
||
}
|
||
|
||
private boolean needDepartmentFilter(MappedStatement ms) {
|
||
// 检查SQL是否涉及需要部门隔离的表
|
||
String sql = ms.getBoundSql(null).getSql();
|
||
return containsTargetTables(sql) && !isSuperAdmin();
|
||
}
|
||
|
||
private void addDepartmentFilter(Object parameter) {
|
||
Long currentDeptId = getCurrentUserDeptId();
|
||
// 动态添加部门过滤条件
|
||
if (parameter instanceof Map) {
|
||
((Map<String, Object>) parameter).put("deptId", currentDeptId);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 权限验证器
|
||
|
||
#### DepartmentPermissionValidator
|
||
|
||
```java
|
||
@Component
|
||
public class DepartmentPermissionValidator {
|
||
|
||
public void validateDataAccess(Long recordId, String tableName) {
|
||
if (isSuperAdmin()) {
|
||
return; // 超级管理员跳过验证
|
||
}
|
||
|
||
Long recordDeptId = getRecordDeptId(recordId, tableName);
|
||
Long userDeptId = getCurrentUserDeptId();
|
||
|
||
if (!Objects.equals(recordDeptId, userDeptId)) {
|
||
throw new DepartmentAccessDeniedException("无权访问其他部门的数据");
|
||
}
|
||
}
|
||
|
||
public void validateDataCreation(Object entity) {
|
||
if (!isSuperAdmin()) {
|
||
// 自动设置当前用户部门ID
|
||
setEntityDeptId(entity, getCurrentUserDeptId());
|
||
}
|
||
}
|
||
|
||
private boolean isSuperAdmin() {
|
||
// 检查当前用户是否为超级管理员
|
||
return SecurityUtils.getLoginUser().getUser().isAdmin();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. 部门上下文管理器
|
||
|
||
#### DepartmentContextHolder
|
||
|
||
```java
|
||
public class DepartmentContextHolder {
|
||
|
||
private static final ThreadLocal<Long> DEPT_CONTEXT = new ThreadLocal<>();
|
||
|
||
public static void setCurrentDeptId(Long deptId) {
|
||
DEPT_CONTEXT.set(deptId);
|
||
}
|
||
|
||
public static Long getCurrentDeptId() {
|
||
return DEPT_CONTEXT.get();
|
||
}
|
||
|
||
public static void clear() {
|
||
DEPT_CONTEXT.remove();
|
||
}
|
||
|
||
public static boolean isSuperAdmin() {
|
||
try {
|
||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||
return loginUser != null && loginUser.getUser().isAdmin();
|
||
} catch (Exception e) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 数据模型
|
||
|
||
### 部门字段规范
|
||
|
||
所有业务表的部门字段统一规范:
|
||
|
||
```sql
|
||
dept_id BIGINT NOT NULL DEFAULT 100 COMMENT '部门ID'
|
||
```
|
||
|
||
- 字段名: `dept_id`
|
||
- 数据类型: `BIGINT`
|
||
- 约束: `NOT NULL`
|
||
- 默认值: `100` (安信平台根部门ID)
|
||
- 外键: 关联 `sys_dept.dept_id`
|
||
|
||
### 索引策略
|
||
|
||
为提高查询性能,每个表都需要创建以下索引:
|
||
|
||
1. **单列索引**: `idx_tablename_dept_id` - 用于部门过滤查询
|
||
2. **复合索引**: `idx_tablename_dept_status` - 用于部门+状态的组合查询
|
||
|
||
## 错误处理
|
||
|
||
### 异常类型
|
||
|
||
```java
|
||
// 部门访问拒绝异常
|
||
public class DepartmentAccessDeniedException extends RuntimeException {
|
||
public DepartmentAccessDeniedException(String message) {
|
||
super(message);
|
||
}
|
||
}
|
||
|
||
// 部门数据不一致异常
|
||
public class DepartmentDataInconsistencyException extends RuntimeException {
|
||
public DepartmentDataInconsistencyException(String message) {
|
||
super(message);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 错误处理策略
|
||
|
||
1. **权限验证失败**: 返回HTTP 403状态码和明确的错误信息
|
||
2. **数据不一致**: 记录错误日志并阻止操作
|
||
3. **系统异常**: 记录详细日志并返回通用错误信息
|
||
|
||
## 测试策略
|
||
|
||
### 单元测试
|
||
|
||
1. **数据访问拦截器测试**: 验证部门过滤条件的正确添加
|
||
2. **权限验证器测试**: 验证各种权限场景的处理
|
||
3. **部门上下文管理器测试**: 验证线程安全和上下文管理
|
||
|
||
### 集成测试
|
||
|
||
1. **数据库操作测试**: 验证CRUD操作的部门隔离效果
|
||
2. **API接口测试**: 验证REST接口的权限控制
|
||
3. **性能测试**: 验证添加部门隔离后的查询性能
|
||
|
||
## 正确性属性
|
||
|
||
*属性是一个特征或行为,应该在系统的所有有效执行中保持为真——本质上是关于系统应该做什么的正式声明。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。*
|
||
|
||
基于需求分析,以下是部门数据隔离系统必须满足的正确性属性:
|
||
|
||
### 属性 1: 数据迁移完整性
|
||
*对于任何* 业务表中的现有记录,执行数据迁移后,所有记录都应该被分配一个有效的部门ID
|
||
**验证: 需求 2.1, 2.3**
|
||
|
||
### 属性 2: 部门分配一致性
|
||
*对于任何* 数据迁移操作,分配的部门ID应该符合系统配置的默认部门或业务逻辑规则
|
||
**验证: 需求 2.2**
|
||
|
||
### 属性 3: 查询结果部门隔离
|
||
*对于任何* 非超级管理员用户的查询操作,返回的结果应该只包含该用户所属部门的数据
|
||
**验证: 需求 3.1**
|
||
|
||
### 属性 4: 自动部门设置
|
||
*对于任何* 用户创建的新记录,系统应该自动将该记录的部门ID设置为当前用户所属的部门ID
|
||
**验证: 需求 3.2**
|
||
|
||
### 属性 5: 修改权限验证
|
||
*对于任何* 用户尝试修改的记录,只有当该记录属于用户所在部门时,修改操作才应该被允许
|
||
**验证: 需求 3.3**
|
||
|
||
### 属性 6: 删除权限验证
|
||
*对于任何* 用户尝试删除的记录,只有当该记录属于用户所在部门时,删除操作才应该被允许
|
||
**验证: 需求 3.4**
|
||
|
||
### 属性 7: 超级管理员数据访问
|
||
*对于任何* 超级管理员用户,系统应该允许其查看和操作所有部门的数据,不受部门隔离限制
|
||
**验证: 需求 4.1, 4.2**
|
||
|
||
### 属性 8: 超级管理员操作日志
|
||
*对于任何* 超级管理员的操作,系统应该记录详细的操作日志
|
||
**验证: 需求 4.4**
|
||
|
||
### 属性 9: 关联数据部门一致性
|
||
*对于任何* 创建涉及关联数据的记录(如服务周期关联合同、债权关联合同),所有关联的数据应该属于同一个部门
|
||
**验证: 需求 5.1, 5.2, 5.3**
|
||
|
||
### 属性 10: 业务逻辑透明性
|
||
*对于任何* 现有的业务逻辑执行,添加部门隔离功能后应该保持原有的业务流程不变,只是透明地添加部门过滤
|
||
**验证: 需求 7.2**
|
||
|
||
## 测试策略
|
||
|
||
### 双重测试方法
|
||
|
||
**单元测试**: 验证具体示例、边界情况和错误条件
|
||
**属性测试**: 通过所有输入验证通用属性
|
||
|
||
两者互补且都是全面覆盖所必需的。
|
||
|
||
### 单元测试重点
|
||
|
||
单元测试应该专注于:
|
||
- 具体的数据库结构修改验证
|
||
- 特定的权限验证场景
|
||
- 错误处理和异常情况
|
||
- 配置和开关功能测试
|
||
|
||
### 属性测试重点
|
||
|
||
属性测试应该专注于:
|
||
- 跨所有输入的通用属性验证
|
||
- 通过随机化实现全面的输入覆盖
|
||
|
||
### 属性测试配置
|
||
|
||
- 每个属性测试最少100次迭代(由于随机化)
|
||
- 每个属性测试必须引用其设计文档属性
|
||
- 标签格式: **功能: department-data-isolation, 属性 {编号}: {属性文本}** |