diff --git a/README.md b/README.md index 00c0346f..9d913594 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,8 @@ Snowy(SnowyAdmin)是国内首个国密前后端分离快速开发平台,集成国密加解密插件, 软件层面完全符合等保测评要求,同时实现国产化机型、中间件、数据库适配,是您的不二之选! -技术框架与密码结合,让更多的人认识密码,使用密码;更是让前后分离“密”不可分。 +将国密能力内置于技术框架底层,让密码技术从"专业门槛"变为"开箱即用",真正实现业务安全从底层做起。 +历经多年开源社区打磨与企业客户实践验证,新版本在大数据处理能力与安全体系方面实现了全面升级。 采用SpringBoot+MybatisPlus+AntDesignVue+Vite 等更多组件及前沿技术开发,注释丰富,代码简洁,开箱即用! @@ -61,6 +62,15 @@ gitcode下载地址:[https://gitcode.com/xiaonuobase/Snowy](https://gitcode.co 文档地址:[https://xiaonuo.vip/doc](https://xiaonuo.vip/doc) +## 商业产品 + +- 如果开源版本不能满足您的需求,还可以看看我们官方推出的基于开源版开发的商业化产品 + +| 产品名称 | 演示 | 用途 | +|-------------|------------------------------------------------------|---------------------------------| +| AI智能化零代码开发平台 | [https://alsc.xiaonuo.vip](https://alsc.xiaonuo.vip) | AI智能驱动,拖拉拽即可搭建业务系统,无需编写一行代码 | +| 国产数据中台 | [https://data.xiaonuo.vip](https://data.xiaonuo.vip) | 覆盖数据采集、存储、治理、安全、资产化、服务全流程的一站式数据管理平台 | + ## 快速启动 全栈工程师推荐idea @@ -240,7 +250,7 @@ QQ技术群:732230670(已满)、685395081 微信技术群: -因群达到200人以上,需加微信拉群,禁止群内艾特群主及管理员,私信提问技术问题无时间精力回答(免开尊口),请群内互动互助才是建群的意义,否则我认为你没有加群的必要 +因群达到200人以上,需加微信拉群,禁止群内艾特群主及管理员,私信提问技术问题无时间精力回答,请群内互动互助交流技术才是建群的意义 @@ -326,6 +336,6 @@ QQ技术群:732230670(已满)、685395081 - 代码可用于个人项目等接私活或企业项目脚手架使用,Snowy全系开源版完全免费 -- 二次开发如用于开源竞品请先联系群主沟通,禁止任何变相的二开行为,未经审核视为侵权 +- 二次开源不可参与同类竞争,可在其他赛道进行,有好的案例可以提供,我们会挂在本页进行宣传 - 请不要删除和修改Snowy源码头部的版权与作者声明及出处 diff --git a/snowy-admin-web/src/api/biz/bizOrgApi.js b/snowy-admin-web/src/api/biz/bizOrgApi.js index d3dc0c71..a9191429 100644 --- a/snowy-admin-web/src/api/biz/bizOrgApi.js +++ b/snowy-admin-web/src/api/biz/bizOrgApi.js @@ -22,10 +22,6 @@ export default { orgPage(data) { return request('page', data, 'get') }, - // 获取机构列表 - orgList(data) { - return request('list', data, 'get') - }, // 获取机构树(懒加载) orgTree(data) { return request('tree', data, 'get') diff --git a/snowy-admin-web/src/views/biz/user/form.vue b/snowy-admin-web/src/views/biz/user/form.vue index 0dffbff7..15ed8515 100644 --- a/snowy-admin-web/src/views/biz/user/form.vue +++ b/snowy-admin-web/src/views/biz/user/form.vue @@ -337,7 +337,7 @@ const isFullTree = ref(false) // 加载全量树(用于需要展开到指定节点的场景) const loadFullTree = () => { - return bizUserApi.userOrgTreeSelector({ searchKey: '' }).then((res) => { + return bizUserApi.orgTreeSelector({ searchKey: '' }).then((res) => { if (res !== null) { treeData.value = res // 只有一个根节点时才自动展开 @@ -349,7 +349,7 @@ } // 加载懒加载树(无需展开到指定节点时使用) const loadLazyTree = () => { - return bizUserApi.userOrgTreeSelector().then((res) => { + return bizUserApi.orgTreeSelector().then((res) => { if (res !== null) { treeData.value = res.map((item) => { return { @@ -418,7 +418,7 @@ return } bizUserApi - .userOrgTreeSelector({ + .orgTreeSelector({ parentId: treeNode.dataRef.id }) .then((res) => { diff --git a/snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/api/SaBaseLoginUserApi.java b/snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/api/SaBaseLoginUserApi.java index 178735f5..1dad9799 100644 --- a/snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/api/SaBaseLoginUserApi.java +++ b/snowy-plugin-api/snowy-plugin-auth-api/src/main/java/vip/xiaonuo/auth/api/SaBaseLoginUserApi.java @@ -187,4 +187,14 @@ public interface SaBaseLoginUserApi { * @date 2026/2/12 */ void refreshUserDataScope(String userId, List dataScopeList); + + /** + * 刷新在线用户的权限缓存(Session),权限变更后调用,确保实时生效。 + * 如果用户不在线则跳过。 + * + * @param userId 用户ID + * @author xuyuxiang + * @date 2026/3/12 + */ + void refreshOnlineUserPermission(String userId); } diff --git a/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/provider/ClientLoginUserApiProvider.java b/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/provider/ClientLoginUserApiProvider.java index 7503075e..a60ed2e6 100644 --- a/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/provider/ClientLoginUserApiProvider.java +++ b/snowy-plugin/snowy-plugin-client/src/main/java/vip/xiaonuo/client/modular/user/provider/ClientLoginUserApiProvider.java @@ -223,4 +223,9 @@ public class ClientLoginUserApiProvider implements SaBaseLoginUserApi { public void refreshUserDataScope(String userId, List dataScopeList) { // C端用户无数据范围,无需刷新预计算表 } + + @Override + public void refreshOnlineUserPermission(String userId) { + // C端用户暂无数据权限机制,无需刷新 + } } diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java index 6e84b7c7..aec51198 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/role/service/impl/SysRoleServiceImpl.java @@ -58,6 +58,7 @@ import vip.xiaonuo.sys.modular.role.mapper.SysRoleMapper; import vip.xiaonuo.sys.modular.role.param.*; import vip.xiaonuo.sys.modular.role.result.*; import vip.xiaonuo.sys.modular.role.service.SysRoleService; +import vip.xiaonuo.auth.api.SaBaseLoginUserApi; import vip.xiaonuo.sys.modular.user.entity.SysUser; import vip.xiaonuo.sys.modular.user.enums.SysUserStatusEnum; import vip.xiaonuo.sys.modular.user.service.SysUserService; @@ -95,6 +96,9 @@ public class SysRoleServiceImpl extends ServiceImpl impl @Resource private CommonCacheOperator commonCacheOperator; + @Resource(name = "loginUserApi") + private SaBaseLoginUserApi loginUserApi; + @Override public Page page(SysRolePageParam sysRolePageParam) { QueryWrapper queryWrapper = new QueryWrapper().checkSqlInjection(); @@ -313,6 +317,10 @@ public class SysRoleServiceImpl extends ServiceImpl impl .map(JSONUtil::toJsonStr).collect(Collectors.toList()); sysRelationService.saveRelationBatchWithClear(id, apiUrlList, SysRelationCategoryEnum.SYS_ROLE_HAS_PERMISSION.getValue(), extJsonList); + // 刷新拥有该角色的所有在线用户的权限缓存 + List userIdList = sysRelationService.getRelationObjectIdListByTargetIdAndCategory( + id, SysRelationCategoryEnum.SYS_USER_HAS_ROLE.getValue()); + userIdList.forEach(loginUserApi::refreshOnlineUserPermission); } @Override @@ -325,7 +333,11 @@ public class SysRoleServiceImpl extends ServiceImpl impl public void grantUser(SysRoleGrantUserParam sysRoleGrantUserParam) { String id = sysRoleGrantUserParam.getId(); List grantInfoList = sysRoleGrantUserParam.getGrantInfoList(); + // 记录变更前拥有该角色的用户(用于后续刷新被移除用户的权限) + Set affectedUserIds = new HashSet<>(); if(sysRoleGrantUserParam.getRemoveFirst()) { + affectedUserIds.addAll(sysRelationService.getRelationObjectIdListByTargetIdAndCategory( + id, SysRelationCategoryEnum.SYS_USER_HAS_ROLE.getValue())); sysRelationService.remove(new LambdaQueryWrapper().eq(SysRelation::getTargetId, id) .eq(SysRelation::getCategory, SysRelationCategoryEnum.SYS_USER_HAS_ROLE.getValue())); } @@ -336,6 +348,9 @@ public class SysRoleServiceImpl extends ServiceImpl impl sysRelation.setCategory(SysRelationCategoryEnum.SYS_USER_HAS_ROLE.getValue()); return sysRelation; }).collect(Collectors.toList())); + // 合并新增用户,刷新所有受影响用户的权限缓存 + affectedUserIds.addAll(grantInfoList); + affectedUserIds.forEach(loginUserApi::refreshOnlineUserPermission); } @Override diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysLoginUserApiProvider.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysLoginUserApiProvider.java index 77c8b54c..03a27d12 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysLoginUserApiProvider.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/provider/SysLoginUserApiProvider.java @@ -12,10 +12,16 @@ */ package vip.xiaonuo.sys.modular.user.provider; +import cn.dev33.satoken.session.SaSession; +import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import vip.xiaonuo.auth.api.SaBaseLoginUserApi; import vip.xiaonuo.auth.core.pojo.SaBaseClientLoginUser; @@ -34,6 +40,7 @@ import java.util.stream.Collectors; * @author xuyuxiang * @date 2022/4/29 13:36 **/ +@Slf4j @Service("loginUserApi") public class SysLoginUserApiProvider implements SaBaseLoginUserApi { @@ -216,4 +223,50 @@ public class SysLoginUserApiProvider implements SaBaseLoginUserApi { public void refreshUserDataScope(String userId, List dataScopeList) { sysUserDataScopeService.refreshByUserId(userId, dataScopeList); } + + @Override + public void refreshOnlineUserPermission(String userId) { + // 获取该用户所有在线token + List tokenList = StpUtil.getTokenValueListByLoginId(userId); + if (ObjectUtil.isEmpty(tokenList)) { + return; + } + // 获取用户基本信息 + SaBaseLoginUser saBaseLoginUser = this.getUserById(userId); + if (ObjectUtil.isEmpty(saBaseLoginUser)) { + return; + } + // 获取角色列表 + List roleList = this.getRoleListByUserId(userId); + List roleIdList = roleList.stream().map(j -> j.getStr("id")).collect(Collectors.toList()); + List roleCodeList = roleList.stream().map(j -> j.getStr("code")).collect(Collectors.toList()); + List userAndRoleIdList = CollectionUtil.unionAll(roleIdList, CollectionUtil.newArrayList(userId)); + // 重新计算权限数据 + List buttonCodeList = this.getButtonCodeListListByUserAndRoleIdList(userAndRoleIdList); + List mobileButtonCodeList = this.getMobileButtonCodeListListByUserIdAndRoleIdList(userAndRoleIdList); + List dataScopeList = Convert.toList(SaBaseLoginUser.DataScope.class, + this.getPermissionListByUserIdAndRoleIdList(userAndRoleIdList, saBaseLoginUser.getOrgId())); + List permissionCodeList = dataScopeList.stream() + .map(SaBaseLoginUser.DataScope::getApiUrl).collect(Collectors.toList()); + // 填充到用户对象 + saBaseLoginUser.setButtonCodeList(buttonCodeList); + saBaseLoginUser.setMobileButtonCodeList(mobileButtonCodeList); + saBaseLoginUser.setDataScopeList(dataScopeList); + saBaseLoginUser.setPermissionCodeList(permissionCodeList); + saBaseLoginUser.setRoleCodeList(roleCodeList); + // 写入该用户的所有TokenSession + for (String token : tokenList) { + try { + SaSession session = StpUtil.getTokenSessionByToken(token); + if (session != null) { + session.set("loginUser", saBaseLoginUser); + } + } catch (Exception e) { + log.warn(">>> 刷新用户权限缓存时跳过无效token,userId:{},token:{}", userId, token); + } + } + // 刷新预计算表 + this.refreshUserDataScope(userId, dataScopeList); + log.info(">>> 已刷新在线用户权限缓存,userId:{},token数:{}", userId, tokenList.size()); + } } diff --git a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java index 2bf4c380..6aa42340 100644 --- a/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java +++ b/snowy-plugin/snowy-plugin-sys/src/main/java/vip/xiaonuo/sys/modular/user/service/impl/SysUserServiceImpl.java @@ -65,6 +65,7 @@ import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import vip.xiaonuo.auth.api.SaBaseLoginUserApi; import vip.xiaonuo.auth.core.util.StpLoginUserUtil; import vip.xiaonuo.common.cache.CommonCacheOperator; import vip.xiaonuo.common.enums.CommonGenderEnum; @@ -260,6 +261,9 @@ public class SysUserServiceImpl extends ServiceImpl impl @Resource private SysUserPasswordService sysUserPasswordService; + @Resource(name = "loginUserApi") + private SaBaseLoginUserApi loginUserApi; + @Override public SysLoginUser getUserById(String id) { SysUser sysUser = this.getById(id); @@ -1340,6 +1344,8 @@ public class SysUserServiceImpl extends ServiceImpl impl public void grantRole(SysUserGrantRoleParam sysUserGrantRoleParam) { sysRelationService.saveRelationBatchWithClear(sysUserGrantRoleParam.getId(), sysUserGrantRoleParam.getRoleIdList(), SysRelationCategoryEnum.SYS_USER_HAS_ROLE.getValue()); + // 刷新该用户的权限缓存 + loginUserApi.refreshOnlineUserPermission(sysUserGrantRoleParam.getId()); } @Override @@ -1378,6 +1384,8 @@ public class SysUserServiceImpl extends ServiceImpl impl List extJsonList = sysUserGrantResourceParam.getGrantInfoList().stream() .map(JSONUtil::toJsonStr).collect(Collectors.toList()); sysRelationService.saveRelationBatchWithClear(sysUserGrantResourceParam.getId(), menuIdList, SysRelationCategoryEnum.SYS_USER_HAS_RESOURCE.getValue(), extJsonList); + // 刷新该用户的权限缓存 + loginUserApi.refreshOnlineUserPermission(sysUserGrantResourceParam.getId()); } @Override @@ -1399,6 +1407,8 @@ public class SysUserServiceImpl extends ServiceImpl impl .map(JSONUtil::toJsonStr).collect(Collectors.toList()); sysRelationService.saveRelationBatchWithClear(id, apiUrlList, SysRelationCategoryEnum.SYS_USER_HAS_PERMISSION.getValue(), extJsonList); + // 刷新该用户的权限缓存 + loginUserApi.refreshOnlineUserPermission(id); } @Override