16 KiB
16 KiB
多身份功能 - 低风险渐进式实现方案
创建时间:2026-02-26
核心思想:不破坏现有逻辑,逐步扩展功能
🎯 设计思路
核心原则
- 保留现有逻辑 - 不修改现有代码
- 增量式开发 - 只添加新功能,不改旧功能
- 向后兼容 - 新旧逻辑并存
- 可回滚 - 每个阶段都可以独立回滚
关键设计
- 保留
user.user_type作为主身份(默认身份) - 新增
user_roles表存储附加身份 - 现有代码继续使用
user.user_type,不受影响 - 新功能使用
user_roles表
📅 分阶段实施方案
阶段1:数据库扩展(风险:⭐ 极低)
工作内容: 只添加新表,不修改现有表
时间: 2小时
风险: ⭐ 极低(只是添加新表,不影响现有功能)
1.1 创建 user_roles 表
-- 用户角色关联表
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;
验证:
-- 检查数据是否同步成功
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 创建实体类
// 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
// 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
// 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
// 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
// 只添加新方法,不修改现有方法
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 修改登录逻辑
// 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 优化身份切换组件
<!-- 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天:数据库 + 后端
- ✅ 执行数据库脚本(30分钟)
- ✅ 创建实体类和Mapper(1小时)
- ✅ 创建Service和Controller(2小时)
- ✅ 测试后端接口(1小时)
- ✅ 验证数据一致性(30分钟)
第2天:前端集成
- ✅ 修改 store/user.js(1小时)
- ✅ 修改登录逻辑(30分钟)
- ✅ 创建身份切换组件(2小时)
- ✅ 创建申请身份页面(1小时)
- ✅ 联调测试(1.5小时)
第3天:测试上线
- ✅ 完整功能测试(2小时)
- ✅ 边界情况测试(1小时)
- ✅ 性能测试(1小时)
- ✅ 上线部署(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天 | 只添加新功能 |
🎯 总结
这个方案的核心优势:
- 不破坏现有逻辑 - 保留
user.user_type,现有代码不受影响 - 增量式开发 - 只添加新表和新接口
- 风险可控 - 每个阶段都可以独立测试和回滚
- 快速上线 - 3天即可完成
- 易于维护 - 代码清晰,逻辑简单
这是最安全、最快速的实现方案! ✅