【升级】大数据下的优化

This commit is contained in:
俞宝山
2026-02-25 21:03:02 +08:00
parent e89a5e17cd
commit 783fe18619
22 changed files with 720 additions and 9 deletions

View File

@@ -123,8 +123,8 @@
</s-table>
</template>
</XnResizablePanel>
<Form ref="formRef" @successful="tableRef.refresh()" />
<CopyForm ref="copyFormRef" @successful="tableRef.clearRefreshSelected()" />
<Form ref="formRef" @successful="tableRef.refresh(); refreshTreeData()" />
<CopyForm ref="copyFormRef" @successful="tableRef.clearRefreshSelected(); refreshTreeData()" />
</template>
<script setup name="bizOrg">
@@ -282,6 +282,21 @@
})
}
loadTreeData()
// 刷新树数据(增删改后调用,使用全量树接口保留展开状态)
const refreshTreeData = () => {
treeLoading.value = true
treeData.value = []
bizOrgApi
.orgTree()
.then((res) => {
if (res !== null) {
treeData.value = res
}
})
.finally(() => {
treeLoading.value = false
})
}
// 懒加载子节点
const onLoadData = (treeNode) => {
return new Promise((resolve) => {
@@ -323,12 +338,14 @@
]
bizOrgApi.orgDelete(params).then(() => {
tableRef.value.refresh(true)
refreshTreeData()
})
}
// 批量删除
const deleteBatchOrg = (params) => {
bizOrgApi.orgDelete(params).then(() => {
tableRef.value.clearRefreshSelected()
refreshTreeData()
})
}
// 批量复制

View File

@@ -122,8 +122,8 @@
</s-table>
</template>
</XnResizablePanel>
<Form ref="formRef" @successful="tableRef.refresh()" />
<CopyForm ref="copyFormRef" @successful="tableRef.clearRefreshSelected()" />
<Form ref="formRef" @successful="tableRef.refresh(); refreshTreeData()" />
<CopyForm ref="copyFormRef" @successful="tableRef.clearRefreshSelected(); refreshTreeData()" />
</template>
<script setup name="sysOrg">
@@ -281,6 +281,21 @@
})
}
loadTreeData()
// 刷新树数据(增删改后调用,使用全量树接口保留展开状态)
const refreshTreeData = () => {
treeLoading.value = true
treeData.value = []
orgApi
.orgTree()
.then((res) => {
if (res !== null) {
treeData.value = res
}
})
.finally(() => {
treeLoading.value = false
})
}
// 懒加载子节点
const onLoadData = (treeNode) => {
return new Promise((resolve) => {
@@ -322,12 +337,14 @@
]
orgApi.orgDelete(params).then(() => {
tableRef.value.refresh(true)
refreshTreeData()
})
}
// 批量删除
const deleteBatchOrg = (params) => {
orgApi.orgDelete(params).then(() => {
tableRef.value.clearRefreshSelected()
refreshTreeData()
})
}
// 批量复制

View File

@@ -62,4 +62,29 @@ public class CommonSqlUtil {
}
});
}
/**
* 使用预计算表的子查询替代IN查询适用于大数据量场景
* 通过MAP表查找SCOPE_KEY再从SCOPE表获取orgId列表
* column IN (SELECT ORG_ID FROM SYS_USER_DATA_SCOPE WHERE USER_ID = '{userId}'
* AND SCOPE_KEY = (SELECT SCOPE_KEY FROM SYS_USER_DATA_SCOPE_MAP WHERE USER_ID = '{userId}' AND API_URL = '{apiUrl}'))
* <p>
* 按API维度精确过滤相同orgId集合的API共享SCOPE_KEY大幅减少预计算表数据量。
* SQL固定长度不受数据量影响数据库可缓存执行计划走索引高效查询。
* </p>
*
* @param wrapper MyBatis-Plus LambdaQueryWrapper
* @param column 查询列
* @param userId 当前登录用户ID
* @param apiUrl 当前请求的API地址
* @param <T> 实体类型
*/
public static <T> void scopeIn(LambdaQueryWrapper<T> wrapper, SFunction<T, ?> column, String userId, String apiUrl) {
// 防御性处理移除单引号防止SQL注入
String safeUserId = userId.replace("'", "");
String safeApiUrl = apiUrl.replace("'", "");
wrapper.inSql(column, "SELECT ORG_ID FROM SYS_USER_DATA_SCOPE WHERE USER_ID = '" + safeUserId
+ "' AND SCOPE_KEY = (SELECT SCOPE_KEY FROM SYS_USER_DATA_SCOPE_MAP WHERE USER_ID = '"
+ safeUserId + "' AND API_URL = '" + safeApiUrl + "')");
}
}

