liriming
2024-10-16 e1cf0b3ad257b06e7bc55bf719d06d98cb20b27a
Merge remote-tracking branch 'origin/master'
已添加1个文件
已修改22个文件
1231 ■■■■■ 文件已修改
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/LoginUser.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/api/controller/ApiIndItemController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/collection/utils/IndSqlUtils.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/item/entity/IndItemAtomEntity.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/item/entity/IndItemCalEntity.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/item/entity/IndItemDerEntity.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/item/entity/IndItemEntity.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/value/dto/QuerySourceValueDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-infra/iailab-module-infra-biz/src/main/java/com/iailab/module/infra/controller/admin/actuator/ActuatorController.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-api/src/main/java/com/iailab/module/system/api/oauth2/OAuth2TokenApi.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/app/AppController.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/auth/AuthController.java 88 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/oauth2/OAuth2OpenController.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/oauth2/vo/open/OAuth2OpenLoginReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/permission/PermissionController.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/convert/auth/AuthConvert.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/app/AppService.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/app/AppServiceImpl.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/permission/MenuService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/permission/MenuServiceImpl.java 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/permission/PermissionService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/permission/PermissionServiceImpl.java 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-module-system/iailab-module-system-biz/src/test/java/com/iailab/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java 674 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
iailab-framework/iailab-common-security/src/main/java/com/iailab/framework/security/core/LoginUser.java
@@ -47,6 +47,10 @@
     * 过期时间
     */
    private LocalDateTime expiresTime;
    /**
     * 访问令牌
     */
    private String accessToken;
    // ========== 上下文 ==========
    /**
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/api/controller/ApiIndItemController.java
@@ -2,6 +2,7 @@
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.object.ConvertUtils;
import com.iailab.framework.tenant.core.context.TenantContextHolder;
import com.iailab.module.data.api.ind.dto.ApiIndItemQueryDTO;
import com.iailab.module.data.api.ind.dto.ApiIndItemValueDTO;
import com.iailab.module.data.ind.collection.IndItemCollector;
@@ -16,6 +17,7 @@
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.security.PermitAll;
import java.util.ArrayList;
import java.util.List;
import static com.iailab.framework.common.pojo.CommonResult.success;
@@ -38,8 +40,16 @@
    @GetMapping("/query-ind/default-value")
    @Operation(summary = "查询指标默认值")
    public CommonResult<List<ApiIndItemValueDTO>> queryIndItemDefaultValue(@RequestParam String itemNo) {
        TenantContextHolder.setTenantId(161L);
        List<IndItemValueVO> list = indItemCollector.queryValue(itemNo);
        return success(ConvertUtils.sourceToTarget(list, ApiIndItemValueDTO.class));
        List<ApiIndItemValueDTO> dtoList = new ArrayList<>();
        list.forEach(item -> {
            ApiIndItemValueDTO dto = new ApiIndItemValueDTO();
            dto.setDataTime(item.getDataTime());
            dto.setDataValue(item.getDataValue().doubleValue());
            dtoList.add(dto);
        });
        return success(dtoList);
    }
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/collection/utils/IndSqlUtils.java
@@ -50,6 +50,7 @@
        if (indItem == null) {
            return result;
        }
        result.setIndItemAtom(indItem);
        IndDataSetDTO dataSet = indDataSetService.getDet(indItem.getDataSet());
        if (dataSet == null) {
            return result;
@@ -94,11 +95,15 @@
        // 拼接SELECT
        StringBuilder selectSql = new StringBuilder();
        if (StringUtils.isNotBlank(indItem.getDimension())){
        if (StringUtils.isNotBlank(indItem.getDimension()) && StringUtils.isNotBlank(result.getIndItemAtom().getStatFunc())){
            selectSql.append(indItem.getDimension());
            selectSql.append(", ");
            selectSql.append(result.getSelectSql());
        }else{
        }
        else if(StringUtils.isBlank(indItem.getDimension()) && StringUtils.isNotBlank(result.getIndItemAtom().getStatFunc())){
            selectSql.append(result.getSelectSql());
        }
        else{
            selectSql.append(result.getSelectSql());
            selectSql.append(", ");
            selectSql.append(indItem.getTimeLabel());
@@ -188,9 +193,9 @@
                whereSql.append(indItem.getTimeLabel());
                whereSql.append(" <= '");
                whereSql.append(DateUtils.format(indItem.getTimeEnd(), PATTERN_MON));
                whereSql.append("' AND '");
                whereSql.append("' AND ");
                whereSql.append(indItem.getTimeLabel());
                whereSql.append("' >= '");
                whereSql.append(" >= '");
                whereSql.append(DateUtils.format(indItem.getTimeStart(), PATTERN_MON));
                whereSql.append("'");
                break;
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/item/entity/IndItemAtomEntity.java
@@ -1,8 +1,6 @@
package com.iailab.module.data.ind.item.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
@@ -32,20 +30,24 @@
    /**
     * 数据源
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String dataSource;
    /**
     * 数据集
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String dataSet;
    /**
     * 使用字段
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String usingField;
    /**
     * 统计方式
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String statFunc;
}
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/item/entity/IndItemCalEntity.java
@@ -1,8 +1,6 @@
package com.iailab.module.data.ind.item.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
@@ -32,5 +30,6 @@
    /**
     * 计算表达式
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String expression;
}
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/item/entity/IndItemDerEntity.java
@@ -1,8 +1,6 @@
package com.iailab.module.data.ind.item.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
@@ -37,11 +35,13 @@
    /**
     * 时间标识
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String timeLabel;
    /**
     * 时间限定
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String timeLimit;
    /**
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/item/entity/IndItemEntity.java
@@ -1,9 +1,6 @@
package com.iailab.module.data.ind.item.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
@@ -55,17 +52,19 @@
    /**
     * 指标精度
     */
    @TableField("`precision`")
    @TableField(value = "`precision`", updateStrategy = FieldStrategy.ALWAYS)
    private Integer precision;
    /**
     * 时间粒度
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String timeGranularity;
    /**
     * 数量单位
     */
    @TableField(updateStrategy = FieldStrategy.ALWAYS)
    private String unit;
    /**
iailab-module-data/iailab-module-data-biz/src/main/java/com/iailab/module/data/ind/value/dto/QuerySourceValueDTO.java
@@ -1,5 +1,6 @@
package com.iailab.module.data.ind.value.dto;
import com.iailab.module.data.ind.item.vo.IndItemAtomVO;
import lombok.Data;
/**
@@ -21,4 +22,6 @@
    private String groupSql;
    private String orderBySql;
    private IndItemAtomVO indItemAtom;
}
iailab-module-infra/iailab-module-infra-biz/src/main/java/com/iailab/module/infra/controller/admin/actuator/ActuatorController.java
对比新文件
@@ -0,0 +1,92 @@
package com.iailab.module.infra.controller.admin.actuator;
import cn.hutool.core.util.NumberUtil;
import com.alibaba.fastjson.JSONObject;
import com.iailab.framework.common.pojo.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.swing.filechooser.FileSystemView;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * @Description: 硬盘内存监控等
 * @author: iailab
 */
