+14
-10
@@ -3,6 +3,7 @@ package com.yunhai.userbase.controller.oapi;
|
|||||||
import com.yunhai.platform.utils.model.BoaGroup;
|
import com.yunhai.platform.utils.model.BoaGroup;
|
||||||
import com.yunhai.platform.utils.model.BoaModule;
|
import com.yunhai.platform.utils.model.BoaModule;
|
||||||
import com.yunhai.platform.utils.model.BoaUser;
|
import com.yunhai.platform.utils.model.BoaUser;
|
||||||
|
import com.yunhai.userbase.config.Config;
|
||||||
import com.yunhai.userbase.model.groupauthz.UserPermissionAppVo;
|
import com.yunhai.userbase.model.groupauthz.UserPermissionAppVo;
|
||||||
import com.yunhai.userbase.model.groupauthz.UserPermissionCategoryVo;
|
import com.yunhai.userbase.model.groupauthz.UserPermissionCategoryVo;
|
||||||
import com.yunhai.userbase.model.groupauthz.UserPermissionModuleVo;
|
import com.yunhai.userbase.model.groupauthz.UserPermissionModuleVo;
|
||||||
@@ -20,16 +21,18 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户中心开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /open/api/user/...})。<br><br>
|
* 用户中心开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/user/...})。<br>
|
||||||
* 路径与基础平台 {@code DefaultUserCenterClient} 硬编码的契约严格一致:
|
* <br>
|
||||||
* {@code GET /open/api/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}、
|
* 路径复用 {@link Config#OPEN_API_PATH} 统一前缀,与基础平台 {@code DefaultUserCenterClient}
|
||||||
* {@code /open/api/user/modules?userId=&schId=}。<br>
|
* 硬编码的契约严格一致:
|
||||||
|
* {@code GET /oapi/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}、
|
||||||
|
* {@code /oapi/user/modules?userId=&schId=}。<br>
|
||||||
* 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求,
|
* 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求,
|
||||||
* 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。
|
* 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/open/api/user/")
|
@RequestMapping(Config.OPEN_API_PATH + "user/")
|
||||||
public class OpenBaseController {
|
public class UserController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private UserCenterService userCenterService;
|
private UserCenterService userCenterService;
|
||||||
@@ -68,7 +71,7 @@ public class OpenBaseController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("modules")
|
@GetMapping("modules")
|
||||||
public List<BoaModule> modules(@RequestParam("userId") Integer userId,
|
public List<BoaModule> modules(@RequestParam("userId") Integer userId,
|
||||||
@RequestParam(value = "schId", required = false) Integer schId) {
|
@RequestParam(value = "schId", required = false) Integer schId) {
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
@@ -82,7 +85,8 @@ public class OpenBaseController {
|
|||||||
}
|
}
|
||||||
List<BoaModule> result = new ArrayList<>();
|
List<BoaModule> result = new ArrayList<>();
|
||||||
for (UserPermissionCategoryVo category : tree) {
|
for (UserPermissionCategoryVo category : tree) {
|
||||||
if (category.getApps() == null) continue;
|
if (category.getApps() == null)
|
||||||
|
continue;
|
||||||
for (UserPermissionAppVo appVo : category.getApps()) {
|
for (UserPermissionAppVo appVo : category.getApps()) {
|
||||||
String icon = appVo.getPcIcon();
|
String icon = appVo.getPcIcon();
|
||||||
collectModules(appVo.getModules(), icon, result);
|
collectModules(appVo.getModules(), icon, result);
|
||||||
@@ -95,8 +99,8 @@ public class OpenBaseController {
|
|||||||
* 递归收集已授权模块树到扁平列表,并把应用 PC 图标作为模块的 icon 兜底(BoaModule.icon)。
|
* 递归收集已授权模块树到扁平列表,并把应用 PC 图标作为模块的 icon 兜底(BoaModule.icon)。
|
||||||
*/
|
*/
|
||||||
private void collectModules(List<UserPermissionModuleVo> modules,
|
private void collectModules(List<UserPermissionModuleVo> modules,
|
||||||
String appIcon,
|
String appIcon,
|
||||||
List<BoaModule> collector) {
|
List<BoaModule> collector) {
|
||||||
if (modules == null || modules.isEmpty()) {
|
if (modules == null || modules.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@ public interface SysGroupAppRelRepository extends JpaRepository<SysGroupAppRel,
|
|||||||
|
|
||||||
List<SysGroupAppRel> findByGroupId(Integer groupId);
|
List<SysGroupAppRel> findByGroupId(Integer groupId);
|
||||||
|
|
||||||
|
List<SysGroupAppRel> findByGroupIdIn(List<Integer> groupIds);
|
||||||
|
|
||||||
boolean existsByGroupIdAndAppId(Integer groupId, Integer appId);
|
boolean existsByGroupIdAndAppId(Integer groupId, Integer appId);
|
||||||
|
|
||||||
long countByAppId(Integer appId);
|
long countByAppId(Integer appId);
|
||||||
|
|||||||
@@ -97,6 +97,29 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
|
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
|
||||||
|
|
||||||
|
// ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId) =====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按用户需求:父组(学校组)若开通了应用(GetSchoolAppId返回非空),则其所有子孙组的模块权限分配面板也能看到这些应用(无需子组重复开通)。<br>
|
||||||
|
* 取应用范围=sys_group_app_rel里group_id属于「本组+全部祖先组」的app_id并集;开通不继承,模块权限池继承。<br>
|
||||||
|
* 对 hasModulePermission / UserPermissionTree 不做修改,避免越权(最终用户拿到权限仍需要自己的归属组开通应用+继承到模块权限,双保险)。
|
||||||
|
*/
|
||||||
|
private Set<Integer> collectAppIdsForAuthzScope(Integer groupId) {
|
||||||
|
Map<Integer, SysGroup> allGroupMap = buildAllGroupMap();
|
||||||
|
List<Integer> chain = ancestorChain(groupId, allGroupMap);
|
||||||
|
if (chain == null || chain.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
List<SysGroupAppRel> rels = sysGroupAppRelRepository.findByGroupIdIn(chain);
|
||||||
|
Set<Integer> appIds = new LinkedHashSet<>();
|
||||||
|
for (SysGroupAppRel rel : rels) {
|
||||||
|
if (rel.getAppId() != null) {
|
||||||
|
appIds.add(rel.getAppId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appIds;
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== 功能1:应用开通 =====================
|
// ===================== 功能1:应用开通 =====================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -105,7 +128,6 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
if (!sysGroupRepository.existsById(groupId)) {
|
if (!sysGroupRepository.existsById(groupId)) {
|
||||||
throw new DException("系统组不存在");
|
throw new DException("系统组不存在");
|
||||||
}
|
}
|
||||||
// 系统全量应用 + 该组已开通集合,标记 checked 回显
|
|
||||||
List<ApplicationInfo> apps = applicationInfoRepository.findAll();
|
List<ApplicationInfo> apps = applicationInfoRepository.findAll();
|
||||||
apps.sort(appOrder());
|
apps.sort(appOrder());
|
||||||
Set<Integer> openedAppIds = new HashSet<>();
|
Set<Integer> openedAppIds = new HashSet<>();
|
||||||
@@ -177,29 +199,22 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
if (!sysGroupRepository.existsById(groupId)) {
|
if (!sysGroupRepository.existsById(groupId)) {
|
||||||
throw new DException("系统组不存在");
|
throw new DException("系统组不存在");
|
||||||
}
|
}
|
||||||
// 1. 该组已开通的应用;无记录则右侧不显示任何应用
|
Set<Integer> appIdSet = collectAppIdsForAuthzScope(groupId);
|
||||||
List<SysGroupAppRel> rels = sysGroupAppRelRepository.findByGroupId(groupId);
|
if (appIdSet.isEmpty()) {
|
||||||
if (rels.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
List<Integer> appIds = new ArrayList<>();
|
List<Integer> appIds = new ArrayList<>(appIdSet);
|
||||||
for (SysGroupAppRel rel : rels) {
|
|
||||||
appIds.add(rel.getAppId());
|
|
||||||
}
|
|
||||||
List<ApplicationInfo> apps = applicationInfoRepository.findAllById(appIds);
|
List<ApplicationInfo> apps = applicationInfoRepository.findAllById(appIds);
|
||||||
apps.sort(appOrder());
|
apps.sort(appOrder());
|
||||||
// 2. 已开通应用下的全部模块,按 appId 分桶
|
|
||||||
List<AppModule> allModules = appModuleRepository.findByAppIdIn(appIds);
|
List<AppModule> allModules = appModuleRepository.findByAppIdIn(appIds);
|
||||||
Map<Integer, List<AppModule>> appModuleMap = new HashMap<>();
|
Map<Integer, List<AppModule>> appModuleMap = new HashMap<>();
|
||||||
for (AppModule m : allModules) {
|
for (AppModule m : allModules) {
|
||||||
appModuleMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m);
|
appModuleMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m);
|
||||||
}
|
}
|
||||||
// 3. 权限回显:该组已勾选的模块权限记录
|
|
||||||
Map<Integer, SysGroupModulePerm> permMap = new HashMap<>();
|
Map<Integer, SysGroupModulePerm> permMap = new HashMap<>();
|
||||||
for (SysGroupModulePerm perm : sysGroupModulePermRepository.findByGroupId(groupId)) {
|
for (SysGroupModulePerm perm : sysGroupModulePermRepository.findByGroupId(groupId)) {
|
||||||
permMap.put(perm.getModuleId(), perm);
|
permMap.put(perm.getModuleId(), perm);
|
||||||
}
|
}
|
||||||
// 4. 逐应用组装模块树
|
|
||||||
List<GroupAuthzAppVo> result = new ArrayList<>();
|
List<GroupAuthzAppVo> result = new ArrayList<>();
|
||||||
for (ApplicationInfo app : apps) {
|
for (ApplicationInfo app : apps) {
|
||||||
GroupAuthzAppVo appVo = new GroupAuthzAppVo();
|
GroupAuthzAppVo appVo = new GroupAuthzAppVo();
|
||||||
@@ -290,15 +305,10 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
}
|
}
|
||||||
List<GroupModulePermRequest.ModulePermItem> perms =
|
List<GroupModulePermRequest.ModulePermItem> perms =
|
||||||
request.getPerms() == null ? Collections.emptyList() : request.getPerms();
|
request.getPerms() == null ? Collections.emptyList() : request.getPerms();
|
||||||
// 勾选的模块必须属于该组已开通应用(边界情况:未开通应用的模块权限无效,不允许写入)
|
Set<Integer> appIdSet = collectAppIdsForAuthzScope(groupId);
|
||||||
Set<Integer> validModuleIds = new HashSet<>();
|
Set<Integer> validModuleIds = new HashSet<>();
|
||||||
List<SysGroupAppRel> rels = sysGroupAppRelRepository.findByGroupId(groupId);
|
if (!appIdSet.isEmpty()) {
|
||||||
if (!rels.isEmpty()) {
|
for (AppModule m : appModuleRepository.findByAppIdIn(new ArrayList<>(appIdSet))) {
|
||||||
List<Integer> appIds = new ArrayList<>();
|
|
||||||
for (SysGroupAppRel rel : rels) {
|
|
||||||
appIds.add(rel.getAppId());
|
|
||||||
}
|
|
||||||
for (AppModule m : appModuleRepository.findByAppIdIn(appIds)) {
|
|
||||||
validModuleIds.add(m.getId());
|
validModuleIds.add(m.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,31 +428,22 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
Integer moduleId = item.getModuleId();
|
Integer moduleId = item.getModuleId();
|
||||||
int mode = item.getMode();
|
int mode = item.getMode();
|
||||||
boolean isAll = item.getIsAll() != null && item.getIsAll() == 1;
|
boolean isAll = item.getIsAll() != null && item.getIsAll() == 1;
|
||||||
// 基础校验
|
|
||||||
if (!sysGroupRepository.existsById(groupId)) {
|
if (!sysGroupRepository.existsById(groupId)) {
|
||||||
throw new DException("系统组不存在: " + groupId);
|
throw new DException("系统组不存在: " + groupId);
|
||||||
}
|
}
|
||||||
if (!applicationInfoRepository.existsById(appId)) {
|
if (!applicationInfoRepository.existsById(appId)) {
|
||||||
throw new DException("应用不存在: " + appId);
|
throw new DException("应用不存在: " + appId);
|
||||||
}
|
}
|
||||||
// 操作模式:仅 0=追加 / 2=删除 合法(同 SetSchoolApp,经 IEnum.getByKey 解析)
|
|
||||||
SysModeEnum modeEnum = IEnum.getByKey(SysModeEnum.class, mode);
|
SysModeEnum modeEnum = IEnum.getByKey(SysModeEnum.class, mode);
|
||||||
if (modeEnum == null || (modeEnum != SysModeEnum.APPEND && modeEnum != SysModeEnum.DELETE)) {
|
if (modeEnum == null || (modeEnum != SysModeEnum.APPEND && modeEnum != SysModeEnum.DELETE)) {
|
||||||
throw new DException("不支持的操作模式: " + mode + ",模块授权仅支持 0(追加)/2(删除)");
|
throw new DException("不支持的操作模式: " + mode + ",模块授权仅支持 0(追加)/2(删除)");
|
||||||
}
|
}
|
||||||
// 解析本次要操作的模块集合:moduleId=0 → 该应用下全部模块(展开,不写 module_id=0 伪记录)
|
|
||||||
List<Integer> targetModuleIds = resolveModuleIds(appId, moduleId);
|
List<Integer> targetModuleIds = resolveModuleIds(appId, moduleId);
|
||||||
// isAll 语义(重点):映射 sys_group_module_perm.inherit_flag。
|
Set<Integer> scopeAppIds = collectAppIdsForAuthzScope(groupId);
|
||||||
// isAll=1 → inheritFlag=true:子孙组在 hasModulePermission 判定时沿祖先链自动继承(逻辑继承,不物理写子组)。
|
|
||||||
// isAll=0 → inheritFlag=false:仅本组生效。
|
|
||||||
// —— 与「应用不继承 / 模块可继承」对称,复用现有权限判定,不破坏其他功能。
|
|
||||||
// —— 若需「物理写子组」:将下方 upsert/delete 作用范围由 [groupId] 改为 [groupId + 其全部子孙组] 即可
|
|
||||||
// (子孙组集合可由 sys_group 全量构建 parentId→children 映射后 DFS 得到,参考本类 ancestorChain 的上行写法)。
|
|
||||||
for (Integer mid : targetModuleIds) {
|
for (Integer mid : targetModuleIds) {
|
||||||
if (modeEnum == SysModeEnum.APPEND) {
|
if (modeEnum == SysModeEnum.APPEND) {
|
||||||
// 边界一致:该组须已开通该应用,未开通应用的模块权限无意义
|
if (!scopeAppIds.contains(appId)) {
|
||||||
if (!sysGroupAppRelRepository.existsByGroupIdAndAppId(groupId, appId)) {
|
throw new DException("组及其祖先均未开通应用 " + appId + ",无法授权其模块 " + mid);
|
||||||
throw new DException("组 " + groupId + " 未开通应用 " + appId + ",无法授权其模块 " + mid);
|
|
||||||
}
|
}
|
||||||
SysGroupModulePerm existing = sysGroupModulePermRepository.findByGroupIdAndModuleId(groupId, mid);
|
SysGroupModulePerm existing = sysGroupModulePermRepository.findByGroupIdAndModuleId(groupId, mid);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
@@ -457,7 +458,7 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
perm.setInheritFlag(isAll);
|
perm.setInheritFlag(isAll);
|
||||||
sysGroupModulePermRepository.save(perm);
|
sysGroupModulePermRepository.save(perm);
|
||||||
}
|
}
|
||||||
} else { // DELETE 取消模块授权
|
} else {
|
||||||
sysGroupModulePermRepository.deleteByGroupIdAndModuleIdBulk(groupId, mid);
|
sysGroupModulePermRepository.deleteByGroupIdAndModuleIdBulk(groupId, mid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user