Browse Source

新增数据库存储、Redis存储方案

mxd 4 năm trước cách đây
mục cha
commit
c272edff36

+ 7 - 0
src/main/java/org/ssssssss/magicapi/adapter/Resource.java

@@ -64,6 +64,13 @@ public interface Resource {
 	 */
 	byte[] read();
 
+	/**
+	 * 获取子目录
+	 */
+	default Resource getDirectory(String name){
+		return getResource(name);
+	}
+
 	/**
 	 * 获取子资源
 	 */

+ 93 - 0
src/main/java/org/ssssssss/magicapi/adapter/resource/DatabaseResource.java

@@ -0,0 +1,93 @@
+package org.ssssssss.magicapi.adapter.resource;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.ssssssss.magicapi.adapter.Resource;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class DatabaseResource extends KeyValueResource {
+
+	private final JdbcTemplate template;
+
+	private final String tableName;
+
+	public DatabaseResource(JdbcTemplate template, String tableName, String separator, String path, KeyValueResource parent) {
+		super(separator, path, parent);
+		this.template = template;
+		this.tableName = tableName;
+	}
+
+	public DatabaseResource(JdbcTemplate template, String tableName, String separator, String path) {
+		this(template, tableName, separator, path, null);
+	}
+
+	public DatabaseResource(JdbcTemplate template, String tableName, String path) {
+		this(template, tableName, "/", path);
+	}
+
+	public DatabaseResource(JdbcTemplate template, String tableName) {
+		this(template, tableName, "/magic-api");
+	}
+
+	@Override
+	public byte[] read() {
+		String sql = String.format("select file_content from %s where file_path = ?", tableName);
+		String value = template.queryForObject(sql, String.class, this.path);
+		return value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
+	}
+
+	@Override
+	public boolean exists() {
+		String sql = String.format("select count(*) from %s where file_path = ?", tableName);
+		Long value = template.queryForObject(sql, Long.class, this.path);
+		return value != null && value > 0;
+	}
+
+	@Override
+	public boolean write(String content) {
+		String sql = String.format("update %s set file_content = ? where file_path = ?", tableName);
+		if(exists()){
+			return template.update(sql, content,this.path) >= 0;
+		}
+		sql = String.format("insert into %s (file_path,file_content) values(?,?)", tableName);
+		return template.update(sql, this.path, content) > 0;
+	}
+
+	@Override
+	public Resource getResource(String name) {
+		return new DatabaseResource(template, tableName, separator, (isDirectory() ? this.path : this.path + separator) + name, this);
+	}
+
+	@Override
+	public Set<String> keys() {
+		String sql = String.format("select file_path from %s where file_path like '%s%%'", tableName, isDirectory() ? this.path : (this.path + separator));
+		return new HashSet<>(template.queryForList(sql, String.class));
+	}
+
+	@Override
+	public boolean renameTo(Map<String, String> renameKeys) {
+		List<Object[]> args = renameKeys.entrySet().stream().map(entry -> new Object[]{entry.getValue(), entry.getKey()}).collect(Collectors.toList());
+		String sql = String.format("update %s set file_path = ? where file_path = ?", tableName);
+		return Arrays.stream(template.batchUpdate(sql,args)).sum() > 0;
+	}
+
+	@Override
+	public boolean delete() {
+		String sql = String.format("delete from %s where file_path = ? or file_path like '%s%%'",tableName, isDirectory() ? this.path : this.path + separator);
+		return template.update(sql,this.path) > 0;
+	}
+
+	@Override
+	public Function<String, Resource> mappedFunction() {
+		return it -> new DatabaseResource(template, tableName, separator, it, this);
+	}
+
+	@Override
+	public String toString() {
+		return String.format("db://%s/%s", tableName, Objects.toString(this.path, ""));
+	}
+
+}

+ 2 - 2
src/main/java/org/ssssssss/magicapi/adapter/resource/FileResource.java

@@ -13,7 +13,7 @@ public class FileResource implements Resource {
 
 	private File file;
 
-	private boolean readonly;
+	private final boolean readonly;
 
 	public FileResource(File file, boolean readonly) {
 		this.file = file;
@@ -110,6 +110,6 @@ public class FileResource implements Resource {
 
 	@Override
 	public String toString() {
-		return String.format("file resource [%s]", this.file.getAbsolutePath());
+		return String.format("file://%s", this.file.getAbsolutePath());
 	}
 }

+ 6 - 6
src/main/java/org/ssssssss/magicapi/adapter/resource/JarResource.java

@@ -15,17 +15,17 @@ import java.util.zip.ZipEntry;
 
 public class JarResource implements Resource {
 
-	private JarFile jarFile;
+	private final JarFile jarFile;
 
-	private ZipEntry entry;
+	private final ZipEntry entry;
 
-	private List<JarEntry> entries;
+	private final List<JarEntry> entries;
 
-	private String entryName;
+	private final String entryName;
 
 	private JarResource parent = this;
 
-	private boolean inSpringBoot;
+	private final boolean inSpringBoot;
 
 	public JarResource(JarFile jarFile, String entryName, List<JarEntry> entries, boolean inSpringBoot) {
 		this.jarFile = jarFile;
@@ -122,6 +122,6 @@ public class JarResource implements Resource {
 
 	@Override
 	public String toString() {
-		return String.format("class path resource [%s]", this.entryName);
+		return String.format("jar://%s", this.entryName);
 	}
 }

+ 117 - 0
src/main/java/org/ssssssss/magicapi/adapter/resource/KeyValueResource.java

@@ -0,0 +1,117 @@
+package org.ssssssss.magicapi.adapter.resource;
+
+import org.ssssssss.magicapi.adapter.Resource;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public abstract class KeyValueResource implements Resource {
+
+	protected String separator;
+
+	protected String path;
+
+	protected KeyValueResource parent;
+
+	public KeyValueResource(String separator, String path, KeyValueResource parent) {
+		this.separator = separator;
+		this.path = path;
+		this.parent = parent;
+	}
+
+	@Override
+	public boolean isDirectory() {
+		return this.path.endsWith(separator);
+	}
+
+	@Override
+	public final boolean renameTo(Resource resource) {
+		if (!(resource instanceof KeyValueResource)) {
+			throw new IllegalArgumentException("无法将" + this.getAbsolutePath() + "重命名为:" + resource.getAbsolutePath());
+		}
+		KeyValueResource targetResource = (KeyValueResource) resource;
+		// 判断移动的是否是文件夹
+		if (resource.isDirectory()) {
+			Set<String> oldKeys = this.keys();
+			Map<String,String> mappings = new HashMap<>(oldKeys.size());
+			int keyLen = this.path.length();
+			oldKeys.forEach(oldKey -> mappings.put(oldKey,targetResource.path + oldKey.substring(keyLen)));
+			return renameTo(mappings);
+		} else {
+			return renameTo(Collections.singletonMap(this.path, targetResource.path));
+		}
+	}
+
+	@Override
+	public boolean delete() {
+		return this.keys().stream().allMatch(this::deleteByKey);
+	}
+
+	protected boolean deleteByKey(String key){
+		return false;
+	}
+
+	/**
+	 * 需要做修改的key,原key: 新key
+	 */
+	protected abstract boolean renameTo(Map<String, String> renameKeys);
+
+	@Override
+	public String name() {
+		String name = this.path;
+		if(isDirectory()){
+			name = this.path.substring(0,name.length() - 1);
+		}
+		int index = name.lastIndexOf(separator);
+		return index > -1 ? name.substring(index + 1) : name;
+	}
+
+	@Override
+	public Resource getDirectory(String name) {
+		return getResource(name + separator);
+	}
+
+	@Override
+	public boolean mkdir() {
+		if(!isDirectory()){
+			this.path += separator;
+		}
+		return write("this is directory");
+	}
+
+	@Override
+	public Resource parent() {
+		return this.parent;
+	}
+
+	@Override
+	public boolean write(byte[] bytes) {
+		return write(new String(bytes, StandardCharsets.UTF_8));
+	}
+
+	@Override
+	public List<Resource> resources() {
+		return keys().stream().map(mappedFunction()).collect(Collectors.toList());
+	}
+
+	protected abstract Function<String, Resource> mappedFunction();
+
+	protected abstract Set<String> keys();
+
+	@Override
+	public List<Resource> dirs() {
+		return resources().stream().filter(Resource::isDirectory).collect(Collectors.toList());
+	}
+
+	@Override
+	public List<Resource> files(String suffix) {
+		return resources().stream().filter(it -> it.name().endsWith(suffix)).collect(Collectors.toList());
+	}
+
+	@Override
+	public String getAbsolutePath() {
+		return this.path;
+	}
+}

+ 101 - 0
src/main/java/org/ssssssss/magicapi/adapter/resource/RedisResource.java

@@ -0,0 +1,101 @@
+package org.ssssssss.magicapi.adapter.resource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.ssssssss.magicapi.adapter.Resource;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+public class RedisResource extends KeyValueResource {
+
+	private final StringRedisTemplate redisTemplate;
+
+	private static final Logger logger = LoggerFactory.getLogger(RedisResource.class);
+
+	public RedisResource(StringRedisTemplate redisTemplate, String path, RedisResource parent) {
+		this(redisTemplate, path, ":", parent);
+	}
+
+	public RedisResource(StringRedisTemplate redisTemplate, String path, String separator, RedisResource parent) {
+		super(separator, path, parent);
+		this.redisTemplate = redisTemplate;
+	}
+
+	public RedisResource(StringRedisTemplate redisTemplate, String path) {
+		this(redisTemplate, path, null);
+	}
+
+	@Override
+	public byte[] read() {
+		String value = redisTemplate.opsForValue().get(path);
+		return value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
+	}
+
+	@Override
+	public boolean write(String content) {
+		this.redisTemplate.opsForValue().set(this.path, content);
+		return true;
+	}
+
+	@Override
+	public Resource getResource(String name) {
+		return new RedisResource(this.redisTemplate, (isDirectory() ? this.path : this.path + separator) + name, this);
+	}
+
+	@Override
+	protected boolean renameTo(Map<String, String> renameKeys) {
+		renameKeys.forEach(this.redisTemplate::rename);
+		return true;
+	}
+
+	@Override
+	public boolean exists() {
+		return Boolean.TRUE.equals(this.redisTemplate.hasKey(this.path));
+	}
+
+
+	@Override
+	protected boolean deleteByKey(String key) {
+		return Boolean.TRUE.equals(this.redisTemplate.delete(key));
+	}
+
+	@Override
+	protected Function<String, Resource> mappedFunction() {
+		return (it) -> new RedisResource(this.redisTemplate, it, this);
+	}
+
+	@Override
+	protected Set<String> keys() {
+		Set<String> keys = this.redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
+			ScanOptions options = new ScanOptions.ScanOptionsBuilder()
+					.count(Long.MAX_VALUE)
+					.match((isDirectory() ? this.path : (this.path + separator)) + "*")
+					.build();
+			Set<String> returnKeys = new HashSet<>();
+			try (Cursor<byte[]> cursor = connection.scan(options)) {
+				while (cursor.hasNext()) {
+					returnKeys.add(new String(cursor.next()));
+				}
+			} catch (IOException e) {
+				logger.error("扫描key出错", e);
+			}
+			return returnKeys;
+		});
+		return keys == null ? Collections.emptySet() : keys;
+	}
+
+	@Override
+	public String toString() {
+		return String.format("redis://%s", getAbsolutePath());
+	}
+}

+ 12 - 15
src/main/java/org/ssssssss/magicapi/config/MappingHandlerMapping.java

@@ -38,9 +38,9 @@ public class MappingHandlerMapping {
 	/**
 	 * 已缓存的映射信息
 	 */
-	private static Map<String, MappingNode> mappings = new ConcurrentHashMap<>();
+	private static final Map<String, MappingNode> mappings = new ConcurrentHashMap<>();
 
-	private static Logger logger = LoggerFactory.getLogger(MappingHandlerMapping.class);
+	private static final Logger logger = LoggerFactory.getLogger(MappingHandlerMapping.class);
 
 	/**
 	 * spring中的请求映射处理器
@@ -55,7 +55,7 @@ public class MappingHandlerMapping {
 	/**
 	 * 请求到达时处理的方法
 	 */
-	private Method method = RequestHandler.class.getDeclaredMethod("invoke", HttpServletRequest.class, HttpServletResponse.class, Map.class, Map.class);
+	private final Method method = RequestHandler.class.getDeclaredMethod("invoke", HttpServletRequest.class, HttpServletResponse.class, Map.class, Map.class);
 
 	/**
 	 * 接口信息读取
@@ -85,7 +85,7 @@ public class MappingHandlerMapping {
 	/**
 	 * 缓存已映射的接口信息
 	 */
-	private List<ApiInfo> apiInfos = Collections.synchronizedList(new ArrayList<>());
+	private final List<ApiInfo> apiInfos = Collections.synchronizedList(new ArrayList<>());
 
 	public MappingHandlerMapping() throws NoSuchMethodException {
 	}
@@ -212,10 +212,8 @@ public class MappingHandlerMapping {
 			}
 			if (!allowOverride) {
 				Map<RequestMappingInfo, HandlerMethod> handlerMethods = this.requestMappingHandlerMapping.getHandlerMethods();
-				if (handlerMethods != null) {
-					if (handlerMethods.get(getRequestMapping(info.getMethod(), path)) != null) {
-						return true;
-					}
+				if (handlerMethods.get(getRequestMapping(info.getMethod(), path)) != null) {
+					return true;
 				}
 			}
 		}
@@ -232,10 +230,11 @@ public class MappingHandlerMapping {
 	 */
 	public boolean checkGroup(Group group) {
 		TreeNode<Group> oldTree = groups.findTreeNode((item) -> item.getId().equals(group.getId()));
-		// 如果只改了名字,则不做任何操作
-		if (Objects.equals(oldTree.getNode().getParentId(), group.getParentId()) &&
-				Objects.equals(oldTree.getNode().getPath(), group.getPath())) {
-			return true;
+		// 如果移动目录或改了名字,判断是否冲突
+		if (Objects.equals(oldTree.getNode().getParentId(), group.getParentId()) || Objects.equals(oldTree.getNode().getName(), group.getName())) {
+			if(groupServiceProvider.exists(group)){
+				return false;
+			}
 		}
 		// 新的接口分组路径
 		String newPath = groupServiceProvider.getFullPath(group.getParentId());
@@ -298,9 +297,7 @@ public class MappingHandlerMapping {
 		}
 		if (!allowOverride) {
 			Map<RequestMappingInfo, HandlerMethod> handlerMethods = this.requestMappingHandlerMapping.getHandlerMethods();
-			if (handlerMethods != null) {
-				return handlerMethods.get(getRequestMapping(info)) != null;
-			}
+			return handlerMethods.get(getRequestMapping(info)) != null;
 		}
 		return false;
 	}

+ 3 - 3
src/main/java/org/ssssssss/magicapi/controller/MagicAPIController.java

@@ -177,15 +177,15 @@ public class MagicAPIController extends MagicController {
 			}
 			if (StringUtils.isBlank(info.getId())) {
 				// 先判断接口是否存在
-				if (magicApiService.exists(info.getGroupId(), info.getMethod(), info.getPath())) {
-					return new JsonBean<>(0, String.format("接口%s:%s已存在", info.getMethod(), info.getPath()));
+				if (magicApiService.exists(info.getName(), info.getGroupId(), info.getMethod(), info.getPath())) {
+					return new JsonBean<>(0, String.format("接口%s:%s已存在或接口名称重复", info.getMethod(), info.getPath()));
 				}
 				if (!magicApiService.insert(info)) {
 					return new JsonBean<>(0, "保存失败,请检查接口名称是否重复且不能包含特殊字符。");
 				}
 			} else {
 				// 先判断接口是否存在
-				if (magicApiService.existsWithoutId(info.getGroupId(), info.getMethod(), info.getPath(), info.getId())) {
+				if (magicApiService.existsWithoutId(info.getName(), info.getGroupId(), info.getMethod(), info.getPath(), info.getId())) {
 					return new JsonBean<>(0, String.format("接口%s:%s已存在", info.getMethod(), info.getPath()));
 				}
 				Optional<ApiInfo> optional = configuration.getMappingHandlerMapping().getApiInfos().stream()

+ 10 - 7
src/main/java/org/ssssssss/magicapi/controller/MagicGroupController.java

@@ -51,23 +51,23 @@ public class MagicGroupController extends MagicController {
 				}
 				isApi = false;
 			}
-			List<String> groupIds = treeNode.flat().stream().map(Group::getId).collect(Collectors.toList());
+			List<String> children = treeNode.flat().stream().map(Group::getId).collect(Collectors.toList());
 			boolean success;
 			if (isApi) {
 				// 删除接口
-				if (success = configuration.getMagicApiService().deleteGroup(groupIds)) {
+				if (success = configuration.getMagicApiService().deleteGroup(groupId, children)) {
 					// 取消注册
-					configuration.getMappingHandlerMapping().deleteGroup(groupIds);
-					groupIds.forEach(configuration.getGroupServiceProvider()::delete);
+					configuration.getMappingHandlerMapping().deleteGroup(children);
+					children.forEach(configuration.getGroupServiceProvider()::delete);
 					// 重新加载分组
 					configuration.getMappingHandlerMapping().loadGroup();
 				}
 			} else {
 				// 删除函数
-				if (success = configuration.getFunctionServiceProvider().deleteGroup(groupIds)) {
+				if (success = configuration.getFunctionServiceProvider().deleteGroup(groupId, children)) {
 					// 取消注册
-					configuration.getMagicFunctionManager().deleteGroup(groupIds);
-					groupIds.forEach(configuration.getGroupServiceProvider()::delete);
+					configuration.getMagicFunctionManager().deleteGroup(children);
+					children.forEach(configuration.getGroupServiceProvider()::delete);
 					// 重新加载分组
 					configuration.getMagicFunctionManager().loadGroup();
 				}
@@ -103,6 +103,9 @@ public class MagicGroupController extends MagicController {
 		if (StringUtils.isBlank(group.getType())) {
 			return new JsonBean<>(0, "分组类型不能为空");
 		}
+		if (groupServiceProvider.exists(group)) {
+			return new JsonBean<>(-20, "修改分组后,名称会有冲突,请检查!");
+		}
 		try {
 			boolean isApiGroup = "1".equals(group.getType());
 			boolean isFunctionGroup = "2".equals(group.getType());

+ 6 - 4
src/main/java/org/ssssssss/magicapi/provider/ApiServiceProvider.java

@@ -17,26 +17,28 @@ public abstract class ApiServiceProvider extends StoreServiceProvider<ApiInfo> {
 	/**
 	 * 判断接口是否存在
 	 *
+	 * @param name 接口名称
 	 * @param groupId 分组Id
 	 * @param method  请求方法
 	 * @param path    请求路径
 	 */
-	public boolean exists(String groupId, String method, String path){
+	public boolean exists(String name, String groupId, String method, String path) {
 		return infos.values().stream()
-				.anyMatch(it -> groupId.equals(it.getGroupId()) && method.equals(it.getMethod()) && path.equals(it.getPath()));
+				.anyMatch(it -> groupId.equals(it.getGroupId()) && (name.equals(it.getName()) || (method.equals(it.getMethod()) && path.equals(it.getPath()))));
 	}
 
 	/**
 	 * 判断接口是否存在
 	 *
+	 * @param name 接口名称
 	 * @param groupId 分组ID
 	 * @param method  请求方法
 	 * @param path    请求路径
 	 * @param id      排除接口
 	 */
-	public boolean existsWithoutId(String groupId, String method, String path, String id){
+	public boolean existsWithoutId(String name, String groupId, String method, String path, String id) {
 		return infos.values().stream()
-				.anyMatch(it -> !id.equals(it.getId()) && groupId.equals(it.getGroupId()) && method.equals(it.getMethod()) && path.equals(it.getPath()));
+				.anyMatch(it -> !id.equals(it.getId()) && groupId.equals(it.getGroupId()) && (name.equals(it.getName()) || (method.equals(it.getMethod()) && path.equals(it.getPath()))));
 	}
 
 	@Override

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

@@ -24,6 +24,11 @@ public interface GroupServiceProvider {
 	 */
 	boolean delete(String groupId);
 
+	/**
+	 * 分组是否存在
+	 */
+	boolean exists(Group group);
+
 	/**
 	 * 是否有该分组
 	 */

+ 14 - 8
src/main/java/org/ssssssss/magicapi/provider/StoreServiceProvider.java

@@ -31,7 +31,13 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 		this.clazz = clazz;
 		this.workspace = workspace;
 		this.groupServiceProvider = groupServiceProvider;
-		this.backupResource = this.workspace.parent().getResource("backups");
+		if(!this.workspace.exists()){
+			this.workspace.mkdir();
+		}
+		this.backupResource = this.workspace.parent().getDirectory("backups");
+		if(!this.backupResource.exists()){
+			this.backupResource.mkdir();
+		}
 	}
 
 
@@ -55,7 +61,7 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 	 * 备份历史记录
 	 */
 	public boolean backup(T info) {
-		Resource directory = this.backupResource.getResource(info.getId());
+		Resource directory = this.backupResource.getDirectory(info.getId());
 		if(!directory.readonly() && (directory.exists() || directory.mkdir())){
 			Resource resource = directory.getResource(String.format("%s.ms", System.currentTimeMillis()));
 			return resource.write(serialize(info));
@@ -70,7 +76,7 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 	 * @return 时间戳列表
 	 */
 	public List<Long> backupList(String id) {
-		Resource directory = this.backupResource.getResource(id);
+		Resource directory = this.backupResource.getDirectory(id);
 		List<Resource> resources = directory.files(".ms");
 		return resources.stream().map(it -> Long.valueOf(it.name().replace(".ms",""))).collect(Collectors.toList());
 	}
@@ -82,7 +88,7 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 	 * @param timestamp 时间戳
 	 */
 	public T backupInfo(String id, Long timestamp) {
-		Resource directory = this.backupResource.getResource(id);
+		Resource directory = this.backupResource.getDirectory(id);
 		if(directory.exists()){
 			Resource resource = directory.getResource(String.format("%s.ms", timestamp));
 			if(resource.exists()){
@@ -195,11 +201,11 @@ public abstract class StoreServiceProvider<T extends MagicEntity> {
 	/**
 	 * 根据组ID删除
 	 */
-	public boolean deleteGroup(List<String> groupIds) {
+	public boolean deleteGroup(String rootId,List<String> groupIds) {
+		if (!groupServiceProvider.getGroupResource(rootId).delete()) {
+			return false;
+		}
 		for (String groupId : groupIds) {
-			if (!groupServiceProvider.getGroupResource(groupId).delete()) {
-				return false;
-			}
 			List<String> infoIds = infos.values().stream().filter(info -> groupId.equals(info.getGroupId()))
 					.map(T::getId)
 					.collect(Collectors.toList());

+ 13 - 4
src/main/java/org/ssssssss/magicapi/provider/impl/DefaultGroupServiceProvider.java

@@ -30,7 +30,7 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 	public boolean insert(Group group) {
 		group.setId(UUID.randomUUID().toString().replace("-", ""));
 		Resource directory = this.getGroupResource(group.getParentId());
-		directory = directory == null ? this.getGroupResource(group.getType(),group.getName()) : directory.getResource(group.getName());
+		directory = directory == null ? this.getGroupResource(group.getType(),group.getName()) : directory.getDirectory(group.getName());
 		if (!directory.exists() && directory.mkdir()) {
 			Resource resource = directory.getResource(metabase);
 			if (resource.write(JsonUtils.toJsonString(group))) {
@@ -42,14 +42,14 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 	}
 
 	private Resource getGroupResource(String type, String name) {
-		return this.workspace.getResource("1".equals(type) ? "api" : "function").getResource(name);
+		return this.workspace.getDirectory("1".equals(type) ? "api" : "function").getDirectory(name);
 	}
 
 	@Override
 	public boolean update(Group group) {
 		Resource oldResource = this.getGroupResource(group.getId());
 		Resource newResource = this.getGroupResource(group.getParentId());
-		newResource = newResource == null ? getGroupResource(group.getType(),group.getName()) : newResource.getResource(group.getName());
+		newResource = newResource == null ? getGroupResource(group.getType(),group.getName()) : newResource.getDirectory(group.getName());
 		// 重命名或移动目录
 		if(oldResource.renameTo(newResource)){
 			Resource target = newResource.getResource(metabase);
@@ -67,6 +67,15 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 		return true;
 	}
 
+	@Override
+	public boolean exists(Group group) {
+		Resource resource = getGroupResource(group.getParentId());
+		if(resource == null){
+			return getGroupResource(group.getType(),group.getName()).exists();
+		}
+		return resource.getDirectory(group.getName()).exists();
+	}
+
 	@Override
 	public boolean containsApiGroup(String groupId) {
 		return "0".equals(groupId) || cacheApiTree.containsKey(groupId);
@@ -88,7 +97,7 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 
 	@Override
 	public List<Group> groupList(String type) {
-		Resource resource = this.workspace.getResource("1".equals(type) ? "api" : "function");
+		Resource resource = this.workspace.getDirectory("1".equals(type) ? "api" : "function");
 		return resource.dirs().stream().map(it -> it.getResource(metabase)).filter(Resource::exists)
 				.map(it -> {
 					Group group = JsonUtils.readValue(it.read(), Group.class);