uni-app—— uni-app 多租户系统中切换租户时的状态管理问题
·
问题背景
在一个多租户管理系统中,用户可以管理多个组织。当用户切换组织后,审批模块出现了以下异常:
- 组织A没有配置审批模版,但前端却显示了审批模版
- 审批列表显示的是上一个组织的数据
- 草稿编辑页在切换组织后仍可继续编辑原组织的草稿
这是一个典型的多租户系统状态管理问题。
问题分析
经过排查,发现问题出在前后端都没有正确处理租户切换:
1. 后端:仅信任前端传参,未校验登录态
// 问题代码:ApprovalTemplateController.java
@GetMapping("/approval/template/available")
public ResponseDTO<List<ApprovalTemplateVO>> getAvailableTemplates(
@RequestParam Long orgId) {
// 直接使用前端传入的 orgId,存在跨组织数据泄露风险
return approvalTemplateService.getAvailableTemplates(orgId);
}
2. 前端:模板列表有缓存,切换组织后未清空
// 问题代码:create.vue
const templateList = ref([])
const templateLoaded = ref(false)
const loadTemplates = async () => {
// 只判断是否已加载,未考虑组织切换
if (templateLoaded.value) return
const res = await getAvailableTemplates()
templateList.value = res.data
templateLoaded.value = true
}
3. 前端:列表页切换组织后未重置分页状态
// 问题代码:index.vue
// 切换组织后,tabData 和分页状态仍保留上一个组织的数据
解决方案
1. 后端:强制校验登录态组织ID
@GetMapping("/approval/template/available")
public ResponseDTO<List<ApprovalTemplateVO>> getAvailableTemplates(
@RequestParam(required = false) Long orgId) {
// 强制以登录态中的组织ID为准
Long actualOrgId = SecurityUtil.getOrgId();
// 如果登录态中没有组织ID,返回空列表
if (actualOrgId == null) {
return ResponseDTO.ok(Collections.emptyList());
}
// 记录日志:前端传参与登录态不一致(可能是缓存问题或恶意请求)
if (orgId != null && !orgId.equals(actualOrgId)) {
log.warn("orgId mismatch: param={}, session={}",
orgId, actualOrgId);
}
return approvalTemplateService.getAvailableTemplates(actualOrgId);
}
2. 前端:增加组织ID校验,切换时清空缓存
// create.vue
const templateList = ref([])
const templateLoaded = ref(false)
const loadedOrgId = ref<number | null>(null) // 记录已加载的组织ID
// 重置模板缓存
const resetTemplateCache = () => {
templateList.value = []
templateLoaded.value = false
loadedOrgId.value = null
}
const loadTemplates = async () => {
const currentOrgId = getCurrentOrgId()
// 检测组织是否变化
if (loadedOrgId.value !== currentOrgId) {
resetTemplateCache()
}
if (templateLoaded.value) return
const res = await getAvailableTemplates({ orgId: currentOrgId })
templateList.value = res.data
templateLoaded.value = true
loadedOrgId.value = currentOrgId
}
// 监听组织切换事件
onMounted(() => {
uni.$on('switchOrg', () => {
resetTemplateCache()
loadTemplates()
loadCategories() // 同时重新加载分类数据
})
})
onUnmounted(() => {
uni.$off('switchOrg')
})
3. 前端:列表页监听切换事件,重置状态
// index.vue
const resetListState = () => {
// 清空所有tab的数据和分页状态
Object.keys(tabData.value).forEach(key => {
tabData.value[key] = {
list: [],
page: 1,
hasMore: true,
loading: false
}
})
}
onMounted(() => {
uni.$on('switchOrg', () => {
resetListState()
loadCurrentTabData() // 重新加载当前tab数据
})
})
4. 前端:编辑页切换组织时提示并返回
// edit.vue
onMounted(() => {
uni.$on('switchOrg', () => {
uni.showToast({
title: '组织已切换,请重新操作',
icon: 'none'
})
// 返回上一页,避免跨组织编辑
setTimeout(() => {
uni.navigateBack()
}, 1500)
})
})
最佳实践总结
后端安全原则
- 永远不要信任前端传参:涉及租户隔离的接口,必须以服务端session/token中的租户ID为准
- 参数校验要记录日志:前端传参与登录态不一致时记录日志,便于排查问题或发现恶意请求
- 防御性返回:登录态异常时返回空数据,而非报错
前端状态管理原则
- 缓存要关联租户ID:任何缓存都要记录对应的租户ID,切换时校验并清空
- 全局事件统一处理:租户切换时触发全局事件,各页面各自响应
- 编辑态要阻断:正在编辑的数据,切换租户后应提示并返回,避免数据错乱
通用检查清单
切换租户时,检查以下状态是否需要重置:
| 状态类型 | 示例 | 处理方式 |
|---|---|---|
| 列表数据 | 审批列表、模板列表 | 清空并重新加载 |
| 分页状态 | page、hasMore | 重置为初始值 |
| 表单缓存 | 已加载的选项数据 | 清空并重新加载 |
| 编辑状态 | 草稿编辑中 | 提示并返回上一页 |
| 搜索条件 | 筛选项 | 根据业务决定是否保留 |
总结
多租户系统的租户切换看似简单,实际上涉及前后端多个层面的状态管理。核心原则是:
- 后端:强制校验,不信任前端
- 前端:缓存关联租户,切换时清空
- 交互:编辑态要阻断,避免跨租户操作
这个问题在 SaaS 系统、多账号系统、多组织系统中都会遇到,值得在架构设计阶段就考虑清楚。
更多推荐
所有评论(0)