@Slf4j
@RestController
@RequestMapping("/infra/actuator")
public class ActuatorController {
    /**
     * 内存详情
     * @return
     * @throws Exception
     */
    @GetMapping("/memory/info")
    public CommonResult<?> getRedisInfo() throws Exception {
        OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean();
        JSONObject operatingSystemJson = JSONObject.parseObject(JSONObject.toJSONString(operatingSystemMXBean));
        long totalPhysicalMemory = operatingSystemJson.getLongValue("totalPhysicalMemorySize");
        long freePhysicalMemory = operatingSystemJson.getLongValue("freePhysicalMemorySize");
        long usedPhysicalMemory = totalPhysicalMemory - freePhysicalMemory;
        Runtime runtime = Runtime.getRuntime();
        Map<String,Number> result = new HashMap<>();
        result.put("memory.physical.total", totalPhysicalMemory);
        result.put("memory.physical.used", freePhysicalMemory);
        result.put("memory.physical.free", usedPhysicalMemory);
        result.put("memory.physical.usage", NumberUtil.div(usedPhysicalMemory, totalPhysicalMemory));
        result.put("memory.runtime.total", runtime.totalMemory());
        result.put("memory.runtime.used", runtime.freeMemory());
        result.put("memory.runtime.max", runtime.totalMemory() - runtime.freeMemory());
        result.put("memory.runtime.free", runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory());
        result.put("memory.runtime.usage", NumberUtil.div(runtime.totalMemory() - runtime.freeMemory(), runtime.totalMemory()));
        return CommonResult.success(result);
    }
    /**
     * @功能:获取磁盘信息
     * @return
     */
    @GetMapping("/disk/info")
    public CommonResult<List<Map<String,Object>>> queryDiskInfo(){
        CommonResult<List<Map<String,Object>>> res = new CommonResult<>();
        try {
            // 当前文件系统类
            FileSystemView fsv = FileSystemView.getFileSystemView();
            // 列出所有windows 磁盘
            File[] fs = File.listRoots();
            log.info("查询磁盘信息:"+fs.length+"个");
            List<Map<String,Object>> list = new ArrayList<>();
            for (int i = 0; i < fs.length; i++) {
                if(fs[i].getTotalSpace()==0) {
                    continue;
                }
                Map<String,Object> map = new HashMap(5);
                map.put("name", fsv.getSystemDisplayName(fs[i]));
                map.put("max", fs[i].getTotalSpace());
                map.put("rest", fs[i].getFreeSpace());
                map.put("restPPT", (fs[i].getTotalSpace()-fs[i].getFreeSpace())*100/fs[i].getTotalSpace());
                list.add(map);
                log.info(map.toString());
            }
            res.setData(list);
            res.success("查询成功");
        } catch (Exception e) {
            res.setMsg("查询失败"+e.getMessage());
        }
        return res;
    }
}
iailab-module-system/iailab-module-system-api/src/main/java/com/iailab/module/system/api/oauth2/OAuth2TokenApi.java
@@ -44,7 +44,7 @@
    @Operation(summary = "刷新访问令牌")
    @Parameters({
        @Parameter(name = "refreshToken", description = "刷新令牌", required = true, example = "haha"),
        @Parameter(name = "clientId", description = "客户端编号", required = true, example = "iailabyuanma")
        @Parameter(name = "clientId", description = "客户端编号", required = true, example = "iailab")
    })
    CommonResult<OAuth2AccessTokenRespDTO> refreshAccessToken(@RequestParam("refreshToken") String refreshToken,
                                                              @RequestParam("clientId") String clientId);
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/app/AppController.java
@@ -10,6 +10,7 @@
import com.iailab.module.system.controller.admin.app.vo.AppPageReqVO;
import com.iailab.module.system.controller.admin.app.vo.AppRespVO;
import com.iailab.module.system.controller.admin.app.vo.AppSaveReqVO;
import com.iailab.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import com.iailab.module.system.dal.dataobject.app.AppDO;
import com.iailab.module.system.service.app.AppService;
import io.swagger.v3.oas.annotations.Operation;
@@ -85,14 +86,14 @@
        return success(BeanUtils.toBean(appDOS, AppRespVO.class));
    }
