Jelajahi Sumber

调整代码

mxd 4 tahun lalu
induk
melakukan
9556caca42
35 mengubah file dengan 783 tambahan dan 244 penghapusan
  1. 24 0
      db/v0.4.x-v0.5.sql
  2. 1 1
      src/main/java/org/ssssssss/magicapi/cache/SqlCache.java
  3. 145 42
      src/main/java/org/ssssssss/magicapi/config/MappingHandlerMapping.java
  4. 5 3
      src/main/java/org/ssssssss/magicapi/config/RequestHandler.java
  5. 110 39
      src/main/java/org/ssssssss/magicapi/config/WebUIController.java
  6. 1 1
      src/main/java/org/ssssssss/magicapi/dialect/DB2Dialect.java
  7. 1 1
      src/main/java/org/ssssssss/magicapi/dialect/Dialect.java
  8. 1 1
      src/main/java/org/ssssssss/magicapi/dialect/MySQLDialect.java
  9. 1 1
      src/main/java/org/ssssssss/magicapi/dialect/OracleDialect.java
  10. 1 1
      src/main/java/org/ssssssss/magicapi/dialect/PostgreSQLDialect.java
  11. 1 1
      src/main/java/org/ssssssss/magicapi/dialect/SQLServer2005Dialect.java
  12. 1 1
      src/main/java/org/ssssssss/magicapi/dialect/SQLServerDialect.java
  13. 2 1
      src/main/java/org/ssssssss/magicapi/interceptor/RequestInterceptor.java
  14. 9 24
      src/main/java/org/ssssssss/magicapi/model/ApiInfo.java
  15. 62 0
      src/main/java/org/ssssssss/magicapi/model/Group.java
  16. 22 0
      src/main/java/org/ssssssss/magicapi/model/Options.java
  17. 84 0
      src/main/java/org/ssssssss/magicapi/model/TreeNode.java
  18. 2 2
      src/main/java/org/ssssssss/magicapi/modules/AssertModule.java
  19. 1 1
      src/main/java/org/ssssssss/magicapi/modules/BoundSql.java
  20. 3 3
      src/main/java/org/ssssssss/magicapi/modules/EnvModule.java
  21. 1 1
      src/main/java/org/ssssssss/magicapi/modules/MongoCollectionExtension.java
  22. 1 1
      src/main/java/org/ssssssss/magicapi/modules/MongoFindIterableExtension.java
  23. 3 3
      src/main/java/org/ssssssss/magicapi/modules/MongoModule.java
  24. 3 4
      src/main/java/org/ssssssss/magicapi/modules/RedisModule.java
  25. 2 2
      src/main/java/org/ssssssss/magicapi/modules/RequestModule.java
  26. 3 3
      src/main/java/org/ssssssss/magicapi/modules/ResponseModule.java
  27. 38 38
      src/main/java/org/ssssssss/magicapi/modules/SQLModule.java
  28. 1 1
      src/main/java/org/ssssssss/magicapi/modules/Transaction.java
  29. 38 34
      src/main/java/org/ssssssss/magicapi/provider/ApiServiceProvider.java
  30. 49 0
      src/main/java/org/ssssssss/magicapi/provider/GroupServiceProvider.java
  31. 19 20
      src/main/java/org/ssssssss/magicapi/provider/impl/DefaultApiServiceProvider.java
  32. 111 0
      src/main/java/org/ssssssss/magicapi/provider/impl/DefaultGroupServiceProvider.java
  33. 1 1
      src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java
  34. 21 13
      src/main/java/org/ssssssss/magicapi/swagger/SwaggerProvider.java
  35. 15 0
      src/main/java/org/ssssssss/magicapi/utils/PathUtils.java

+ 24 - 0
db/v0.4.x-v0.5.sql

