peixue-dev/Archive/[一次性]多身份功能-低风险实现方案-2026-02-26.md

632 lines
16 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.

# 多身份功能 - 低风险渐进式实现方案
> 创建时间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. ✅ 创建实体类和Mapper1小时
3. ✅ 创建Service和Controller2小时
4. ✅ 测试后端接口1小时
5. ✅ 验证数据一致性30分钟
### 第2天前端集成
1. ✅ 修改 store/user.js1小时
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. **易于维护** - 代码清晰,逻辑简单
**这是最安全、最快速的实现方案!**