632 lines
16 KiB
Markdown
632 lines
16 KiB
Markdown
# 多身份功能 - 低风险渐进式实现方案
|
||
|
||
> 创建时间:2026-02-26
|
||
> 核心思想:不破坏现有逻辑,逐步扩展功能
|
||
|
||
---
|
||
|
||
## 🎯 设计思路
|
||
|
||
### 核心原则
|
||
1. **保留现有逻辑** - 不修改现有代码
|
||
2. **增量式开发** - 只添加新功能,不改旧功能
|
||
3. **向后兼容** - 新旧逻辑并存
|
||
4. **可回滚** - 每个阶段都可以独立回滚
|
||
|
||
### 关键设计
|
||
- 保留 `user.user_type` 作为**主身份**(默认身份)
|
||
- 新增 `user_roles` 表存储**附加身份**
|
||
- 现有代码继续使用 `user.user_type`,不受影响
|
||
- 新功能使用 `user_roles` 表
|
||
|
||
---
|
||
|
||
## 📅 分阶段实施方案
|
||
|
||
### 阶段1:数据库扩展(风险:⭐ 极低)
|
||
|
||
**工作内容:** 只添加新表,不修改现有表
|
||
|
||
**时间:** 2小时
|
||
|
||
**风险:** ⭐ 极低(只是添加新表,不影响现有功能)
|
||
|
||
#### 1.1 创建 user_roles 表
|
||
|
||
```sql
|
||
-- 用户角色关联表
|
||
CREATE TABLE IF NOT EXISTS `user_roles` (
|
||
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
|
||
`user_id` BIGINT NOT NULL COMMENT '用户ID',
|
||
`role_type` VARCHAR(50) NOT NULL COMMENT '角色类型:teacher/manager/distributor/provider/parent',
|
||
`is_primary` TINYINT DEFAULT 0 COMMENT '是否主身份:0=否,1=是',
|
||
`status` TINYINT DEFAULT 1 COMMENT '状态:0=禁用,1=启用',
|
||
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||
INDEX `idx_user_id` (`user_id`),
|
||
INDEX `idx_role_type` (`role_type`),
|
||
UNIQUE KEY `uk_user_role` (`user_id`, `role_type`)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
|
||
|
||
-- 初始化数据:将现有用户的主身份同步到 user_roles 表
|
||
INSERT INTO `user_roles` (`user_id`, `role_type`, `is_primary`, `status`)
|
||
SELECT
|
||
`id` as user_id,
|
||
`user_type` as role_type,
|
||
1 as is_primary,
|
||
1 as status
|
||
FROM `user`
|
||
WHERE `user_type` IS NOT NULL
|
||
ON DUPLICATE KEY UPDATE `is_primary` = 1;
|
||
```
|
||
|
||
**验证:**
|
||
```sql
|
||
-- 检查数据是否同步成功
|
||
SELECT
|
||
u.id,
|
||
u.phone,
|
||
u.user_type as primary_role,
|
||
GROUP_CONCAT(ur.role_type) as all_roles
|
||
FROM user u
|
||
LEFT JOIN user_roles ur ON u.id = ur.user_id
|
||
GROUP BY u.id
|
||
LIMIT 10;
|
||
```
|
||
|
||
**影响:** ✅ 无影响,只是添加新表
|
||
|
||
---
|
||
|
||
### 阶段2:后端支持(风险:⭐⭐ 低)
|
||
|
||
**工作内容:** 添加新接口,不修改现有接口
|
||
|
||
**时间:** 1天
|
||
|
||
**风险:** ⭐⭐ 低(新增接口,不影响现有功能)
|
||
|
||
#### 2.1 创建实体类
|
||
|
||
```java
|
||
// UserRole.java - 新建文件
|
||
@Data
|
||
@TableName("user_roles")
|
||
public class UserRole {
|
||
@TableId(type = IdType.AUTO)
|
||
private Long id;
|
||
|
||
private Long userId;
|
||
|
||
private String roleType;
|
||
|
||
private Integer isPrimary;
|
||
|
||
private Integer status;
|
||
|
||
@TableField(fill = FieldFill.INSERT)
|
||
private LocalDateTime createTime;
|
||
|
||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||
private LocalDateTime updateTime;
|
||
}
|
||
```
|
||
|
||
#### 2.2 创建 Mapper
|
||
|
||
```java
|
||
// UserRoleMapper.java - 新建文件
|
||
@Mapper
|
||
public interface UserRoleMapper extends BaseMapper<UserRole> {
|
||
|
||
/**
|
||
* 获取用户的所有角色
|
||
*/
|
||
@Select("SELECT * FROM user_roles WHERE user_id = #{userId} AND status = 1")
|
||
List<UserRole> selectByUserId(Long userId);
|
||
|
||
/**
|
||
* 获取用户的主身份
|
||
*/
|
||
@Select("SELECT * FROM user_roles WHERE user_id = #{userId} AND is_primary = 1 AND status = 1")
|
||
UserRole selectPrimaryRole(Long userId);
|
||
}
|
||
```
|
||
|
||
#### 2.3 创建 Service
|
||
|
||
```java
|
||
// UserRoleService.java - 新建文件
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class UserRoleService {
|
||
|
||
private final UserRoleMapper userRoleMapper;
|
||
|
||
/**
|
||
* 获取用户的所有角色
|
||
*/
|
||
public List<String> getUserRoles(Long userId) {
|
||
List<UserRole> roles = userRoleMapper.selectByUserId(userId);
|
||
return roles.stream()
|
||
.map(UserRole::getRoleType)
|
||
.collect(Collectors.toList());
|
||
}
|
||
|
||
/**
|
||
* 添加角色(不影响主身份)
|
||
*/
|
||
@Transactional
|
||
public boolean addRole(Long userId, String roleType) {
|
||
// 检查是否已存在
|
||
LambdaQueryWrapper<UserRole> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(UserRole::getUserId, userId)
|
||
.eq(UserRole::getRoleType, roleType);
|
||
|
||
if (userRoleMapper.selectCount(wrapper) > 0) {
|
||
throw new BusinessException("该身份已存在");
|
||
}
|
||
|
||
// 添加新角色
|
||
UserRole userRole = new UserRole();
|
||
userRole.setUserId(userId);
|
||
userRole.setRoleType(roleType);
|
||
userRole.setIsPrimary(0); // 附加身份
|
||
userRole.setStatus(1);
|
||
|
||
return userRoleMapper.insert(userRole) > 0;
|
||
}
|
||
|
||
/**
|
||
* 切换主身份
|
||
*/
|
||
@Transactional
|
||
public boolean switchPrimaryRole(Long userId, String roleType) {
|
||
// 检查该角色是否存在
|
||
LambdaQueryWrapper<UserRole> wrapper = new LambdaQueryWrapper<>();
|
||
wrapper.eq(UserRole::getUserId, userId)
|
||
.eq(UserRole::getRoleType, roleType)
|
||
.eq(UserRole::getStatus, 1);
|
||
|
||
UserRole targetRole = userRoleMapper.selectOne(wrapper);
|
||
if (targetRole == null) {
|
||
throw new BusinessException("该身份不存在");
|
||
}
|
||
|
||
// 取消所有主身份标记
|
||
userRoleMapper.update(null,
|
||
new LambdaUpdateWrapper<UserRole>()
|
||
.eq(UserRole::getUserId, userId)
|
||
.set(UserRole::getIsPrimary, 0)
|
||
);
|
||
|
||
// 设置新的主身份
|
||
targetRole.setIsPrimary(1);
|
||
userRoleMapper.updateById(targetRole);
|
||
|
||
// 同步更新 user 表的 user_type(保持兼容)
|
||
User user = new User();
|
||
user.setId(userId);
|
||
user.setRole(roleType);
|
||
userMapper.updateById(user);
|
||
|
||
return true;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2.4 创建 Controller
|
||
|
||
```java
|
||
// UserRoleController.java - 新建文件
|
||
@RestController
|
||
@RequestMapping("/api/user/roles")
|
||
@RequiredArgsConstructor
|
||
public class UserRoleController {
|
||
|
||
private final UserRoleService userRoleService;
|
||
|
||
/**
|
||
* 获取当前用户的所有身份
|
||
*/
|
||
@GetMapping("/list")
|
||
public Result<List<String>> getUserRoles() {
|
||
Long userId = SecurityUtils.getCurrentUserId();
|
||
List<String> roles = userRoleService.getUserRoles(userId);
|
||
return Result.success(roles);
|
||
}
|
||
|
||
/**
|
||
* 申请新身份
|
||
*/
|
||
@PostMapping("/apply")
|
||
public Result<Void> applyRole(@RequestParam String roleType) {
|
||
Long userId = SecurityUtils.getCurrentUserId();
|
||
userRoleService.addRole(userId, roleType);
|
||
return Result.success();
|
||
}
|
||
|
||
/**
|
||
* 切换主身份
|
||
*/
|
||
@PostMapping("/switch")
|
||
public Result<Void> switchRole(@RequestParam String roleType) {
|
||
Long userId = SecurityUtils.getCurrentUserId();
|
||
userRoleService.switchPrimaryRole(userId, roleType);
|
||
return Result.success();
|
||
}
|
||
}
|
||
```
|
||
|
||
**影响:** ✅ 只添加新接口,现有功能不受影响
|
||
|
||
---
|
||
|
||
### 阶段3:前端集成(风险:⭐⭐ 低)
|
||
|
||
**工作内容:** 添加新功能,不修改现有逻辑
|
||
|
||
**时间:** 1天
|
||
|
||
**风险:** ⭐⭐ 低(新增功能,不影响现有页面)
|
||
|
||
#### 3.1 修改 store/user.js
|
||
|
||
```javascript
|
||
// 只添加新方法,不修改现有方法
|
||
export const useUserStore = defineStore('user', {
|
||
state: () => ({
|
||
token: uni.getStorageSync('token') || '',
|
||
userInfo: uni.getStorageSync('userInfo') || null,
|
||
isLogin: false,
|
||
currentRole: uni.getStorageSync('currentRole') || 'user',
|
||
// ✅ 新增:所有身份列表
|
||
allRoles: uni.getStorageSync('allRoles') || []
|
||
}),
|
||
|
||
getters: {
|
||
// 现有 getters 保持不变...
|
||
|
||
// ✅ 新增:是否有多个身份
|
||
hasMultipleRoles: (state) => state.allRoles.length > 1,
|
||
|
||
// ✅ 新增:可切换的身份列表
|
||
availableRoles: (state) => {
|
||
return state.allRoles.map(role => ({
|
||
value: role,
|
||
label: getRoleName(role),
|
||
icon: getRoleIcon(role)
|
||
}))
|
||
}
|
||
},
|
||
|
||
actions: {
|
||
// 现有 actions 保持不变...
|
||
|
||
// ✅ 新增:加载所有身份
|
||
async loadAllRoles() {
|
||
try {
|
||
const res = await uni.request({
|
||
url: '/api/user/roles/list',
|
||
method: 'GET',
|
||
header: { Authorization: this.token }
|
||
})
|
||
|
||
if (res.data.code === 200) {
|
||
this.allRoles = res.data.data
|
||
uni.setStorageSync('allRoles', res.data.data)
|
||
}
|
||
} catch (error) {
|
||
console.error('加载身份列表失败:', error)
|
||
}
|
||
},
|
||
|
||
// ✅ 新增:切换身份(调用后端API)
|
||
async switchRoleWithApi(roleType) {
|
||
try {
|
||
const res = await uni.request({
|
||
url: '/api/user/roles/switch',
|
||
method: 'POST',
|
||
data: { roleType },
|
||
header: { Authorization: this.token }
|
||
})
|
||
|
||
if (res.data.code === 200) {
|
||
this.currentRole = roleType
|
||
uni.setStorageSync('currentRole', roleType)
|
||
|
||
// 刷新页面
|
||
uni.reLaunch({ url: '/pages/index/index' })
|
||
|
||
uni.showToast({
|
||
title: '切换成功',
|
||
icon: 'success'
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: res.data.message || '切换失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('切换身份失败:', error)
|
||
uni.showToast({
|
||
title: '切换失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
},
|
||
|
||
// ✅ 新增:申请新身份
|
||
async applyNewRole(roleType) {
|
||
try {
|
||
const res = await uni.request({
|
||
url: '/api/user/roles/apply',
|
||
method: 'POST',
|
||
data: { roleType },
|
||
header: { Authorization: this.token }
|
||
})
|
||
|
||
if (res.data.code === 200) {
|
||
// 重新加载身份列表
|
||
await this.loadAllRoles()
|
||
|
||
uni.showToast({
|
||
title: '申请成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
return true
|
||
} else {
|
||
uni.showToast({
|
||
title: res.data.message || '申请失败',
|
||
icon: 'none'
|
||
})
|
||
return false
|
||
}
|
||
} catch (error) {
|
||
console.error('申请身份失败:', error)
|
||
uni.showToast({
|
||
title: '申请失败',
|
||
icon: 'none'
|
||
})
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
// 辅助函数
|
||
function getRoleName(role) {
|
||
const roleMap = {
|
||
user: '家长',
|
||
parent: '家长',
|
||
teacher: '陪伴员',
|
||
manager: '管理师',
|
||
distributor: '分销员',
|
||
serviceProvider: '服务商'
|
||
}
|
||
return roleMap[role] || '未知'
|
||
}
|
||
|
||
function getRoleIcon(role) {
|
||
const iconMap = {
|
||
user: '👨👩👧',
|
||
parent: '👨👩👧',
|
||
teacher: '👨🏫',
|
||
manager: '👔',
|
||
distributor: '💼',
|
||
serviceProvider: '🏢'
|
||
}
|
||
return iconMap[role] || '❓'
|
||
}
|
||
```
|
||
|
||
#### 3.2 修改登录逻辑
|
||
|
||
```javascript
|
||
// pages/login/index.vue
|
||
async handleLogin() {
|
||
try {
|
||
// 现有登录逻辑...
|
||
const res = await loginApi.login(this.form)
|
||
|
||
if (res.code === 200) {
|
||
const userStore = useUserStore()
|
||
userStore.setToken(res.data.token)
|
||
userStore.setUserInfo(res.data.userInfo)
|
||
userStore.setRole(res.data.userInfo.role)
|
||
|
||
// ✅ 新增:加载所有身份
|
||
await userStore.loadAllRoles()
|
||
|
||
// 跳转首页...
|
||
}
|
||
} catch (error) {
|
||
// 错误处理...
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.3 优化身份切换组件
|
||
|
||
```vue
|
||
<!-- pages/user/components/RoleSelector.vue - 新建组件 -->
|
||
<template>
|
||
<view class="role-selector">
|
||
<view class="current-role">
|
||
<text class="label">当前身份:</text>
|
||
<text class="value">{{ currentRoleName }}</text>
|
||
</view>
|
||
|
||
<!-- 多身份时显示切换选项 -->
|
||
<view v-if="hasMultipleRoles" class="role-list">
|
||
<view class="section-title">切换身份</view>
|
||
<view
|
||
v-for="role in availableRoles"
|
||
:key="role.value"
|
||
class="role-item"
|
||
:class="{ active: role.value === currentRole }"
|
||
@click="switchRole(role.value)"
|
||
>
|
||
<text class="role-icon">{{ role.icon }}</text>
|
||
<text class="role-name">{{ role.label }}</text>
|
||
<text v-if="role.value === currentRole" class="check">✓</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 申请新身份 -->
|
||
<view class="apply-section">
|
||
<view class="section-title">申请新身份</view>
|
||
<button class="apply-btn" @click="showApplyModal">+ 申请其他身份</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { useUserStore } from '@/store/user'
|
||
|
||
export default {
|
||
setup() {
|
||
const userStore = useUserStore()
|
||
|
||
return {
|
||
currentRole: computed(() => userStore.currentRole),
|
||
currentRoleName: computed(() => userStore.roleName),
|
||
hasMultipleRoles: computed(() => userStore.hasMultipleRoles),
|
||
availableRoles: computed(() => userStore.availableRoles),
|
||
|
||
async switchRole(roleType) {
|
||
if (roleType === userStore.currentRole) return
|
||
|
||
uni.showModal({
|
||
title: '确认切换',
|
||
content: `确定要切换到${getRoleName(roleType)}身份吗?`,
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
await userStore.switchRoleWithApi(roleType)
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
showApplyModal() {
|
||
uni.navigateTo({
|
||
url: '/pages/user/apply-role'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
**影响:** ✅ 只添加新组件和功能,现有页面不受影响
|
||
|
||
---
|
||
|
||
## 🎯 实施步骤
|
||
|
||
### 第1天:数据库 + 后端
|
||
1. ✅ 执行数据库脚本(30分钟)
|
||
2. ✅ 创建实体类和Mapper(1小时)
|
||
3. ✅ 创建Service和Controller(2小时)
|
||
4. ✅ 测试后端接口(1小时)
|
||
5. ✅ 验证数据一致性(30分钟)
|
||
|
||
### 第2天:前端集成
|
||
1. ✅ 修改 store/user.js(1小时)
|
||
2. ✅ 修改登录逻辑(30分钟)
|
||
3. ✅ 创建身份切换组件(2小时)
|
||
4. ✅ 创建申请身份页面(1小时)
|
||
5. ✅ 联调测试(1.5小时)
|
||
|
||
### 第3天:测试上线
|
||
1. ✅ 完整功能测试(2小时)
|
||
2. ✅ 边界情况测试(1小时)
|
||
3. ✅ 性能测试(1小时)
|
||
4. ✅ 上线部署(1小时)
|
||
|
||
**总工作量:3天**
|
||
|
||
---
|
||
|
||
## ✅ 优势分析
|
||
|
||
### 1. 风险极低 ⭐
|
||
- 不修改现有代码,只添加新功能
|
||
- 现有逻辑继续使用 `user.user_type`
|
||
- 新功能使用 `user_roles` 表
|
||
- 两套逻辑并存,互不影响
|
||
|
||
### 2. 可回滚 🔄
|
||
- 每个阶段都可以独立回滚
|
||
- 删除 `user_roles` 表即可完全回滚
|
||
- 不影响现有数据
|
||
|
||
### 3. 向后兼容 ✅
|
||
- 保留 `user.user_type` 字段
|
||
- 切换主身份时同步更新 `user.user_type`
|
||
- 现有代码无需修改
|
||
|
||
### 4. 性能优化 ⚡
|
||
- 使用索引优化查询
|
||
- 可以添加缓存(Redis)
|
||
- 不影响现有接口性能
|
||
|
||
### 5. 易于扩展 🚀
|
||
- 未来可以添加更多身份
|
||
- 可以添加身份审核流程
|
||
- 可以添加身份权限管理
|
||
|
||
---
|
||
|
||
## 🧪 测试清单
|
||
|
||
### 功能测试
|
||
- [ ] 用户登录后能看到所有身份
|
||
- [ ] 切换身份功能正常
|
||
- [ ] 申请新身份功能正常
|
||
- [ ] 主身份标记正确
|
||
- [ ] user.user_type 同步更新
|
||
|
||
### 兼容性测试
|
||
- [ ] 现有功能不受影响
|
||
- [ ] 单身份用户正常使用
|
||
- [ ] 多身份用户正常使用
|
||
- [ ] 角色权限验证正常
|
||
|
||
### 性能测试
|
||
- [ ] 查询性能正常
|
||
- [ ] 切换身份响应快
|
||
- [ ] 不影响现有接口性能
|
||
|
||
### 边界测试
|
||
- [ ] 重复申请身份的处理
|
||
- [ ] 切换到不存在的身份
|
||
- [ ] 并发切换身份
|
||
- [ ] 数据一致性验证
|
||
|
||
---
|
||
|
||
## 📊 风险对比
|
||
|
||
| 方案 | 风险 | 工作量 | 影响范围 |
|
||
|------|------|--------|----------|
|
||
| 完全重构 | 🔴🔴🔴🔴 高 | 7-10天 | 全部代码 |
|
||
| **渐进式方案** | ⭐ 极低 | 3天 | 只添加新功能 |
|
||
|
||
---
|
||
|
||
## 🎯 总结
|
||
|
||
这个方案的核心优势:
|
||
|
||
1. **不破坏现有逻辑** - 保留 `user.user_type`,现有代码不受影响
|
||
2. **增量式开发** - 只添加新表和新接口
|
||
3. **风险可控** - 每个阶段都可以独立测试和回滚
|
||
4. **快速上线** - 3天即可完成
|
||
5. **易于维护** - 代码清晰,逻辑简单
|
||
|
||
**这是最安全、最快速的实现方案!** ✅
|