@@ -0,0 +1,24 @@
+-- 创建分组表
+CREATE TABLE `magic_group`  (
+    `id` varchar(32) NOT NULL,
+    `group_name` varchar(64) NULL COMMENT '组名',
+    `group_type` varchar(1) NULL COMMENT '组类型,1:接口分组,2:函数分组',
+    `group_path` varchar(64) NULL COMMENT '分组路径',
+    `parent_id` varchar(32) NULL COMMENT '父级ID',
+    `deleted` char(1) NULL DEFAULT 0 COMMENT '是否被删除,1:是,0:否',
+    PRIMARY KEY (`id`)
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'MagicAPI分组信息表' ROW_FORMAT = Dynamic;
+-- 插入分组数据
+insert into magic_group select md5(uuid()),api_group_name,'1',api_group_prefix,'0','0' from magic_api_info group by api_group_name;
+-- 修改字段
+ALTER TABLE `magic_api_info` ADD COLUMN `api_group_id` varchar(32) NULL COMMENT '分组ID' AFTER `api_name`;
+-- 修改字段
+ALTER TABLE `magic_api_info_his` ADD COLUMN `api_group_id` varchar(32) NULL COMMENT '分组ID' AFTER `api_name`;
+-- 赋值api_group_id字段
+UPDATE magic_api_info mai JOIN magic_group mg ON mg.group_name = mai.api_group_name AND mg.group_path = mai.api_group_prefix SET mai.api_group_id = mg.id;
+-- 对关联不上的,归根节点
+UPDATE magic_api_info SET api_group_id = '0' where api_group_id IS NULL;
+-- 删除字段
+ALTER TABLE `magic_api_info` DROP COLUMN `api_group_name`,DROP COLUMN `api_group_prefix`;
+ALTER TABLE `magic_api_info_his` DROP COLUMN `api_group_name`,DROP COLUMN `api_group_prefix`;
+

+ 1 - 1
src/main/java/org/ssssssss/magicapi/cache/SqlCache.java

@@ -1,6 +1,6 @@
 package org.ssssssss.magicapi.cache;
 
-import org.ssssssss.magicapi.functions.BoundSql;
+import org.ssssssss.magicapi.modules.BoundSql;
 import org.ssssssss.magicapi.utils.MD5Utils;
 
 import java.util.Arrays;

+ 145 - 42
src/main/java/org/ssssssss/magicapi/config/MappingHandlerMapping.java

@@ -11,7 +11,12 @@ import org.springframework.web.method.HandlerMethod;
 import org.springframework.web.servlet.HandlerMapping;
 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+import org.ssssssss.magicapi.model.ApiInfo;
+import org.ssssssss.magicapi.model.Group;
+import org.ssssssss.magicapi.model.TreeNode;
 import org.ssssssss.magicapi.provider.ApiServiceProvider;
+import org.ssssssss.magicapi.provider.GroupServiceProvider;
+import org.ssssssss.magicapi.utils.PathUtils;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -53,11 +58,22 @@ public class MappingHandlerMapping {
 	 * 接口信息读取
 	 */
 	private ApiServiceProvider magicApiService;
+
+	/**
+	 * 分组信息读取
+	 */
+	private GroupServiceProvider groupServiceProvider;
+
 	/**
 	 * 统一接口前缀
 	 */
 	private String prefix;
 
+	/**
+	 * 接口分组
+	 */
+	private TreeNode<Group> groups;
+
 	/**
 	 * 是否覆盖应用接口
 	 */
@@ -102,10 +118,9 @@ public class MappingHandlerMapping {
 	 *
 	 * @param requestMethod  请求方法
 	 * @param requestMapping 请求路径
-	 * @return
 	 */
 	public static String buildMappingKey(String requestMethod, String requestMapping) {
-		//TODO 判断 requestMapping 是否已 “/” 开头
+
 		if (!StringUtils.isEmpty(requestMapping) && !requestMapping.startsWith("/")) {
 			requestMapping = "/" + requestMapping;
 		}
@@ -124,17 +139,30 @@ public class MappingHandlerMapping {
 		this.magicApiService = magicApiService;
 	}
 
+	public void setGroupServiceProvider(GroupServiceProvider groupServiceProvider) {
+		this.groupServiceProvider = groupServiceProvider;
+	}
+
 	public List<ApiInfo> getApiInfos() {
 		return apiInfos;
 	}
 
+	/**
+	 * 加载所有分组
+	 */
+	public void loadGroup() {
+		groups = groupServiceProvider.apiGroupList();
+	}
+
 	/**
 	 * 注册请求
 	 */
 	public void registerAllMapping() {
 		try {
+			loadGroup();
 			List<ApiInfo> list = magicApiService.listWithScript();
 			if (list != null) {
+				list = list.stream().filter(it -> groupServiceProvider.getFullPath(it.getGroupId()) != null).collect(Collectors.toList());
 				for (ApiInfo info : list) {
 					try {
 						// 当接口存在时,刷新缓存
@@ -169,34 +197,86 @@ public class MappingHandlerMapping {
 		return mappings.get(buildMappingKey(method, requestMapping));
 	}
 
-	public void updateGroupPrefix(String oldGroupName, String newGroupName, String prefix) {
-		for (ApiInfo info : apiInfos) {
-			if (oldGroupName.equals(info.getGroupName())) {
-				unregisterMapping(info.getId(), false);
-				info.setGroupName(newGroupName);
-				info.setGroupPrefix(prefix);
-				registerMapping(info, false);
+	/**
+	 * 检测是否允许修改
+	 */
+	boolean checkGroup(Group group) {
+		Group oldGroup = groups.findNode((item) -> item.getId().equals(group.getId()));
+		// 如果只改了名字,则不做任何操作
+		if (Objects.equals(oldGroup.getParentId(), group.getParentId()) &&
+				Objects.equals(oldGroup.getPath(), group.getPath())) {
+			return true;
+		}
+		// 新的接口分组路径
+		String newPath = groupServiceProvider.getFullPath(group.getParentId()) + "/" + Objects.toString(group.getPath(), "");
+		// 获取要移动的接口
+		List<ApiInfo> infos = apiInfos.stream().filter(info -> Objects.equals(info.getGroupId(), oldGroup.getId())).collect(Collectors.toList());
+
+		// 判断是否有冲突
+		for (ApiInfo info : infos) {
+			String path = getRequestPath(newPath, info.getPath());
+			String mappingKey = buildMappingKey(info.getMethod(), path);
+			if (mappings.containsKey(mappingKey)) {
+				return false;
+			}
+			if (!allowOverride) {
+				Map<RequestMappingInfo, HandlerMethod> handlerMethods = this.requestMappingHandlerMapping.getHandlerMethods();
+				if (handlerMethods != null) {
+					if (handlerMethods.get(getRequestMapping(info.getMethod(), path)) != null) {
+						return false;
+					}
+				}
 			}
 		}
+		return true;
+	}
+
+	/**
+	 * 删除分组
+	 */
+	void deleteGroup(String groupId) {
+		// 找到下级所有分组
+		List<String> groupIds = groups.findNodes((item) -> item.getId().equals(groupId)).stream().map(Group::getId).collect(Collectors.toList());
+		groupIds.add(groupId);
+		// 找到对应的所有接口
+		List<ApiInfo> deleteInfos = apiInfos.stream().filter(info -> groupIds.contains(info.getGroupId())).collect(Collectors.toList());
+		for (ApiInfo info : deleteInfos) {
+			unregisterMapping(info.getId(), true);
+		}
+		// 全部删除
+		apiInfos.removeAll(deleteInfos);
+	}
+
+	/**
+	 * 修改分组
+	 */
+	void updateGroup(Group group) {
+		loadGroup();    // 重新加载分组
+		Group oldGroup = groups.findNode((item) -> item.getId().equals(group.getId()));
+		apiInfos.stream().filter(info -> Objects.equals(info.getGroupId(), oldGroup.getId())).forEach(info -> {
+			unregisterMapping(info.getId(), false);
+			info.setGroupId(group.getId());
+			registerMapping(info, false);
+		});
 	}
 
 	/**
 	 * 判断是否已注册
 	 */
-	public boolean hasRegisterMapping(ApiInfo info) {
+	boolean hasRegisterMapping(ApiInfo info) {
 		if (info.getId() != null) {
 			ApiInfo oldInfo = mappings.get(info.getId());
 			if (oldInfo != null
-					&& Objects.equals(oldInfo.getGroupPrefix(), info.getGroupPrefix())
+					&& Objects.equals(oldInfo.getGroupId(), info.getGroupId())
 					&& Objects.equals(oldInfo.getMethod(), info.getMethod())
 					&& Objects.equals(oldInfo.getPath(), info.getPath())) {
 				return false;
 			}
 		}
-		if(mappings.containsKey(getMappingKey(info))){
+		if (mappings.containsKey(getMappingKey(info))) {
 			return true;
 		}
-		if(!allowOverride){
+		if (!allowOverride) {
 			Map<RequestMappingInfo, HandlerMethod> handlerMethods = this.requestMappingHandlerMapping.getHandlerMethods();
 			if (handlerMethods != null) {
 				return handlerMethods.get(getRequestMapping(info)) != null;
@@ -205,10 +285,23 @@ public class MappingHandlerMapping {
 		return false;
 	}
 
+	/**
+	 * 接口移动
+	 */
+	boolean move(String id, String groupId) {
+		ApiInfo oldInfo = mappings.get(id);
+		if (oldInfo == null) {
+			return false;
+		}
+		oldInfo.setGroupId(groupId);
+		registerMapping(oldInfo, false);
+		return true;
+	}
+
 	/**
 	 * 注册请求映射
 	 */
-	public void registerMapping(ApiInfo info, boolean delete) {
+	void registerMapping(ApiInfo info, boolean delete) {
 		// 先判断是否已注册,如果已注册,则先取消注册在进行注册。
 		ApiInfo oldInfo = mappings.get(info.getId());
 		String newMappingKey = getMappingKey(info);
@@ -219,12 +312,12 @@ public class MappingHandlerMapping {
 				if (!info.equals(oldInfo)) {
 					mappings.put(info.getId(), info);
 					mappings.put(newMappingKey, info);
-					logger.info("刷新接口:{}", info.getName());
+					logger.info("刷新接口:{},{}", info.getName(), newMappingKey);
 				}
 				return;
 			}
 			// URL不一致时,需要取消注册旧接口,重新注册新接口
-			logger.info("取消注册接口:{}", oldInfo.getName());
+			logger.info("取消注册接口:{},{}", oldInfo.getName(), newMappingKey);
 			// 取消注册
 			mappings.remove(oldMappingKey);
 			requestMappingHandlerMapping.unregisterMapping(getRequestMapping(oldInfo));
@@ -232,17 +325,11 @@ public class MappingHandlerMapping {
 		// 注册
 		RequestMappingInfo requestMapping = getRequestMapping(info);
 		// 如果与应用冲突
-		if (requestMappingHandlerMapping.getHandlerMethods().containsKey(requestMapping)) {
-			if (!allowOverride) {
-				// 不允许覆盖
-				logger.error("接口{}与应用冲突,无法注册", info.getName());
-				return;
-			}
-			logger.warn("取消注册应用接口:{}", requestMapping);
-			// 取消注册原接口
-			requestMappingHandlerMapping.unregisterMapping(requestMapping);
+		if (!overrideApplicationMapping(requestMapping)) {
+			logger.error("接口{},{}与应用冲突,无法注册", info.getName(), newMappingKey);
+			return;
 		}
-		logger.info("注册接口:{}", info.getName());
+		logger.info("注册接口:{},{}", info.getName(), newMappingKey);
 		mappings.put(info.getId(), info);
 		mappings.put(newMappingKey, info);
 		requestMappingHandlerMapping.registerMapping(requestMapping, handler, method);
@@ -255,7 +342,7 @@ public class MappingHandlerMapping {
 	/**
 	 * 取消注册请求映射
 	 */
-	public void unregisterMapping(String id, boolean delete) {
+	void unregisterMapping(String id, boolean delete) {
 		ApiInfo info = mappings.remove(id);
 		if (info != null) {
 			logger.info("取消注册接口:{}", info.getName());
@@ -271,35 +358,51 @@ public class MappingHandlerMapping {
 	 * 根据接口信息获取绑定map的key
 	 */
 	private String getMappingKey(ApiInfo info) {
-		return buildMappingKey(info.getMethod(), getRequestPath(info.getGroupPrefix(), info.getPath()));
+		return buildMappingKey(info.getMethod(), getRequestPath(info.getGroupId(), info.getPath()));
 	}
 
 	/**
 	 * 处理前缀
 	 *
-	 * @param groupPrefix 分组前缀
-	 * @param path        请求路径
+	 * @param groupId 分组ID
+	 * @param path    请求路径
 	 */
-	public String getRequestPath(String groupPrefix, String path) {
-		groupPrefix = groupPrefix == null ? "" : groupPrefix;
-		while (groupPrefix.endsWith("/")) {
-			groupPrefix = groupPrefix.substring(0, groupPrefix.length() - 1);
-		}
-		while (path.startsWith("/")) {
-			path = path.substring(1);
-		}
-		path = groupPrefix + "/" + path;
+	public String getRequestPath(String groupId, String path) {
+		path = groupServiceProvider.getFullPath(groupId) + "/" + path;
 		if (prefix != null) {
-			path = prefix + (path.startsWith("/") ? path.substring(1) : path);
+			path = prefix + "/" + path;
 		}
-		return path;
+		return PathUtils.replaceSlash(path);
+	}
+
+	/**
+	 * 覆盖应用接口
+	 */
+	private boolean overrideApplicationMapping(RequestMappingInfo requestMapping) {
+		if (requestMappingHandlerMapping.getHandlerMethods().containsKey(requestMapping)) {
+			if (!allowOverride) {
+				// 不允许覆盖
+				return false;
+			}
+			logger.warn("取消注册应用接口:{}", requestMapping);
+			// 取消注册原接口
+			requestMappingHandlerMapping.unregisterMapping(requestMapping);
+		}
+		return true;
 	}
 
 	/**
 	 * 根据接口信息构建 RequestMappingInfo
 	 */
 	private RequestMappingInfo getRequestMapping(ApiInfo info) {
-		return RequestMappingInfo.paths(getRequestPath(info.getGroupPrefix(), info.getPath())).methods(RequestMethod.valueOf(info.getMethod().toUpperCase())).build();
+		return RequestMappingInfo.paths(getRequestPath(info.getGroupId(), info.getPath())).methods(RequestMethod.valueOf(info.getMethod().toUpperCase())).build();
+	}
+
+	/**
+	 * 根据接口信息构建 RequestMappingInfo
+	 */
+	private RequestMappingInfo getRequestMapping(String method, String path) {
+		return RequestMappingInfo.paths(path).methods(RequestMethod.valueOf(method.toUpperCase())).build();
 	}
 
 	public void enableRefresh(int interval) {

+ 5 - 3
src/main/java/org/ssssssss/magicapi/config/RequestHandler.java

@@ -18,10 +18,12 @@ import org.ssssssss.magicapi.context.CookieContext;
 import org.ssssssss.magicapi.context.HeaderContext;
 import org.ssssssss.magicapi.context.RequestContext;
 import org.ssssssss.magicapi.context.SessionContext;
-import org.ssssssss.magicapi.functions.ResponseFunctions;
+import org.ssssssss.magicapi.interceptor.RequestInterceptor;
 import org.ssssssss.magicapi.logging.MagicLoggerContext;
+import org.ssssssss.magicapi.model.ApiInfo;
 import org.ssssssss.magicapi.model.JsonBean;
 import org.ssssssss.magicapi.model.JsonBodyBean;
+import org.ssssssss.magicapi.modules.ResponseModule;
 import org.ssssssss.magicapi.provider.ResultProvider;
 import org.ssssssss.magicapi.script.ScriptManager;
 import org.ssssssss.script.MagicScript;
@@ -247,7 +249,7 @@ public class RequestHandler {
 				return ResponseEntity.ok(new JsonBean<>(entity.getBody()));
 			}
 			return ResponseEntity.ok(new JsonBean<>(convertToBase64(entity.getBody())));
-		} else if (result instanceof ResponseFunctions.NullValue) {
+		} else if (result instanceof ResponseModule.NullValue) {
 			return new JsonBean<>(1, "empty.");
 		}
 		return new JsonBean<>(resultProvider.buildResult(result));
@@ -379,7 +381,7 @@ public class RequestHandler {
 	private Object response(Object value) {
 		if (value instanceof ResponseEntity) {
 			return value;
-		} else if (value instanceof ResponseFunctions.NullValue) {
+		} else if (value instanceof ResponseModule.NullValue) {
 			return null;
 		}
 		return resultProvider.buildResult(value);

+ 110 - 39
src/main/java/org/ssssssss/magicapi/config/WebUIController.java

@@ -6,10 +6,14 @@ import org.slf4j.LoggerFactory;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-import org.ssssssss.magicapi.functions.SQLExecutor;
+import org.ssssssss.magicapi.interceptor.RequestInterceptor;
 import org.ssssssss.magicapi.logging.MagicLoggerContext;
+import org.ssssssss.magicapi.model.ApiInfo;
+import org.ssssssss.magicapi.model.Group;
 import org.ssssssss.magicapi.model.JsonBean;
+import org.ssssssss.magicapi.modules.SQLModule;
 import org.ssssssss.magicapi.provider.ApiServiceProvider;
+import org.ssssssss.magicapi.provider.GroupServiceProvider;
 import org.ssssssss.magicapi.provider.MagicAPIService;
 import org.ssssssss.magicapi.utils.MD5Utils;
 import org.ssssssss.script.MagicModuleLoader;
@@ -36,10 +40,15 @@ public class WebUIController {
 	private MappingHandlerMapping mappingHandlerMapping;
 
 	/**
-	 * 接口查询service
+	 * 接口查询Service
 	 */
 	private ApiServiceProvider magicApiService;
 
+	/**
+	 * 分组查询Service
+	 */
+	private GroupServiceProvider groupServiceProvider;
+
 
 	/**
 	 * 拦截器
@@ -65,10 +74,14 @@ public class WebUIController {
 
 	public WebUIController() {
 		// 给前端添加代码提示
-		MagicScriptEngine.addScriptClass(SQLExecutor.class);
+		MagicScriptEngine.addScriptClass(SQLModule.class);
 		MagicScriptEngine.addScriptClass(MagicAPIService.class);
 	}
 
+	public void setGroupServiceProvider(GroupServiceProvider groupServiceProvider) {
+		this.groupServiceProvider = groupServiceProvider;
+	}
+
 	public void setMagicDynamicDataSource(MagicDynamicDataSource magicDynamicDataSource) {
 		this.magicDynamicDataSource = magicDynamicDataSource;
 	}
@@ -93,15 +106,15 @@ public class WebUIController {
 		this.password = password;
 	}
 
+
 	/**
 	 * 删除接口
 	 *
-	 * @param request
-	 * @param id      接口ID
+	 * @param id 接口ID
 	 */
 	@RequestMapping("/delete")
 	@ResponseBody
-	public JsonBean<Boolean> delete(HttpServletRequest request, String id) {
+	public JsonBean<Boolean> delete(String id, HttpServletRequest request) {
 		if (!allowVisit(request, RequestInterceptor.Authorization.DELETE)) {
 			return new JsonBean<>(-10, "无权限执行删除方法");
 		}
@@ -118,28 +131,45 @@ public class WebUIController {
 	}
 
 	/**
-	 * 删除接口分组
-	 *
-	 * @param apiIds    接口ID列表,逗号分隔
-	 * @param groupName 分组名称
+	 * 创建分组
+	 */
+	@RequestMapping("/group/create")
+	@ResponseBody
+	public JsonBean<String> createGroup(Group group, HttpServletRequest request) {
+		if (!allowVisit(request, RequestInterceptor.Authorization.SAVE)) {
+			return new JsonBean<>(-10, "无权限执行保存方法");
+		}
+		if (StringUtils.isBlank(group.getParentId())) {
+			group.setParentId("0");
+		}
+		if (StringUtils.isBlank(group.getName())) {
+			return new JsonBean<>(0, "分组名称不能为空");
+		}
+		if (StringUtils.isBlank(group.getType())) {
+			return new JsonBean<>(0, "分组类型不能为空");
+		}
+		try {
+			groupServiceProvider.insert(group);
+			return new JsonBean<>(group.getId());
+		} catch (Exception e) {
+			logger.error("保存分组出错", e);
+			return new JsonBean<>(-1, e.getMessage());
+		}
+	}
+
+	/**
+	 * 删除分组
 	 */
 	@RequestMapping("/group/delete")
 	@ResponseBody
-	public JsonBean<Boolean> deleteGroup(HttpServletRequest request, String apiIds, String groupName) {
+	public JsonBean<Boolean> deleteGroup(String groupId, HttpServletRequest request) {
 		if (!allowVisit(request, RequestInterceptor.Authorization.DELETE)) {
 			return new JsonBean<>(-10, "无权限执行删除方法");
 		}
 		try {
-			boolean success = this.magicApiService.deleteGroup(groupName);
+			boolean success = this.magicApiService.deleteGroup(groupId) && this.groupServiceProvider.delete(groupId);
 			if (success) {    //删除成功时取消注册
-				if (StringUtils.isNotBlank(apiIds)) {
-					String[] ids = apiIds.split(",");
-					if (ids.length > 0) {
-						for (String id : ids) {
-							mappingHandlerMapping.unregisterMapping(id, true);
-						}
-					}
-				}
+				mappingHandlerMapping.deleteGroup(groupId);
 			}
 			return new JsonBean<>(success);
 		} catch (Exception e) {
@@ -150,29 +180,52 @@ public class WebUIController {
 
 	/**
 	 * 修改分组
-	 *
-	 * @param groupName    分组名称
-	 * @param oldGroupName 原分组名称
-	 * @param prefix       分组前缀
 	 */
 	@RequestMapping("/group/update")
 	@ResponseBody
-	public JsonBean<Boolean> groupUpdate(String groupName, String oldGroupName, String prefix, HttpServletRequest request) {
+	public synchronized JsonBean<Boolean> groupUpdate(Group group, HttpServletRequest request) {
 		if (!allowVisit(request, RequestInterceptor.Authorization.SAVE)) {
 			return new JsonBean<>(-10, "无权限执行删除方法");
 		}
+		if (StringUtils.isBlank(group.getParentId())) {
+			group.setParentId("0");
+		}
+		if (StringUtils.isBlank(group.getName())) {
+			return new JsonBean<>(0, "分组名称不能为空");
+		}
+		if (StringUtils.isBlank(group.getType())) {
+			return new JsonBean<>(0, "分组类型不能为空");
+		}
 		try {
-			boolean success = magicApiService.updateGroup(oldGroupName, groupName, prefix);
-			if (success) {
-				mappingHandlerMapping.updateGroupPrefix(oldGroupName, groupName, prefix);
+			boolean isApiGroup = "1".equals(group.getType());
+			if (!isApiGroup || mappingHandlerMapping.checkGroup(group)) {
+				boolean success = groupServiceProvider.update(group);
+				if (success && isApiGroup) {    // 如果数据库修改成功,则修改接口路径
+					mappingHandlerMapping.updateGroup(group);
+				}
+				return new JsonBean<>(success);
 			}
-			return new JsonBean<>(success);
+			return new JsonBean<>(-20, "修改分组后,接口路径会有冲突,请检查!");
 		} catch (Exception e) {
 			logger.error("修改分组出错", e);
 			return new JsonBean<>(-1, e.getMessage());
 		}
 	}
 
+	/**
+	 * 查询所有分组
+	 */
+	@RequestMapping("/group/list")
+	@ResponseBody
+	public JsonBean<List<Group>> groupList() {
+		try {
+			return new JsonBean<>(groupServiceProvider.groupList());
+		} catch (Exception e) {
+			logger.error("查询分组列表失败", e);
+			return new JsonBean<>(-1, e.getMessage());
+		}
+	}
+
 	/**
 	 * 查询所有接口
 	 */
@@ -212,9 +265,9 @@ public class WebUIController {
 	public JsonBean<Map<String, Map<String, ScriptClass>>> classes() {
 		Map<String, ScriptClass> classMap = MagicScriptEngine.getScriptClassMap();
 		classMap.putAll(MagicModuleLoader.getModules());
-		ScriptClass db = classMap.get(SQLExecutor.class.getName());
+		ScriptClass db = classMap.get(SQLModule.class.getName());
 		if (db != null) {
-			List<ScriptClass.ScriptAttribute> attributes =  new ArrayList<>();
+			List<ScriptClass.ScriptAttribute> attributes = new ArrayList<>();
 			// 给与前台动态数据源提示
 			magicDynamicDataSource.datasources().stream().filter(StringUtils::isNotBlank)
 					.forEach(item -> attributes.add(new ScriptClass.ScriptAttribute("db", item)));
@@ -290,6 +343,30 @@ public class WebUIController {
 		return new JsonBean<>(magicApiService.backupInfo(id, timestamp));
 	}
 
+	/**
+	 * 移动接口
+	 */
+	@RequestMapping("/api/move")
+	@ResponseBody
+	public JsonBean<Boolean> apiMove(String id, String groupId, HttpServletRequest request) {
+		if (!allowVisit(request, RequestInterceptor.Authorization.SAVE)) {
+			return new JsonBean<>(-10, "无权限执行保存方法");
+		}
+		if (!groupServiceProvider.contains(groupId)) {
+			return new JsonBean<>(0, "找不到分组信息");
+		}
+		try {
+			if (!mappingHandlerMapping.move(id, groupId)) {
+				return new JsonBean<>(0, "移动接口失败!");
+			} else {
+				return new JsonBean<>(magicApiService.move(id, groupId));
+			}
+		} catch (Exception e) {
+			logger.error("移动接口出错", e);
+			return new JsonBean<>(-1, e.getMessage());
+		}
+	}
+
 	/**
 	 * 保存接口
 	 *
@@ -305,12 +382,6 @@ public class WebUIController {
 			if (StringUtils.isBlank(info.getMethod())) {
 				return new JsonBean<>(0, "请求方法不能为空");
 			}
-			if (info.getGroupName() != null && (info.getGroupName().contains("'") || info.getGroupName().contains("\""))) {
-				return new JsonBean<>(0, "分组名不能包含特殊字符' \"");
-			}
-			if (info.getGroupPrefix() != null && (info.getGroupPrefix().contains("'") || info.getGroupPrefix().contains("\""))) {
-				return new JsonBean<>(0, "分组前缀不能包含特殊字符' \"");
-			}
 			if (StringUtils.isBlank(info.getPath())) {
 				return new JsonBean<>(0, "请求路径不能为空");
 			}
@@ -325,13 +396,13 @@ public class WebUIController {
 			}
 			if (StringUtils.isBlank(info.getId())) {
 				// 先判断接口是否存在
-				if (magicApiService.exists(info.getGroupPrefix(), info.getMethod(), info.getPath())) {
+				if (magicApiService.exists(info.getGroupId(), info.getMethod(), info.getPath())) {
 					return new JsonBean<>(0, String.format("接口%s:%s已存在", info.getMethod(), info.getPath()));
 				}
 				magicApiService.insert(info);
 			} else {
 				// 先判断接口是否存在
-				if (magicApiService.existsWithoutId(info.getGroupPrefix(), info.getMethod(), info.getPath(), info.getId())) {
+				if (magicApiService.existsWithoutId(info.getGroupId(), info.getMethod(), info.getPath(), info.getId())) {
 					return new JsonBean<>(0, String.format("接口%s:%s已存在", info.getMethod(), info.getPath()));
 				}
 				magicApiService.update(info);

+ 1 - 1
src/main/java/org/ssssssss/magicapi/dialect/DB2Dialect.java

@@ -1,7 +1,7 @@
 package org.ssssssss.magicapi.dialect;
 
 
-import org.ssssssss.magicapi.functions.BoundSql;
+import org.ssssssss.magicapi.modules.BoundSql;
 
 public class DB2Dialect implements Dialect {
     @Override

+ 1 - 1
src/main/java/org/ssssssss/magicapi/dialect/Dialect.java

@@ -1,7 +1,7 @@
 package org.ssssssss.magicapi.dialect;
 
 
-import org.ssssssss.magicapi.functions.BoundSql;
+import org.ssssssss.magicapi.modules.BoundSql;
 
 public interface Dialect {
 

+ 1 - 1
src/main/java/org/ssssssss/magicapi/dialect/MySQLDialect.java

@@ -1,7 +1,7 @@
 package org.ssssssss.magicapi.dialect;
 
 
-import org.ssssssss.magicapi.functions.BoundSql;
+import org.ssssssss.magicapi.modules.BoundSql;
 
 public class MySQLDialect implements Dialect {
 

+ 1 - 1
src/main/java/org/ssssssss/magicapi/dialect/OracleDialect.java

@@ -1,7 +1,7 @@
 package org.ssssssss.magicapi.dialect;
 
 
-import org.ssssssss.magicapi.functions.BoundSql;
+import org.ssssssss.magicapi.modules.BoundSql;
 
 public class OracleDialect implements Dialect {
 

+ 1 - 1
src/main/java/org/ssssssss/magicapi/dialect/PostgreSQLDialect.java

@@ -1,7 +1,7 @@
 package org.ssssssss.magicapi.dialect;
 
 
-import org.ssssssss.magicapi.functions.BoundSql;
+import org.ssssssss.magicapi.modules.BoundSql;
 
 public class PostgreSQLDialect implements Dialect {
     @Override

+ 1 - 1
src/main/java/org/ssssssss/magicapi/dialect/SQLServer2005Dialect.java

@@ -1,7 +1,7 @@
 package org.ssssssss.magicapi.dialect;
 
 import org.apache.commons.lang3.StringUtils;
-import org.ssssssss.magicapi.functions.BoundSql;
+import org.ssssssss.magicapi.modules.BoundSql;
 
 public class SQLServer2005Dialect implements Dialect {
     @Override

+ 1 - 1
src/main/java/org/ssssssss/magicapi/dialect/SQLServerDialect.java

@@ -1,7 +1,7 @@
 package org.ssssssss.magicapi.dialect;
 
 
-import org.ssssssss.magicapi.functions.BoundSql;
+import org.ssssssss.magicapi.modules.BoundSql;
 
 public class SQLServerDialect implements Dialect {
     @Override

+ 2 - 1
src/main/java/org/ssssssss/magicapi/config/RequestInterceptor.java → src/main/java/org/ssssssss/magicapi/interceptor/RequestInterceptor.java

@@ -1,5 +1,6 @@
-package org.ssssssss.magicapi.config;
+package org.ssssssss.magicapi.interceptor;
 
+import org.ssssssss.magicapi.model.ApiInfo;
 import org.ssssssss.script.MagicScriptContext;
 
 import javax.servlet.http.HttpServletRequest;

+ 9 - 24
src/main/java/org/ssssssss/magicapi/config/ApiInfo.java → src/main/java/org/ssssssss/magicapi/model/ApiInfo.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.config;
+package org.ssssssss.magicapi.model;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 
@@ -38,14 +38,9 @@ public class ApiInfo {
 	private String name;
 
 	/**
-	 * 接口分组名称
+	 * 分组ID
 	 */
-	private String groupName;
-
-	/**
-	 * 分组前缀
-	 */
-	private String groupPrefix;
+	private String groupId;
 
 	/**
 	 * 设置的请求参数
@@ -107,14 +102,6 @@ public class ApiInfo {
 		this.name = name;
 	}
 
-	public void setGroupName(String groupName) {
-		this.groupName = groupName;
-	}
-
-	public String getGroupName() {
-		return groupName;
-	}
-
 	public String getParameter() {
 		return parameter;
 	}
@@ -131,15 +118,14 @@ public class ApiInfo {
 		this.output = output;
 	}
 
-	public String getGroupPrefix() {
-		return groupPrefix;
+	public String getGroupId() {
+		return groupId;
 	}
 
-	public void setGroupPrefix(String groupPrefix) {
-		this.groupPrefix = groupPrefix;
+	public void setGroupId(String groupId) {
+		this.groupId = groupId;
 	}
 
-
 	public Map getOptionMap() {
 		return optionMap;
 	}
@@ -178,8 +164,7 @@ public class ApiInfo {
 				Objects.equals(path, apiInfo.path) &&
 				Objects.equals(script, apiInfo.script) &&
 				Objects.equals(name, apiInfo.name) &&
-				Objects.equals(groupName, apiInfo.groupName) &&
-				Objects.equals(groupPrefix, apiInfo.groupPrefix) &&
+				Objects.equals(groupId, apiInfo.groupId) &&
 				Objects.equals(parameter, apiInfo.parameter) &&
 				Objects.equals(option, apiInfo.option) &&
 				Objects.equals(output, apiInfo.output);
@@ -187,6 +172,6 @@ public class ApiInfo {
 
 	@Override
 	public int hashCode() {
-		return Objects.hash(id, method, path, script, name, groupName, groupPrefix, parameter, option, output);
+		return Objects.hash(id, method, path, script, name, groupId, parameter, option, output);
 	}
 }

+ 62 - 0
src/main/java/org/ssssssss/magicapi/model/Group.java

@@ -0,0 +1,62 @@
+package org.ssssssss.magicapi.model;
+
+public class Group {
+
+	private String id;
+
+	private String name;
+
+	private String type;
+
+	private String parentId;
+
+	private String path;
+
+	public Group(String id, String name) {
+		this.id = id;
+		this.name = name;
+	}
+
+	public Group() {
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	public String getParentId() {
+		return parentId;
+	}
+
+	public void setParentId(String parentId) {
+		this.parentId = parentId;
+	}
+
+	public String getPath() {
+		return path;
+	}
+
+	public void setPath(String path) {
+		this.path = path;
+	}
+}

+ 22 - 0
src/main/java/org/ssssssss/magicapi/model/Options.java

@@ -0,0 +1,22 @@
+package org.ssssssss.magicapi.model;
+
+public enum Options {
+
+	WRAP_REQUEST_PARAMETERS("包装请求参数到一个变量中", "wrap_request_parameter");
+
+	private final String name;
+	private final String value;
+
+	Options(String name, String value) {
+		this.name = name;
+		this.value = value;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public String getValue() {
+		return value;
+	}
+}

+ 84 - 0
src/main/java/org/ssssssss/magicapi/model/TreeNode.java

@@ -0,0 +1,84 @@
+package org.ssssssss.magicapi.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+public class TreeNode<T> {
+
+	private T node;
+
+	private List<TreeNode<T>> children = new ArrayList<>();
+
+	public TreeNode() {
+	}
+
+	public TreeNode(T node) {
+		this.node = node;
+	}
+
+	public T getNode() {
+		return node;
+	}
+
+	public void setNode(T node) {
+		this.node = node;
+	}
+
+	public List<TreeNode<T>> getChildren() {
+		return children;
+	}
+
+	public void setChildren(List<TreeNode<T>> children) {
+		this.children = children;
+	}
+
+	public List<T> findNodes(Function<T, Boolean> mapping) {
+		return findNodes(this.children, mapping);
+	}
+
+	public T findNode(Function<T, Boolean> mapping) {
+		return findNode(this.children, mapping);
+	}
+
+	private T findNode(List<TreeNode<T>> childs, Function<T, Boolean> mapping) {
+		for (TreeNode<T> item : childs) {
+			if (mapping.apply(item.node)) {
+				return item.node;
+			}
+			T found = findNode(childs, mapping);
+			if (found != null) {
+				return found;
+			}
+		}
+		return null;
+	}
+
+	public void moveTo(TreeNode<T> node) {
+		node.children.add(this);
+	}
+
+	private List<T> findNodes(List<TreeNode<T>> childs, Function<T, Boolean> mapping) {
+		List<T> nodes = new ArrayList<>();
+		childs.forEach(item -> {
+			if (mapping.apply(item.getNode())) {
+				nodes.add(item.getNode());
+			}
+			nodes.addAll(findNodes(item.children, mapping));
+		});
+		return nodes;
+	}
+
+	public List<T> flat() {
+		return flat(this.children);
+	}
+
+	private List<T> flat(List<TreeNode<T>> childs) {
+		List<T> result = new ArrayList<>();
+		for (TreeNode<T> item : childs) {
+			result.add(item.getNode());
+			result.addAll(flat(childs));
+		}
+		return result;
+	}
+}

+ 2 - 2
src/main/java/org/ssssssss/magicapi/functions/AssertFunctions.java → src/main/java/org/ssssssss/magicapi/modules/AssertModule.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import org.apache.commons.lang3.StringUtils;
 import org.ssssssss.magicapi.config.MagicModule;
@@ -10,7 +10,7 @@ import java.util.regex.Pattern;
 /**
  * 断言模块
  */
-public class AssertFunctions implements MagicModule {
+public class AssertModule implements MagicModule {
 
 	/**
 	 * 判断值不能为null

+ 1 - 1
src/main/java/org/ssssssss/magicapi/functions/BoundSql.java → src/main/java/org/ssssssss/magicapi/modules/BoundSql.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import org.ssssssss.magicapi.cache.SqlCache;
 import org.ssssssss.script.MagicScriptContext;

+ 3 - 3
src/main/java/org/ssssssss/magicapi/functions/EnvFunctions.java → src/main/java/org/ssssssss/magicapi/modules/EnvModule.java

@@ -1,14 +1,14 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import org.springframework.core.env.Environment;
 import org.ssssssss.magicapi.config.MagicModule;
 import org.ssssssss.script.annotation.Comment;
 
-public class EnvFunctions implements MagicModule {
+public class EnvModule implements MagicModule {
 
 	private Environment environment;
 
-	public EnvFunctions(Environment environment) {
+	public EnvModule(Environment environment) {
 		this.environment = environment;
 	}
 

+ 1 - 1
src/main/java/org/ssssssss/magicapi/functions/MongoCollectionExtension.java → src/main/java/org/ssssssss/magicapi/modules/MongoCollectionExtension.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import com.mongodb.client.FindIterable;
 import com.mongodb.client.MongoCollection;

+ 1 - 1
src/main/java/org/ssssssss/magicapi/functions/MongoFindIterableExtension.java → src/main/java/org/ssssssss/magicapi/modules/MongoFindIterableExtension.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import com.mongodb.client.FindIterable;
 import com.mongodb.client.MongoCursor;

+ 3 - 3
src/main/java/org/ssssssss/magicapi/functions/MongoFunctions.java → src/main/java/org/ssssssss/magicapi/modules/MongoModule.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import com.mongodb.MongoClient;
 import com.mongodb.client.MongoCollection;
@@ -9,11 +9,11 @@ import java.util.HashMap;
 /**
  * mongo模块
  */
-public class MongoFunctions extends HashMap<String,Object> implements MagicModule {
+public class MongoModule extends HashMap<String, Object> implements MagicModule {
 
 	private MongoClient mongoClient;
 
-	public MongoFunctions(MongoClient mongoClient) {
+	public MongoModule(MongoClient mongoClient) {
 		this.mongoClient = mongoClient;
 	}
 

+ 3 - 4
src/main/java/org/ssssssss/magicapi/functions/RedisFunctions.java → src/main/java/org/ssssssss/magicapi/modules/RedisModule.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisCallback;
@@ -7,13 +7,12 @@ import org.ssssssss.magicapi.config.MagicModule;
 import org.ssssssss.script.functions.DynamicMethod;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 
 /**
  * redis模块
  */
-public class RedisFunctions implements MagicModule, DynamicMethod {
+public class RedisModule implements MagicModule, DynamicMethod {
 
 	@Override
 	public String getModuleName() {
@@ -22,7 +21,7 @@ public class RedisFunctions implements MagicModule, DynamicMethod {
 
 	private StringRedisTemplate redisTemplate;
 
-	public RedisFunctions(RedisConnectionFactory connectionFactory) {
+	public RedisModule(RedisConnectionFactory connectionFactory) {
 		this.redisTemplate = new StringRedisTemplate(connectionFactory);
 	}
 

+ 2 - 2
src/main/java/org/ssssssss/magicapi/functions/RequestFunctions.java → src/main/java/org/ssssssss/magicapi/modules/RequestModule.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartRequest;
@@ -15,7 +15,7 @@ import java.util.List;
 /**
  * request 模块
  */
-public class RequestFunctions {
+public class RequestModule {
 
 	/**
 	 * 获取文件信息

+ 3 - 3
src/main/java/org/ssssssss/magicapi/functions/ResponseFunctions.java → src/main/java/org/ssssssss/magicapi/modules/ResponseModule.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.http.HttpHeaders;
@@ -21,11 +21,11 @@ import java.util.Map;
 /**
  * response模块
  */
-public class ResponseFunctions {
+public class ResponseModule {
 
 	private ResultProvider resultProvider;
 
-	public ResponseFunctions(ResultProvider resultProvider) {
+	public ResponseModule(ResultProvider resultProvider) {
 		this.resultProvider = resultProvider;
 	}
 

+ 38 - 38
src/main/java/org/ssssssss/magicapi/functions/SQLExecutor.java → src/main/java/org/ssssssss/magicapi/modules/SQLModule.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
 import org.springframework.jdbc.core.RowMapper;
@@ -31,7 +31,7 @@ import java.util.function.Function;
 /**
  * 数据库查询模块
  */
-public class SQLExecutor extends HashMap<String, SQLExecutor> implements MagicModule {
+public class SQLModule extends HashMap<String, SQLModule> implements MagicModule {
 
 	@UnableCall
 	private MagicDynamicDataSource dynamicDataSource;
@@ -66,11 +66,11 @@ public class SQLExecutor extends HashMap<String, SQLExecutor> implements MagicMo
 	@UnableCall
 	private long ttl;
 
-	public SQLExecutor() {
+	public SQLModule() {
 
 	}
 
-	public SQLExecutor(MagicDynamicDataSource dynamicDataSource) {
+	public SQLModule(MagicDynamicDataSource dynamicDataSource) {
 		this.dynamicDataSource = dynamicDataSource;
 		this.dataSourceNode = dynamicDataSource.getDataSource();
 	}
@@ -127,19 +127,19 @@ public class SQLExecutor extends HashMap<String, SQLExecutor> implements MagicMo
 	}
 
 	@UnableCall
-	private SQLExecutor cloneSQLExecutor() {
-		SQLExecutor sqlExecutor = new SQLExecutor();
-		sqlExecutor.setDynamicDataSource(this.dynamicDataSource);
-		sqlExecutor.setDataSourceNode(this.dataSourceNode);
-		sqlExecutor.setPageProvider(this.pageProvider);
-		sqlExecutor.setColumnMapperProvider(this.columnMapperAdapter);
-		sqlExecutor.setColumnMapRowMapper(this.columnMapRowMapper);
-		sqlExecutor.setRowMapColumnMapper(this.rowMapColumnMapper);
-		sqlExecutor.setSqlCache(this.sqlCache);
-		sqlExecutor.setTtl(this.ttl);
-		sqlExecutor.setResultProvider(this.resultProvider);
-		sqlExecutor.setDialectAdapter(this.dialectAdapter);
-		return sqlExecutor;
+	private SQLModule cloneSQLModule() {
+		SQLModule sqlModule = new SQLModule();
+		sqlModule.setDynamicDataSource(this.dynamicDataSource);
+		sqlModule.setDataSourceNode(this.dataSourceNode);
+		sqlModule.setPageProvider(this.pageProvider);
+		sqlModule.setColumnMapperProvider(this.columnMapperAdapter);
+		sqlModule.setColumnMapRowMapper(this.columnMapRowMapper);
+		sqlModule.setRowMapColumnMapper(this.rowMapColumnMapper);
+		sqlModule.setSqlCache(this.sqlCache);
+		sqlModule.setTtl(this.ttl);
+		sqlModule.setResultProvider(this.resultProvider);
+		sqlModule.setDialectAdapter(this.dialectAdapter);
+		return sqlModule;
 	}
 
 	/**
@@ -192,14 +192,14 @@ public class SQLExecutor extends HashMap<String, SQLExecutor> implements MagicMo
 	 * @return
 	 */
 	@Comment("使用缓存")
-	public SQLExecutor cache(@Comment("缓存名") String cacheName, @Comment("过期时间") long ttl) {
+	public SQLModule cache(@Comment("缓存名") String cacheName, @Comment("过期时间") long ttl) {
 		if (cacheName == null) {
 			return this;
 		}
-		SQLExecutor query = cloneSQLExecutor();
-		query.setCacheName(cacheName);
-		query.setTtl(ttl);
-		return query;
+		SQLModule sqlModule = cloneSQLModule();
+		sqlModule.setCacheName(cacheName);
+		sqlModule.setTtl(ttl);
+		return sqlModule;
 	}
 
 	/**
@@ -209,55 +209,55 @@ public class SQLExecutor extends HashMap<String, SQLExecutor> implements MagicMo
 	 * @return
 	 */
 	@Comment("使用缓存,过期时间采用默认配置")
-	public SQLExecutor cache(@Comment("缓存名") String cacheName) {
+	public SQLModule cache(@Comment("缓存名") String cacheName) {
 		return cache(cacheName, 0);
 	}
 
 	@Comment("采用驼峰列名")
-	public SQLExecutor camel() {
+	public SQLModule camel() {
 		return columnCase("camel");
 	}
 
 	@Comment("采用帕斯卡列名")
-	public SQLExecutor pascal() {
+	public SQLModule pascal() {
 		return columnCase("pascal");
 	}
 
 	@Comment("采用全小写列名")
-	public SQLExecutor lower() {
+	public SQLModule lower() {
 		return columnCase("lower");
 	}
 
 	@Comment("采用全大写列名")
-	public SQLExecutor upper() {
+	public SQLModule upper() {
 		return columnCase("upper");
 	}
 
 	@Comment("列名保持原样")
-	public SQLExecutor normal() {
+	public SQLModule normal() {
 		return columnCase("default");
 	}
 
 	@Comment("指定列名转换")
-	public SQLExecutor columnCase(String name) {
-		SQLExecutor sqlExecutor = cloneSQLExecutor();
-		sqlExecutor.setColumnMapRowMapper(this.columnMapperAdapter.getColumnMapRowMapper(name));
-		sqlExecutor.setRowMapColumnMapper(this.columnMapperAdapter.getRowMapColumnMapper(name));
-		return sqlExecutor;
+	public SQLModule columnCase(String name) {
+		SQLModule sqlModule = cloneSQLModule();
+		sqlModule.setColumnMapRowMapper(this.columnMapperAdapter.getColumnMapRowMapper(name));
+		sqlModule.setRowMapColumnMapper(this.columnMapperAdapter.getRowMapColumnMapper(name));
+		return sqlModule;
 	}
 
 	/**
 	 * 数据源切换
 	 */
 	@Override
-	public SQLExecutor get(Object key) {
-		SQLExecutor sqlExecutor = cloneSQLExecutor();
+	public SQLModule get(Object key) {
+		SQLModule sqlModule = cloneSQLModule();
 		if (key == null) {
-			sqlExecutor.setDataSourceNode(dynamicDataSource.getDataSource());
+			sqlModule.setDataSourceNode(dynamicDataSource.getDataSource());
 		} else {
-			sqlExecutor.setDataSourceNode(dynamicDataSource.getDataSource(key.toString()));
+			sqlModule.setDataSourceNode(dynamicDataSource.getDataSource(key.toString()));
 		}
-		return sqlExecutor;
+		return sqlModule;
 	}
 
 

+ 1 - 1
src/main/java/org/ssssssss/magicapi/functions/Transaction.java → src/main/java/org/ssssssss/magicapi/modules/Transaction.java

@@ -1,4 +1,4 @@
-package org.ssssssss.magicapi.functions;
+package org.ssssssss.magicapi.modules;
 
 import org.springframework.jdbc.datasource.DataSourceTransactionManager;
 import org.springframework.transaction.TransactionDefinition;

+ 38 - 34
src/main/java/org/ssssssss/magicapi/provider/ApiServiceProvider.java

@@ -1,6 +1,6 @@
 package org.ssssssss.magicapi.provider;
 
-import org.ssssssss.magicapi.config.ApiInfo;
+import org.ssssssss.magicapi.model.ApiInfo;
 
 import java.util.List;
 
@@ -11,105 +11,109 @@ public interface ApiServiceProvider {
 	/**
 	 * 删除接口
 	 *
-	 * @param id	接口ID
+	 * @param id 接口ID
 	 */
 	boolean delete(String id);
 
-	/**
-	 * 根据组名删除接口
-	 *
-	 * @param groupName 	分组名称
-	 */
-	boolean deleteGroup(String groupName);
-
 	/**
 	 * 查询所有接口(提供给页面,无需带script)
-	 *
 	 */
 	List<ApiInfo> list();
 
 	/**
 	 * 查询所有接口(内部使用,需要带Script)
-	 *
 	 */
 	List<ApiInfo> listWithScript();
 
 	/**
 	 * 查询接口详情(主要给页面使用)
 	 *
-	 * @param id	接口ID
+	 * @param id 接口ID
 	 */
 	ApiInfo get(String id);
 
 	/**
-	 * 判断接口是否存在
-	 * @param groupPrefix 分组前缀
-	 * @param method 请求方法
-	 * @param path   请求路径
+	 * 移动接口
+	 *
+	 * @param id      接口ID
+	 * @param groupId 分组ID
+	 */
+	boolean move(String id, String groupId);
+
+	/**
+	 * 根据组ID删除
 	 */
-	boolean exists(String groupPrefix, String method, String path);
+	boolean deleteGroup(String groupId);
 
 	/**
-	 * 修改分组信息
-	 * @param oldGroupName	旧分组名称
-	 * @param groupName	新分组名称
-	 * @param groupPrefix	分组前缀
+	 * 判断接口是否存在
+	 *
+	 * @param groupId 分组Id
+	 * @param method  请求方法
+	 * @param path    请求路径
 	 */
-	boolean updateGroup(String oldGroupName,String groupName, String groupPrefix);
+	boolean exists(String groupId, String method, String path);
 
 	/**
 	 * 判断接口是否存在
 	 *
-	 * @param groupPrefix 分组前缀
-	 * @param method 请求方法
-	 * @param path   请求路径
-	 * @param id     排除接口
+	 * @param groupId 分组ID
+	 * @param method  请求方法
+	 * @param path    请求路径
+	 * @param id      排除接口
 	 */
-	boolean existsWithoutId(String groupPrefix, String method, String path, String id);
+	boolean existsWithoutId(String groupId, String method, String path, String id);
 
 	/**
 	 * 添加接口信息
-	 * @param info	接口信息
+	 *
+	 * @param info 接口信息
 	 */
 	boolean insert(ApiInfo info);
 
 	/**
 	 * 修改接口信息
-	 * @param info	接口信息
+	 *
+	 * @param info 接口信息
 	 */
 	boolean update(ApiInfo info);
 
 	/**
 	 * 备份历史记录
 	 *
-	 * @param apiId	接口ID
+	 * @param apiId 接口ID
 	 */
 	void backup(String apiId);
 
+
 	/**
 	 * 查询API历史记录
-	 * @param apiId	接口ID
+	 *
+	 * @param apiId 接口ID
 	 * @return 时间戳列表
 	 */
 	List<Long> backupList(String apiId);
 
 	/**
 	 * 查询API历史记录详情
-	 * @param apiId	接口ID
+	 *
+	 * @param apiId     接口ID
 	 * @param timestamp 时间戳
 	 */
 	ApiInfo backupInfo(String apiId, Long timestamp);
 
 	/**
 	 * 包装接口信息(可用于加密)
-	 * @param info	接口信息
+	 *
+	 * @param info 接口信息
 	 */
 	default void wrap(ApiInfo info) {
 	}
 
 	/**
 	 * 解除包装接口信息(可用于解密)
-	 * @param info	接口信息
+	 *
+	 * @param info 接口信息
 	 */
 	default void unwrap(ApiInfo info) {
 	}

+ 49 - 0
src/main/java/org/ssssssss/magicapi/provider/GroupServiceProvider.java

@@ -0,0 +1,49 @@
+package org.ssssssss.magicapi.provider;
+
+import org.ssssssss.magicapi.model.Group;
+import org.ssssssss.magicapi.model.TreeNode;
+
+import java.util.List;
+
+public interface GroupServiceProvider {
+
+	/**
+	 * 添加分组
+	 */
+	boolean insert(Group group);
+
+	/**
+	 * 修改分组
+	 */
+	boolean update(Group group);
+
+	/**
+	 * 删除分组
+	 */
+	boolean delete(String groupId);
+
+	/**
+	 * 是否有该分组
+	 */
+	boolean contains(String groupId);
+
+	/**
+	 * 接口分组列表
+	 */
+	TreeNode<Group> apiGroupList();
+
+	/**
+	 * 分组列表
+	 */
+	List<Group> groupList();
+
+	/**
+	 * 根据分组Id获取分组路径
+	 */
+	String getFullPath(String groupId);
+
+	/**
+	 * 根据分组Id获取分组名称
+	 */
+	String getFullName(String groupId);
+}

+ 19 - 20
src/main/java/org/ssssssss/magicapi/provider/impl/DefaultApiServiceProvider.java

@@ -2,7 +2,7 @@ package org.ssssssss.magicapi.provider.impl;
 
 import org.springframework.jdbc.core.BeanPropertyRowMapper;
 import org.springframework.jdbc.core.JdbcTemplate;
-import org.ssssssss.magicapi.config.ApiInfo;
+import org.ssssssss.magicapi.model.ApiInfo;
 import org.ssssssss.magicapi.provider.ApiServiceProvider;
 
 import java.util.List;
@@ -12,8 +12,7 @@ public class DefaultApiServiceProvider extends BeanPropertyRowMapper<ApiInfo> im
 
 	private final String COMMON_COLUMNS = "id,\n" +
 			"api_name,\n" +
-			"api_group_name,\n" +
-			"api_group_prefix,\n" +
+			"api_group_id,\n" +
 			"api_path,\n" +
 			"api_method";
 
@@ -34,11 +33,6 @@ public class DefaultApiServiceProvider extends BeanPropertyRowMapper<ApiInfo> im
 		return template.update(deleteById, id) > 0;
 	}
 
-	public boolean deleteGroup(String groupName) {
-		String deleteByGroupName = "delete from magic_api_info where api_group_name = ?";
-		return template.update(deleteByGroupName, groupName) > 0;
-	}
-
 	public List<ApiInfo> list() {
 		String selectList = "select " + COMMON_COLUMNS + " from magic_api_info order by api_update_time desc";
 		return template.query(selectList, this);
@@ -62,15 +56,19 @@ public class DefaultApiServiceProvider extends BeanPropertyRowMapper<ApiInfo> im
 		return info;
 	}
 
-	public boolean exists(String groupPrefix, String method, String path) {
-		String exists = "select count(*) from magic_api_info where api_method = ? and api_path = ? and api_group_prefix = ?";
-		return template.queryForObject(exists, Integer.class, method, path, groupPrefix) > 0;
+	@Override
+	public boolean move(String id, String groupId) {
+		return template.update("update magic_api_info SET api_group_id = ? where id = ?", groupId, id) > 0;
 	}
 
 	@Override
-	public boolean updateGroup(String oldGroupName, String groupName, String groupPrefix) {
-		String updateGroup = "update magic_api_info set api_group_name = ?,api_group_prefix=?,api_update_time = ? where api_group_name = ?";
-		return template.update(updateGroup, groupName, groupPrefix, System.currentTimeMillis(), oldGroupName) > 0;
+	public boolean deleteGroup(String groupId) {
+		return template.update("delete from magic_api_info where api_group_id = ?", groupId) > 0;
+	}
+
+	public boolean exists(String groupId, String method, String path) {
+		String exists = "select count(*) from magic_api_info where api_method = ? and api_path = ? and api_group_id = ?";
+		return template.queryForObject(exists, Integer.class, method, path, groupId) > 0;
 	}
 
 	public boolean existsWithoutId(String groupPrefix, String method, String path, String id) {
@@ -82,14 +80,14 @@ public class DefaultApiServiceProvider extends BeanPropertyRowMapper<ApiInfo> im
 		info.setId(UUID.randomUUID().toString().replace("-", ""));
 		wrap(info);
 		long time = System.currentTimeMillis();
-		String insert = "insert into magic_api_info(id,api_method,api_path,api_script,api_name,api_group_name,api_parameter,api_option,api_output,api_group_prefix,api_create_time,api_update_time) values(?,?,?,?,?,?,?,?,?,?,?,?)";
-		return template.update(insert, info.getId(), info.getMethod(), info.getPath(), info.getScript(), info.getName(), info.getGroupName(), info.getParameter(), info.getOption(), info.getOutput(), info.getGroupPrefix(), time, time) > 0;
+		String insert = "insert into magic_api_info(id,api_method,api_path,api_script,api_name,api_group_id,api_parameter,api_option,api_output,api_create_time,api_update_time) values(?,?,?,?,?,?,?,?,?,?,?,?)";
+		return template.update(insert, info.getId(), info.getMethod(), info.getPath(), info.getScript(), info.getName(), info.getGroupId(), info.getParameter(), info.getOption(), info.getOutput(), time, time) > 0;
 	}
 
 	public boolean update(ApiInfo info) {
 		wrap(info);
-		String update = "update magic_api_info set api_method = ?,api_path = ?,api_script = ?,api_name = ?,api_group_name = ?,api_parameter = ?,api_option = ?,api_output = ?,api_group_prefix = ?,api_update_time = ? where id = ?";
-		return template.update(update, info.getMethod(), info.getPath(), info.getScript(), info.getName(), info.getGroupName(), info.getParameter(), info.getOption(), info.getOutput(), info.getGroupPrefix(), System.currentTimeMillis(), info.getId()) > 0;
+		String update = "update magic_api_info set api_method = ?,api_path = ?,api_script = ?,api_name = ?,api_group_id = ?,api_parameter = ?,api_option = ?,api_output = ?,api_update_time = ? where id = ?";
+		return template.update(update, info.getMethod(), info.getPath(), info.getScript(), info.getName(), info.getGroupId(), info.getParameter(), info.getOption(), info.getOutput(), System.currentTimeMillis(), info.getId()) > 0;
 	}
 
 	@Override
@@ -98,6 +96,7 @@ public class DefaultApiServiceProvider extends BeanPropertyRowMapper<ApiInfo> im
 		template.update(backupSql, apiId);
 	}
 
+
 	@Override
 	public List<Long> backupList(String apiId) {
 		return template.queryForList("select api_update_time from magic_api_info_his where id = ? order by api_update_time desc", Long.class, apiId);
@@ -107,7 +106,7 @@ public class DefaultApiServiceProvider extends BeanPropertyRowMapper<ApiInfo> im
 	public ApiInfo backupInfo(String apiId, Long timestamp) {
 		String selectOne = "select " + COMMON_COLUMNS + "," + SCRIPT_COLUMNS + " from magic_api_info_his where id = ? and api_update_time = ?";
 		List<ApiInfo> list = template.query(selectOne, this, apiId, timestamp);
-		if(list != null && !list.isEmpty()){
+		if (list != null && !list.isEmpty()) {
 			ApiInfo info = list.get(0);
 			unwrap(info);
 			return info;
@@ -117,6 +116,6 @@ public class DefaultApiServiceProvider extends BeanPropertyRowMapper<ApiInfo> im
 
 	@Override
 	protected String lowerCaseName(String name) {
-		return super.lowerCaseName(name).replace("api_","");
+		return super.lowerCaseName(name).replace("api_", "");
 	}
 }

+ 111 - 0
src/main/java/org/ssssssss/magicapi/provider/impl/DefaultGroupServiceProvider.java

@@ -0,0 +1,111 @@
+package org.ssssssss.magicapi.provider.impl;
+
+import org.springframework.jdbc.core.BeanPropertyRowMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.ssssssss.magicapi.model.Group;
+import org.ssssssss.magicapi.model.TreeNode;
+import org.ssssssss.magicapi.provider.GroupServiceProvider;
+import org.ssssssss.magicapi.utils.PathUtils;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class DefaultGroupServiceProvider extends BeanPropertyRowMapper<Group> implements GroupServiceProvider {
+
+	private JdbcTemplate template;
+
+	private Map<String, Group> cacheTree = new HashMap<>();
+
+	public DefaultGroupServiceProvider(JdbcTemplate template) {
+		super(Group.class);
+		this.template = template;
+	}
+
+	@Override
+	public boolean insert(Group group) {
+		group.setId(UUID.randomUUID().toString().replace("-", ""));
+		String insertGroup = "insert into magic_group(id,group_name,group_type,group_path,parent_id) values(?,?,?,?,?)";
+		return template.update(insertGroup, group.getId(), group.getName(), group.getType(), group.getPath(), group.getParentId()) > 0;
+	}
+
+	@Override
+	public boolean update(Group group) {
+		String updateGroup = "update magic_group set group_name = ?,group_path=?,parent_id = ? where id = ?";
+		return template.update(updateGroup, group.getName(), group.getPath(), group.getParentId(), group.getId()) > 0;
+	}
+
+	@Override
+	public boolean delete(String groupId) {
+		String deleteByGroupId = "delete from magic_api_info where api_group_id = ?";
+		return template.update(deleteByGroupId, groupId) > 0;
+	}
+
+	@Override
+	public boolean contains(String groupId) {
+		return "0".equals(groupId) || cacheTree.containsKey(groupId);
+	}
+
+	@Override
+	public TreeNode<Group> apiGroupList() {
+		List<Group> groups = template.query("select * from magic_group where group_type = '1' ", this);
+		TreeNode<Group> root = new TreeNode<>();
+		root.setNode(new Group("0", "root"));
+		convertToTree(groups, root);
+		Map<String, Group> groupMap = new HashMap<>();
+		groups.forEach(group -> groupMap.put(group.getId(), group));
+		cacheTree = groupMap;
+		return root;
+	}
+
+	@Override
+	public List<Group> groupList() {
+		return template.query("select * from magic_group",this);
+	}
+
+	@Override
+	public String getFullPath(String groupId) {
+		StringBuilder path = new StringBuilder();
+		Group group;
+		while ((group = cacheTree.get(groupId)) != null) {
+			path.insert(0, '/' + Objects.toString(group.getPath(), ""));
+			groupId = group.getParentId();
+		}
+		// 需要找到根节点,否则说明中间被删除了
+		if (!"0".equals(groupId)) {
+			return null;
+		}
+		return PathUtils.replaceSlash(path.toString());
+	}
+
+	@Override
+	public String getFullName(String groupId) {
+		if (groupId == null || "0".equals(groupId)) {
+			return "";
+		}
+		StringBuilder name = new StringBuilder();
+		Group group;
+		while ((group = cacheTree.get(groupId)) != null) {
+			name.insert(0, '/' + group.getName());
+			groupId = group.getParentId();
+		}
+		// 需要找到根节点,否则说明中间被删除了
+		if (!"0".equals(groupId)) {
+			return null;
+		}
+		return name.substring(1);
+	}
+
+	private void convertToTree(List<Group> groups, TreeNode<Group> current) {
+		List<TreeNode<Group>> treeNodes = groups.stream()
+				.filter(it -> current.getNode().getId().equals(it.getParentId()))
+				.map(TreeNode::new)
+				.collect(Collectors.toList());
+		current.setChildren(treeNodes);
+		treeNodes.forEach(it -> convertToTree(groups, it));
+	}
+
+	@Override
+	protected String lowerCaseName(String name) {
+		return super.lowerCaseName(name).replace("group_", "");
+	}
+}

+ 1 - 1
src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java

@@ -1,8 +1,8 @@
 package org.ssssssss.magicapi.provider.impl;
 
-import org.ssssssss.magicapi.config.ApiInfo;
 import org.ssssssss.magicapi.config.MappingHandlerMapping;
 import org.ssssssss.magicapi.exception.MagicServiceException;
+import org.ssssssss.magicapi.model.ApiInfo;
 import org.ssssssss.magicapi.provider.MagicAPIService;
 import org.ssssssss.magicapi.provider.ResultProvider;
 import org.ssssssss.magicapi.script.ScriptManager;

+ 21 - 13
src/main/java/org/ssssssss/magicapi/swagger/SwaggerProvider.java

@@ -2,8 +2,9 @@ package org.ssssssss.magicapi.swagger;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.web.bind.annotation.ResponseBody;
-import org.ssssssss.magicapi.config.ApiInfo;
 import org.ssssssss.magicapi.config.MappingHandlerMapping;
+import org.ssssssss.magicapi.model.ApiInfo;
+import org.ssssssss.magicapi.provider.GroupServiceProvider;
 
 import java.io.IOException;
 import java.util.List;
@@ -33,10 +34,16 @@ public class SwaggerProvider {
 	 */
 	private String version;
 
+	private GroupServiceProvider groupServiceProvider;
+
 	public void setMappingHandlerMapping(MappingHandlerMapping mappingHandlerMapping) {
 		this.mappingHandlerMapping = mappingHandlerMapping;
 	}
 
+	public void setGroupServiceProvider(GroupServiceProvider groupServiceProvider) {
+		this.groupServiceProvider = groupServiceProvider;
+	}
+
 	public void setDescription(String description) {
 		this.description = description;
 	}
@@ -50,47 +57,48 @@ public class SwaggerProvider {
 	}
 
 	@ResponseBody
-	public SwaggerEntity swaggerJson(){
+	public SwaggerEntity swaggerJson() {
 		List<ApiInfo> infos = mappingHandlerMapping.getApiInfos();
 		SwaggerEntity swaggerEntity = new SwaggerEntity();
 		SwaggerEntity.License license = new SwaggerEntity.License("MIT", "https://gitee.com/ssssssss-team/magic-api/blob/master/LICENSE");
 		swaggerEntity.setInfo(new SwaggerEntity.Info(this.description, this.version, this.title, license));
 		ObjectMapper mapper = new ObjectMapper();
 		for (ApiInfo info : infos) {
-			swaggerEntity.addTag(info.getGroupName(),info.getGroupPrefix());
+			String groupName = groupServiceProvider.getFullName(info.getGroupId());
+			swaggerEntity.addTag(groupName, groupServiceProvider.getFullPath(info.getGroupId()));
 			SwaggerEntity.Path path = new SwaggerEntity.Path();
-			path.addTag(info.getGroupName());
+			path.addTag(groupName);
 			try {
-				path.addResponse("200",mapper.readValue(Objects.toString(info.getOutput(),"{}"),Object.class));
+				path.addResponse("200", mapper.readValue(Objects.toString(info.getOutput(), "{}"), Object.class));
 			} catch (IOException ignored) {
 			}
 			path.addConsume("*/*");
 			path.addProduce("application/json");
 			path.setSummary(info.getName());
 			try {
-				Map map = mapper.readValue(Objects.toString(info.getParameter(),"{}"),Map.class);
+				Map map = mapper.readValue(Objects.toString(info.getParameter(), "{}"), Map.class);
 				Object request = map.get("request");
-				if(request instanceof Map){
+				if (request instanceof Map) {
 					Map requestMap = (Map) request;
 					Set keys = requestMap.keySet();
 					for (Object key : keys) {
-						path.addParameter(new SwaggerEntity.Parameter(key.toString(),"query","string", requestMap.getOrDefault(key,"")));
+						path.addParameter(new SwaggerEntity.Parameter(key.toString(), "query", "string", requestMap.getOrDefault(key, "")));
 					}
 				}
 				Object header = map.get("header");
-				if(header instanceof Map){
+				if (header instanceof Map) {
 					Map headers = (Map) header;
 					Set keys = headers.keySet();
 					for (Object key : keys) {
-						path.addParameter(new SwaggerEntity.Parameter(key.toString(),"header","string", headers.getOrDefault(key,"")));
+						path.addParameter(new SwaggerEntity.Parameter(key.toString(), "header", "string", headers.getOrDefault(key, "")));
 					}
 				}
-				if(map.containsKey("body")){
-					path.addParameter(new SwaggerEntity.Parameter("body","body",null,map.get("body")));
+				if (map.containsKey("body")) {
+					path.addParameter(new SwaggerEntity.Parameter("body", "body", null, map.get("body")));
 				}
 			} catch (IOException ignored) {
 			}
-			swaggerEntity.addPath(mappingHandlerMapping.getRequestPath(info.getGroupPrefix(),info.getPath()),info.getMethod(),path);
+			swaggerEntity.addPath(mappingHandlerMapping.getRequestPath(info.getGroupId(), info.getPath()), info.getMethod(), path);
 		}
 		return swaggerEntity;
 	}

+ 15 - 0
src/main/java/org/ssssssss/magicapi/utils/PathUtils.java

@@ -0,0 +1,15 @@
+package org.ssssssss.magicapi.utils;
+
+import java.util.regex.Pattern;
+
+public class PathUtils {
+
+	private static final Pattern REPLACE_SLASH_REGX = Pattern.compile("/+");
+
+	/**
+	 * 将多个/替换为一个/
+	 */
+	public static String replaceSlash(String path) {
+		return REPLACE_SLASH_REGX.matcher(path).replaceAll("/");
+	}
+}