diff --git a/application-webadmin/src/main/java/supie/webadmin/app/controller/CustomizeRouteController.java b/application-webadmin/src/main/java/supie/webadmin/app/controller/CustomizeRouteController.java new file mode 100644 index 0000000000000000000000000000000000000000..7a84489b7d84af6ba2062ae996fdb69dbd58d687 --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/app/controller/CustomizeRouteController.java @@ -0,0 +1,262 @@ +package supie.webadmin.app.controller; + +import io.swagger.annotations.ApiOperation; +import supie.common.log.annotation.OperationLog; +import supie.common.log.model.constant.SysOperationLogType; +import com.github.pagehelper.page.PageMethod; +import supie.webadmin.app.controller.dynamicRoutingAPI.MyDynamicController; +import supie.webadmin.app.vo.*; +import supie.webadmin.app.dto.*; +import supie.webadmin.app.model.*; +import supie.webadmin.app.service.*; +import supie.common.core.object.*; +import supie.common.core.util.*; +import supie.common.core.constant.*; +import supie.common.core.annotation.MyRequestBody; +import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; +import io.swagger.annotations.Api; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +/** + * 自定义动态路由操作控制器类。 + * + * @author rm -rf .bug + * @date 2020-11-12 + */ +@Api(tags = "自定义动态路由管理接口") +@Slf4j +@RestController +@RequestMapping("/admin/app/customizeRoute") +public class CustomizeRouteController { + + @Autowired + private CustomizeRouteService customizeRouteService; + @Autowired + private MyDynamicController myDynamicController; + + /** + * 新增自定义动态路由数据。 + * + * @param customizeRouteDto 新增对象。 + * @return 应答结果对象,包含新增对象主键Id。 + */ + @ApiOperationSupport(ignoreParameters = { + "customizeRouteDto.id", + "customizeRouteDto.searchString", + "customizeRouteDto.updateTimeStart", + "customizeRouteDto.updateTimeEnd", + "customizeRouteDto.createTimeStart", + "customizeRouteDto.createTimeEnd"}) + @OperationLog(type = SysOperationLogType.ADD) + @PostMapping("/add") + public ResponseResult add(@MyRequestBody CustomizeRouteDto customizeRouteDto) { + String errorMessage = MyCommonUtil.getModelValidationError(customizeRouteDto, false); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CustomizeRoute customizeRoute = MyModelUtil.copyTo(customizeRouteDto, CustomizeRoute.class); + customizeRoute = customizeRouteService.saveNew(customizeRoute); + return ResponseResult.success(customizeRoute.getId()); + } + + /** + * 更新自定义动态路由数据。 + * + * @param customizeRouteDto 更新对象。 + * @return 应答结果对象。 + */ + @ApiOperationSupport(ignoreParameters = { + "customizeRouteDto.searchString", + "customizeRouteDto.updateTimeStart", + "customizeRouteDto.updateTimeEnd", + "customizeRouteDto.createTimeStart", + "customizeRouteDto.createTimeEnd"}) + @OperationLog(type = SysOperationLogType.UPDATE) + @PostMapping("/update") + public ResponseResult update(@MyRequestBody CustomizeRouteDto customizeRouteDto) { + String errorMessage = MyCommonUtil.getModelValidationError(customizeRouteDto, true); + if (errorMessage != null) { + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + CustomizeRoute customizeRoute = MyModelUtil.copyTo(customizeRouteDto, CustomizeRoute.class); + CustomizeRoute originalCustomizeRoute = customizeRouteService.getById(customizeRoute.getId()); + if (originalCustomizeRoute == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!customizeRouteService.update(customizeRoute, originalCustomizeRoute)) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + return ResponseResult.success(); + } + + /** + * 删除自定义动态路由数据。 + * + * @param id 删除对象主键Id。 + * @return 应答结果对象。 + */ + @OperationLog(type = SysOperationLogType.DELETE) + @PostMapping("/delete") + public ResponseResult delete(@MyRequestBody Long id) { + if (MyCommonUtil.existBlankArgument(id)) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + return this.doDelete(id); + } + + /** + * 列出符合过滤条件的自定义动态路由列表。 + * + * @param customizeRouteDtoFilter 过滤对象。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @PostMapping("/list") + public ResponseResult> list( + @MyRequestBody CustomizeRouteDto customizeRouteDtoFilter, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + CustomizeRoute customizeRouteFilter = MyModelUtil.copyTo(customizeRouteDtoFilter, CustomizeRoute.class); + String orderBy = MyOrderParam.buildOrderBy(orderParam, CustomizeRoute.class); + List customizeRouteList = + customizeRouteService.getCustomizeRouteListWithRelation(customizeRouteFilter, orderBy); + return ResponseResult.success(MyPageUtil.makeResponseData(customizeRouteList, CustomizeRoute.INSTANCE)); + } + + /** + * 分组列出符合过滤条件的自定义动态路由列表。 + * + * @param customizeRouteDtoFilter 过滤对象。 + * @param groupParam 分组参数。 + * @param orderParam 排序参数。 + * @param pageParam 分页参数。 + * @return 应答结果对象,包含查询结果集。 + */ + @PostMapping("/listWithGroup") + public ResponseResult> listWithGroup( + @MyRequestBody CustomizeRouteDto customizeRouteDtoFilter, + @MyRequestBody(required = true) MyGroupParam groupParam, + @MyRequestBody MyOrderParam orderParam, + @MyRequestBody MyPageParam pageParam) { + String orderBy = MyOrderParam.buildOrderBy(orderParam, CustomizeRoute.class, false); + groupParam = MyGroupParam.buildGroupBy(groupParam, CustomizeRoute.class); + if (groupParam == null) { + return ResponseResult.error( + ErrorCodeEnum.INVALID_ARGUMENT_FORMAT, "数据参数错误,分组参数不能为空!"); + } + if (pageParam != null) { + PageMethod.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + } + CustomizeRoute filter = MyModelUtil.copyTo(customizeRouteDtoFilter, CustomizeRoute.class); + MyGroupCriteria criteria = groupParam.getGroupCriteria(); + List resultList = customizeRouteService.getGroupedCustomizeRouteListWithRelation( + filter, criteria.getGroupSelect(), criteria.getGroupBy(), orderBy); + // 分页连同对象数据转换copy工作,下面的方法一并完成。 + return ResponseResult.success(MyPageUtil.makeResponseData(resultList, CustomizeRoute.INSTANCE)); + } + + /** + * 查看指定自定义动态路由对象详情。 + * + * @param id 指定对象主键Id。 + * @return 应答结果对象,包含对象详情。 + */ + @GetMapping("/view") + public ResponseResult view(@RequestParam Long id) { + CustomizeRoute customizeRoute = customizeRouteService.getByIdWithRelation(id, MyRelationParam.full()); + if (customizeRoute == null) { + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); + } + CustomizeRouteVo customizeRouteVo = CustomizeRoute.INSTANCE.fromModel(customizeRoute); + return ResponseResult.success(customizeRouteVo); + } + + private ResponseResult doDelete(Long id) { + String errorMessage; + // 验证关联Id的数据合法性 + CustomizeRoute originalCustomizeRoute = customizeRouteService.getById(id); + if (originalCustomizeRoute == null) { + // NOTE: 修改下面方括号中的话述 + errorMessage = "数据验证失败,当前 [对象] 并不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + if (!customizeRouteService.remove(id)) { + errorMessage = "数据操作失败,删除的对象不存在,请刷新后重试!"; + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, errorMessage); + } + return ResponseResult.success(); + } + + /** + * 上线API + */ + @ApiOperation("上线API") + @PostMapping("/registerApi") + public ResponseResult registerApi(@MyRequestBody Long id) { + if (id == null) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + CustomizeRoute originalCustomizeRoute = customizeRouteService.getById(id); + if (originalCustomizeRoute == null) { + // NOTE: 修改下面方括号中的话述 + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, + "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"); + } + // 数据存在,注册该路由 + customizeRouteService.registerApi(originalCustomizeRoute); + return ResponseResult.success(); + } + + /** + * 下线API + */ + @ApiOperation("下线API") + @PostMapping("/unregisterApi") + public ResponseResult unregisterApi(@MyRequestBody Long id) { + if (id == null) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + CustomizeRoute originalCustomizeRoute = customizeRouteService.getById(id); + if (originalCustomizeRoute == null) { + // NOTE: 修改下面方括号中的话述 + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, + "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"); + } + // 数据存在,删除该路由 + customizeRouteService.unregisterApi(originalCustomizeRoute); + return ResponseResult.success(); + } + + /** + * 测试动态API + */ + @ApiOperation("测试动态API") + @PostMapping("/testCustomizeRoute") + public ResponseResult testCustomizeRoute(@MyRequestBody Long id, @MyRequestBody Map params) { + if (id == null) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST); + } + CustomizeRoute originalCustomizeRoute = customizeRouteService.getById(id); + if (originalCustomizeRoute == null) { + // NOTE: 修改下面方括号中的话述 + return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST, + "数据验证失败,当前 [数据] 并不存在,请刷新后重试!"); + } + try { + return myDynamicController.performCustomizeRouteBusiness(params, originalCustomizeRoute); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/application-webadmin/src/main/java/supie/webadmin/app/controller/dynamicRoutingAPI/MyDynamicController.java b/application-webadmin/src/main/java/supie/webadmin/app/controller/dynamicRoutingAPI/MyDynamicController.java index 019ff0810a307ea2a41978ca2e9f268dff7dd4de..f6034b526addd643caf11608613b7ead420a2b49 100644 --- a/application-webadmin/src/main/java/supie/webadmin/app/controller/dynamicRoutingAPI/MyDynamicController.java +++ b/application-webadmin/src/main/java/supie/webadmin/app/controller/dynamicRoutingAPI/MyDynamicController.java @@ -1,14 +1,29 @@ package supie.webadmin.app.controller.dynamicRoutingAPI; -import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ResponseBody; -import supie.common.core.annotation.MyRequestBody; +import supie.common.core.constant.ErrorCodeEnum; +import supie.common.core.object.MyPageParam; import supie.common.core.object.ResponseResult; +import supie.webadmin.app.dao.CustomizeRouteMapper; +import supie.webadmin.app.dao.ProjectEngineMapper; +import supie.webadmin.app.model.CustomizeRoute; +import supie.webadmin.app.model.ProjectEngine; +import supie.webadmin.app.service.databasemanagement.Strategy; +import supie.webadmin.app.service.databasemanagement.StrategyFactory; -import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import java.util.*; /** * 描述: @@ -21,21 +36,76 @@ import java.util.Map; @Component public class MyDynamicController { - private static int sum = 0; - -// @ResponseBody -// public String test(String name) { -// sum++; -// log.error("==============》第[" + sum + "]次调用自定义接口《=============="); -// return DateUtil.now() + "<--[" + sum + "]-->" + name; -// } - - public ResponseResult test(@MyRequestBody Map params) { - sum++; - log.error("==============》第[" + sum + "]次调用自定义接口《=============="); - params.put("nowDate", DateUtil.now()); - params.put("sum", sum); - return ResponseResult.success(JSONUtil.toJsonStr(params)); + @Autowired + private CustomizeRouteMapper customizeRouteMapper; + @Autowired + private ProjectEngineMapper projectEngineMapper; + @Autowired + private StrategyFactory strategyFactory; + + /** + * 执行SQL + */ + @ResponseBody + public ResponseResult executeSql(@RequestBody Map params, HttpServletRequest request) { + String url = request.getRequestURI(); + QueryWrapper customizeRouteQueryWrapper = new QueryWrapper<>(); + customizeRouteQueryWrapper.eq("url", url); + CustomizeRoute customizeRoute = customizeRouteMapper.selectOne(customizeRouteQueryWrapper); + return performCustomizeRouteBusiness(params, customizeRoute); + } + + @NotNull + public ResponseResult performCustomizeRouteBusiness(Map params, CustomizeRoute customizeRoute) { + String sqlScript = customizeRoute.getSqlScript(); + List parameterList = JSONUtil.toList(JSONUtil.parseArray(customizeRoute.getParameter()), Parameter.class); + Set paramsKey = params.keySet(); + for (Parameter parameter : parameterList) { + if (parameter.getRequired() && !paramsKey.contains(parameter.getName())) { + return ResponseResult.error(ErrorCodeEnum.ARGUMENT_NULL_EXIST, "缺少[" + parameter.getName() + "]变量!"); + } + String name = "${" + parameter.getName() + "}"; + String defaultValue = params.get(parameter.getName()).toString(); + if (StrUtil.isBlank(defaultValue)) { + defaultValue = parameter.getDefaultValue(); + } + sqlScript = sqlScript.replace(name, defaultValue); + } + ProjectEngine projectEngine = projectEngineMapper.selectByProjectId(customizeRoute.getProjectId()); + Strategy strategy = strategyFactory.getStrategy( + projectEngine.getEngineType(), projectEngine.getEngineHost(), projectEngine.getEnginePort(), + customizeRoute.getDatabaseName(), projectEngine.getEngineUsername(), projectEngine.getEnginePassword()); + Map resultData; + Map resultMap = new HashMap<>(); + if (paramsKey.contains("pageParam")) { + MyPageParam pageParam = JSONUtil.toBean(JSONUtil.toJsonStr(params.get("pageParam")), MyPageParam.class); + PageHelper.startPage(pageParam.getPageNum(), pageParam.getPageSize()); + resultData = strategy.executeSql(sqlScript); + strategy.closeAll(); + if (Boolean.FALSE.equals(resultData.get("isSuccess"))) { + return ResponseResult.error(ErrorCodeEnum.NO_ERROR, + "(" + resultData.get("sql").toString() + ")" + resultData.get("message").toString()); + } + Map queryResultData = (Map) resultData.get("queryResultData"); + List> queryDataList = (List>) queryResultData.get("queryDataList"); + PageInfo> mapPageInfo = new PageInfo<>(queryDataList); + resultMap.put("totalCount", mapPageInfo.getTotal()); + } else { + resultData = strategy.executeSql(sqlScript); + strategy.closeAll(); + } + resultMap.put("url", customizeRoute.getUrl()); + resultMap.put("resultData", resultData); + return ResponseResult.success(resultMap); + } + + @Data + public class Parameter { + private String name; // 参数名 + private String describe; // 描述 + private String type; // 类型 + private Boolean required; // 是否可为空 + private String defaultValue;// 默认值 } } diff --git a/application-webadmin/src/main/java/supie/webadmin/app/dao/CustomizeRouteMapper.java b/application-webadmin/src/main/java/supie/webadmin/app/dao/CustomizeRouteMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..2186718e7cc5c42d5b2a3032dcf7027a2209ebf4 --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/app/dao/CustomizeRouteMapper.java @@ -0,0 +1,57 @@ +package supie.webadmin.app.dao; + +import supie.common.core.annotation.EnableDataPerm; +import supie.common.core.base.dao.BaseDaoMapper; +import supie.webadmin.app.model.CustomizeRoute; +import org.apache.ibatis.annotations.Param; + +import java.util.*; + +/** + * 自定义动态路由数据操作访问接口。 + * + * @author rm -rf .bug + * @date 2020-11-12 + */ +@EnableDataPerm +public interface CustomizeRouteMapper extends BaseDaoMapper { + + /** + * 批量插入对象列表。 + * + * @param customizeRouteList 新增对象列表。 + */ + void insertList(List customizeRouteList); + + /** + * 获取分组计算后的数据对象列表。 + * + * @param customizeRouteFilter 主表过滤对象。 + * @param groupSelect 分组显示字段列表字符串,SELECT从句的参数。 + * @param groupBy 分组字段列表字符串,GROUP BY从句的参数。 + * @param orderBy 排序字符串,ORDER BY从句的参数。 + * @return 对象列表。 + */ + List getGroupedCustomizeRouteList( + @Param("customizeRouteFilter") CustomizeRoute customizeRouteFilter, + @Param("groupSelect") String groupSelect, + @Param("groupBy") String groupBy, + @Param("orderBy") String orderBy); + + /** + * 获取过滤后的对象列表。 + * + * @param customizeRouteFilter 主表过滤对象。 + * @param orderBy 排序字符串,order by从句的参数。 + * @return 对象列表。 + */ + List getCustomizeRouteList( + @Param("customizeRouteFilter") CustomizeRoute customizeRouteFilter, @Param("orderBy") String orderBy); + + /** + * 查询需要注册的路径 + * @return + */ + List queryRegisterApi(); + +} diff --git a/application-webadmin/src/main/java/supie/webadmin/app/dao/ProjectEngineMapper.java b/application-webadmin/src/main/java/supie/webadmin/app/dao/ProjectEngineMapper.java index f824e3a6bd5404de3ca09fc9b80448c641baa1e8..c5e3fe60669742d37d01bb432d594d1fff51bc4f 100644 --- a/application-webadmin/src/main/java/supie/webadmin/app/dao/ProjectEngineMapper.java +++ b/application-webadmin/src/main/java/supie/webadmin/app/dao/ProjectEngineMapper.java @@ -47,4 +47,11 @@ public interface ProjectEngineMapper extends BaseDaoMapper { */ List getProjectEngineList( @Param("projectEngineFilter") ProjectEngine projectEngineFilter, @Param("orderBy") String orderBy); + + /** + * 通过项目ID查询存算引擎 + * @param projectId + * @return + */ + ProjectEngine selectByProjectId(@Param("projectId") Long projectId); } diff --git a/application-webadmin/src/main/java/supie/webadmin/app/dao/mapper/CustomizeRouteMapper.xml b/application-webadmin/src/main/java/supie/webadmin/app/dao/mapper/CustomizeRouteMapper.xml new file mode 100644 index 0000000000000000000000000000000000000000..f8f25a89e1014153348f1133c1f42ac0b81cc0fe --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/app/dao/mapper/CustomizeRouteMapper.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO sdt_customize_route + (id, + name, + update_time, + create_time, + create_user_id, + update_user_id, + data_user_id, + data_dept_id, + is_delete, + route_describe, + url, + request_type, + state, + project_id, + database_name, + sql_script, + parameter, + process_id) + VALUES + + (#{item.id}, + #{item.name}, + #{item.updateTime}, + #{item.createTime}, + #{item.createUserId}, + #{item.updateUserId}, + #{item.dataUserId}, + #{item.dataDeptId}, + #{item.isDelete}, + #{item.routeDescribe}, + #{item.url}, + #{item.requestType}, + #{item.state}, + #{item.projectId}, + #{item.databaseName}, + #{item.sqlScript}, + #{item.parameter}, + #{item.processId}) + + + + + + + + AND sdt_customize_route.is_delete = ${@supie.common.core.constant.GlobalDeletedFlag@NORMAL} + + + + + + + AND sdt_customize_route.id = #{customizeRouteFilter.id} + + + + AND sdt_customize_route.name LIKE #{safeCustomizeRouteName} + + + AND sdt_customize_route.update_time >= #{customizeRouteFilter.updateTimeStart} + + + AND sdt_customize_route.update_time <= #{customizeRouteFilter.updateTimeEnd} + + + AND sdt_customize_route.create_time >= #{customizeRouteFilter.createTimeStart} + + + AND sdt_customize_route.create_time <= #{customizeRouteFilter.createTimeEnd} + + + AND sdt_customize_route.create_user_id = #{customizeRouteFilter.createUserId} + + + AND sdt_customize_route.update_user_id = #{customizeRouteFilter.updateUserId} + + + AND sdt_customize_route.data_user_id = #{customizeRouteFilter.dataUserId} + + + AND sdt_customize_route.data_dept_id = #{customizeRouteFilter.dataDeptId} + + + + AND sdt_customize_route.route_describe LIKE #{safeCustomizeRouteDescribe} + + + + AND sdt_customize_route.url LIKE #{safeCustomizeRouteUrl} + + + AND sdt_customize_route.request_type = #{customizeRouteFilter.requestType} + + + AND sdt_customize_route.state = #{customizeRouteFilter.state} + + + AND sdt_customize_route.project_id = #{customizeRouteFilter.projectId} + + + + AND sdt_customize_route.database_name LIKE #{safeCustomizeRouteDatabaseName} + + + + AND sdt_customize_route.sql_script LIKE #{safeCustomizeRouteSqlScript} + + + + AND sdt_customize_route.parameter LIKE #{safeCustomizeRouteParameter} + + + AND sdt_customize_route.process_id = #{customizeRouteFilter.processId} + + + + AND CONCAT(IFNULL(sdt_customize_route.name,''), IFNULL(sdt_customize_route.route_describe,''), IFNULL(sdt_customize_route.url,''), IFNULL(sdt_customize_route.database_name,''), IFNULL(sdt_customize_route.sql_script,''), IFNULL(sdt_customize_route.parameter,'')) LIKE #{safeCustomizeRouteSearchString} + + + + + + + + + + diff --git a/application-webadmin/src/main/java/supie/webadmin/app/dao/mapper/ProjectEngineMapper.xml b/application-webadmin/src/main/java/supie/webadmin/app/dao/mapper/ProjectEngineMapper.xml index 05bd743da80db76222de43d10b17c94140a39754..d37d5bf7e434d930cc90a4505f937e88090720a4 100644 --- a/application-webadmin/src/main/java/supie/webadmin/app/dao/mapper/ProjectEngineMapper.xml +++ b/application-webadmin/src/main/java/supie/webadmin/app/dao/mapper/ProjectEngineMapper.xml @@ -153,4 +153,13 @@ ORDER BY ${orderBy} + + + diff --git a/application-webadmin/src/main/java/supie/webadmin/app/dto/CustomizeRouteDto.java b/application-webadmin/src/main/java/supie/webadmin/app/dto/CustomizeRouteDto.java new file mode 100644 index 0000000000000000000000000000000000000000..0d518feb453aad4f7ac8cc84f4af86bcf9ecbc64 --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/app/dto/CustomizeRouteDto.java @@ -0,0 +1,130 @@ +package supie.webadmin.app.dto; + +import supie.common.core.validator.UpdateGroup; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.*; + +/** + * CustomizeRouteDto对象。 + * + * @author rm -rf .bug + * @date 2020-11-12 + */ +@ApiModel("CustomizeRouteDto对象") +@Data +public class CustomizeRouteDto { + + /** + * 主键ID。 + */ + @ApiModelProperty(value = "主键ID", required = true) + @NotNull(message = "数据验证失败,主键ID不能为空!", groups = {UpdateGroup.class}) + private Long id; + + /** + * 名称。 + */ + @ApiModelProperty(value = "名称") + private String name; + + /** + * 数据所属人。 + */ + @ApiModelProperty(value = "数据所属人") + private Long dataUserId; + + /** + * 数据所属部门。 + */ + @ApiModelProperty(value = "数据所属部门") + private Long dataDeptId; + + /** + * 描述。 + */ + @ApiModelProperty(value = "描述") + private String routeDescribe; + + /** + * 地址(不可重复)。 + */ + @ApiModelProperty(value = "地址(不可重复)", required = true) + @NotBlank(message = "数据验证失败,地址(不可重复)不能为空!") + private String url; + + /** + * 请求类型(1:GET。2:POST。默认为POST)。 + */ + @ApiModelProperty(value = "请求类型(1:GET。2:POST。默认为POST)") + private Integer requestType; + + /** + * 状态(1:上线。-1:下线)。 + */ + @ApiModelProperty(value = "状态(1:上线。-1:下线)") + private Integer state; + + /** + * 存算引擎项目ID。 + */ + @ApiModelProperty(value = "存算引擎项目ID") + private Long projectId; + + /** + * 目标数据库名称。 + */ + @ApiModelProperty(value = "目标数据库名称") + private String databaseName; + + /** + * SQL语句。 + */ + @ApiModelProperty(value = "SQL语句") + private String sqlScript; + + /** + * 参数集(JSON字符串形式存储)。 + */ + @ApiModelProperty(value = "参数集(JSON字符串形式存储)") + private String parameter; + + /** + * 业务规程ID。 + */ + @ApiModelProperty(value = "业务规程ID") + private Long processId; + + /** + * updateTime 范围过滤起始值(>=)。 + */ + @ApiModelProperty(value = "updateTime 范围过滤起始值(>=)") + private String updateTimeStart; + + /** + * updateTime 范围过滤结束值(<=)。 + */ + @ApiModelProperty(value = "updateTime 范围过滤结束值(<=)") + private String updateTimeEnd; + + /** + * createTime 范围过滤起始值(>=)。 + */ + @ApiModelProperty(value = "createTime 范围过滤起始值(>=)") + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + */ + @ApiModelProperty(value = "createTime 范围过滤结束值(<=)") + private String createTimeEnd; + + /** + * name / routeDescribe / url / database_name / sql_script / parameter LIKE搜索字符串。 + */ + @ApiModelProperty(value = "LIKE模糊搜索字符串") + private String searchString; +} diff --git a/application-webadmin/src/main/java/supie/webadmin/app/model/CustomizeRoute.java b/application-webadmin/src/main/java/supie/webadmin/app/model/CustomizeRoute.java new file mode 100644 index 0000000000000000000000000000000000000000..bf8e336a98f0ddbb04979d714789dbdbee2b2ef7 --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/app/model/CustomizeRoute.java @@ -0,0 +1,137 @@ +package supie.webadmin.app.model; + +import com.baomidou.mybatisplus.annotation.*; +import supie.common.core.util.MyCommonUtil; +import supie.common.core.annotation.*; +import supie.common.core.base.model.BaseModel; +import supie.common.core.base.mapper.BaseModelMapper; +import supie.webadmin.app.vo.CustomizeRouteVo; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.mapstruct.*; +import org.mapstruct.factory.Mappers; + +/** + * CustomizeRoute实体对象。 + * + * @author rm -rf .bug + * @date 2020-11-12 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "sdt_customize_route") +public class CustomizeRoute extends BaseModel { + + /** + * 主键ID。 + */ + @TableId(value = "id") + private Long id; + + /** + * 名称。 + */ + private String name; + + /** + * 数据所属人。 + */ + @UserFilterColumn + private Long dataUserId; + + /** + * 数据所属部门。 + */ + @DeptFilterColumn + private Long dataDeptId; + + /** + * 逻辑删除标记字段(1: 正常 -1: 已删除)。 + */ + @TableLogic + private Integer isDelete; + + /** + * 描述。 + */ + private String routeDescribe; + + /** + * 地址(不可重复)。 + */ + private String url; + + /** + * 请求类型(1:GET。2:POST。默认为POST)。 + */ + private Integer requestType; + + /** + * 状态(1:上线。-1:下线)。 + */ + private Integer state; + + /** + * 存算引擎项目ID。 + */ + private Long projectId; + + /** + * 目标数据库名称。 + */ + private String databaseName; + + /** + * SQL语句。 + */ + private String sqlScript; + + /** + * 参数集(JSON字符串形式存储)。 + */ + private String parameter; + + /** + * 业务规程ID。 + */ + private Long processId; + + /** + * updateTime 范围过滤起始值(>=)。 + */ + @TableField(exist = false) + private String updateTimeStart; + + /** + * updateTime 范围过滤结束值(<=)。 + */ + @TableField(exist = false) + private String updateTimeEnd; + + /** + * createTime 范围过滤起始值(>=)。 + */ + @TableField(exist = false) + private String createTimeStart; + + /** + * createTime 范围过滤结束值(<=)。 + */ + @TableField(exist = false) + private String createTimeEnd; + + /** + * name / routeDescribe / url / database_name / sql_script / parameter LIKE搜索字符串。 + */ + @TableField(exist = false) + private String searchString; + + public void setSearchString(String searchString) { + this.searchString = MyCommonUtil.replaceSqlWildcard(searchString); + } + + @Mapper + public interface CustomizeRouteModelMapper extends BaseModelMapper { + } + public static final CustomizeRouteModelMapper INSTANCE = Mappers.getMapper(CustomizeRouteModelMapper.class); +} diff --git a/application-webadmin/src/main/java/supie/webadmin/app/service/CustomizeRouteService.java b/application-webadmin/src/main/java/supie/webadmin/app/service/CustomizeRouteService.java new file mode 100644 index 0000000000000000000000000000000000000000..61dc0e46b45f435eb5faecb4614d27d43da7da5b --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/app/service/CustomizeRouteService.java @@ -0,0 +1,99 @@ +package supie.webadmin.app.service; + +import supie.webadmin.app.model.*; +import supie.common.core.base.service.IBaseService; + +import java.util.*; + +/** + * 自定义动态路由数据操作服务接口。 + * + * @author rm -rf .bug + * @date 2020-11-12 + */ +public interface CustomizeRouteService extends IBaseService { + + /** + * 保存新增对象。 + * + * @param customizeRoute 新增对象。 + * @return 返回新增对象。 + */ + CustomizeRoute saveNew(CustomizeRoute customizeRoute); + + /** + * 利用数据库的insertList语法,批量插入对象列表。 + * + * @param customizeRouteList 新增对象列表。 + */ + void saveNewBatch(List customizeRouteList); + + /** + * 更新数据对象。 + * + * @param customizeRoute 更新的对象。 + * @param originalCustomizeRoute 原有数据对象。 + * @return 成功返回true,否则false。 + */ + boolean update(CustomizeRoute customizeRoute, CustomizeRoute originalCustomizeRoute); + + /** + * 删除指定数据。 + * + * @param id 主键Id。 + * @return 成功返回true,否则false。 + */ + boolean remove(Long id); + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getCustomizeRouteListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getCustomizeRouteList(CustomizeRoute filter, String orderBy); + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getCustomizeRouteList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + List getCustomizeRouteListWithRelation(CustomizeRoute filter, String orderBy); + + /** + * 获取分组过滤后的数据查询结果,以及关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * + * @param filter 过滤对象。 + * @param groupSelect 分组显示列表参数。位于SQL语句SELECT的后面。 + * @param groupBy 分组参数。位于SQL语句的GROUP BY后面。 + * @param orderBy 排序字符串,ORDER BY从句的参数。 + * @return 分组过滤结果集。 + */ + List getGroupedCustomizeRouteListWithRelation( + CustomizeRoute filter, String groupSelect, String groupBy, String orderBy); + + /** + * 上线 API + * + * @param originalCustomizeRoute 原始自定义路线 + * @author 王立宏 + * @date 2023/11/16 05:06 + */ + void registerApi(CustomizeRoute originalCustomizeRoute); + + /** + * 下线 API + * + * @param originalCustomizeRoute 原始自定义路线 + * @author 王立宏 + * @date 2023/11/16 05:07 + */ + void unregisterApi(CustomizeRoute originalCustomizeRoute); + +} diff --git a/application-webadmin/src/main/java/supie/webadmin/app/service/databasemanagement/Strategy.java b/application-webadmin/src/main/java/supie/webadmin/app/service/databasemanagement/Strategy.java index 835863b59e50ba08191f3740ccf00af20985c574..86abdf157a9e11982f776ccd205f5459afd01b0f 100644 --- a/application-webadmin/src/main/java/supie/webadmin/app/service/databasemanagement/Strategy.java +++ b/application-webadmin/src/main/java/supie/webadmin/app/service/databasemanagement/Strategy.java @@ -38,6 +38,13 @@ public interface Strategy { */ List> queryTableFields(String databaseName, String tableName); + /** + * 执行单条SQL语句 + * @param sqlScript + * @return + */ + Map executeSql(String sqlScript); + /** * 执行位置数量的SQL * @param sql 字符(会将SQl以“;”切开成List来执行,结果也会按照语句的顺序来显示) diff --git a/application-webadmin/src/main/java/supie/webadmin/app/service/databasemanagement/strategyImpl/BaseDataSource.java b/application-webadmin/src/main/java/supie/webadmin/app/service/databasemanagement/strategyImpl/BaseDataSource.java index 923ffbf704f52a65a6b7631b02134d556b9649ed..53b0e702a28d67abb2e02c77b010a620fc1b1253 100644 --- a/application-webadmin/src/main/java/supie/webadmin/app/service/databasemanagement/strategyImpl/BaseDataSource.java +++ b/application-webadmin/src/main/java/supie/webadmin/app/service/databasemanagement/strategyImpl/BaseDataSource.java @@ -86,6 +86,72 @@ public class BaseDataSource { close(connection); } + /** + * 执行单条SQL语句 + * + * @param sqlScript + * @return + */ + public Map executeSql(String sqlScript) { + Map resultMapData = new HashMap<>(); + try { + Statement statement = connection.createStatement(); + resultMapData.put("sql", sqlScript); + try { + boolean result = statement.execute(sqlScript); + if (result) { + ResultSet resultSet = statement.getResultSet(); // 查询结果 + ResultSetMetaData metaData = resultSet.getMetaData(); + Map queryResultData = new LinkedHashMap<>(); + + LinkedList fieldList = null; + LinkedList> queryDataList = new LinkedList<>(); + while (resultSet.next()) { + // 获取字段数量 + int columnCount = metaData.getColumnCount(); + // 遍历每个字段 + Map queryDataMap = new LinkedHashMap<>(); + Boolean setFieldList = false; + if (fieldList == null) { + fieldList = new LinkedList<>(); + setFieldList = true; + } + for (int i = 1; i <= columnCount; i++) { + // 获取字段名 + String columnName = metaData.getColumnName(i); + if (StrUtil.isBlankIfStr(columnName)) continue; + if (StrUtil.isBlankIfStr(resultSet)) { + throw new RuntimeException("resultSet为空"); + } + // 获取字段值 + Object columnValue = resultSet.getObject(columnName); + // 存入字段名和字段值 + if (setFieldList) fieldList.add(columnName); + queryDataMap.put(columnName, columnValue); + } + queryDataList.add(queryDataMap); + } + resultSet.close(); + queryResultData.put("fieldList", fieldList); + queryResultData.put("queryDataList", queryDataList); + resultMapData.put("queryResultData", queryResultData); + } else { + int affectedDataNumber = statement.getUpdateCount(); // 影响的行数 + resultMapData.put("updateResultData", affectedDataNumber); + } + resultMapData.put("isSuccess", true); + resultMapData.put("message", "SUCCESS"); + } catch (SQLException e) { + resultMapData.put("isSuccess", false); + resultMapData.put("message", e.getMessage()); + } + statement.close(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + return resultMapData; + } + /** * 执行位置数量的SQL * @param sql 字符(会将SQl以“;”切开成List来执行,结果也会按照语句的顺序来显示) diff --git a/application-webadmin/src/main/java/supie/webadmin/app/service/impl/CustomizeRouteServiceImpl.java b/application-webadmin/src/main/java/supie/webadmin/app/service/impl/CustomizeRouteServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..13c9e29ac2c31a680b6ed7a86fb12684bd16bcc9 --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/app/service/impl/CustomizeRouteServiceImpl.java @@ -0,0 +1,275 @@ +package supie.webadmin.app.service.impl; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import supie.common.core.util.ApplicationContextHolder; +import supie.webadmin.app.controller.dynamicRoutingAPI.MyDynamicController; +import supie.webadmin.app.service.*; +import supie.webadmin.app.dao.*; +import supie.webadmin.app.model.*; +import supie.common.core.base.dao.BaseDaoMapper; +import supie.common.core.constant.GlobalDeletedFlag; +import supie.common.core.object.MyRelationParam; +import supie.common.core.base.service.BaseService; +import supie.common.core.util.MyModelUtil; +import supie.common.sequence.wrapper.IdGeneratorWrapper; +import com.github.pagehelper.Page; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; +import java.util.*; + +/** + * 自定义动态路由数据操作服务类。 + * + * @author rm -rf .bug + * @date 2020-11-12 + */ +@Slf4j +@Service("customizeRouteService") +public class CustomizeRouteServiceImpl extends BaseService implements CustomizeRouteService, ApplicationRunner { + + private static RequestMappingHandlerMapping requestMappingHandlerMapping = null; + + @Autowired + private CustomizeRouteMapper customizeRouteMapper; + @Autowired + private IdGeneratorWrapper idGenerator; + + /** + * 返回当前Service的主表Mapper对象。 + * + * @return 主表Mapper对象。 + */ + @Override + protected BaseDaoMapper mapper() { + return customizeRouteMapper; + } + + /** + * 保存新增对象。 + * + * @param customizeRoute 新增对象。 + * @return 返回新增对象。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public CustomizeRoute saveNew(CustomizeRoute customizeRoute) { + customizeRouteMapper.insert(this.buildDefaultValue(customizeRoute)); + return customizeRoute; + } + + /** + * 利用数据库的insertList语法,批量插入对象列表。 + * + * @param customizeRouteList 新增对象列表。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public void saveNewBatch(List customizeRouteList) { + if (CollUtil.isNotEmpty(customizeRouteList)) { + customizeRouteList.forEach(this::buildDefaultValue); + customizeRouteMapper.insertList(customizeRouteList); + } + } + + /** + * 更新数据对象。 + * + * @param customizeRoute 更新的对象。 + * @param originalCustomizeRoute 原有数据对象。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean update(CustomizeRoute customizeRoute, CustomizeRoute originalCustomizeRoute) { + MyModelUtil.fillCommonsForUpdate(customizeRoute, originalCustomizeRoute); + // 这里重点提示,在执行主表数据更新之前,如果有哪些字段不支持修改操作,请用原有数据对象字段替换当前数据字段。 + UpdateWrapper uw = this.createUpdateQueryForNullValue(customizeRoute, customizeRoute.getId()); + return customizeRouteMapper.update(customizeRoute, uw) == 1; + } + + /** + * 删除指定数据。 + * + * @param id 主键Id。 + * @return 成功返回true,否则false。 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public boolean remove(Long id) { + return customizeRouteMapper.deleteById(id) == 1; + } + + /** + * 获取单表查询结果。由于没有关联数据查询,因此在仅仅获取单表数据的场景下,效率更高。 + * 如果需要同时获取关联数据,请移步(getCustomizeRouteListWithRelation)方法。 + * + * @param filter 过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getCustomizeRouteList(CustomizeRoute filter, String orderBy) { + return customizeRouteMapper.getCustomizeRouteList(filter, orderBy); + } + + /** + * 获取主表的查询结果,以及主表关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * 该查询会涉及到一对一从表的关联过滤,或一对多从表的嵌套关联过滤,因此性能不如单表过滤。 + * 如果仅仅需要获取主表数据,请移步(getCustomizeRouteList),以便获取更好的查询性能。 + * + * @param filter 主表过滤对象。 + * @param orderBy 排序参数。 + * @return 查询结果集。 + */ + @Override + public List getCustomizeRouteListWithRelation(CustomizeRoute filter, String orderBy) { + List resultList = customizeRouteMapper.getCustomizeRouteList(filter, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + /** + * 获取分组过滤后的数据查询结果,以及关联的字典数据和一对一从表数据,以及一对一从表的字典数据。 + * + * @param filter 过滤对象。 + * @param groupSelect 分组显示列表参数。位于SQL语句SELECT的后面。 + * @param groupBy 分组参数。位于SQL语句的GROUP BY后面。 + * @param orderBy 排序字符串,ORDER BY从句的参数。 + * @return 分组过滤结果集。 + */ + @Override + public List getGroupedCustomizeRouteListWithRelation( + CustomizeRoute filter, String groupSelect, String groupBy, String orderBy) { + List resultList = + customizeRouteMapper.getGroupedCustomizeRouteList(filter, groupSelect, groupBy, orderBy); + // 在缺省生成的代码中,如果查询结果resultList不是Page对象,说明没有分页,那么就很可能是数据导出接口调用了当前方法。 + // 为了避免一次性的大量数据关联,规避因此而造成的系统运行性能冲击,这里手动进行了分批次读取,开发者可按需修改该值。 + int batchSize = resultList instanceof Page ? 0 : 1000; + // NOTE: 这里只是包含了关联数据,聚合计算数据没有包含。 + // 主要原因是,由于聚合字段通常被视为普通字段使用,不会在group by的从句中出现,语义上也不会在此关联。 + this.buildRelationForDataList(resultList, MyRelationParam.normal(), batchSize); + return resultList; + } + + private CustomizeRoute buildDefaultValue(CustomizeRoute customizeRoute) { + if (customizeRoute.getId() == null) { + customizeRoute.setId(idGenerator.nextLongId()); + } + MyModelUtil.fillCommonsForInsert(customizeRoute); + customizeRoute.setIsDelete(GlobalDeletedFlag.NORMAL); + return customizeRoute; + } + + /** + * 程序启动成功后注册数据库中状态为上线的路径 + * @param args + * @throws Exception + */ + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("启动完成,开始注册动态路由!"); + if (requestMappingHandlerMapping == null) { + requestMappingHandlerMapping = ApplicationContextHolder.getBean("requestMappingHandlerMapping"); + } + List customizeRouteList = customizeRouteMapper.queryRegisterApi(); + String path; + RequestMethod requestMethod = RequestMethod.POST; + MyDynamicController myDynamicController = ApplicationContextHolder.getBean(MyDynamicController.class); + Method method = MyDynamicController.class.getDeclaredMethod("executeSql", Map.class, HttpServletRequest.class); + for (CustomizeRoute customizeRoute : customizeRouteList) { + path = customizeRoute.getUrl(); + if (customizeRoute.getRequestType() == 1) { + requestMethod = RequestMethod.GET; + } else if (customizeRoute.getRequestType() == 2) { + requestMethod = RequestMethod.POST; + } + RequestMappingInfo mappingInfo = RequestMappingInfo + .paths(path) + .methods(requestMethod) + .build(); + // 反射获取ExampleController中的hello方法,用于执行实际逻辑 + requestMappingHandlerMapping.registerMapping(mappingInfo, myDynamicController, method); + } + } + + /** + * 上线 API + * + * @param originalCustomizeRoute 原始自定义路线 + * @author 王立宏 + * @date 2023/11/16 05:06 + */ + @Override + public void registerApi(CustomizeRoute originalCustomizeRoute) { + if (requestMappingHandlerMapping == null) { + requestMappingHandlerMapping = ApplicationContextHolder.getBean("requestMappingHandlerMapping"); + } + String path = originalCustomizeRoute.getUrl(); + RequestMethod requestMethod = RequestMethod.POST; + if (originalCustomizeRoute.getRequestType() == 1) { + requestMethod = RequestMethod.GET; + } else if (originalCustomizeRoute.getRequestType() == 2) { + requestMethod = RequestMethod.POST; + } + MyDynamicController myDynamicController = ApplicationContextHolder.getBean(MyDynamicController.class); + RequestMappingInfo mappingInfo = RequestMappingInfo + .paths(path) + .methods(requestMethod) + .build(); + try { + // 反射获取ExampleController中的hello方法,用于执行实际逻辑 + Method method = MyDynamicController.class.getDeclaredMethod("executeSql", Map.class, HttpServletRequest.class); + requestMappingHandlerMapping.registerMapping(mappingInfo, myDynamicController, method); + // 修改数据库中显示的状态 + originalCustomizeRoute.setState(1); + MyModelUtil.fillCommonsForUpdate(originalCustomizeRoute, originalCustomizeRoute); + UpdateWrapper uw = this.createUpdateQueryForNullValue(originalCustomizeRoute, originalCustomizeRoute.getId()); + customizeRouteMapper.update(originalCustomizeRoute, uw); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** + * 下线 API + * + * @param originalCustomizeRoute 原始自定义路线 + * @author 王立宏 + * @date 2023/11/16 05:07 + */ + @Override + public void unregisterApi(CustomizeRoute originalCustomizeRoute) { + if (requestMappingHandlerMapping == null) { + requestMappingHandlerMapping = ApplicationContextHolder.getBean("requestMappingHandlerMapping"); + } + String path = originalCustomizeRoute.getUrl(); + log.warn("删除路由[" + path + "]"); + RequestMappingInfo requestMappingInfo = requestMappingHandlerMapping.getHandlerMethods().keySet().stream() + .filter(mapping -> mapping.getPatternsCondition().getPatterns().contains(path)) + .findFirst() + .orElse(null); + // 删除指定路径的映射 + requestMappingHandlerMapping.unregisterMapping(requestMappingInfo); + // 修改数据库中显示的状态 + originalCustomizeRoute.setState(-1); + MyModelUtil.fillCommonsForUpdate(originalCustomizeRoute, originalCustomizeRoute); + UpdateWrapper uw = this.createUpdateQueryForNullValue(originalCustomizeRoute, originalCustomizeRoute.getId()); + customizeRouteMapper.update(originalCustomizeRoute, uw); + } + +} diff --git a/application-webadmin/src/main/java/supie/webadmin/app/vo/CustomizeRouteVo.java b/application-webadmin/src/main/java/supie/webadmin/app/vo/CustomizeRouteVo.java new file mode 100644 index 0000000000000000000000000000000000000000..b18349bc12fd13d91357a44a3c09b9f25a1d299b --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/app/vo/CustomizeRouteVo.java @@ -0,0 +1,99 @@ +package supie.webadmin.app.vo; + +import supie.common.core.base.vo.BaseVo; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * CustomizeRouteVO视图对象。 + * + * @author rm -rf .bug + * @date 2020-11-12 + */ +@ApiModel("CustomizeRouteVO视图对象") +@Data +@EqualsAndHashCode(callSuper = true) +public class CustomizeRouteVo extends BaseVo { + + /** + * 主键ID。 + */ + @ApiModelProperty(value = "主键ID") + private Long id; + + /** + * 名称。 + */ + @ApiModelProperty(value = "名称") + private String name; + + /** + * 数据所属人。 + */ + @ApiModelProperty(value = "数据所属人") + private Long dataUserId; + + /** + * 数据所属部门。 + */ + @ApiModelProperty(value = "数据所属部门") + private Long dataDeptId; + + /** + * 描述。 + */ + @ApiModelProperty(value = "描述") + private String routeDescribe; + + /** + * 地址(不可重复)。 + */ + @ApiModelProperty(value = "地址(不可重复)") + private String url; + + /** + * 请求类型(1:GET。2:POST。默认为POST)。 + */ + @ApiModelProperty(value = "请求类型(1:GET。2:POST。默认为POST)") + private Integer requestType; + + /** + * 状态(1:上线。-1:下线)。 + */ + @ApiModelProperty(value = "状态(1:上线。-1:下线)") + private Integer state; + + /** + * 存算引擎项目ID。 + */ + @ApiModelProperty(value = "存算引擎项目ID") + private Long projectId; + + /** + * 目标数据库名称。 + */ + @ApiModelProperty(value = "目标数据库名称") + private String databaseName; + + /** + * SQL语句。 + */ + @ApiModelProperty(value = "SQL语句") + private String sqlScript; + + /** + * 参数集(JSON字符串形式存储)。 + */ + @ApiModelProperty(value = "参数集(JSON字符串形式存储)") + private String parameter; + + /** + * 业务规程ID。 + */ + @ApiModelProperty(value = "业务规程ID") + private Long processId; +} diff --git a/application-webadmin/src/main/java/supie/webadmin/config/InterceptorConfig.java b/application-webadmin/src/main/java/supie/webadmin/config/InterceptorConfig.java index 4ed0fe9cf89cb273ca0fb388012b5f24e896286a..579b55604d7dc2b6e43ca0123edc7c8e4041ac59 100644 --- a/application-webadmin/src/main/java/supie/webadmin/config/InterceptorConfig.java +++ b/application-webadmin/src/main/java/supie/webadmin/config/InterceptorConfig.java @@ -1,5 +1,6 @@ package supie.webadmin.config; +import supie.webadmin.interceptor.ApiAuthenticationInterceptor; import supie.webadmin.interceptor.AuthenticationInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -17,5 +18,6 @@ public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/admin/**"); + registry.addInterceptor(new ApiAuthenticationInterceptor()).addPathPatterns("/api/**"); } } diff --git a/application-webadmin/src/main/java/supie/webadmin/interceptor/ApiAuthenticationInterceptor.java b/application-webadmin/src/main/java/supie/webadmin/interceptor/ApiAuthenticationInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..f32e220f0732d20835aae84f4a38e72cad9f0059 --- /dev/null +++ b/application-webadmin/src/main/java/supie/webadmin/interceptor/ApiAuthenticationInterceptor.java @@ -0,0 +1,370 @@ +package supie.webadmin.interceptor; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrFormatter; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; +import io.jsonwebtoken.Claims; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RBucket; +import org.redisson.api.RSet; +import org.redisson.api.RedissonClient; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import supie.common.core.annotation.NoAuthInterface; +import supie.common.core.cache.CacheConfig; +import supie.common.core.constant.ApplicationConstant; +import supie.common.core.constant.DataPermRuleType; +import supie.common.core.constant.ErrorCodeEnum; +import supie.common.core.exception.MyRuntimeException; +import supie.common.core.object.ResponseResult; +import supie.common.core.object.TokenData; +import supie.common.core.util.ApplicationContextHolder; +import supie.common.core.util.JwtUtil; +import supie.common.core.util.RedisKeyUtil; +import supie.webadmin.config.ApplicationConfig; +import supie.webadmin.config.ThirdPartyAuthConfig; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 描述: + * + * @author 王立宏 + * @date 2023/11/16 16:50 + * @path SDT-supie.webadmin.interceptor-ApiAuthenticationInterceptor + */ +@Slf4j +public class ApiAuthenticationInterceptor implements HandlerInterceptor { + + private final ApplicationConfig appConfig = + ApplicationContextHolder.getBean("applicationConfig"); + + private final ThirdPartyAuthConfig thirdPartyAuthConfig = + ApplicationContextHolder.getBean("thirdPartyAuthConfig"); + + private final RedissonClient redissonClient = ApplicationContextHolder.getBean(RedissonClient.class); + + private final CacheManager cacheManager = ApplicationContextHolder.getBean("caffeineCacheManager"); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String url = request.getRequestURI(); + log.error("请求了自定义地址[" + url + "]"); + return true; + +// String token = this.getTokenFromRequest(request); +// boolean noLoginUrl = this.isNoAuthInterface(handler); +// // 如果接口方法标记NoAuthInterface注解,可以直接跳过Token鉴权验证,这里主要为了测试接口方便 +// if (noLoginUrl && StrUtil.isBlank(token)) { +// return true; +// } +// String appCode = this.getAppCodeFromRequest(request); +// if (StrUtil.isNotBlank(appCode)) { +// return this.handleThirdPartyRequest(appCode, token, url, response); +// } +// Claims c = JwtUtil.parseToken(token, appConfig.getTokenSigningKey()); +// if (JwtUtil.isNullOrExpired(c)) { +// // 如果免登陆接口携带的是过期的Token,这个时候直接返回给Controller即可。 +// // 这样可以规避不必要的重新登录,而对于Controller,可以将本次请求视为未登录用户的请求。 +// if (noLoginUrl) { +// return true; +// } +// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// this.outputResponseMessage(response, +// ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "用户会话已过期或尚未登录,请重新登录!")); +// return false; +// } +// String sessionId = (String) c.get("sessionId"); +// String sessionIdKey = RedisKeyUtil.makeSessionIdKey(sessionId); +// RBucket sessionData = redissonClient.getBucket(sessionIdKey); +// TokenData tokenData = null; +// if (sessionData.isExists()) { +// tokenData = JSON.parseObject(sessionData.get(), TokenData.class); +// } +// if (tokenData == null) { +// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// this.outputResponseMessage(response, +// ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, "用户会话已失效,请重新登录!")); +// return false; +// } +// tokenData.setToken(token); +// TokenData.addToRequest(tokenData); +// // 如果url是免登陆、白名单中,则不需要进行鉴权操作 +// if (!noLoginUrl && Boolean.FALSE.equals(tokenData.getIsAdmin()) && !this.hasPermission(sessionId, url)) { +// response.setStatus(HttpServletResponse.SC_FORBIDDEN); +// this.outputResponseMessage(response, ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION)); +// return false; +// } +// if (JwtUtil.needToRefresh(c)) { +// String refreshedToken = JwtUtil.generateToken(c, appConfig.getExpiration(), appConfig.getTokenSigningKey()); +// response.addHeader(appConfig.getRefreshedTokenHeaderKey(), refreshedToken); +// } +// return true; + } + + private String getTokenFromRequest(HttpServletRequest request) { + String token = request.getHeader(appConfig.getTokenHeaderKey()); + if (StrUtil.isBlank(token)) { + token = request.getParameter(appConfig.getTokenHeaderKey()); + } + if (StrUtil.isBlank(token)) { + token = request.getHeader(ApplicationConstant.HTTP_HEADER_INTERNAL_TOKEN); + } + return token; + } + + private String getAppCodeFromRequest(HttpServletRequest request) { + String token = request.getHeader("AppCode"); + if (StrUtil.isBlank(token)) { + token = request.getParameter("AppCode"); + } + return token; + } + + @SuppressWarnings("unchecked") + private boolean hasPermission(String sessionId, String url) { + // 为了提升效率,先检索Caffeine的一级缓存,如果不存在,再检索Redis的二级缓存,并将结果存入一级缓存。 + Set localPermSet; + String permKey = RedisKeyUtil.makeSessionPermIdKey(sessionId); + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name()); + Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL."); + Cache.ValueWrapper wrapper = cache.get(permKey); + if (wrapper == null) { + RSet permSet = redissonClient.getSet(permKey); + localPermSet = permSet.readAll(); + cache.put(permKey, localPermSet); + } else { + localPermSet = (Set) wrapper.get(); + } + return CollUtil.contains(localPermSet, url); + } + + private boolean isNoAuthInterface(Object handler) { + if (handler instanceof HandlerMethod) { + HandlerMethod hm = (HandlerMethod) handler; + return hm.getBeanType().getAnnotation(NoAuthInterface.class) != null + || hm.getMethodAnnotation(NoAuthInterface.class) != null; + } + return false; + } + + private boolean handleThirdPartyRequest(String appCode, String token, String url, HttpServletResponse response) { + ThirdPartyAuthConfig.AuthProperties authProps = thirdPartyAuthConfig.getApplicationMap().get(appCode); + if (authProps == null) { + String msg = StrFormatter.format("请求的 appCode[{}] 信息,在当前服务中尚未配置!", appCode); + this.outputResponseMessage(response, ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, msg)); + return false; + } + ResponseResult responseResult = this.getAndCacheThirdPartyTokenData(authProps, token); + if (!responseResult.isSuccess()) { + this.outputResponseMessage(response, + ResponseResult.error(ErrorCodeEnum.UNAUTHORIZED_LOGIN, responseResult.getErrorMessage())); + return false; + } + TokenData tokenData = responseResult.getData(); + tokenData.setAppCode(appCode); + tokenData.setSessionId(this.prependAppCode(authProps.getAppCode(), tokenData.getSessionId())); + TokenData.addToRequest(tokenData); + if (Boolean.FALSE.equals(tokenData.getIsAdmin()) + && !this.hasThirdPartyPermission(authProps, tokenData, url)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + this.outputResponseMessage(response, ResponseResult.error(ErrorCodeEnum.NO_OPERATION_PERMISSION)); + return false; + } + return true; + } + + private ResponseResult getAndCacheThirdPartyTokenData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + if (authProps.getTokenExpiredSeconds() == 0) { + return this.getThirdPartyTokenData(authProps, token); + } + String tokeKey = this.prependAppCode(authProps.getAppCode(), RedisKeyUtil.makeSessionIdKey(token)); + RBucket sessionData = redissonClient.getBucket(tokeKey); + if (sessionData.isExists()) { + return ResponseResult.success(JSON.parseObject(sessionData.get(), TokenData.class)); + } + ResponseResult responseResult = this.getThirdPartyTokenData(authProps, token); + if (responseResult.isSuccess()) { + sessionData.set(JSON.toJSONString(responseResult.getData()), authProps.getTokenExpiredSeconds(), TimeUnit.SECONDS); + } + return responseResult; + } + + private String prependAppCode(String appCode, String key) { + return appCode.toUpperCase() + ":" + key; + } + + private ResponseResult getThirdPartyTokenData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + try { + String resultData = this.invokeThirdPartyUrl(authProps.getBaseUrl() + "/getTokenData", token); + return JSON.parseObject(resultData, new TypeReference>() {}); + } catch (MyRuntimeException ex) { + return ResponseResult.error(ErrorCodeEnum.FAILED_TO_INVOKE_THIRDPARTY_URL, ex.getMessage()); + } + } + + private ResponseResult getThirdPartyPermData( + ThirdPartyAuthConfig.AuthProperties authProps, String token) { + try { + String resultData = this.invokeThirdPartyUrl(authProps.getBaseUrl() + "/getPermData", token); + return JSON.parseObject(resultData, new TypeReference>() {}); + } catch (MyRuntimeException ex) { + return ResponseResult.error(ErrorCodeEnum.FAILED_TO_INVOKE_THIRDPARTY_URL, ex.getMessage()); + } + } + + private String invokeThirdPartyUrl(String url, String token) { + Map headerMap = new HashMap<>(1); + headerMap.put("Authorization", token); + StringBuilder fullUrl = new StringBuilder(128); + fullUrl.append(url).append("?token=").append(token); + HttpResponse httpResponse = HttpUtil.createGet(fullUrl.toString()).addHeaders(headerMap).execute(); + if (!httpResponse.isOk()) { + String msg = StrFormatter.format( + "Failed to call [{}] with ERROR HTTP Status [{}] and [{}].", + url, httpResponse.getStatus(), httpResponse.body()); + log.error(msg); + throw new MyRuntimeException(msg); + } + return httpResponse.body(); + } + + @SuppressWarnings("unchecked") + private boolean hasThirdPartyPermission( + ThirdPartyAuthConfig.AuthProperties authProps, TokenData tokenData, String url) { + // 为了提升效率,先检索Caffeine的一级缓存,如果不存在,再检索Redis的二级缓存,并将结果存入一级缓存。 + String permKey = RedisKeyUtil.makeSessionPermIdKey(tokenData.getSessionId()); + Cache cache = cacheManager.getCache(CacheConfig.CacheEnum.USER_PERMISSION_CACHE.name()); + Assert.notNull(cache, "Cache USER_PERMISSION_CACHE can't be NULL"); + Cache.ValueWrapper wrapper = cache.get(permKey); + if (wrapper != null) { + Object cachedData = wrapper.get(); + if (cachedData != null) { + return ((Set) cachedData).contains(url); + } + } + Set localPermSet; + RSet permSet = redissonClient.getSet(permKey); + if (permSet.isExists()) { + localPermSet = permSet.readAll(); + cache.put(permKey, localPermSet); + return localPermSet.contains(url); + } + ResponseResult responseResult = this.getThirdPartyPermData(authProps, tokenData.getToken()); + this.cacheThirdPartyDataPermData(authProps, tokenData, responseResult.getData().getDataPerms()); + if (CollUtil.isEmpty(responseResult.getData().urlPerms)) { + return false; + } + permSet.addAll(responseResult.getData().urlPerms); + permSet.expire(authProps.getPermExpiredSeconds(), TimeUnit.SECONDS); + localPermSet = new HashSet<>(responseResult.getData().urlPerms); + cache.put(permKey, localPermSet); + return localPermSet.contains(url); + } + + private void cacheThirdPartyDataPermData( + ThirdPartyAuthConfig.AuthProperties authProps, TokenData tokenData, List dataPerms) { + if (CollUtil.isEmpty(dataPerms)) { + return; + } + Map> dataPermMap = + dataPerms.stream().collect(Collectors.groupingBy(AuthenticationInterceptor.ThirdPartyAppDataPermData::getRuleType)); + Map> normalizedDataPermMap = new HashMap<>(dataPermMap.size()); + for (Map.Entry> entry : dataPermMap.entrySet()) { + List ruleTypeDataPermDataList; + if (entry.getKey().equals(DataPermRuleType.TYPE_DEPT_AND_CHILD_DEPT)) { + ruleTypeDataPermDataList = + normalizedDataPermMap.computeIfAbsent(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST, k -> new LinkedList<>()); + } else { + ruleTypeDataPermDataList = + normalizedDataPermMap.computeIfAbsent(entry.getKey(), k -> new LinkedList<>()); + } + ruleTypeDataPermDataList.addAll(entry.getValue()); + } + Map resultDataPermMap = new HashMap<>(normalizedDataPermMap.size()); + for (Map.Entry> entry : normalizedDataPermMap.entrySet()) { + if (entry.getKey().equals(DataPermRuleType.TYPE_CUSTOM_DEPT_LIST)) { + String deptIds = entry.getValue().stream() + .map(AuthenticationInterceptor.ThirdPartyAppDataPermData::getDeptIds).collect(Collectors.joining(",")); + resultDataPermMap.put(entry.getKey(), deptIds); + } else { + resultDataPermMap.put(entry.getKey(), "null"); + } + } + Map> menuDataPermMap = new HashMap<>(1); + menuDataPermMap.put(ApplicationConstant.DATA_PERM_ALL_MENU_ID, resultDataPermMap); + String dataPermSessionKey = RedisKeyUtil.makeSessionDataPermIdKey(tokenData.getSessionId()); + RBucket bucket = redissonClient.getBucket(dataPermSessionKey); + bucket.set(JSON.toJSONString(menuDataPermMap), authProps.getPermExpiredSeconds(), TimeUnit.SECONDS); + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) throws Exception { + // 这里需要空注解,否则sonar会不happy。 + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + // 这里需要空注解,否则sonar会不happy。 + } + + private void outputResponseMessage(HttpServletResponse response, ResponseResult respObj) { + PrintWriter out; + try { + out = response.getWriter(); + } catch (IOException e) { + log.error("Failed to call OutputResponseMessage.", e); + return; + } + response.setContentType("application/json; charset=utf-8"); + out.print(JSON.toJSONString(respObj)); + out.flush(); + out.close(); + } + + @Data + public static class ThirdPartyAppPermData { + /** + * 当前用户会话可访问的url接口地址列表。 + */ + private List urlPerms; + /** + * 当前用户会话的数据权限列表。 + */ + private List dataPerms; + } + + @Data + public static class ThirdPartyAppDataPermData { + /** + * 数据权限的规则类型。需要按照橙单的约定返回。具体值可参考DataPermRuleType常量类。 + */ + private Integer ruleType; + /** + * 部门Id集合,多个部门Id之间逗号分隔。 + * 注意:仅当ruleType为3、4、5时需要包含该字段值。 + */ + private String deptIds; + } + +} diff --git a/application-webadmin/src/main/java/supie/webadmin/interceptor/AuthenticationInterceptor.java b/application-webadmin/src/main/java/supie/webadmin/interceptor/AuthenticationInterceptor.java index 9073ab52786ed5efd489ce0f457b0e05fd95066f..ad2b1c4685ae616ba2f6a5a0b099c99d90dae19a 100644 --- a/application-webadmin/src/main/java/supie/webadmin/interceptor/AuthenticationInterceptor.java +++ b/application-webadmin/src/main/java/supie/webadmin/interceptor/AuthenticationInterceptor.java @@ -343,7 +343,7 @@ public class AuthenticationInterceptor implements HandlerInterceptor { /** * 当前用户会话可访问的url接口地址列表。 */ - private List urlPerms; + public List urlPerms; /** * 当前用户会话的数据权限列表。 */