de78c21799
- 添加 phone 字段迁移到 uc_user 表的设计 - 新增统一认证接口 /user/inner/auth/load(POST) - 支持 AccountType 枚举:USERNAME/PHONE/EMAIL - 废弃旧的 loadByUsername 接口 - 添加数据库变更 SQL 脚本
640 lines
18 KiB
Markdown
640 lines
18 KiB
Markdown
# 用户聚合查询设计规格
|
||
|
||
> **日期**: 2026-06-06
|
||
> **状态**: 已批准
|
||
> **作者**: AI Assistant
|
||
> **相关模块**: rui-service-user
|
||
|
||
---
|
||
|
||
## 1. 背景与问题
|
||
|
||
### 1.1 现状
|
||
|
||
当前用户数据分散在4张表中:
|
||
|
||
| 表名 | 说明 |
|
||
|------|------|
|
||
| `uc_user` | 用户基础信息(用户名、密码、状态等) |
|
||
| `uc_user_detail` | 用户详情(昵称、邮箱、手机号等) |
|
||
| `uc_user_dept` | 用户部门关联(支持多部门,有主部门标记) |
|
||
| `uc_user_post` | 用户岗位关联(支持多岗位) |
|
||
|
||
### 1.2 问题
|
||
|
||
- **前端请求过多**:获取完整用户信息需要3个独立请求
|
||
- **列表页性能差**:100条用户数据需要 1 + 100 + 100 = 201 个请求
|
||
- **数据一致性难保证**:多个请求可能部分失败
|
||
- **手机号位置不合理**:手机号在 `uc_user_detail` 表,但短信登录需要频繁查询,应该提升到 `uc_user` 表
|
||
- **认证接口不灵活**:当前 `loadByUsername` 只支持用户名,无法扩展支持手机号、邮箱等多种登录方式
|
||
|
||
### 1.3 约束条件
|
||
|
||
- 一个用户通常关联 **1个主部门 + 少量岗位**
|
||
- 需要支持 **多租户**(tenantId)
|
||
- 必须 **保持向后兼容**
|
||
- 需要 **缓存优化**
|
||
- 手机号需要 **唯一约束**(按租户)
|
||
|
||
---
|
||
|
||
## 2. 设计目标
|
||
|
||
1. **减少前端请求**:从3个减少到1个
|
||
2. **优化列表页性能**:批量查询,避免 N+1
|
||
3. **引入缓存**:Redis 缓存用户聚合数据
|
||
4. **手机号迁移**:将 `phone` 从 `uc_user_detail` 迁移到 `uc_user` 表
|
||
5. **统一认证接口**:支持多种登录方式(用户名、手机号、邮箱等)
|
||
6. **保持兼容性**:现有接口不受影响
|
||
|
||
---
|
||
|
||
## 3. 技术方案
|
||
|
||
### 3.1 总体架构
|
||
|
||
```
|
||
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
|
||
│ 前端 │────▶│ UserController │────▶│ UserService │
|
||
└─────────────┘ └──────────────────┘ └──────┬───────┘
|
||
│
|
||
┌────────────────────────────┼────────────────────────────┐
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||
│ Redis Cache │ │ uc_user_dept │ │ uc_user_post │
|
||
│ user:agg:{id} │ │ (主部门+关联) │ │ (岗位关联) │
|
||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||
```
|
||
|
||
### 3.2 核心策略
|
||
|
||
- **保留关联表**:不改表结构,保持多对多关系的灵活性
|
||
- **聚合查询**:后端做 JOIN 或批量 IN 查询,一次性返回
|
||
- **两级缓存**:
|
||
- L1:单用户聚合数据缓存(Redis)
|
||
- L2:批量查询结果不缓存(避免缓存过大)
|
||
- **缓存失效**:数据变更时主动失效
|
||
|
||
---
|
||
|
||
## 4. 数据模型
|
||
|
||
### 4.1 新增 VO 对象
|
||
|
||
```java
|
||
package com.rui.service.user.vo;
|
||
|
||
import com.rui.service.user.entity.User;
|
||
import io.swagger.v3.oas.annotations.media.Schema;
|
||
import lombok.Data;
|
||
import lombok.EqualsAndHashCode;
|
||
|
||
import java.util.List;
|
||
|
||
/**
|
||
* 用户聚合信息(包含部门、岗位)
|
||
*/
|
||
@Data
|
||
@EqualsAndHashCode(callSuper = true)
|
||
@Schema(description = "用户聚合信息")
|
||
public class UserAggregateVO extends User {
|
||
|
||
@Schema(description = "部门列表")
|
||
private List<UserDeptVO> depts;
|
||
|
||
@Schema(description = "岗位列表")
|
||
private List<UserPostVO> posts;
|
||
|
||
@Schema(description = "主部门ID")
|
||
private Long mainDeptId;
|
||
|
||
@Schema(description = "主部门名称")
|
||
private String mainDeptName;
|
||
|
||
@Schema(description = "部门编码")
|
||
private String deptCode;
|
||
|
||
@Schema(description = "岗位编码")
|
||
private String postCode;
|
||
}
|
||
```
|
||
|
||
### 4.2 部门/岗位 VO
|
||
|
||
```java
|
||
@Data
|
||
@Schema(description = "用户部门信息")
|
||
public class UserDeptVO {
|
||
@Schema(description = "用户ID")
|
||
private Long userId;
|
||
|
||
@Schema(description = "部门ID")
|
||
private Long deptId;
|
||
|
||
@Schema(description = "部门编码")
|
||
private String deptCode;
|
||
|
||
@Schema(description = "部门名称")
|
||
private String deptName;
|
||
|
||
@Schema(description = "是否主部门")
|
||
private Boolean main;
|
||
}
|
||
|
||
@Data
|
||
@Schema(description = "用户岗位信息")
|
||
public class UserPostVO {
|
||
@Schema(description = "用户ID")
|
||
private Long userId;
|
||
|
||
@Schema(description = "岗位ID")
|
||
private Long postId;
|
||
|
||
@Schema(description = "岗位编码")
|
||
private String postCode;
|
||
|
||
@Schema(description = "岗位名称")
|
||
private String postName;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 接口设计
|
||
|
||
### 5.1 新增接口
|
||
|
||
#### 5.1.1 获取用户聚合信息(单用户)
|
||
|
||
```
|
||
GET /user/admin/user/{id}/aggregate
|
||
```
|
||
|
||
**响应示例:**
|
||
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"msg": "success",
|
||
"data": {
|
||
"id": 1,
|
||
"username": "admin",
|
||
"phone": "13800138000",
|
||
"userNo": "U001",
|
||
"userType": 2,
|
||
"status": 1,
|
||
"depts": [
|
||
{
|
||
"deptId": 1,
|
||
"deptCode": "TECH",
|
||
"deptName": "技术部",
|
||
"main": true
|
||
},
|
||
{
|
||
"deptId": 2,
|
||
"deptCode": "PROD",
|
||
"deptName": "产品部",
|
||
"main": false
|
||
}
|
||
],
|
||
"posts": [
|
||
{
|
||
"postId": 1,
|
||
"postCode": "JAVA_DEV",
|
||
"postName": "Java开发工程师"
|
||
}
|
||
],
|
||
"mainDeptId": 1,
|
||
"mainDeptName": "技术部",
|
||
"deptCode": "TECH",
|
||
"postCode": "JAVA_DEV"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 5.1.2 用户列表(增强版)
|
||
|
||
复用现有 `/user/admin/user` 列表接口,在响应中增加 `depts` 和 `posts` 字段。
|
||
|
||
**实现方式:**
|
||
- 列表查询时,先查询用户基础数据
|
||
- 批量查询所有用户的部门和岗位
|
||
- 组装到响应中
|
||
|
||
#### 5.1.3 统一用户认证查询(内部接口)
|
||
|
||
```
|
||
POST /user/inner/auth/load
|
||
```
|
||
|
||
**请求体:**
|
||
|
||
```json
|
||
{
|
||
"account": "13800138000",
|
||
"loginType": "PHONE"
|
||
}
|
||
```
|
||
|
||
**AccountType 枚举:**
|
||
|
||
```java
|
||
public enum AccountType {
|
||
USERNAME("用户名"),
|
||
PHONE("手机号"),
|
||
EMAIL("邮箱");
|
||
|
||
private final String description;
|
||
|
||
AccountType(String description) {
|
||
this.description = description;
|
||
}
|
||
}
|
||
```
|
||
|
||
**响应:** 与现有 `loadByUsername` 保持一致,返回用户认证信息
|
||
|
||
**实现逻辑:**
|
||
|
||
```java
|
||
@PostMapping("/auth/load")
|
||
public Result<JSONObject> loadUser(@RequestBody LoginAccountDTO loginAccount) {
|
||
User user;
|
||
|
||
switch (loginAccount.getLoginType()) {
|
||
case PHONE:
|
||
user = userService.lambdaQuery()
|
||
.eq(User::getPhone, loginAccount.getAccount())
|
||
.one();
|
||
break;
|
||
case EMAIL:
|
||
user = userService.lambdaQuery()
|
||
.eq(User::getEmail, loginAccount.getAccount())
|
||
.one();
|
||
break;
|
||
case USERNAME:
|
||
default:
|
||
user = userService.lambdaQuery()
|
||
.eq(User::getUsername, loginAccount.getAccount())
|
||
.one();
|
||
break;
|
||
}
|
||
|
||
if (user == null) {
|
||
return Result.ok(null);
|
||
}
|
||
|
||
// 组装认证信息(与现有逻辑一致)
|
||
JSONObject info = buildAuthInfo(user);
|
||
return Result.ok(info);
|
||
}
|
||
```
|
||
|
||
### 5.2 现有接口处理
|
||
|
||
**保留但标记为弃用:**
|
||
|
||
- `GET /user/inner/auth/loadByUsername/{username}` — **@Deprecated**,建议迁移到新的 `/auth/load`
|
||
|
||
**继续保留的接口:**
|
||
|
||
- `GET /user/admin/user/{id}` — 用户基础信息
|
||
- `GET /user/admin/user-dept/user/{userId}` — 用户部门列表
|
||
- `GET /user/admin/user-post/user/{userId}` — 用户岗位列表
|
||
- `GET /user/admin/detail` — 用户详情(uc_user_detail,phone 字段将移除)
|
||
|
||
---
|
||
|
||
## 6. 缓存设计
|
||
|
||
### 6.1 缓存策略
|
||
|
||
| 缓存项 | Key 格式 | TTL | 说明 |
|
||
|--------|---------|-----|------|
|
||
| 用户聚合信息 | `user:agg:{tenantId}:{userId}` | 10分钟 | 单用户完整数据 |
|
||
| 用户部门列表 | `user:dept:{tenantId}:{userId}` | 10分钟 | 部门ID列表 |
|
||
| 用户岗位列表 | `user:post:{tenantId}:{userId}` | 10分钟 | 岗位ID列表 |
|
||
|
||
### 6.2 缓存失效
|
||
|
||
**触发时机:**
|
||
- 用户部门变更(assignDepts、setMainDept)
|
||
- 用户岗位变更(assignPosts)
|
||
- 用户信息变更(update)
|
||
- 用户删除
|
||
|
||
**失效逻辑:**
|
||
|
||
```java
|
||
private void evictUserCache(Long userId) {
|
||
Long tenantId = AuthUtil.getTenantId();
|
||
redisUtil.del(String.format("user:agg:%s:%s", tenantId, userId));
|
||
redisUtil.del(String.format("user:dept:%s:%s", tenantId, userId));
|
||
redisUtil.del(String.format("user:post:%s:%s", tenantId, userId));
|
||
}
|
||
```
|
||
|
||
### 6.3 缓存穿透防护
|
||
|
||
- 查询不到数据时,缓存空值(TTL 1分钟)
|
||
- 使用布隆过滤器(可选,初期可不用)
|
||
|
||
---
|
||
|
||
## 7. 查询逻辑
|
||
|
||
### 7.1 单用户聚合查询
|
||
|
||
```java
|
||
@Cacheable(value = "user:agg", key = "#tenantId + ':' + #userId")
|
||
public UserAggregateVO getUserAggregate(Long userId, Long tenantId) {
|
||
// 1. 查询用户基础信息
|
||
User user = userMapper.selectById(userId);
|
||
if (user == null) {
|
||
return null;
|
||
}
|
||
|
||
// 2. 查询部门(带名称)
|
||
List<UserDeptVO> depts = userDeptMapper.selectDeptListByUserId(userId);
|
||
|
||
// 3. 查询岗位(带名称)
|
||
List<UserPostVO> posts = userPostMapper.selectPostListByUserId(userId);
|
||
|
||
// 4. 组装
|
||
UserAggregateVO vo = new UserAggregateVO();
|
||
BeanUtils.copyProperties(user, vo);
|
||
vo.setDepts(depts);
|
||
vo.setPosts(posts);
|
||
|
||
// 6. 提取主部门
|
||
depts.stream()
|
||
.filter(UserDeptVO::getMain)
|
||
.findFirst()
|
||
.ifPresent(main -> {
|
||
vo.setMainDeptId(main.getDeptId());
|
||
vo.setMainDeptName(main.getDeptName());
|
||
});
|
||
|
||
return vo;
|
||
}
|
||
```
|
||
|
||
### 7.2 批量列表查询优化
|
||
|
||
**问题:** 列表页有100条数据,不能每条都查一次数据库
|
||
|
||
**方案:** 批量 IN 查询
|
||
|
||
```java
|
||
public Page<UserAggregateVO> listUserAggregate(Page<User> page, QueryWrapper<User> query) {
|
||
// 1. 查询用户基础数据
|
||
Page<User> userPage = userMapper.selectPage(page, query);
|
||
List<User> users = userPage.getRecords();
|
||
|
||
if (users.isEmpty()) {
|
||
return new Page<>();
|
||
}
|
||
|
||
// 2. 提取所有用户ID
|
||
List<Long> userIds = users.stream()
|
||
.map(User::getId)
|
||
.collect(Collectors.toList());
|
||
|
||
// 3. 批量查询部门(一次查询)
|
||
Map<Long, List<UserDeptVO>> deptMap = userDeptMapper
|
||
.selectDeptListByUserIds(userIds)
|
||
.stream()
|
||
.collect(Collectors.groupingBy(UserDeptVO::getUserId));
|
||
|
||
// 4. 批量查询岗位(一次查询)
|
||
Map<Long, List<UserPostVO>> postMap = userPostMapper
|
||
.selectPostListByUserIds(userIds)
|
||
.stream()
|
||
.collect(Collectors.groupingBy(UserPostVO::getUserId));
|
||
|
||
// 5. 组装
|
||
List<UserAggregateVO> records = users.stream().map(user -> {
|
||
UserAggregateVO vo = new UserAggregateVO();
|
||
BeanUtils.copyProperties(user, vo);
|
||
vo.setDepts(deptMap.getOrDefault(user.getId(), Collections.emptyList()));
|
||
vo.setPosts(postMap.getOrDefault(user.getId(), Collections.emptyList()));
|
||
|
||
// 提取主部门
|
||
vo.getDepts().stream()
|
||
.filter(UserDeptVO::getMain)
|
||
.findFirst()
|
||
.ifPresent(main -> {
|
||
vo.setMainDeptId(main.getDeptId());
|
||
vo.setMainDeptName(main.getDeptName());
|
||
});
|
||
|
||
return vo;
|
||
}).collect(Collectors.toList());
|
||
|
||
// 7. 构建分页结果
|
||
Page<UserAggregateVO> result = new Page<>();
|
||
result.setCurrent(userPage.getCurrent());
|
||
result.setSize(userPage.getSize());
|
||
result.setTotal(userPage.getTotal());
|
||
result.setRecords(records);
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
**SQL 示例(批量查询部门):**
|
||
|
||
```xml
|
||
<select id="selectDeptListByUserIds" resultType="com.rui.service.user.vo.UserDeptVO">
|
||
SELECT
|
||
ud.user_id as userId,
|
||
ud.dept_id as deptId,
|
||
d.name as deptName,
|
||
ud.is_main as main
|
||
FROM uc_user_dept ud
|
||
INNER JOIN uc_dept d ON ud.dept_id = d.id
|
||
WHERE ud.user_id IN
|
||
<foreach collection="userIds" item="id" open="(" separator="," close=")">
|
||
#{id}
|
||
</foreach>
|
||
AND ud.deleted = 0
|
||
AND d.deleted = 0
|
||
</select>
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 性能优化点
|
||
|
||
### 8.1 数据库层面
|
||
|
||
1. **索引优化**:确保 `uc_user_dept.user_id` 和 `uc_user_post.user_id` 有索引
|
||
2. **批量查询**:使用 IN 代替循环查询
|
||
3. **按需加载**:列表页只加载必要的字段
|
||
|
||
### 8.2 缓存层面
|
||
|
||
1. **单用户缓存**:详情页使用缓存,10分钟TTL
|
||
2. **列表页不缓存**:列表数据变化频繁,直接查数据库
|
||
3. **缓存预热**:系统启动时可选择预热(可选)
|
||
|
||
### 8.3 代码层面
|
||
|
||
1. **并行查询**:单用户查询时,部门、岗位、详情可并行(CompletableFuture)
|
||
2. **懒加载**:如果前端不需要详情,可以不加载 `uc_user_detail`
|
||
|
||
---
|
||
|
||
## 9. 边界情况处理
|
||
|
||
### 9.1 用户无部门/岗位
|
||
|
||
- `depts` 返回空列表 `[]`
|
||
- `posts` 返回空列表 `[]`
|
||
- `mainDeptId` 和 `mainDeptName` 为 `null`
|
||
|
||
### 9.2 缓存穿透
|
||
|
||
- 用户不存在时,缓存空值(TTL 1分钟)
|
||
- 使用 `Optional` 包装返回
|
||
|
||
### 9.3 缓存雪崩
|
||
|
||
- TTL 加随机偏移:`10分钟 + random(0, 60)秒`
|
||
- 使用互斥锁(可选)
|
||
|
||
### 9.4 数据更新同步
|
||
|
||
- 所有更新操作后主动失效缓存
|
||
- 使用事务确保数据库和缓存一致性
|
||
|
||
---
|
||
|
||
## 10. 兼容性
|
||
|
||
### 10.1 向后兼容
|
||
|
||
- 现有接口完全保留
|
||
- 新增 `/aggregate` 接口,不影响旧接口
|
||
- 前端可以逐步迁移
|
||
|
||
### 10.2 前端迁移路径
|
||
|
||
1. **第一阶段**:新增聚合接口,前端详情页切换到新接口
|
||
2. **第二阶段**:列表页切换到批量查询
|
||
3. **第三阶段**:废弃旧接口(可选)
|
||
|
||
---
|
||
|
||
## 11. 安全考虑
|
||
|
||
1. **权限校验**:复用现有 `@AutoPermission("uc:user")`
|
||
2. **数据隔离**:所有查询自动加上 `tenant_id` 条件
|
||
3. **敏感信息**:密码字段不返回
|
||
|
||
---
|
||
|
||
## 12. 监控与日志
|
||
|
||
1. **缓存命中率**:监控 `user:agg:*` 的命中情况
|
||
2. **查询耗时**:记录批量查询的执行时间
|
||
3. **慢查询**:超过100ms的查询记录日志
|
||
|
||
---
|
||
|
||
## 13. 风险评估
|
||
|
||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||
|------|------|------|----------|
|
||
| 缓存数据不一致 | 中 | 中 | 更新时主动失效缓存 |
|
||
| 批量查询性能差 | 低 | 高 | 索引优化 + 分页 |
|
||
| 内存占用过高 | 低 | 中 | 控制缓存TTL + 分页大小 |
|
||
|
||
---
|
||
|
||
## 14. 附录
|
||
|
||
### 14.1 涉及文件清单
|
||
|
||
**新增文件:**
|
||
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserAggregateVO.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserDeptVO.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserPostVO.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/dto/LoginAccountDTO.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/enums/AccountType.java`
|
||
|
||
**修改文件:**
|
||
- `rui-service-user/src/main/java/com/rui/service/user/entity/User.java`(添加 phone 字段)
|
||
- `rui-service-user/src/main/java/com/rui/service/user/entity/UserDetail.java`(移除 phone 字段)
|
||
- `rui-service-user/src/main/java/com/rui/service/user/service/IUserService.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/service/impl/UserServiceImpl.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/controller/UserController.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/controller/inner/UserInnerController.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/mapper/UserDeptMapper.java`
|
||
- `rui-service-user/src/main/java/com/rui/service/user/mapper/UserPostMapper.java`
|
||
- `rui-service-user/src/main/resources/mapper/UserDeptMapper.xml`
|
||
- `rui-service-user/src/main/resources/mapper/UserPostMapper.xml`
|
||
- `rui-common/rui-common-oauth2/src/main/java/com/rui/common/oauth2/feign/UserAuthFeign.java`
|
||
|
||
**SQL 脚本:**
|
||
- `sql/upgrade-v2.x-add-phone-to-user.sql`(新增)
|
||
|
||
### 14.2 数据库变更
|
||
|
||
#### 14.2.1 uc_user 表添加 phone 字段
|
||
|
||
```sql
|
||
-- 添加 phone 字段
|
||
ALTER TABLE rui_uc_user
|
||
ADD COLUMN phone VARCHAR(20) DEFAULT NULL COMMENT '手机号' AFTER username;
|
||
|
||
-- 添加唯一索引(按租户)
|
||
ALTER TABLE rui_uc_user
|
||
ADD UNIQUE KEY uk_phone (tenant_id, phone);
|
||
|
||
-- 添加普通索引(用于查询)
|
||
ALTER TABLE rui_uc_user
|
||
ADD INDEX idx_phone (phone);
|
||
```
|
||
|
||
#### 14.2.2 uc_user_detail 表移除 phone 字段
|
||
|
||
```sql
|
||
-- 迁移数据(如果有)
|
||
-- UPDATE rui_uc_user u
|
||
-- JOIN rui_uc_user_detail d ON u.id = d.user_id
|
||
-- SET u.phone = d.phone
|
||
-- WHERE u.phone IS NULL AND d.phone IS NOT NULL;
|
||
|
||
-- 移除 phone 字段
|
||
ALTER TABLE rui_uc_user_detail
|
||
DROP COLUMN phone;
|
||
```
|
||
|
||
#### 14.2.3 索引检查
|
||
|
||
```sql
|
||
-- uc_user_dept 表
|
||
CREATE INDEX idx_user_dept_user_id ON uc_user_dept(user_id);
|
||
CREATE INDEX idx_user_dept_dept_id ON uc_user_dept(dept_id);
|
||
|
||
-- uc_user_post 表
|
||
CREATE INDEX idx_user_post_user_id ON uc_user_post(user_id);
|
||
CREATE INDEX idx_user_post_post_id ON uc_user_post(post_id);
|
||
```
|
||
|
||
---
|
||
|
||
## 15. 决策记录
|
||
|
||
| 决策 | 选择 | 理由 |
|
||
|------|------|------|
|
||
| 是否改表结构 | 是 | 手机号是登录凭证,应提升到 uc_user 表 |
|
||
| 手机号位置 | uc_user 表 | 便于认证查询,支持唯一约束 |
|
||
| 认证接口方式 | POST + JSON + 枚举 | 支持多种登录方式,便于扩展 |
|
||
| 列表页是否缓存 | 否 | 数据变化频繁,直接查库更可靠 |
|
||
| 单用户查询方式 | 并行查询 | 减少RT,提升用户体验 |
|
||
| 缓存失效策略 | 主动失效 | 数据更新时立即失效,保证一致性 |
|
||
| 是否返回密码 | 否 | 安全考虑,聚合数据不包含敏感字段 |
|