View File

@@ -177,4 +177,14 @@ public interface SaBaseLoginUserApi {
* @date 2022/3/10 16:14
**/
void doRegister(String account, String password);
/**
* 刷新用户数据范围预计算表
*
* @param userId 用户ID
* @param dataScopeList 用户的数据范围集合per-API维度
* @author yubaoshan
* @date 2026/2/12
*/
void refreshUserDataScope(String userId, List<SaBaseLoginUser.DataScope> dataScopeList);
}

View File

@@ -81,4 +81,12 @@ public interface SysOrgApi {
* @date 2025/01/10 14:45
**/
List<JSONObject> getOrgListByIdListWithoutException(List<String> orgIdList);
/**
* 清除组织缓存
*
* @author yubaoshan
* @date 2026/2/12
**/
void clearOrgCache();
}

View File

@@ -727,6 +727,8 @@ public class AuthServiceImpl implements AuthService {
saBaseLoginUser.setRoleCodeList(roleCodeList);
// 缓存用户信息此处使用TokenSession为了指定时间内无操作则自动下线
StpUtil.getTokenSession().set("loginUser", saBaseLoginUser);
// 刷新用户数据范围预计算表
loginUserApi.refreshUserDataScope(saBaseLoginUser.getId(), saBaseLoginUser.getDataScopeList());
}
/**

View File

@@ -0,0 +1,86 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点需要更多功能代码获取Snowy商业授权许可请在官网购买授权地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.biz.core.listener;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import vip.xiaonuo.biz.core.enums.BizDataTypeEnum;
import vip.xiaonuo.biz.modular.org.service.BizOrgService;
import vip.xiaonuo.common.listener.CommonDataChangeListener;
import java.util.List;
/**
* 业务模块数据变化侦听器
* 监听其他模块如sys对共享表的变更同步清除biz模块缓存
*
* @author yubaoshan
* @date 2026/2/12
**/
@Component
public class BizDataChangeListener implements CommonDataChangeListener {
@Resource
private BizOrgService bizOrgService;
@Override
public void doAddWithDataId(String dataType, String dataId) {
}
@Override
public void doAddWithDataIdList(String dataType, List<String> dataIdList) {
if(dataType.equals(BizDataTypeEnum.ORG.getValue())) {
bizOrgService.clearOrgCache();
}
}
@Override
public void doAddWithData(String dataType, JSONObject jsonObject) {
}
@Override
public void doAddWithDataList(String dataType, JSONArray jsonArray) {
}
@Override
public void doUpdateWithDataId(String dataType, String dataId) {
}
@Override
public void doUpdateWithDataIdList(String dataType, List<String> dataIdList) {
if(dataType.equals(BizDataTypeEnum.ORG.getValue())) {
bizOrgService.clearOrgCache();
}
}
@Override
public void doUpdateWithData(String dataType, JSONObject jsonObject) {
}
@Override
public void doUpdateWithDataList(String dataType, JSONArray jsonArray) {
}
@Override
public void doDeleteWithDataId(String dataType, String dataId) {
}
@Override
public void doDeleteWithDataIdList(String dataType, List<String> dataIdList) {
if(dataType.equals(BizDataTypeEnum.ORG.getValue())) {
bizOrgService.clearOrgCache();
}
}
}

View File

@@ -197,4 +197,13 @@ public interface BizOrgService extends IService<BizOrg> {
* @date 2025/12/24 01:30
*/
void copy(BizOrgCopyParam bizOrgCopyParam);
/**
* 清除机构缓存Redis缓存 + 版本号递增)
* 当其他模块修改了SYS_ORG表数据时需要调用此方法同步清除缓存
*
* @author yubaoshan
* @date 2026/2/12
*/
void clearOrgCache();
}