//    @GetMapping("/getAppMenu")
//    @Operation(summary = "获得应用菜单列表")
//    @PreAuthorize("@ss.hasPermission('system:app-menu:query')")
//    @Parameter(name = "id", description = "ID", required = true, example = "1024")
//    public CommonResult<List<AppRespVO>> getAppMenu(@RequestParam("id") Long id) {
//        List<AppMenuRespDTO> appDOS = appService.getAppMenu(id);
//        return success(BeanUtils.toBean(appDOS, AppRespVO.class));
//    }
    @GetMapping("/getAppMenu")
    @Operation(summary = "获得应用菜单列表")
    @PreAuthorize("@ss.hasPermission('system:app-menu:query')")
    @Parameter(name = "id", description = "ID", required = true, example = "1024")
    public CommonResult<List<AppRespVO>> getAppMenu(@RequestParam("id") Long id) {
        List<AuthPermissionInfoRespVO.MenuVO> appDOS = appService.getAppMenu(id);
        return success(BeanUtils.toBean(appDOS, AppRespVO.class));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出租户 Excel")
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/auth/AuthController.java
@@ -5,16 +5,21 @@
import com.iailab.framework.common.enums.CommonStatusEnum;
import com.iailab.framework.common.enums.UserTypeEnum;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.object.BeanUtils;
import com.iailab.framework.security.config.SecurityProperties;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.module.system.controller.admin.app.vo.AppMenuRespVO;
import com.iailab.module.system.controller.admin.app.vo.AppRespVO;
import com.iailab.module.system.controller.admin.auth.vo.*;
import com.iailab.module.system.controller.admin.permission.vo.menu.MenuListReqVO;
import com.iailab.module.system.controller.admin.permission.vo.menu.MenuRespVO;
import com.iailab.module.system.convert.auth.AuthConvert;
import com.iailab.module.system.dal.dataobject.app.AppDO;
import com.iailab.module.system.dal.dataobject.permission.MenuDO;
import com.iailab.module.system.dal.dataobject.permission.RoleDO;
import com.iailab.module.system.dal.dataobject.user.AdminUserDO;
import com.iailab.module.system.enums.logger.LoginLogTypeEnum;
import com.iailab.module.system.enums.permission.MenuTypeEnum;
import com.iailab.module.system.service.app.AppService;
import com.iailab.module.system.service.auth.AdminAuthService;
import com.iailab.module.system.service.permission.MenuService;
@@ -34,9 +39,8 @@
import javax.annotation.security.PermitAll;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import static com.iailab.framework.common.pojo.CommonResult.success;
import static com.iailab.framework.common.util.collection.CollectionUtils.convertSet;
@@ -121,32 +125,94 @@
        return success(AuthConvert.INSTANCE.convert(user, roles, menuList));
    }
    @GetMapping("/get-app-permission-info")
    @GetMapping("/get-app-permission")
    @Operation(summary = "获取登录用户的app权限信息")
    public CommonResult<AuthPermissionInfoRespVO> getAppPermissionInfo() {
    public CommonResult<List<AppRespVO>> getAppPermission() {
        List<AppRespVO> appList = new ArrayList<>();
        // 1.1 获得用户信息
        AdminUserDO user = userService.getUser(getLoginUserId());
        if (user == null) {
            return success(null);
        }
        // 1.2 获得角色列表
        Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
        if (CollUtil.isEmpty(roleIds)) {
            return success(AuthConvert.INSTANCE.convert(user, Collections.emptyList(), Collections.emptyList()));
            return success(appList);
        }
        List<RoleDO> roles = roleService.getRoleList(roleIds);
        roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
        // 1.3 获得应用菜单列表
        MenuListReqVO reqVO = new MenuListReqVO();
        List<MenuDO> appMenuList = menuService.getAppMenuList(reqVO);
        Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
        List<MenuDO> menuList = menuService.getMenuList(menuIds);
        menuList.retainAll(appMenuList);
        //只要一级菜单,一级菜单即是应用
        menuList = menuList.stream().filter(menu -> menu.getParentId() == 0l).collect(Collectors.toList());
        menuList = menuService.filterDisableMenus(menuList);
        List<Long> ids = menuList.stream().map(MenuDO::getAppId).collect(Collectors.toList());
        List<AppDO> appDOS = appService.selectBatchIds(ids);
        //排序
        Collections.sort(appDOS, Comparator.comparing(AppDO::getOrderNum));
        // 2. 拼接结果返回
        return success(AuthConvert.INSTANCE.convertAppMenu(user, roles, menuList));
        return success(BeanUtils.toBean(appDOS, AppRespVO.class));
    }
    @GetMapping("/get-app-menu-permission")
    @Operation(summary = "获取登录用户的app权限信息")
    public CommonResult<List<AuthPermissionInfoRespVO.MenuVO>> getAppMenuPermission(@RequestParam("id") Long id) {
        List<AuthPermissionInfoRespVO.MenuVO> menuVOS = new ArrayList<>();
        // 1.1 获得用户信息
        AdminUserDO user = userService.getUser(getLoginUserId());
        if (user == null) {
            return success(null);
        }
        // 1.2 获得角色列表
        Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
        if (CollUtil.isEmpty(roleIds)) {
            return success(menuVOS);
        }
        List<RoleDO> roles = roleService.getRoleList(roleIds);
        roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
        // 1.3 获得应用菜单列表
        Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
        List<MenuDO> menuList = menuService.getMenuList(menuIds);
        menuList = menuService.filterDisableMenus(menuList);
        MenuDO menuDO = menuService.getMenuByAppId(id);
        List<MenuDO> children = new LinkedList<>();
        // 遍历每一层
        Collection<Long> parentIds = Collections.singleton(menuDO.getId());
        for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环
            // 查询当前层,所有的子应用菜单
            List<MenuDO> menus = menuService.selectListByParentId(parentIds);
            // 1. 如果没有子菜单,则结束遍历
            if (CollUtil.isEmpty(menus)) {
                break;
            }
            // 2. 如果有子应用菜单,继续遍历
            children.addAll(menus);
            parentIds = convertSet(menus, MenuDO::getId);
        }
        children.retainAll(menuList);
        List<MenuDO> tempChildren = new LinkedList<>();
        //为每一个二级菜单增加一个隐藏父级目录
        children.stream().forEach(menu -> {
            if (menu.getType().equals(MenuTypeEnum.MENU.getType()) && menu.getParentId().equals(menuDO.getId())) {
                MenuDO parentMenu = BeanUtils.toBean(menu, MenuDO.class);
                parentMenu.setId(System.currentTimeMillis() + (int) (Math.random() * (99999 - 10000 + 1)) + 10000);
                parentMenu.setType(MenuTypeEnum.DIR.getType());
                parentMenu.setVisible(true);
                parentMenu.setAlwaysShow(false);
                parentMenu.setParentId(menuDO.getId());
                menu.setParentId(parentMenu.getId());
                tempChildren.add(parentMenu);
                tempChildren.add(menu);
            } else {
                tempChildren.add(menu);
            }
        });
        menuVOS = AuthConvert.INSTANCE.buildMenuTree(tempChildren, menuDO.getId(), menuDO.getPath());
        // 2. 拼接结果返回
        return success(menuVOS);
    }
    // ========== 短信登录相关 ==========
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/oauth2/OAuth2OpenController.java
@@ -11,7 +11,6 @@
import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenLoginReqVO;
import com.iailab.module.system.convert.oauth2.OAuth2OpenConvert;
import com.iailab.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.iailab.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
@@ -85,16 +84,25 @@
    @PostMapping("/token")
    @PermitAll
    @Operation(summary = "获得访问令牌", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用")
    @Parameters({
            @Parameter(name = "grant_type", required = true, description = "授权类型", example = "code"),
            @Parameter(name = "code", description = "授权范围", example = "userinfo.read"),
            @Parameter(name = "redirect_uri", description = "重定向 URI", example = "https://www.baidu.com"),
            @Parameter(name = "state", description = "状态", example = "1"),
            @Parameter(name = "username", example = "tudou"),
            @Parameter(name = "password", example = "cai"), // 多个使用空格分隔
            @Parameter(name = "scope", example = "user_info"),
            @Parameter(name = "refresh_token", example = "123424233"),
    })
    public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request,
                                                                     @RequestBody OAuth2OpenLoginReqVO openLoginReqVO) {
        String code = openLoginReqVO.getCode();
        String scope = openLoginReqVO.getScope();
        String grantType = openLoginReqVO.getGrantType();
        String redirectUri = openLoginReqVO.getRedirectUri();
        String state = openLoginReqVO.getState();
        String username = openLoginReqVO.getUsername();
        String password = openLoginReqVO.getPassword();
        String refreshToken = openLoginReqVO.getRefreshToken();
                                                                     @RequestParam("grant_type") String grantType,
                                                                     @RequestParam(value = "code", required = false) String code, // 授权码模式
                                                                     @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式
                                                                     @RequestParam(value = "state", required = false) String state, // 授权码模式
                                                                     @RequestParam(value = "username", required = false) String username, // 密码模式
                                                                     @RequestParam(value = "password", required = false) String password, // 密码模式
                                                                     @RequestParam(value = "scope", required = false) String scope, // 密码模式
                                                                     @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式
        List<String> scopes = OAuth2Utils.buildScopes(scope);
        // 1.1 校验授权类型
        OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGrantType(grantType);
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/oauth2/vo/open/OAuth2OpenLoginReqVO.java
@@ -21,6 +21,12 @@
@Builder
public class OAuth2OpenLoginReqVO {
//    @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou")
//    private String clientId;
//
//    @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "fan")
//    private String clientSecret;
    @Schema(description = "授权类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "password")
    private String grantType;
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/controller/admin/permission/PermissionController.java
@@ -63,14 +63,14 @@
        return success(true);
    }
    @PostMapping("/assign-role-app-menu")
    @Operation(summary = "赋予角色菜单")
    @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')")
    public CommonResult<Boolean> assignRoleAppMenu(@Validated @RequestBody PermissionAssignRoleMenuReqVO reqVO) {
        // 执行菜单的分配
        permissionService.assignRoleAppMenu(reqVO.getRoleId(), reqVO.getMenuIds());
        return success(true);
    }
//    @PostMapping("/assign-role-app-menu")
//    @Operation(summary = "赋予角色菜单")
//    @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')")
//    public CommonResult<Boolean> assignRoleAppMenu(@Validated @RequestBody PermissionAssignRoleMenuReqVO reqVO) {
//        // 执行菜单的分配
//        permissionService.assignRoleAppMenu(reqVO.getRoleId(), reqVO.getMenuIds());
//        return success(true);
//    }
    @PostMapping("/assign-role-data-scope")
    @Operation(summary = "赋予角色数据权限")
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/convert/auth/AuthConvert.java
@@ -161,7 +161,6 @@
                parentNode.setChildren(new ArrayList<>());
            }
            parentNode.getChildren().add(childNode);
        });
        // 获得到所有的根节点
        List<AuthPermissionInfoRespVO.MenuVO> menuVOS = filterList(treeNodeMap.values(), node -> id.equals(node.getParentId()));
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/app/AppService.java
@@ -1,8 +1,10 @@
package com.iailab.module.system.service.app;
import com.iailab.framework.common.pojo.PageResult;
import com.iailab.module.system.api.app.dto.AppMenuRespDTO;
import com.iailab.module.system.controller.admin.app.vo.AppPageReqVO;
import com.iailab.module.system.controller.admin.app.vo.AppSaveReqVO;
import com.iailab.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import com.iailab.module.system.dal.dataobject.app.AppDO;
import java.util.List;
@@ -28,6 +30,8 @@
    AppDO getAppByTenantId(Long tenantId);
