Files
cattleTransportation/基于手机号的数据权限过滤功能说明.md
2025-10-16 17:31:44 +08:00

333 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 基于手机号的数据权限过滤功能实现说明
## 功能概述
实现了基于登录用户手机号的数据权限过滤功能,保护用户隐私,确保普通用户只能查看与自己相关的装车订单和运送清单。
## 核心需求
1. **角色识别**:系统识别四个角色的手机号
- 司机Driver
- 供应商Supplier可能有多个
- 资金方Fund
- 采购商Buyer
2. **权限规则**
- **超级管理员roleId=1**:可以查看所有订单和运送清单
- **普通用户**:只能查看与自己手机号相关的数据(四个角色中任意一个匹配即可)
## 技术实现
### 1. 后端修改
#### 1.1 实体类修改
**文件**: `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/entity/Delivery.java`
添加了三个新的手机号字段(司机手机号已存在):
```java
/**
* 供应商手机号(逗号分隔)
*/
@TableField(exist = false)
private String supplierMobile;
/**
* 资金方手机号
*/
@TableField(exist = false)
private String fundMobile;
/**
* 采购商手机号
*/
@TableField(exist = false)
private String buyerMobile;
```
#### 1.2 服务层修改
**文件**: `tradeCattle/aiotagro-cattle-trade/src/main/java/com/aiotagro/cattletrade/business/service/impl/DeliveryServiceImpl.java`
修改了两个关键方法:
##### 1装车订单列表查询 - `pageQueryListLog`
```java
public PageResultResponse<Delivery> pageQueryListLog(DeliverListDto dto) {
// 1. 获取当前登录用户手机号
String currentUserMobile = SecurityUtil.getUserMobile();
// 2. 查询所有数据
List<Delivery> resList = list(deliveryLambdaQueryWrapper);
// 3. 关联查询四个角色的手机号
resList.forEach(deliveryLogVo -> {
// 查询供应商手机号supplierId是逗号分隔的
// 查询资金方手机号
// 查询采购商手机号
// 查询司机手机号
});
// 4. 数据权限过滤(非超级管理员)
if (!SecurityUtil.isSuperAdmin()) {
resList = resList.stream().filter(delivery -> {
// 匹配司机手机号
// 匹配供应商手机号
// 匹配资金方手机号
// 匹配采购商手机号
return hasPermission;
}).collect(Collectors.toList());
}
return new PageResultResponse(filteredTotal, resList);
}
```
##### 2运送清单列表查询 - `pageQuery`
实现逻辑与 `pageQueryListLog` 相同,确保两个页面的权限过滤一致。
### 2. 权限判断逻辑
#### 2.1 超级管理员判断
```java
// 位置SecurityUtil.java
public static boolean isSuperAdmin() {
Integer roleId = getRoleId();
return roleId != null && roleId.equals(RoleConstants.SUPER_ADMIN_ROLE_ID);
}
// RoleConstants.SUPER_ADMIN_ROLE_ID = 1
```
#### 2.2 手机号匹配逻辑
普通用户的数据访问权限判断:
```java
boolean hasPermission = false;
// 1. 检查是否是司机
if (currentUserMobile.equals(delivery.getDriverMobile())) {
hasPermission = true;
}
// 2. 检查是否是供应商(可能有多个)
if (!hasPermission && delivery.getSupplierMobile() != null) {
String[] supplierMobiles = delivery.getSupplierMobile().split(",");
for (String mobile : supplierMobiles) {
if (currentUserMobile.equals(mobile.trim())) {
hasPermission = true;
break;
}
}
}
// 3. 检查是否是资金方
if (!hasPermission && currentUserMobile.equals(delivery.getFundMobile())) {
hasPermission = true;
}
// 4. 检查是否是采购商
if (!hasPermission && currentUserMobile.equals(delivery.getBuyerMobile())) {
hasPermission = true;
}
return hasPermission;
```
### 3. 角色手机号关联查询
系统在返回列表数据前会根据角色ID查询对应的手机号
```java
// 1. 供应商手机号查询supplierId可能是逗号分隔的多个ID
if (delivery.getSupplierId() != null) {
String[] supplierIds = delivery.getSupplierId().split(",");
List<String> supplierMobiles = new ArrayList<>();
for (String supplierId : supplierIds) {
Member supplier = memberMapper.selectById(Integer.parseInt(supplierId));
if (supplier != null) {
supplierMobiles.add(supplier.getMobile());
}
}
delivery.setSupplierMobile(String.join(",", supplierMobiles));
}
// 2. 资金方手机号查询
if (delivery.getFundId() != null) {
Member fund = memberMapper.selectById(delivery.getFundId());
if (fund != null) {
delivery.setFundMobile(fund.getMobile());
}
}
// 3. 采购商手机号查询
if (delivery.getBuyerId() != null) {
Member buyer = memberMapper.selectById(delivery.getBuyerId());
if (buyer != null) {
delivery.setBuyerMobile(buyer.getMobile());
}
}
// 4. 司机手机号查询
if (delivery.getDriverId() != null) {
Map<String, Object> driverInfo = memberDriverMapper.selectDriverById(delivery.getDriverId());
if (driverInfo != null) {
delivery.setDriverMobile((String) driverInfo.get("mobile"));
}
}
```
## 影响范围
### 1. 受影响的页面
- **装车订单列表页** (`/shipping/loadingOrder`)
- 接口:`/delivery/pageDeliveryOrderList`
- **运送清单列表页** (`/entry/attestation`)
- 接口:`/delivery/pageQueryList`
### 2. 不受影响的功能
- 订单详情查询
- 订单创建/编辑
- 设备分配
- 其他管理功能
## 测试建议
### 测试场景1超级管理员登录
**步骤**
1. 使用 roleId=1 的超级管理员账号登录
2. 访问装车订单列表页
3. 访问运送清单列表页
**预期结果**
- 可以看到系统中所有的订单和运送清单
- 控制台输出:`=== 超级管理员,不执行数据权限过滤 ===`
### 测试场景2司机登录
**步骤**
1. 使用司机账号登录手机号13800138001
2. 访问装车订单列表页
3. 访问运送清单列表页
**预期结果**
- 只能看到 driverMobile = '13800138001' 的订单
- 其他订单不可见
- 控制台输出匹配日志
### 测试场景3供应商登录
**步骤**
1. 使用供应商账号登录手机号13900139001
2. 访问装车订单列表页
3. 访问运送清单列表页
**预期结果**
- 只能看到 supplierMobile 包含 '13900139001' 的订单
- 其他订单不可见
### 测试场景4资金方/采购商登录
**步骤**
1. 使用资金方或采购商账号登录
2. 访问装车订单列表页
3. 访问运送清单列表页
**预期结果**
- 只能看到与自己手机号匹配的订单
- 其他订单不可见
### 测试场景5多角色用户
**步骤**
1. 创建一个订单,将同一个用户设置为多个角色(如既是司机又是供应商)
2. 使用该用户登录
3. 访问列表页
**预期结果**
- 该用户可以看到此订单(任意角色匹配即可)
## 调试信息
系统在控制台输出详细的调试日志,方便问题排查:
```
=== 装车订单列表查询 - 当前登录用户手机号: 13800138001 ===
=== 非超级管理员,执行数据权限过滤 ===
当前用户手机号: 13800138001
订单 ZC20251016142501 - 匹配司机手机号
订单 ZC20251016142502 - 无权限,过滤掉
过滤后的订单数量: 1
```
## 数据库表结构
### delivery 表
相关字段:
- `supplier_id` VARCHAR - 供应商ID逗号分隔
- `fund_id` INT - 资金方ID
- `driver_id` INT - 司机ID
- `buyer_id` INT - 采购商ID
### member 表
相关字段:
- `id` INT - 会员ID
- `mobile` VARCHAR - 手机号
- `type` INT - 会员类型
### member_driver 表
相关字段:
- `id` INT - 司机ID
- `member_id` INT - 关联 member.id
- `username` VARCHAR - 司机姓名
- `mobile` VARCHAR - 司机手机号
## 性能优化建议
1. **批量查询优化**:当前实现对每个订单单独查询角色手机号,如果订单数量较多,建议改为批量查询:
```java
// 收集所有角色ID
List<Integer> allMemberIds = new ArrayList<>();
// ... 添加所有 fundId, buyerId 等
// 批量查询
List<Member> members = memberMapper.selectBatchIds(allMemberIds);
Map<Integer, String> idToMobileMap = ...;
```
2. **缓存优化**:对于频繁访问的会员信息,可以考虑添加 Redis 缓存。
3. **分页问题**:当前实现在内存中过滤数据后更新分页信息,这可能导致分页不准确。建议:
- 方案A在数据库层面进行过滤通过 SQL JOIN 查询)
- 方案B在应用层正确处理分页逻辑
## 注意事项
1. **手机号格式**确保系统中所有手机号格式一致都是11位数字无空格
2. **空值处理**:代码中已处理 null 值情况,避免空指针异常
3. **供应商多值处理**supplierId 可能是逗号分隔的多个值,需要分别处理
4. **调试日志**:生产环境建议关闭或降低日志级别
## 版本信息
- 实现日期2025-10-16
- 修改文件:
- `Delivery.java`
- `DeliveryServiceImpl.java`
- 影响接口:
- `/delivery/pageDeliveryOrderList` (装车订单列表)
- `/delivery/pageQueryList` (运送清单列表)