View File

@@ -23,6 +23,7 @@ import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -44,11 +45,13 @@ import vip.xiaonuo.biz.modular.position.service.BizPositionService;
import vip.xiaonuo.biz.modular.user.entity.BizUser;
import vip.xiaonuo.biz.modular.user.service.BizUserService;
import vip.xiaonuo.common.cache.CommonCacheOperator;
import vip.xiaonuo.common.util.CommonServletUtil;
import vip.xiaonuo.common.util.CommonSqlUtil;
import vip.xiaonuo.common.enums.CommonSortOrderEnum;
import vip.xiaonuo.common.exception.CommonException;
import vip.xiaonuo.common.listener.CommonDataChangeEventCenter;
import vip.xiaonuo.common.page.CommonPageRequest;
import vip.xiaonuo.sys.api.SysOrgApi;
import vip.xiaonuo.sys.api.SysRoleApi;
import java.util.*;
@@ -77,6 +80,9 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
@Resource
private CommonCacheOperator commonCacheOperator;
@Resource
private SysOrgApi sysOrgApi;
@Resource
private SysRoleApi sysRoleApi;
@@ -114,7 +120,7 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
return new Page<>();
}
if(ObjectUtil.isNotEmpty(loginUserDataScope)) {
CommonSqlUtil.safeIn(queryWrapper.lambda(), BizOrg::getId, loginUserDataScope);
CommonSqlUtil.scopeIn(queryWrapper.lambda(), BizOrg::getId, StpUtil.getLoginIdAsString(), CommonServletUtil.getRequest().getServletPath());
}
return this.page(CommonPageRequest.defaultPage(), queryWrapper);
}
@@ -273,6 +279,7 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
CommonDataChangeEventCenter.doAddWithData(BizDataTypeEnum.ORG.getValue(), JSONUtil.createArray().put(bizOrg));
// 清除缓存
this.invalidateOrgCaches();
sysOrgApi.clearOrgCache();
}
@Transactional(rollbackFor = Exception.class)
@@ -311,6 +318,7 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
CommonDataChangeEventCenter.doUpdateWithData(BizDataTypeEnum.ORG.getValue(), JSONUtil.createArray().put(bizOrg));
// 清除缓存
this.invalidateOrgCaches();
sysOrgApi.clearOrgCache();
}
@Transactional(rollbackFor = Exception.class)
@@ -367,6 +375,7 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
CommonDataChangeEventCenter.doDeleteWithDataIdList(BizDataTypeEnum.ORG.getValue(), toDeleteOrgIdList);
// 清除缓存
this.invalidateOrgCaches();
sysOrgApi.clearOrgCache();
}
}
@@ -404,6 +413,11 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
commonCacheOperator.put(ORG_CACHE_VERSION_KEY, String.valueOf(System.currentTimeMillis()));
}
@Override
public void clearOrgCache() {
this.invalidateOrgCaches();
}
/**
* 获取当前用户可见的机构ID集合带版本化缓存
* 缓存命中时直接返回,无需加载全量机构数据;缓存未命中时计算并缓存
@@ -502,6 +516,7 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
CommonDataChangeEventCenter.doAddWithData(BizDataTypeEnum.ORG.getValue(), JSONUtil.createArray().put(bizOrg));
// 清除缓存
this.invalidateOrgCaches();
sysOrgApi.clearOrgCache();
return bizOrg.getId();
}
@@ -543,7 +558,7 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
return new Page<>();
}
if(ObjectUtil.isNotEmpty(loginUserDataScope)) {
CommonSqlUtil.safeIn(queryWrapper.lambda(), BizOrg::getId, loginUserDataScope);
CommonSqlUtil.scopeIn(queryWrapper.lambda(), BizOrg::getId, StpUtil.getLoginIdAsString(), CommonServletUtil.getRequest().getServletPath());
}
// 查询部分字段
queryWrapper.lambda().select(BizOrg::getId, BizOrg::getParentId, BizOrg::getName,
@@ -567,7 +582,7 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
return new Page<>();
}
if(ObjectUtil.isNotEmpty(loginUserDataScope)) {
CommonSqlUtil.safeIn(queryWrapper.lambda(), BizUser::getOrgId, loginUserDataScope);
CommonSqlUtil.scopeIn(queryWrapper.lambda(), BizUser::getOrgId, StpUtil.getLoginIdAsString(), CommonServletUtil.getRequest().getServletPath());
}
// 只查询部分字段
queryWrapper.lambda().select(BizUser::getId, BizUser::getAvatar, BizUser::getOrgId, BizUser::getPositionId, BizUser::getAccount,
@@ -646,6 +661,7 @@ public class BizOrgServiceImpl extends ServiceImpl<BizOrgMapper, BizOrg> impleme
});
// 清除缓存
this.invalidateOrgCaches();
sysOrgApi.clearOrgCache();
}
}

View File

@@ -212,4 +212,15 @@ public class ClientLoginUserApiProvider implements SaBaseLoginUserApi {
public void doRegister(String account, String password) {
clientUserService.doRegister(account, password);
}
/**
* C端用户无数据范围不实现
*
* @author yubaoshan
* @date 2026/2/12
**/
@Override
public void refreshUserDataScope(String userId, List<SaBaseLoginUser.DataScope> dataScopeList) {
// C端用户无数据范围无需刷新预计算表
}
}