//    List<AppMenuRespDTO> getAppMenu(Long id);
    List<AuthPermissionInfoRespVO.MenuVO> getAppMenu(Long id);
    List<AppDO> selectBatchIds(List<Long> ids);
}
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/app/AppServiceImpl.java
@@ -1,5 +1,6 @@
package com.iailab.module.system.service.app;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.iailab.framework.common.pojo.PageResult;
@@ -7,8 +8,11 @@
import com.iailab.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.iailab.framework.security.core.util.SecurityFrameworkUtils;
import com.iailab.framework.tenant.core.aop.TenantIgnore;
import com.iailab.module.system.api.app.dto.AppMenuRespDTO;
import com.iailab.module.system.controller.admin.app.vo.AppPageReqVO;
import com.iailab.module.system.controller.admin.app.vo.AppSaveReqVO;
import com.iailab.module.system.controller.admin.auth.vo.AuthPermissionInfoRespVO;
import com.iailab.module.system.convert.auth.AuthConvert;
import com.iailab.module.system.dal.dataobject.app.AppDO;
import com.iailab.module.system.dal.dataobject.permission.MenuDO;
import com.iailab.module.system.dal.dataobject.permission.RoleDO;
@@ -22,6 +26,7 @@
import com.iailab.module.system.dal.mysql.tenant.TenantMapper;
import com.iailab.module.system.dal.mysql.tenant.TenantPackageMapper;
import com.iailab.module.system.enums.permission.MenuTypeEnum;
import com.iailab.module.system.service.permission.MenuService;
import com.iailab.module.system.service.permission.PermissionService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
@@ -32,6 +37,7 @@
import java.util.*;
import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.iailab.framework.common.util.collection.CollectionUtils.convertSet;
import static com.iailab.framework.tenant.core.context.TenantContextHolder.getTenantId;
import static com.iailab.module.system.enums.ErrorCodeConstants.MENU_EXISTS_CHILDREN;
import static com.iailab.module.system.enums.ErrorCodeConstants.MENU_NOT_EXISTS;
@@ -65,6 +71,9 @@
    @Resource
    private TenantMapper tenantMapper;
    @Resource
    private MenuService menuService;
    @Override
@@ -124,13 +133,7 @@
        //查询系统应用菜单
//        List<MenuDO> menuDOS = menuMapper.selectList(menuDOLambdaQueryWrapperX);
//        List<AppDO> systemApps = convertMenuToApp(menuDOS);
        //创建一个系统管理应用菜单
        AppDO aDo = new AppDO();
        aDo.setAppType(1);
        aDo.setAppName("系统管理");
        aDo.setOrderNum(0);
        List<AppDO> systemApps = new ArrayList<>();
        systemApps.add(aDo);
        List<AppDO> appDOS = appMapper.selectList();
        //暂时先遍历处理应用菜单和应用类型
        appDOS.stream().forEach(appDO -> {
@@ -157,27 +160,28 @@
        }
    }
//    @Override
//    public List<MenuRespDTO> getAppMenu(Long id) {
//
//        List<MenuDO> children = new LinkedList<>();
//        // 遍历每一层
//        Collection<Long> parentIds = Collections.singleton(id);
//        for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环
//            // 查询当前层,所有的子应用菜单
//            List<MenuDO> menus = menuMapper.selectListByParentId(parentIds);
//            // 1. 如果没有子菜单,则结束遍历
//            if (CollUtil.isEmpty(menus)) {
//                break;
//            }
//            // 2. 如果有子应用菜单,继续遍历
//            children.addAll(menus);
//            parentIds = convertSet(menus, MenuDO::getId);
//        }
//        children = menuService.filterDisableMenus(children);
//        return AuthConvert.INSTANCE.buildMenuTree(id, children);
//
//    }
    @Override
    public List<AuthPermissionInfoRespVO.MenuVO> getAppMenu(Long id) {
        MenuDO menuDO = menuMapper.selectById(id);
        List<MenuDO> children = new LinkedList<>();
        // 遍历每一层
        Collection<Long> parentIds = Collections.singleton(id);
        for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环
            // 查询当前层,所有的子应用菜单
            List<MenuDO> menus = menuMapper.selectListByParentId(parentIds);
            // 1. 如果没有子菜单,则结束遍历
            if (CollUtil.isEmpty(menus)) {
                break;
            }
            // 2. 如果有子应用菜单,继续遍历
            children.addAll(menus);
            parentIds = convertSet(menus, MenuDO::getId);
        }
        children = menuService.filterDisableMenus(children);
        return AuthConvert.INSTANCE.buildMenuTree(children, id, menuDO.getPath());
    }
    private void dealAppMenu(Integer type, AppDO app){
        String loginUserNickname = SecurityFrameworkUtils.getLoginUserNickname();
@@ -192,8 +196,8 @@
            menuDO.setCreator(loginUserNickname);
            menuDO.setCreateTime(app.getCreateTime());
            menuMapper.insert(menuDO);
            //内置租户角色分配菜单
            assignRoleMenu(menuDO.getId(), app.getTenantId());
//            //内置租户角色分配菜单
//            assignRoleMenu(menuDO.getId(), app.getTenantId());
        } else if(type == 2){
            LambdaUpdateWrapper<MenuDO> updateWrapper = new LambdaUpdateWrapper<>();
            updateWrapper.eq(MenuDO::getAppId, app.getId());
@@ -207,8 +211,8 @@
                menuDO.setCreator(loginUserNickname);
                menuDO.setCreateTime(app.getCreateTime());
                menuMapper.insert(menuDO);
                //内置租户角色分配菜单
                assignRoleMenu(menuDO.getId(), app.getTenantId());
//                //内置租户角色分配菜单
//                assignRoleMenu(menuDO.getId(), app.getTenantId());
            }
        } else if(type == 3){
            //删除租户、角色权限
@@ -218,23 +222,25 @@
            menuWrapper.eq(MenuDO::getType, MenuTypeEnum.DIR.getType());
            MenuDO menu = menuMapper.selectOne(menuWrapper);
            TenantDO tenantDO = tenantMapper.selectById(app.getTenantId());
            TenantPackageDO tenantPackageDO = tenantPackageMapper.selectById(tenantDO.getPackageId());
            Set<Long> menuIds = tenantPackageDO.getMenuIds();
            menuIds.remove(menu.getId());
            // 校验是否还有子菜单
            if (menuMapper.selectCountByParentId(menu.getId()) > 0) {
                throw exception(MENU_EXISTS_CHILDREN);
            if(ObjectUtils.isNotEmpty(menu) && ObjectUtils.isNotEmpty(tenantDO)) {
                TenantPackageDO tenantPackageDO = tenantPackageMapper.selectById(tenantDO.getPackageId());
                Set<Long> menuIds = tenantPackageDO.getMenuIds();
                menuIds.remove(menu.getId());
                // 校验是否还有子菜单
                if (menuMapper.selectCountByParentId(menu.getId()) > 0) {
                    throw exception(MENU_EXISTS_CHILDREN);
                }
                // 校验删除的菜单是否存在
                if (menuMapper.selectById(menu.getId()) == null) {
                    throw exception(MENU_NOT_EXISTS);
                }
                // 标记删除
                menuMapper.deleteById(menu.getId());
                // 删除授予给角色的权限
                permissionService.processMenuDeleted(menu.getId());
                //删除菜单
                menuMapper.delete(menuWrapper);
            }
            // 校验删除的菜单是否存在
            if (menuMapper.selectById(menu.getId()) == null) {
                throw exception(MENU_NOT_EXISTS);
            }
            // 标记删除
            menuMapper.deleteById(menu.getId());
            // 删除授予给角色的权限
            permissionService.processMenuDeleted(menu.getId());
            //删除菜单
            menuMapper.delete(menuWrapper);
        }
    }
