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