View File

@@ -15,11 +15,13 @@ package vip.xiaonuo.sys.core.listener;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser;
import vip.xiaonuo.auth.core.util.StpLoginUserUtil;
import vip.xiaonuo.common.listener.CommonDataChangeListener;
import vip.xiaonuo.sys.core.enums.SysDataTypeEnum;
import vip.xiaonuo.sys.modular.org.service.SysUserDataScopeService;
import java.util.List;
@@ -32,6 +34,9 @@ import java.util.List;
@Component
public class SysDataChangeListener implements CommonDataChangeListener {
@Resource
private SysUserDataScopeService sysUserDataScopeService;
@Override
public void doAddWithDataId(String dataType, String dataId) {
// 此处可做额外处理
@@ -46,6 +51,8 @@ public class SysDataChangeListener implements CommonDataChangeListener {
saBaseLoginUser.setDataScopeList(saBaseLoginUser.getDataScopeList());
// 重新缓存当前登录用户信息
StpUtil.getTokenSession().set("loginUser", saBaseLoginUser);
// 同步更新预计算表为当前用户的所有API追加新机构
sysUserDataScopeService.appendOrgIdsForUser(saBaseLoginUser.getId(), dataIdList);
}
}
@@ -86,6 +93,9 @@ public class SysDataChangeListener implements CommonDataChangeListener {
@Override
public void doDeleteWithDataIdList(String dataType, List<String> dataIdList) {
// 此处可做额外处理
// 组织删除时,精准删除预计算表中对应的机构记录
if(dataType.equals(SysDataTypeEnum.ORG.getValue())) {
sysUserDataScopeService.deleteByOrgIds(dataIdList);
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点需要更多功能代码获取Snowy商业授权许可请在官网购买授权地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.sys.modular.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
/**
* 用户数据范围预计算实体
* <p>
* 通过SCOPE_KEY将相同orgId集合的API分组避免重复存储大幅减少数据量。
* </p>
*
* @author yubaoshan
* @date 2026/2/12
**/
@Getter
@Setter
@TableName("SYS_USER_DATA_SCOPE")
public class SysUserDataScope implements Serializable {
/** 用户ID */
@Schema(description = "用户ID")
private String userId;
/** 作用域KEYorgId集合的MD5摘要 */
@Schema(description = "作用域KEY")
private String scopeKey;
/** 机构ID */
@Schema(description = "机构ID")
private String orgId;
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点需要更多功能代码获取Snowy商业授权许可请在官网购买授权地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.sys.modular.org.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
/**
* 用户数据范围API映射实体
* <p>
* 记录每个API对应的作用域KEY多个API共享相同orgId集合时复用同一个SCOPE_KEY
* 大幅减少预计算表的数据量。
* </p>
*
* @author yubaoshan
* @date 2026/2/12
**/
@Getter
@Setter
@TableName("SYS_USER_DATA_SCOPE_MAP")
public class SysUserDataScopeMap implements Serializable {
/** 用户ID */
@Schema(description = "用户ID")
private String userId;
/** API接口地址 */
@Schema(description = "API接口地址")
private String apiUrl;
/** 作用域KEYorgId集合的MD5摘要 */
@Schema(description = "作用域KEY")
private String scopeKey;
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点需要更多功能代码获取Snowy商业授权许可请在官网购买授权地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.sys.modular.org.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import vip.xiaonuo.sys.modular.org.entity.SysUserDataScopeMap;
/**
* 用户数据范围API映射Mapper
*
* @author yubaoshan
* @date 2026/2/12
**/
@Mapper
public interface SysUserDataScopeMapMapper extends BaseMapper<SysUserDataScopeMap> {
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点需要更多功能代码获取Snowy商业授权许可请在官网购买授权地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.sys.modular.org.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import vip.xiaonuo.sys.modular.org.entity.SysUserDataScope;
/**
* 用户数据范围预计算Mapper
*
* @author yubaoshan
* @date 2026/2/12
**/
@Mapper
public interface SysUserDataScopeMapper extends BaseMapper<SysUserDataScope> {
}

View File

@@ -84,4 +84,9 @@ public class SysOrgApiProvider implements SysOrgApi {
public List<JSONObject> getOrgListByIdListWithoutException(List<String> orgIdList) {
return sysOrgService.listByIds(orgIdList).stream().map(JSONUtil::parseObj).collect(Collectors.toList());
}
@Override
public void clearOrgCache() {
sysOrgService.clearOrgCache();
}
}

View File

@@ -213,4 +213,13 @@ public interface SysOrgService extends IService<SysOrg> {
* @date 2025/5/10 12:13
*/
List<String> getParentIdListByOrgId(String orgId);
/**
* 清除组织缓存(本地内存缓存 + Redis缓存
* 当其他模块修改了SYS_ORG表数据时需要调用此方法同步清除缓存
*
* @author yubaoshan
* @date 2026/2/12
*/
void clearOrgCache();
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点需要更多功能代码获取Snowy商业授权许可请在官网购买授权地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.sys.modular.org.service;
import com.baomidou.mybatisplus.extension.service.IService;
import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser;
import vip.xiaonuo.sys.modular.org.entity.SysUserDataScope;
import java.util.List;
/**
* 用户数据范围预计算Service接口
*
* @author yubaoshan
* @date 2026/2/12
**/
public interface SysUserDataScopeService extends IService<SysUserDataScope> {
/**
* 刷新指定用户的数据范围预计算(登录时调用)
* 按API维度存储每个API的数据范围独立
*
* @param userId 用户ID
* @param dataScopeList 用户的数据范围集合per-API
*/
void refreshByUserId(String userId, List<SaBaseLoginUser.DataScope> dataScopeList);
/**
* 删除指定用户的数据范围预计算
*
* @param userId 用户ID
*/
void deleteByUserId(String userId);
/**
* 删除指定机构ID的预计算记录机构删除时调用
* 精准删除,不影响其他机构的数据
*
* @param orgIds 被删除的机构ID列表
*/
void deleteByOrgIds(List<String> orgIds);
/**
* 为指定用户的所有API追加机构ID机构新增时调用
*
* @param userId 用户ID
* @param orgIds 新增的机构ID列表
*/
void appendOrgIdsForUser(String userId, List<String> orgIds);
}

View File

@@ -401,7 +401,8 @@ public class SysOrgServiceImpl extends ServiceImpl<SysOrgMapper, SysOrg> impleme
/**
* 清除本地内存缓存和Redis缓存
*/
private void clearOrgCache() {
@Override
public void clearOrgCache() {
localOrgListCache = null;
localParentChildrenMap = null;
localAllOrgIdList = null;

View File

@@ -0,0 +1,233 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点需要更多功能代码获取Snowy商业授权许可请在官网购买授权地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.sys.modular.org.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser;
import vip.xiaonuo.sys.modular.org.entity.SysUserDataScope;
import vip.xiaonuo.sys.modular.org.entity.SysUserDataScopeMap;
import vip.xiaonuo.sys.modular.org.mapper.SysUserDataScopeMapper;
import vip.xiaonuo.sys.modular.org.mapper.SysUserDataScopeMapMapper;
import vip.xiaonuo.sys.modular.org.service.SysUserDataScopeService;
import java.util.*;
import java.util.stream.Collectors;
/**
* 用户数据范围预计算Service实现类
* <p>
* 优化策略:
* 1. 作用域去重将orgId集合相同的API分组共享同一个SCOPE_KEY避免重复存储
* 2. 增量刷新对比新旧数据只执行必要的增删操作减少数据库IO
* </p>
*
* @author yubaoshan
* @date 2026/2/12
**/
@Slf4j
@Service
public class SysUserDataScopeServiceImpl extends ServiceImpl<SysUserDataScopeMapper, SysUserDataScope>
implements SysUserDataScopeService {
@Resource
private SysUserDataScopeMapMapper sysUserDataScopeMapMapper;
@Transactional(rollbackFor = Exception.class)
@Override
public void refreshByUserId(String userId, List<SaBaseLoginUser.DataScope> dataScopeList) {
if (ObjectUtil.isEmpty(dataScopeList)) {
// 无数据范围,清空该用户所有记录
this.deleteByUserId(userId);
return;
}
// 1. 按orgId集合分组计算SCOPE_KEY
// key=scopeKey, value=orgId集合
Map<String, List<String>> scopeKeyToOrgIds = new LinkedHashMap<>();
// key=apiUrl, value=scopeKey
Map<String, String> apiToScopeKey = new LinkedHashMap<>();
for (SaBaseLoginUser.DataScope dataScope : dataScopeList) {
if (dataScope.isScopeAll() || ObjectUtil.isEmpty(dataScope.getDataScope())) {
continue;
}
List<String> sortedOrgIds = dataScope.getDataScope().stream()
.distinct().sorted().collect(Collectors.toList());
String scopeKey = SecureUtil.md5(String.join(",", sortedOrgIds));
apiToScopeKey.put(dataScope.getApiUrl(), scopeKey);
scopeKeyToOrgIds.putIfAbsent(scopeKey, sortedOrgIds);
}
if (apiToScopeKey.isEmpty()) {
this.deleteByUserId(userId);
return;
}
// 2. 增量刷新MAP表
Map<String, String> existingMap = sysUserDataScopeMapMapper.selectList(
new LambdaQueryWrapper<SysUserDataScopeMap>()
.eq(SysUserDataScopeMap::getUserId, userId)
).stream().collect(Collectors.toMap(SysUserDataScopeMap::getApiUrl, SysUserDataScopeMap::getScopeKey));
// 需要删除的MAP记录旧的有新的没有
Set<String> apiToDelete = new HashSet<>(existingMap.keySet());
apiToDelete.removeAll(apiToScopeKey.keySet());
if (!apiToDelete.isEmpty()) {
sysUserDataScopeMapMapper.delete(new LambdaQueryWrapper<SysUserDataScopeMap>()
.eq(SysUserDataScopeMap::getUserId, userId)
.in(SysUserDataScopeMap::getApiUrl, apiToDelete));
}
// 需要新增或更新的MAP记录
List<SysUserDataScopeMap> mapToInsert = new ArrayList<>();
List<SysUserDataScopeMap> mapToUpdate = new ArrayList<>();
for (Map.Entry<String, String> entry : apiToScopeKey.entrySet()) {
String apiUrl = entry.getKey();
String newScopeKey = entry.getValue();
String oldScopeKey = existingMap.get(apiUrl);
if (oldScopeKey == null) {
// 新增
SysUserDataScopeMap map = new SysUserDataScopeMap();
map.setUserId(userId);
map.setApiUrl(apiUrl);
map.setScopeKey(newScopeKey);
mapToInsert.add(map);
} else if (!oldScopeKey.equals(newScopeKey)) {
// scope变了更新
SysUserDataScopeMap map = new SysUserDataScopeMap();
map.setUserId(userId);
map.setApiUrl(apiUrl);
map.setScopeKey(newScopeKey);
mapToUpdate.add(map);
}
// scope没变的跳过
}
for (SysUserDataScopeMap map : mapToInsert) {
sysUserDataScopeMapMapper.insert(map);
}
for (SysUserDataScopeMap map : mapToUpdate) {
SysUserDataScopeMap updateEntity = new SysUserDataScopeMap();
updateEntity.setScopeKey(map.getScopeKey());
sysUserDataScopeMapMapper.update(updateEntity, new LambdaQueryWrapper<SysUserDataScopeMap>()
.eq(SysUserDataScopeMap::getUserId, map.getUserId())
.eq(SysUserDataScopeMap::getApiUrl, map.getApiUrl()));
}
// 3. 增量刷新SCOPE表
// 查询该用户当前所有scope记录
Set<String> existingScopeKeys = this.list(
new LambdaQueryWrapper<SysUserDataScope>()
.select(SysUserDataScope::getScopeKey)
.eq(SysUserDataScope::getUserId, userId)
.groupBy(SysUserDataScope::getScopeKey)
).stream().map(SysUserDataScope::getScopeKey).collect(Collectors.toSet());
// 新的需要的scopeKey集合
Set<String> neededScopeKeys = new HashSet<>(scopeKeyToOrgIds.keySet());
// 删除不再需要的scope记录
Set<String> scopeKeysToDelete = new HashSet<>(existingScopeKeys);
scopeKeysToDelete.removeAll(neededScopeKeys);
if (!scopeKeysToDelete.isEmpty()) {
this.remove(new LambdaQueryWrapper<SysUserDataScope>()
.eq(SysUserDataScope::getUserId, userId)
.in(SysUserDataScope::getScopeKey, scopeKeysToDelete));
}
// 新增缺失的scope记录
Set<String> scopeKeysToAdd = new HashSet<>(neededScopeKeys);
scopeKeysToAdd.removeAll(existingScopeKeys);
if (!scopeKeysToAdd.isEmpty()) {
List<SysUserDataScope> newRecords = new ArrayList<>();
for (String scopeKey : scopeKeysToAdd) {
for (String orgId : scopeKeyToOrgIds.get(scopeKey)) {
SysUserDataScope record = new SysUserDataScope();
record.setUserId(userId);
record.setScopeKey(scopeKey);
record.setOrgId(orgId);
newRecords.add(record);
}
}
if (!newRecords.isEmpty()) {
this.saveBatch(newRecords);
}
}
int totalScopeRecords = scopeKeyToOrgIds.values().stream().mapToInt(List::size).sum();
log.debug(">>> 刷新用户数据范围预计算userId={}API数={}去重后scope组={}scope记录数={}",
userId, apiToScopeKey.size(), scopeKeyToOrgIds.size(), totalScopeRecords);
}
@Override
public void deleteByUserId(String userId) {
sysUserDataScopeMapMapper.delete(new LambdaQueryWrapper<SysUserDataScopeMap>()
.eq(SysUserDataScopeMap::getUserId, userId));
this.remove(new LambdaQueryWrapper<SysUserDataScope>()
.eq(SysUserDataScope::getUserId, userId));
}
@Override
public void deleteByOrgIds(List<String> orgIds) {
if (ObjectUtil.isEmpty(orgIds)) {
return;
}
this.remove(new LambdaQueryWrapper<SysUserDataScope>()
.in(SysUserDataScope::getOrgId, orgIds));
log.info(">>> 删除机构数据范围预计算orgIds数量={}", orgIds.size());
}
@Transactional(rollbackFor = Exception.class)
@Override
public void appendOrgIdsForUser(String userId, List<String> orgIds) {
if (ObjectUtil.isEmpty(orgIds)) {
return;
}
// 查询该用户所有的scopeKey去重
Set<String> existingScopeKeys = this.list(
new LambdaQueryWrapper<SysUserDataScope>()
.select(SysUserDataScope::getScopeKey)
.eq(SysUserDataScope::getUserId, userId)
.groupBy(SysUserDataScope::getScopeKey)
).stream().map(SysUserDataScope::getScopeKey).collect(Collectors.toSet());
if (ObjectUtil.isEmpty(existingScopeKeys)) {
return;
}
// 查询已存在的(scopeKey, orgId)组合,避免重复插入
Set<String> existingKeys = this.list(
new LambdaQueryWrapper<SysUserDataScope>()
.select(SysUserDataScope::getScopeKey, SysUserDataScope::getOrgId)
.eq(SysUserDataScope::getUserId, userId)
.in(SysUserDataScope::getOrgId, orgIds)
).stream().map(r -> r.getScopeKey() + "|" + r.getOrgId()).collect(Collectors.toSet());
// 为每个scopeKey追加新的机构ID
List<SysUserDataScope> newRecords = new ArrayList<>();
for (String scopeKey : existingScopeKeys) {
for (String orgId : orgIds) {
if (!existingKeys.contains(scopeKey + "|" + orgId)) {
SysUserDataScope record = new SysUserDataScope();
record.setUserId(userId);
record.setScopeKey(scopeKey);
record.setOrgId(orgId);
newRecords.add(record);
}
}
}
if (ObjectUtil.isNotEmpty(newRecords)) {
this.saveBatch(newRecords);
}
}
}

View File

@@ -20,6 +20,7 @@ import org.springframework.stereotype.Service;
import vip.xiaonuo.auth.api.SaBaseLoginUserApi;
import vip.xiaonuo.auth.core.pojo.SaBaseClientLoginUser;
import vip.xiaonuo.auth.core.pojo.SaBaseLoginUser;
import vip.xiaonuo.sys.modular.org.service.SysUserDataScopeService;
import vip.xiaonuo.sys.modular.user.entity.SysUser;
import vip.xiaonuo.sys.modular.user.result.SysLoginUser;
import vip.xiaonuo.sys.modular.user.service.SysUserService;
@@ -39,6 +40,9 @@ public class SysLoginUserApiProvider implements SaBaseLoginUserApi {
@Resource
private SysUserService sysUserService;
@Resource
private SysUserDataScopeService sysUserDataScopeService;
/**
* 根据id获取B端用户信息查不到则返回null
*
@@ -207,4 +211,9 @@ public class SysLoginUserApiProvider implements SaBaseLoginUserApi {
public void doRegister(String account, String password) {
sysUserService.doRegister(account, password);
}
@Override
public void refreshUserDataScope(String userId, List<SaBaseLoginUser.DataScope> dataScopeList) {
sysUserDataScopeService.refreshByUserId(userId, dataScopeList);
}
}

View File

@@ -1413,4 +1413,37 @@ CREATE TABLE `SYS_USER_PASSWORD` (
-- Records of SYS_USER_PASSWORD
-- ----------------------------
-- ----------------------------
-- Table structure for SYS_USER_DATA_SCOPE
-- ----------------------------
DROP TABLE IF EXISTS `SYS_USER_DATA_SCOPE`;
CREATE TABLE `SYS_USER_DATA_SCOPE` (
`USER_ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户ID',
`SCOPE_KEY` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '作用域KEYorgId集合的MD5摘要',
`ORG_ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '机构ID',
PRIMARY KEY (`USER_ID`, `SCOPE_KEY`, `ORG_ID`) USING BTREE,
INDEX `IDX_USER_SCOPE`(`USER_ID`, `SCOPE_KEY`) USING BTREE,
INDEX `IDX_ORG_ID`(`ORG_ID`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户数据范围预计算' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of SYS_USER_DATA_SCOPE
-- ----------------------------
-- ----------------------------
-- Table structure for SYS_USER_DATA_SCOPE_MAP
-- ----------------------------
DROP TABLE IF EXISTS `SYS_USER_DATA_SCOPE_MAP`;
CREATE TABLE `SYS_USER_DATA_SCOPE_MAP` (
`USER_ID` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户ID',
`API_URL` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'API接口地址',
`SCOPE_KEY` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '作用域KEYorgId集合的MD5摘要',
PRIMARY KEY (`USER_ID`, `API_URL`) USING BTREE,
INDEX `IDX_USER_SCOPE_KEY`(`USER_ID`, `SCOPE_KEY`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户数据范围API映射' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of SYS_USER_DATA_SCOPE_MAP
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;