@@ -269,4 +275,9 @@
        return appDOS;
    }
    public List<AppDO> selectBatchIds(List<Long> ids) {
        List<AppDO> appDOS = appMapper.selectBatchIds(ids);
        return appDOS;
    }
}
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/permission/MenuService.java
@@ -103,6 +103,14 @@
    MenuDO getMenu(Long id);
    /**
     * 根据应用id获得菜单
     *
     * @param id 菜单编号
     * @return 菜单
     */
    MenuDO getMenuByAppId(Long id);
    /**
     * 获得菜单数组
     *
     * @param ids 菜单编号数组
@@ -110,4 +118,11 @@
     */
    List<MenuDO> getMenuList(Collection<Long> ids);
    /**
     * 根据父id查询菜单
     * @param ids
     * @return
     */
    List<MenuDO> selectListByParentId(Collection<Long> ids);
}
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/permission/MenuServiceImpl.java
@@ -2,6 +2,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.iailab.framework.common.enums.CommonStatusEnum;
@@ -12,16 +13,17 @@
import com.iailab.module.system.dal.dataobject.app.AppDO;
import com.iailab.module.system.dal.dataobject.permission.MenuDO;
import com.iailab.module.system.dal.dataobject.permission.RoleDO;
import com.iailab.module.system.dal.dataobject.permission.RoleMenuDO;
import com.iailab.module.system.dal.dataobject.tenant.TenantDO;
import com.iailab.module.system.dal.dataobject.tenant.TenantPackageDO;
import com.iailab.module.system.dal.mysql.permission.MenuMapper;
import com.iailab.module.system.dal.mysql.permission.RoleMenuMapper;
import com.iailab.module.system.dal.redis.RedisKeyConstants;
import com.iailab.module.system.enums.permission.MenuTypeEnum;
import com.iailab.module.system.service.app.AppService;
import com.iailab.module.system.service.tenant.TenantPackageService;
import com.iailab.module.system.service.tenant.TenantService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
@@ -30,10 +32,12 @@
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
import static com.iailab.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
import static com.iailab.framework.common.util.collection.CollectionUtils.convertMap;
import static com.iailab.framework.common.pojo.CommonResult.success;
import static com.iailab.framework.common.util.collection.CollectionUtils.*;
import static com.iailab.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static com.iailab.framework.tenant.core.context.TenantContextHolder.getTenantId;
import static com.iailab.module.system.dal.dataobject.permission.MenuDO.ID_ROOT;
import static com.iailab.module.system.enums.ErrorCodeConstants.*;
@@ -64,6 +68,9 @@
    @Resource
    private RoleService roleService;
    @Resource
    private RoleMenuMapper roleMenuMapper;
    @Override
    @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#createReqVO.permission",
@@ -211,7 +218,22 @@
    public List<MenuDO> getAppMenuList(MenuListReqVO reqVO) {
        // 获取 tenantId
        Long tenantId = getTenantId();
        return menuMapper.selectAppMenuList(tenantId, reqVO);
        List<MenuDO> menuDOS = menuMapper.selectAppMenuList(tenantId, reqVO);
        Set<Long> menuDOIds = menuDOS.stream().map(MenuDO::getId).collect(Collectors.toSet());
        // 获得角色列表
        Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
        List<RoleDO> roles = roleService.getRoleList(roleIds);
        roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
        if (roles.stream().noneMatch(role -> role.getCode().equals("tenant_admin"))) {
            // 获得菜单列表
            Set<Long> menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId));
            //取交集
            menuIds.retainAll(menuDOIds);
            List<MenuDO> menuList = getMenuList(menuIds);
            menuList = filterDisableMenus(menuList);
            return menuList;
        }
        return menuDOS;
    }
    @Override
@@ -227,12 +249,22 @@
    }
    @Override
    public MenuDO getMenuByAppId(Long id) {
        return menuMapper.selectOne(new LambdaQueryWrapper<MenuDO>().eq(MenuDO::getAppId, id).eq(MenuDO::getParentId, 0l));
    }
    @Override
    public List<MenuDO> getMenuList(Collection<Long> ids) {
        // 当 ids 为空时,返回一个空的实例对象
        if (CollUtil.isEmpty(ids)) {
            return Lists.newArrayList();
        }
        return menuMapper.selectBatchIds(ids);
    }
    @Override
    public List<MenuDO> selectListByParentId(Collection<Long> ids) {
        return menuMapper.selectListByParentId(ids);
    }
    /**
@@ -313,14 +345,29 @@
    private void dealPermission(MenuDO menu) {
        Long tenantId = menu.getTenantId();
        RoleDO role = roleService.getTenantAdminRole(tenantId);
        RoleDO tenantRole = roleService.getTenantAdminRole(tenantId);
        TenantDO tenant = tenantService.getTenant(tenantId);
        TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(tenant.getPackageId());
        Set<Long> menuIds = tenantPackage.getMenuIds();
        menuIds.add(menu.getId());
        tenantPackage.setMenuIds(menuIds);
        tenantPackageService.updateTenantPackage(BeanUtils.toBean(tenantPackage, TenantPackageSaveReqVO.class));
        permissionService.assignRoleMenu(role.getId(), menuIds);
        permissionService.assignRoleMenu(tenantRole.getId(), menuIds);
        // 开发者自己创建的应用菜单默认赋权给创建者所拥有的角色
        //查询当前用户所拥有的角色
        Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId());
        List<RoleDO> roles = roleService.getRoleList(roleIds);
        roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色
        roles.removeIf(role -> tenantRole.getId().equals(role.getId())); // 移除租户管理员角色
        if (!roles.isEmpty()) {
            roles.stream().forEach(roleDO -> {
                RoleMenuDO roleMenuDO = new RoleMenuDO();
                roleMenuDO.setMenuId(menu.getId());
                roleMenuDO.setRoleId(roleDO.getId());
                roleMenuDO.setTenantId(tenant.getId());
                roleMenuMapper.insert(roleMenuDO);
            });
        }
    }
}
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/permission/PermissionService.java
@@ -49,7 +49,7 @@
     * @param roleId  角色编号
     * @param menuIds 菜单编号集合
     */
    void assignRoleAppMenu(Long roleId, Set<Long> menuIds);
//    void assignRoleAppMenu(Long roleId, Set<Long> menuIds);
    /**
     * 处理角色删除时,删除关联授权数据
iailab-module-system/iailab-module-system-biz/src/main/java/com/iailab/module/system/service/permission/PermissionServiceImpl.java
@@ -172,34 +172,34 @@
    // ========== 角色-菜单的相关方法  ==========
    @Override
    @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
    @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST,
            allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快
    public void assignRoleAppMenu(Long roleId, Set<Long> menuIds) {
        // 获得角色拥有应用菜单编号
        MenuListReqVO reqVO = new MenuListReqVO();
        List<MenuDO> appMenuList = menuService.getAppMenuList(reqVO);
        Set<Long> appMenuIds = convertSet(appMenuList, MenuDO::getId);
        Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId);
        dbMenuIds.retainAll(appMenuIds);
        // 计算新增和删除的菜单编号
        Set<Long> menuIdList = CollUtil.emptyIfNull(menuIds);
        Collection<Long> createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds);
        Collection<Long> deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList);
        // 执行新增和删除。对于已经授权的菜单,不用做任何处理
        if (CollUtil.isNotEmpty(createMenuIds)) {
            roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> {
                RoleMenuDO entity = new RoleMenuDO();
                entity.setRoleId(roleId);
                entity.setMenuId(menuId);
                return entity;
            }));
        }
        if (CollUtil.isNotEmpty(deleteMenuIds)) {
            roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds);
        }
    }
//    @Override
//    @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换
//    @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST,
//            allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快
//    public void assignRoleAppMenu(Long roleId, Set<Long> menuIds) {
//        // 获得角色拥有应用菜单编号
//        MenuListReqVO reqVO = new MenuListReqVO();
//        List<MenuDO> appMenuList = menuService.getAppMenuList(reqVO);
//        Set<Long> appMenuIds = convertSet(appMenuList, MenuDO::getId);
//        Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId);
//        dbMenuIds.retainAll(appMenuIds);
//        // 计算新增和删除的菜单编号
//        Set<Long> menuIdList = CollUtil.emptyIfNull(menuIds);
//        Collection<Long> createMenuIds = CollUtil.subtract(menuIdList, dbMenuIds);
//        Collection<Long> deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIdList);
//        // 执行新增和删除。对于已经授权的菜单,不用做任何处理
//        if (CollUtil.isNotEmpty(createMenuIds)) {
//            roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> {
//                RoleMenuDO entity = new RoleMenuDO();
//                entity.setRoleId(roleId);
//                entity.setMenuId(menuId);
//                return entity;
//            }));
//        }
//        if (CollUtil.isNotEmpty(deleteMenuIds)) {
//            roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds);
//        }
//    }
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -227,6 +227,10 @@
        if (CollUtil.isEmpty(roleIds)) {
            return Collections.emptySet();
        }
        // 如果是管理员的情况下,获取全部菜单编号
        if (roleService.hasAnySuperAdmin(roleIds)) {
            return convertSet(menuService.getMenuList(), MenuDO::getId);
        }
        return convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId);
    }
iailab-module-system/iailab-module-system-biz/src/test/java/com/iailab/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java
@@ -1,337 +1,337 @@
package com.iailab.module.system.controller.admin.oauth2;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil;
import com.iailab.framework.common.core.KeyValue;
import com.iailab.framework.common.enums.UserTypeEnum;
import com.iailab.framework.common.exception.ErrorCode;
import com.iailab.framework.common.pojo.CommonResult;
import com.iailab.framework.common.util.collection.SetUtils;
import com.iailab.framework.common.util.object.ObjectUtils;
import com.iailab.framework.test.core.ut.BaseMockitoUnitTest;
import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
import com.iailab.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import com.iailab.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
import com.iailab.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
import com.iailab.module.system.enums.oauth2.OAuth2GrantTypeEnum;
import com.iailab.module.system.service.oauth2.OAuth2ApproveService;
import com.iailab.module.system.service.oauth2.OAuth2ClientService;
import com.iailab.module.system.service.oauth2.OAuth2GrantService;
import com.iailab.module.system.service.oauth2.OAuth2TokenService;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import static com.iailab.framework.common.util.collection.SetUtils.asSet;
import static com.iailab.framework.test.core.util.AssertUtils.assertPojoEquals;
import static com.iailab.framework.test.core.util.AssertUtils.assertServiceException;
import static com.iailab.framework.test.core.util.RandomUtils.randomPojo;
import static com.iailab.framework.test.core.util.RandomUtils.randomString;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
 * {@link OAuth2OpenController} 的单元测试
 *
 * @author iailab
 */
