docs(spec): 更新用户聚合查询设计规格
- 添加 phone 字段迁移到 uc_user 表的设计 - 新增统一认证接口 /user/inner/auth/load(POST) - 支持 AccountType 枚举:USERNAME/PHONE/EMAIL - 废弃旧的 loadByUsername 接口 - 添加数据库变更 SQL 脚本
This commit is contained in:
@@ -25,6 +25,8 @@
|
|||||||
- **前端请求过多**:获取完整用户信息需要3个独立请求
|
- **前端请求过多**:获取完整用户信息需要3个独立请求
|
||||||
- **列表页性能差**:100条用户数据需要 1 + 100 + 100 = 201 个请求
|
- **列表页性能差**:100条用户数据需要 1 + 100 + 100 = 201 个请求
|
||||||
- **数据一致性难保证**:多个请求可能部分失败
|
- **数据一致性难保证**:多个请求可能部分失败
|
||||||
|
- **手机号位置不合理**:手机号在 `uc_user_detail` 表,但短信登录需要频繁查询,应该提升到 `uc_user` 表
|
||||||
|
- **认证接口不灵活**:当前 `loadByUsername` 只支持用户名,无法扩展支持手机号、邮箱等多种登录方式
|
||||||
|
|
||||||
### 1.3 约束条件
|
### 1.3 约束条件
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
- 需要支持 **多租户**(tenantId)
|
- 需要支持 **多租户**(tenantId)
|
||||||
- 必须 **保持向后兼容**
|
- 必须 **保持向后兼容**
|
||||||
- 需要 **缓存优化**
|
- 需要 **缓存优化**
|
||||||
|
- 手机号需要 **唯一约束**(按租户)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -40,7 +43,9 @@
|
|||||||
1. **减少前端请求**:从3个减少到1个
|
1. **减少前端请求**:从3个减少到1个
|
||||||
2. **优化列表页性能**:批量查询,避免 N+1
|
2. **优化列表页性能**:批量查询,避免 N+1
|
||||||
3. **引入缓存**:Redis 缓存用户聚合数据
|
3. **引入缓存**:Redis 缓存用户聚合数据
|
||||||
4. **保持兼容性**:现有接口不受影响
|
4. **手机号迁移**:将 `phone` 从 `uc_user_detail` 迁移到 `uc_user` 表
|
||||||
|
5. **统一认证接口**:支持多种登录方式(用户名、手机号、邮箱等)
|
||||||
|
6. **保持兼容性**:现有接口不受影响
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -95,9 +100,6 @@ import java.util.List;
|
|||||||
@Schema(description = "用户聚合信息")
|
@Schema(description = "用户聚合信息")
|
||||||
public class UserAggregateVO extends User {
|
public class UserAggregateVO extends User {
|
||||||
|
|
||||||
@Schema(description = "用户详情")
|
|
||||||
private UserDetailVO detail;
|
|
||||||
|
|
||||||
@Schema(description = "部门列表")
|
@Schema(description = "部门列表")
|
||||||
private List<UserDeptVO> depts;
|
private List<UserDeptVO> depts;
|
||||||
|
|
||||||
@@ -109,6 +111,12 @@ public class UserAggregateVO extends User {
|
|||||||
|
|
||||||
@Schema(description = "主部门名称")
|
@Schema(description = "主部门名称")
|
||||||
private String mainDeptName;
|
private String mainDeptName;
|
||||||
|
|
||||||
|
@Schema(description = "部门编码")
|
||||||
|
private String deptCode;
|
||||||
|
|
||||||
|
@Schema(description = "岗位编码")
|
||||||
|
private String postCode;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -118,9 +126,15 @@ public class UserAggregateVO extends User {
|
|||||||
@Data
|
@Data
|
||||||
@Schema(description = "用户部门信息")
|
@Schema(description = "用户部门信息")
|
||||||
public class UserDeptVO {
|
public class UserDeptVO {
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
@Schema(description = "部门ID")
|
@Schema(description = "部门ID")
|
||||||
private Long deptId;
|
private Long deptId;
|
||||||
|
|
||||||
|
@Schema(description = "部门编码")
|
||||||
|
private String deptCode;
|
||||||
|
|
||||||
@Schema(description = "部门名称")
|
@Schema(description = "部门名称")
|
||||||
private String deptName;
|
private String deptName;
|
||||||
|
|
||||||
@@ -131,9 +145,15 @@ public class UserDeptVO {
|
|||||||
@Data
|
@Data
|
||||||
@Schema(description = "用户岗位信息")
|
@Schema(description = "用户岗位信息")
|
||||||
public class UserPostVO {
|
public class UserPostVO {
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
@Schema(description = "岗位ID")
|
@Schema(description = "岗位ID")
|
||||||
private Long postId;
|
private Long postId;
|
||||||
|
|
||||||
|
@Schema(description = "岗位编码")
|
||||||
|
private String postCode;
|
||||||
|
|
||||||
@Schema(description = "岗位名称")
|
@Schema(description = "岗位名称")
|
||||||
private String postName;
|
private String postName;
|
||||||
}
|
}
|
||||||
@@ -160,23 +180,20 @@ GET /user/admin/user/{id}/aggregate
|
|||||||
"data": {
|
"data": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"username": "admin",
|
"username": "admin",
|
||||||
|
"phone": "13800138000",
|
||||||
"userNo": "U001",
|
"userNo": "U001",
|
||||||
"userType": 2,
|
"userType": 2,
|
||||||
"status": 1,
|
"status": 1,
|
||||||
"detail": {
|
|
||||||
"nickname": "管理员",
|
|
||||||
"realName": "系统管理员",
|
|
||||||
"email": "admin@example.com",
|
|
||||||
"phone": "13800138000"
|
|
||||||
},
|
|
||||||
"depts": [
|
"depts": [
|
||||||
{
|
{
|
||||||
"deptId": 1,
|
"deptId": 1,
|
||||||
|
"deptCode": "TECH",
|
||||||
"deptName": "技术部",
|
"deptName": "技术部",
|
||||||
"main": true
|
"main": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"deptId": 2,
|
"deptId": 2,
|
||||||
|
"deptCode": "PROD",
|
||||||
"deptName": "产品部",
|
"deptName": "产品部",
|
||||||
"main": false
|
"main": false
|
||||||
}
|
}
|
||||||
@@ -184,11 +201,14 @@ GET /user/admin/user/{id}/aggregate
|
|||||||
"posts": [
|
"posts": [
|
||||||
{
|
{
|
||||||
"postId": 1,
|
"postId": 1,
|
||||||
|
"postCode": "JAVA_DEV",
|
||||||
"postName": "Java开发工程师"
|
"postName": "Java开发工程师"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mainDeptId": 1,
|
"mainDeptId": 1,
|
||||||
"mainDeptName": "技术部"
|
"mainDeptName": "技术部",
|
||||||
|
"deptCode": "TECH",
|
||||||
|
"postCode": "JAVA_DEV"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -202,14 +222,87 @@ GET /user/admin/user/{id}/aggregate
|
|||||||
- 批量查询所有用户的部门和岗位
|
- 批量查询所有用户的部门和岗位
|
||||||
- 组装到响应中
|
- 组装到响应中
|
||||||
|
|
||||||
### 5.2 现有接口保持不变
|
#### 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/{id}` — 用户基础信息
|
||||||
- `GET /user/admin/user-dept/user/{userId}` — 用户部门列表
|
- `GET /user/admin/user-dept/user/{userId}` — 用户部门列表
|
||||||
- `GET /user/admin/user-post/user/{userId}` — 用户岗位列表
|
- `GET /user/admin/user-post/user/{userId}` — 用户岗位列表
|
||||||
- `GET /user/admin/detail` — 用户详情(uc_user_detail)
|
- `GET /user/admin/detail` — 用户详情(uc_user_detail,phone 字段将移除)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -262,22 +355,15 @@ public UserAggregateVO getUserAggregate(Long userId, Long tenantId) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 查询用户详情
|
// 2. 查询部门(带名称)
|
||||||
UserDetail detail = userDetailMapper.selectOne(
|
|
||||||
new LambdaQueryWrapper<UserDetail>()
|
|
||||||
.eq(UserDetail::getUserId, userId)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. 查询部门(带名称)
|
|
||||||
List<UserDeptVO> depts = userDeptMapper.selectDeptListByUserId(userId);
|
List<UserDeptVO> depts = userDeptMapper.selectDeptListByUserId(userId);
|
||||||
|
|
||||||
// 4. 查询岗位(带名称)
|
// 3. 查询岗位(带名称)
|
||||||
List<UserPostVO> posts = userPostMapper.selectPostListByUserId(userId);
|
List<UserPostVO> posts = userPostMapper.selectPostListByUserId(userId);
|
||||||
|
|
||||||
// 5. 组装
|
// 4. 组装
|
||||||
UserAggregateVO vo = new UserAggregateVO();
|
UserAggregateVO vo = new UserAggregateVO();
|
||||||
BeanUtils.copyProperties(user, vo);
|
BeanUtils.copyProperties(user, vo);
|
||||||
vo.setDetail(convertDetail(detail));
|
|
||||||
vo.setDepts(depts);
|
vo.setDepts(depts);
|
||||||
vo.setPosts(posts);
|
vo.setPosts(posts);
|
||||||
|
|
||||||
@@ -327,22 +413,12 @@ public Page<UserAggregateVO> listUserAggregate(Page<User> page, QueryWrapper<Use
|
|||||||
.stream()
|
.stream()
|
||||||
.collect(Collectors.groupingBy(UserPostVO::getUserId));
|
.collect(Collectors.groupingBy(UserPostVO::getUserId));
|
||||||
|
|
||||||
// 5. 批量查询详情(一次查询)
|
// 5. 组装
|
||||||
Map<Long, UserDetail> detailMap = userDetailMapper
|
|
||||||
.selectList(
|
|
||||||
new LambdaQueryWrapper<UserDetail>()
|
|
||||||
.in(UserDetail::getUserId, userIds)
|
|
||||||
)
|
|
||||||
.stream()
|
|
||||||
.collect(Collectors.toMap(UserDetail::getUserId, Function.identity()));
|
|
||||||
|
|
||||||
// 6. 组装
|
|
||||||
List<UserAggregateVO> records = users.stream().map(user -> {
|
List<UserAggregateVO> records = users.stream().map(user -> {
|
||||||
UserAggregateVO vo = new UserAggregateVO();
|
UserAggregateVO vo = new UserAggregateVO();
|
||||||
BeanUtils.copyProperties(user, vo);
|
BeanUtils.copyProperties(user, vo);
|
||||||
vo.setDepts(deptMap.getOrDefault(user.getId(), Collections.emptyList()));
|
vo.setDepts(deptMap.getOrDefault(user.getId(), Collections.emptyList()));
|
||||||
vo.setPosts(postMap.getOrDefault(user.getId(), Collections.emptyList()));
|
vo.setPosts(postMap.getOrDefault(user.getId(), Collections.emptyList()));
|
||||||
vo.setDetail(convertDetail(detailMap.get(user.getId())));
|
|
||||||
|
|
||||||
// 提取主部门
|
// 提取主部门
|
||||||
vo.getDepts().stream()
|
vo.getDepts().stream()
|
||||||
@@ -481,20 +557,62 @@ public Page<UserAggregateVO> listUserAggregate(Page<User> page, QueryWrapper<Use
|
|||||||
|
|
||||||
### 14.1 涉及文件清单
|
### 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/UserAggregateVO.java`
|
||||||
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserPostVO.java`(新增)
|
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserDeptVO.java`
|
||||||
- `rui-service-user/src/main/java/com/rui/service/user/service/IUserService.java`(修改)
|
- `rui-service-user/src/main/java/com/rui/service/user/vo/UserPostVO.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/dto/LoginAccountDTO.java`
|
||||||
- `rui-service-user/src/main/java/com/rui/service/user/controller/UserController.java`(修改)
|
- `rui-service-user/src/main/java/com/rui/service/user/enums/AccountType.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`(修改)
|
|
||||||
|
|
||||||
### 14.2 数据库索引检查
|
**修改文件:**
|
||||||
|
- `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
|
```sql
|
||||||
-- uc_user_dept 表
|
-- uc_user_dept 表
|
||||||
@@ -504,9 +622,6 @@ CREATE INDEX idx_user_dept_dept_id ON uc_user_dept(dept_id);
|
|||||||
-- uc_user_post 表
|
-- uc_user_post 表
|
||||||
CREATE INDEX idx_user_post_user_id ON uc_user_post(user_id);
|
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);
|
CREATE INDEX idx_user_post_post_id ON uc_user_post(post_id);
|
||||||
|
|
||||||
-- uc_user_detail 表
|
|
||||||
CREATE INDEX idx_user_detail_user_id ON uc_user_detail(user_id);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -515,7 +630,9 @@ CREATE INDEX idx_user_detail_user_id ON uc_user_detail(user_id);
|
|||||||
|
|
||||||
| 决策 | 选择 | 理由 |
|
| 决策 | 选择 | 理由 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 是否改表结构 | 否 | 关联表设计合理,保持灵活性 |
|
| 是否改表结构 | 是 | 手机号是登录凭证,应提升到 uc_user 表 |
|
||||||
|
| 手机号位置 | uc_user 表 | 便于认证查询,支持唯一约束 |
|
||||||
|
| 认证接口方式 | POST + JSON + 枚举 | 支持多种登录方式,便于扩展 |
|
||||||
| 列表页是否缓存 | 否 | 数据变化频繁,直接查库更可靠 |
|
| 列表页是否缓存 | 否 | 数据变化频繁,直接查库更可靠 |
|
||||||
| 单用户查询方式 | 并行查询 | 减少RT,提升用户体验 |
|
| 单用户查询方式 | 并行查询 | 减少RT,提升用户体验 |
|
||||||
| 缓存失效策略 | 主动失效 | 数据更新时立即失效,保证一致性 |
|
| 缓存失效策略 | 主动失效 | 数据更新时立即失效,保证一致性 |
|
||||||
|
|||||||
Reference in New Issue
Block a user