public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
    @InjectMocks
    private OAuth2OpenController oauth2OpenController;
    @Mock
    private OAuth2GrantService oauth2GrantService;
    @Mock
    private OAuth2ClientService oauth2ClientService;
    @Mock
    private OAuth2ApproveService oauth2ApproveService;
    @Mock
    private OAuth2TokenService oauth2TokenService;
    @Test
    public void testPostAccessToken_authorizationCode() {
        // 准备参数
        String granType = OAuth2GrantTypeEnum.AUTHORIZATION_CODE.getGrantType();
        String code = randomString();
        String redirectUri = randomString();
        String state = randomString();
        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
        // mock 方法(client)
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), eq(granType), eq(new ArrayList<>()), eq(redirectUri))).thenReturn(client);
        // mock 方法(访问令牌)
        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
                .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS));
        when(oauth2GrantService.grantAuthorizationCodeForAccessToken(eq("test_client_id"),
                eq(code), eq(redirectUri), eq(state))).thenReturn(accessTokenDO);
        // 调用
        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
                code, redirectUri, state, null, null, null, null);
        // 断言
        assertEquals(0, result.getCode());
        assertPojoEquals(accessTokenDO, result.getData());
        assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L));  // 执行过程会过去几毫秒
    }
    @Test
    public void testPostAccessToken_password() {
        // 准备参数
        String granType = OAuth2GrantTypeEnum.PASSWORD.getGrantType();
        String username = randomString();
        String password = randomString();
        String scope = "write read";
        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
        // mock 方法(client)
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"),
                eq(granType), eq(Lists.newArrayList("write", "read")), isNull())).thenReturn(client);
        // mock 方法(访问令牌)
        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
                .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS));
        when(oauth2GrantService.grantPassword(eq(username), eq(password), eq("test_client_id"),
                eq(Lists.newArrayList("write", "read")))).thenReturn(accessTokenDO);
        // 调用
        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
                null, null, null, username, password, scope, null);
        // 断言
        assertEquals(0, result.getCode());
        assertPojoEquals(accessTokenDO, result.getData());
        assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L));  // 执行过程会过去几毫秒
    }
    @Test
    public void testPostAccessToken_refreshToken() {
        // 准备参数
        String granType = OAuth2GrantTypeEnum.REFRESH_TOKEN.getGrantType();
        String refreshToken = randomString();
        String password = randomString();
        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
        // mock 方法(client)
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"),
                eq(granType), eq(Lists.newArrayList()), isNull())).thenReturn(client);
        // mock 方法(访问令牌)
        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
                .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS));
        when(oauth2GrantService.grantRefreshToken(eq(refreshToken), eq("test_client_id"))).thenReturn(accessTokenDO);
        // 调用
        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
                null, null, null, null, password, null, refreshToken);
        // 断言
        assertEquals(0, result.getCode());
        assertPojoEquals(accessTokenDO, result.getData());
        assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L));  // 执行过程会过去几毫秒
    }
    @Test
    public void testPostAccessToken_implicit() {
        // 调用,并断言
        assertServiceException(() -> oauth2OpenController.postAccessToken(null,
                        OAuth2GrantTypeEnum.IMPLICIT.getGrantType(), null, null, null,
                        null, null, null, null),
                new ErrorCode(400, "Token 接口不支持 implicit 授权模式"));
    }
    @Test
    public void testRevokeToken() {
        // 准备参数
        HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret");
        String token = randomString();
        // mock 方法(client)
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id");
        when(oauth2ClientService.validOAuthClientFromCache(eq("demo_client_id"),
                eq("demo_client_secret"), isNull(), isNull(), isNull())).thenReturn(client);
        // mock 方法(移除)
        when(oauth2GrantService.revokeToken(eq("demo_client_id"), eq(token))).thenReturn(true);
        // 调用
        CommonResult<Boolean> result = oauth2OpenController.revokeToken(request, token);
        // 断言
        assertEquals(0, result.getCode());
        assertTrue(result.getData());
    }
    @Test
    public void testCheckToken() {
        // 准备参数
        HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret");
        String token = randomString();
        // mock 方法
        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setUserType(UserTypeEnum.ADMIN.getValue()).setExpiresTime(LocalDateTimeUtil.of(1653485731195L));
        when(oauth2TokenService.checkAccessToken(eq(token))).thenReturn(accessTokenDO);
        // 调用
        CommonResult<OAuth2OpenCheckTokenRespVO> result = oauth2OpenController.checkToken(request, token);
        // 断言
        assertEquals(0, result.getCode());
        assertPojoEquals(accessTokenDO, result.getData());
        assertEquals(1653485731L, result.getData().getExp()); // 执行过程会过去几毫秒
    }
    @Test
    public void testAuthorize() {
        // 准备参数
        String clientId = randomString();
        // mock 方法(client)
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id").setScopes(ListUtil.toList("read", "write", "all"));
        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(client);
        // mock 方法(approve)
        List<OAuth2ApproveDO> approves = asList(
                randomPojo(OAuth2ApproveDO.class).setScope("read").setApproved(true),
                randomPojo(OAuth2ApproveDO.class).setScope("write").setApproved(false));
        when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves);
        // 调用
        CommonResult<OAuth2OpenAuthorizeInfoRespVO> result = oauth2OpenController.authorize(clientId);
        // 断言
        assertEquals(0, result.getCode());
        assertPojoEquals(client, result.getData().getClient());
        assertEquals(new KeyValue<>("read", true), result.getData().getScopes().get(0));
        assertEquals(new KeyValue<>("write", false), result.getData().getScopes().get(1));
        assertEquals(new KeyValue<>("all", false), result.getData().getScopes().get(2));
    }
    @Test
    public void testApproveOrDeny_grantTypeError() {
        // 调用,并断言
        assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null,
                        null, null, null, null),
                new ErrorCode(400, "response_type 参数值只允许 code 和 token"));
    }
    @Test // autoApprove = true,但是不通过
    public void testApproveOrDeny_autoApproveNo() {
        // 准备参数
        String responseType = "code";
        String clientId = randomString();
        String scope = "{\"read\": true, \"write\": false}";
        String redirectUri = randomString();
        String state = randomString();
        // mock 方法
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class);
        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"),
                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
        // 调用
        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
                scope, redirectUri, true, state);
        // 断言
        assertEquals(0, result.getCode());
        assertNull(result.getData());
    }
    @Test // autoApprove = false,但是不通过
    public void testApproveOrDeny_ApproveNo() {
        // 准备参数
        String responseType = "token";
        String clientId = randomString();
        String scope = "{\"read\": true, \"write\": false}";
        String redirectUri = "https://www.baidu.com";
        String state = "test";
        // mock 方法
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class);
        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"),
                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
        // 调用
        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
                scope, redirectUri, false, state);
        // 断言
        assertEquals(0, result.getCode());
        assertEquals("https://www.baidu.com#error=access_denied&error_description=User%20denied%20access&state=test", result.getData());
    }
    @Test // autoApprove = true,通过 + token
    public void testApproveOrDeny_autoApproveWithToken() {
        // 准备参数
        String responseType = "token";
        String clientId = randomString();
        String scope = "{\"read\": true, \"write\": false}";
        String redirectUri = "https://www.baidu.com";
        String state = "test";
        // mock 方法(client)
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null);
        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"),
                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
        // mock 方法(场景一)
        when(oauth2ApproveService.checkForPreApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
                eq(clientId), eq(SetUtils.asSet("read", "write")))).thenReturn(true);
        // mock 方法(访问令牌)
        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
                .setAccessToken("test_access_token").setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30010L, ChronoUnit.MILLIS));
        when(oauth2GrantService.grantImplicit(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
                eq(clientId), eq(ListUtil.toList("read")))).thenReturn(accessTokenDO);
        // 调用
        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
                scope, redirectUri, true, state);
        // 断言
        assertEquals(0, result.getCode());
        assertThat(result.getData(), anyOf( // 29 和 30 都有一定概率,主要是时间计算
                is("https://www.baidu.com#access_token=test_access_token&token_type=bearer&state=test&expires_in=29&scope=read"),
                is("https://www.baidu.com#access_token=test_access_token&token_type=bearer&state=test&expires_in=30&scope=read")
        ));
    }
    @Test // autoApprove = false,通过 + code
    public void testApproveOrDeny_approveWithCode() {
        // 准备参数
        String responseType = "code";
        String clientId = randomString();
        String scope = "{\"read\": true, \"write\": false}";
        String redirectUri = "https://www.baidu.com";
        String state = "test";
        // mock 方法(client)
        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null);
        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"),
                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
        // mock 方法(场景二)
        when(oauth2ApproveService.updateAfterApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId),
                eq(MapUtil.builder(new LinkedHashMap<String, Boolean>()).put("read", true).put("write", false).build())))
                .thenReturn(true);
        // mock 方法(访问令牌)
        String authorizationCode = "test_code";
        when(oauth2GrantService.grantAuthorizationCodeForCode(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
                eq(clientId), eq(ListUtil.toList("read")), eq(redirectUri), eq(state))).thenReturn(authorizationCode);
        // 调用
        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
                scope, redirectUri, false, state);
        // 断言
        assertEquals(0, result.getCode());
        assertEquals("https://www.baidu.com?code=test_code&state=test", result.getData());
    }
    private HttpServletRequest mockRequest(String clientId, String secret) {
        HttpServletRequest request = mock(HttpServletRequest.class);
        when(request.getParameter(eq("client_id"))).thenReturn(clientId);
        when(request.getParameter(eq("client_secret"))).thenReturn(secret);
        return request;
    }
}
//package com.iailab.module.system.controller.admin.oauth2;
//
//import cn.hutool.core.collection.ListUtil;
//import cn.hutool.core.date.LocalDateTimeUtil;
//import cn.hutool.core.map.MapUtil;
//import com.iailab.framework.common.core.KeyValue;
//import com.iailab.framework.common.enums.UserTypeEnum;
//import com.iailab.framework.common.exception.ErrorCode;
//import com.iailab.framework.common.pojo.CommonResult;
//import com.iailab.framework.common.util.collection.SetUtils;
//import com.iailab.framework.common.util.object.ObjectUtils;
//import com.iailab.framework.test.core.ut.BaseMockitoUnitTest;
//import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO;
//import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO;
//import com.iailab.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO;
//import com.iailab.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
//import com.iailab.module.system.dal.dataobject.oauth2.OAuth2ApproveDO;
//import com.iailab.module.system.dal.dataobject.oauth2.OAuth2ClientDO;
//import com.iailab.module.system.enums.oauth2.OAuth2GrantTypeEnum;
//import com.iailab.module.system.service.oauth2.OAuth2ApproveService;
//import com.iailab.module.system.service.oauth2.OAuth2ClientService;
//import com.iailab.module.system.service.oauth2.OAuth2GrantService;
//import com.iailab.module.system.service.oauth2.OAuth2TokenService;
//import org.assertj.core.util.Lists;
//import org.junit.jupiter.api.Test;
//import org.mockito.InjectMocks;
//import org.mockito.Mock;
//
//import javax.servlet.http.HttpServletRequest;
//import java.time.LocalDateTime;
//import java.time.temporal.ChronoUnit;
//import java.util.ArrayList;
//import java.util.LinkedHashMap;
//import java.util.List;
//
//import static com.iailab.framework.common.util.collection.SetUtils.asSet;
//import static com.iailab.framework.test.core.util.AssertUtils.assertPojoEquals;
//import static com.iailab.framework.test.core.util.AssertUtils.assertServiceException;
//import static com.iailab.framework.test.core.util.RandomUtils.randomPojo;
//import static com.iailab.framework.test.core.util.RandomUtils.randomString;
//import static java.util.Arrays.asList;
//import static org.hamcrest.CoreMatchers.anyOf;
//import static org.hamcrest.CoreMatchers.is;
//import static org.hamcrest.MatcherAssert.assertThat;
//import static org.junit.jupiter.api.Assertions.*;
//import static org.mockito.ArgumentMatchers.eq;
//import static org.mockito.ArgumentMatchers.isNull;
//import static org.mockito.Mockito.mock;
//import static org.mockito.Mockito.when;
//
///**
// * {@link OAuth2OpenController} 的单元测试
// *
// * @author iailab
// */
//public class OAuth2OpenControllerTest extends BaseMockitoUnitTest {
//
//    @InjectMocks
//    private OAuth2OpenController oauth2OpenController;
//
//    @Mock
//    private OAuth2GrantService oauth2GrantService;
//    @Mock
//    private OAuth2ClientService oauth2ClientService;
//    @Mock
//    private OAuth2ApproveService oauth2ApproveService;
//    @Mock
//    private OAuth2TokenService oauth2TokenService;
//
//    @Test
//    public void testPostAccessToken_authorizationCode() {
//        // 准备参数
//        String granType = OAuth2GrantTypeEnum.AUTHORIZATION_CODE.getGrantType();
//        String code = randomString();
//        String redirectUri = randomString();
//        String state = randomString();
//        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
//        // mock 方法(client)
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
//        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), eq(granType), eq(new ArrayList<>()), eq(redirectUri))).thenReturn(client);
//
//        // mock 方法(访问令牌)
//        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
//                .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS));
//        when(oauth2GrantService.grantAuthorizationCodeForAccessToken(eq("test_client_id"),
//                eq(code), eq(redirectUri), eq(state))).thenReturn(accessTokenDO);
//
//        // 调用
//        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
//                code, redirectUri, state, null, null, null, null);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertPojoEquals(accessTokenDO, result.getData());
//        assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L));  // 执行过程会过去几毫秒
//    }
//
//    @Test
//    public void testPostAccessToken_password() {
//        // 准备参数
//        String granType = OAuth2GrantTypeEnum.PASSWORD.getGrantType();
//        String username = randomString();
//        String password = randomString();
//        String scope = "write read";
//        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
//        // mock 方法(client)
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
//        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"),
//                eq(granType), eq(Lists.newArrayList("write", "read")), isNull())).thenReturn(client);
//
//        // mock 方法(访问令牌)
//        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
//                .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS));
//        when(oauth2GrantService.grantPassword(eq(username), eq(password), eq("test_client_id"),
//                eq(Lists.newArrayList("write", "read")))).thenReturn(accessTokenDO);
//
//        // 调用
//        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
//                null, null, null, username, password, scope, null);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertPojoEquals(accessTokenDO, result.getData());
//        assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L));  // 执行过程会过去几毫秒
//    }
//
//    @Test
//    public void testPostAccessToken_refreshToken() {
//        // 准备参数
//        String granType = OAuth2GrantTypeEnum.REFRESH_TOKEN.getGrantType();
//        String refreshToken = randomString();
//        String password = randomString();
//        HttpServletRequest request = mockRequest("test_client_id", "test_client_secret");
//        // mock 方法(client)
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id");
//        when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"),
//                eq(granType), eq(Lists.newArrayList()), isNull())).thenReturn(client);
//
//        // mock 方法(访问令牌)
//        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
//                .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS));
//        when(oauth2GrantService.grantRefreshToken(eq(refreshToken), eq("test_client_id"))).thenReturn(accessTokenDO);
//
//        // 调用
//        CommonResult<OAuth2OpenAccessTokenRespVO> result = oauth2OpenController.postAccessToken(request, granType,
//                null, null, null, null, password, null, refreshToken);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertPojoEquals(accessTokenDO, result.getData());
//        assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L));  // 执行过程会过去几毫秒
//    }
//
//    @Test
//    public void testPostAccessToken_implicit() {
//        // 调用,并断言
//        assertServiceException(() -> oauth2OpenController.postAccessToken(null,
//                        OAuth2GrantTypeEnum.IMPLICIT.getGrantType(), null, null, null,
//                        null, null, null, null),
//                new ErrorCode(400, "Token 接口不支持 implicit 授权模式"));
//    }
//
//    @Test
//    public void testRevokeToken() {
//        // 准备参数
//        HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret");
//        String token = randomString();
//        // mock 方法(client)
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id");
//        when(oauth2ClientService.validOAuthClientFromCache(eq("demo_client_id"),
//                eq("demo_client_secret"), isNull(), isNull(), isNull())).thenReturn(client);
//        // mock 方法(移除)
//        when(oauth2GrantService.revokeToken(eq("demo_client_id"), eq(token))).thenReturn(true);
//
//        // 调用
//        CommonResult<Boolean> result = oauth2OpenController.revokeToken(request, token);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertTrue(result.getData());
//    }
//
//    @Test
//    public void testCheckToken() {
//        // 准备参数
//        HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret");
//        String token = randomString();
//        // mock 方法
//        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setUserType(UserTypeEnum.ADMIN.getValue()).setExpiresTime(LocalDateTimeUtil.of(1653485731195L));
//        when(oauth2TokenService.checkAccessToken(eq(token))).thenReturn(accessTokenDO);
//
//        // 调用
//        CommonResult<OAuth2OpenCheckTokenRespVO> result = oauth2OpenController.checkToken(request, token);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertPojoEquals(accessTokenDO, result.getData());
//        assertEquals(1653485731L, result.getData().getExp()); // 执行过程会过去几毫秒
//    }
//
//    @Test
//    public void testAuthorize() {
//        // 准备参数
//        String clientId = randomString();
//        // mock 方法(client)
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id").setScopes(ListUtil.toList("read", "write", "all"));
//        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(client);
//        // mock 方法(approve)
//        List<OAuth2ApproveDO> approves = asList(
//                randomPojo(OAuth2ApproveDO.class).setScope("read").setApproved(true),
//                randomPojo(OAuth2ApproveDO.class).setScope("write").setApproved(false));
//        when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves);
//
//        // 调用
//        CommonResult<OAuth2OpenAuthorizeInfoRespVO> result = oauth2OpenController.authorize(clientId);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertPojoEquals(client, result.getData().getClient());
//        assertEquals(new KeyValue<>("read", true), result.getData().getScopes().get(0));
//        assertEquals(new KeyValue<>("write", false), result.getData().getScopes().get(1));
//        assertEquals(new KeyValue<>("all", false), result.getData().getScopes().get(2));
//    }
//
//    @Test
//    public void testApproveOrDeny_grantTypeError() {
//        // 调用,并断言
//        assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null,
//                        null, null, null, null),
//                new ErrorCode(400, "response_type 参数值只允许 code 和 token"));
//    }
//
//    @Test // autoApprove = true,但是不通过
//    public void testApproveOrDeny_autoApproveNo() {
//        // 准备参数
//        String responseType = "code";
//        String clientId = randomString();
//        String scope = "{\"read\": true, \"write\": false}";
//        String redirectUri = randomString();
//        String state = randomString();
//        // mock 方法
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class);
//        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"),
//                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
//
//        // 调用
//        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
//                scope, redirectUri, true, state);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertNull(result.getData());
//    }
//
//    @Test // autoApprove = false,但是不通过
//    public void testApproveOrDeny_ApproveNo() {
//        // 准备参数
//        String responseType = "token";
//        String clientId = randomString();
//        String scope = "{\"read\": true, \"write\": false}";
//        String redirectUri = "https://www.baidu.com";
//        String state = "test";
//        // mock 方法
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class);
//        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"),
//                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
//
//        // 调用
//        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
//                scope, redirectUri, false, state);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertEquals("https://www.baidu.com#error=access_denied&error_description=User%20denied%20access&state=test", result.getData());
//    }
//
//    @Test // autoApprove = true,通过 + token
//    public void testApproveOrDeny_autoApproveWithToken() {
//        // 准备参数
//        String responseType = "token";
//        String clientId = randomString();
//        String scope = "{\"read\": true, \"write\": false}";
//        String redirectUri = "https://www.baidu.com";
//        String state = "test";
//        // mock 方法(client)
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null);
//        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"),
//                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
//        // mock 方法(场景一)
//        when(oauth2ApproveService.checkForPreApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
//                eq(clientId), eq(SetUtils.asSet("read", "write")))).thenReturn(true);
//        // mock 方法(访问令牌)
//        OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class)
//                .setAccessToken("test_access_token").setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30010L, ChronoUnit.MILLIS));
//        when(oauth2GrantService.grantImplicit(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
//                eq(clientId), eq(ListUtil.toList("read")))).thenReturn(accessTokenDO);
//
//        // 调用
//        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
//                scope, redirectUri, true, state);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertThat(result.getData(), anyOf( // 29 和 30 都有一定概率,主要是时间计算
//                is("https://www.baidu.com#access_token=test_access_token&token_type=bearer&state=test&expires_in=29&scope=read"),
//                is("https://www.baidu.com#access_token=test_access_token&token_type=bearer&state=test&expires_in=30&scope=read")
//        ));
//    }
//
//    @Test // autoApprove = false,通过 + code
//    public void testApproveOrDeny_approveWithCode() {
//        // 准备参数
//        String responseType = "code";
//        String clientId = randomString();
//        String scope = "{\"read\": true, \"write\": false}";
//        String redirectUri = "https://www.baidu.com";
//        String state = "test";
//        // mock 方法(client)
//        OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null);
//        when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"),
//                eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client);
//        // mock 方法(场景二)
//        when(oauth2ApproveService.updateAfterApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId),
//                eq(MapUtil.builder(new LinkedHashMap<String, Boolean>()).put("read", true).put("write", false).build())))
//                .thenReturn(true);
//        // mock 方法(访问令牌)
//        String authorizationCode = "test_code";
//        when(oauth2GrantService.grantAuthorizationCodeForCode(isNull(), eq(UserTypeEnum.ADMIN.getValue()),
//                eq(clientId), eq(ListUtil.toList("read")), eq(redirectUri), eq(state))).thenReturn(authorizationCode);
//
//        // 调用
//        CommonResult<String> result = oauth2OpenController.approveOrDeny(responseType, clientId,
//                scope, redirectUri, false, state);
//        // 断言
//        assertEquals(0, result.getCode());
//        assertEquals("https://www.baidu.com?code=test_code&state=test", result.getData());
//    }
//
//    private HttpServletRequest mockRequest(String clientId, String secret) {
//        HttpServletRequest request = mock(HttpServletRequest.class);
//        when(request.getParameter(eq("client_id"))).thenReturn(clientId);
//        when(request.getParameter(eq("client_secret"))).thenReturn(secret);
//        return request;
//    }
//
//}