mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 22:57:17 +08:00 
			
		
		
		
	feat(system/config): 移除系统管理,新增存储配置
This commit is contained in:
		| @@ -29,19 +29,6 @@ | ||||
|             <artifactId>sms4j-spring-boot-starter</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- X File Storage(一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台) --> | ||||
|         <dependency> | ||||
|             <groupId>org.dromara.x-file-storage</groupId> | ||||
|             <artifactId>x-file-storage-spring</artifactId> | ||||
|             <version>2.2.1</version> | ||||
|         </dependency> | ||||
|         <!-- Amazon S3(Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储) --> | ||||
|         <dependency> | ||||
|             <groupId>com.amazonaws</groupId> | ||||
|             <artifactId>aws-java-sdk-s3</artifactId> | ||||
|             <version>1.12.780</version> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- FreeMarker(模板引擎) --> | ||||
|         <dependency> | ||||
|             <groupId>org.freemarker</groupId> | ||||
| @@ -149,5 +136,19 @@ | ||||
|             <groupId>top.continew</groupId> | ||||
|             <artifactId>continew-starter-json-jackson</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|  | ||||
|         <!-- ContiNew Starter 存储模块 - 本地存储 --> | ||||
|         <dependency> | ||||
|             <groupId>top.continew</groupId> | ||||
|             <artifactId>continew-starter-storage-local</artifactId> | ||||
|         </dependency> | ||||
|  | ||||
|  | ||||
|         <!-- ContiNew Starter 存储模块 - 对象存储 --> | ||||
|         <dependency> | ||||
|             <groupId>top.continew</groupId> | ||||
|             <artifactId>continew-starter-storage-oss</artifactId> | ||||
|         </dependency> | ||||
|     </dependencies> | ||||
| </project> | ||||
| @@ -0,0 +1,216 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.common.config.doc; | ||||
|  | ||||
| import cn.dev33.satoken.annotation.SaIgnore; | ||||
| import cn.hutool.core.map.MapUtil; | ||||
| import io.swagger.v3.oas.models.Components; | ||||
| import io.swagger.v3.oas.models.OpenAPI; | ||||
| import io.swagger.v3.oas.models.PathItem; | ||||
| import io.swagger.v3.oas.models.security.SecurityRequirement; | ||||
| import io.swagger.v3.oas.models.security.SecurityScheme; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springdoc.core.customizers.GlobalOpenApiCustomizer; | ||||
| import org.springframework.aop.support.AopUtils; | ||||
| import org.springframework.context.ApplicationContext; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.util.AntPathMatcher; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| import top.continew.starter.apidoc.autoconfigure.SpringDocExtensionProperties; | ||||
| import top.continew.starter.auth.satoken.autoconfigure.SaTokenExtensionProperties; | ||||
| import top.continew.starter.extension.crud.annotation.CrudRequestMapping; | ||||
|  | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.*; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * 全局鉴权参数定制器 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2024/12/31 13:36 | ||||
|  */ | ||||
| @Slf4j | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public class GlobalAuthenticationCustomizer implements GlobalOpenApiCustomizer { | ||||
|  | ||||
|     private final SpringDocExtensionProperties properties; | ||||
|     private final SaTokenExtensionProperties saTokenExtensionProperties; | ||||
|     private final ApplicationContext context; | ||||
|     private final AntPathMatcher pathMatcher = new AntPathMatcher(); | ||||
|  | ||||
|     /** | ||||
|      * 定制 OpenAPI 文档 | ||||
|      * | ||||
|      * @param openApi 当前 OpenAPI 对象 | ||||
|      */ | ||||
|     @Override | ||||
|     public void customise(OpenAPI openApi) { | ||||
|         if (MapUtil.isEmpty(openApi.getPaths())) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // 收集需要排除的路径(包括 Sa-Token 配置中的排除路径和 @SaIgnore 注解路径) | ||||
|         Set<String> excludedPaths = collectExcludedPaths(); | ||||
|  | ||||
|         // 遍历所有路径,为需要鉴权的路径添加安全认证配置 | ||||
|         openApi.getPaths().forEach((path, pathItem) -> { | ||||
|             if (isPathExcluded(path, excludedPaths)) { | ||||
|                 // 路径在排除列表中,跳过处理 | ||||
|                 return; | ||||
|             } | ||||
|             // 为路径添加安全认证参数 | ||||
|             addAuthenticationParameters(pathItem); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 收集所有需要排除的路径 | ||||
|      * | ||||
|      * @return 排除路径集合 | ||||
|      */ | ||||
|     private Set<String> collectExcludedPaths() { | ||||
|         Set<String> excludedPaths = new HashSet<>(); | ||||
|         excludedPaths.addAll(Arrays.asList(saTokenExtensionProperties.getSecurity().getExcludes())); | ||||
|         excludedPaths.addAll(resolveSaIgnorePaths()); | ||||
|         return excludedPaths; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 为路径项添加认证参数 | ||||
|      * | ||||
|      * @param pathItem 当前路径项 | ||||
|      */ | ||||
|     private void addAuthenticationParameters(PathItem pathItem) { | ||||
|         Components components = properties.getComponents(); | ||||
|         if (components == null || MapUtil.isEmpty(components.getSecuritySchemes())) { | ||||
|             return; | ||||
|         } | ||||
|         Map<String, SecurityScheme> securitySchemes = components.getSecuritySchemes(); | ||||
|         List<String> schemeNames = securitySchemes.values().stream().map(SecurityScheme::getName).toList(); | ||||
|         pathItem.readOperations().forEach(operation -> { | ||||
|             SecurityRequirement securityRequirement = new SecurityRequirement(); | ||||
|             schemeNames.forEach(securityRequirement::addList); | ||||
|             operation.addSecurityItem(securityRequirement); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解析所有带有 @SaIgnore 注解的路径 | ||||
|      * | ||||
|      * @return 被忽略的路径集合 | ||||
|      */ | ||||
|     private Set<String> resolveSaIgnorePaths() { | ||||
|         // 获取所有标注 @RestController 的 Bean | ||||
|         Map<String, Object> controllers = context.getBeansWithAnnotation(RestController.class); | ||||
|         Set<String> ignoredPaths = new HashSet<>(); | ||||
|  | ||||
|         // 遍历所有控制器,解析 @SaIgnore 注解路径 | ||||
|         controllers.values().forEach(controllerBean -> { | ||||
|             Class<?> controllerClass = AopUtils.getTargetClass(controllerBean); | ||||
|             List<String> classPaths = getClassPaths(controllerClass); | ||||
|  | ||||
|             // 类级别的 @SaIgnore 注解 | ||||
|             if (controllerClass.isAnnotationPresent(SaIgnore.class)) { | ||||
|                 classPaths.forEach(classPath -> ignoredPaths.add(classPath + "/**")); | ||||
|             } | ||||
|  | ||||
|             // 方法级别的 @SaIgnore 注解 | ||||
|             Arrays.stream(controllerClass.getDeclaredMethods()) | ||||
|                 .filter(method -> method.isAnnotationPresent(SaIgnore.class)) | ||||
|                 .forEach(method -> ignoredPaths.addAll(combinePaths(classPaths, getMethodPaths(method)))); | ||||
|         }); | ||||
|  | ||||
|         return ignoredPaths; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取类上的所有路径 | ||||
|      * | ||||
|      * @param controller 控制器类 | ||||
|      * @return 类路径列表 | ||||
|      */ | ||||
|     private List<String> getClassPaths(Class<?> controller) { | ||||
|         List<String> classPaths = new ArrayList<>(); | ||||
|         // 处理 @RequestMapping 注解 | ||||
|         if (controller.isAnnotationPresent(RequestMapping.class)) { | ||||
|             RequestMapping mapping = controller.getAnnotation(RequestMapping.class); | ||||
|             classPaths.addAll(Arrays.asList(mapping.value())); | ||||
|         } | ||||
|         // 处理 @CrudRequestMapping 注解 | ||||
|         if (controller.isAnnotationPresent(CrudRequestMapping.class)) { | ||||
|             CrudRequestMapping mapping = controller.getAnnotation(CrudRequestMapping.class); | ||||
|             if (!mapping.value().isEmpty()) { | ||||
|                 classPaths.add(mapping.value()); | ||||
|             } | ||||
|         } | ||||
|         return classPaths; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取方法上的所有路径 | ||||
|      * | ||||
|      * @param method 控制器方法 | ||||
|      * @return 方法路径列表 | ||||
|      */ | ||||
|     private List<String> getMethodPaths(Method method) { | ||||
|         List<String> methodPaths = new ArrayList<>(); | ||||
|  | ||||
|         // 检查方法上的各种映射注解 | ||||
|         if (method.isAnnotationPresent(GetMapping.class)) { | ||||
|             methodPaths.addAll(Arrays.asList(method.getAnnotation(GetMapping.class).value())); | ||||
|         } else if (method.isAnnotationPresent(PostMapping.class)) { | ||||
|             methodPaths.addAll(Arrays.asList(method.getAnnotation(PostMapping.class).value())); | ||||
|         } else if (method.isAnnotationPresent(PutMapping.class)) { | ||||
|             methodPaths.addAll(Arrays.asList(method.getAnnotation(PutMapping.class).value())); | ||||
|         } else if (method.isAnnotationPresent(DeleteMapping.class)) { | ||||
|             methodPaths.addAll(Arrays.asList(method.getAnnotation(DeleteMapping.class).value())); | ||||
|         } else if (method.isAnnotationPresent(RequestMapping.class)) { | ||||
|             methodPaths.addAll(Arrays.asList(method.getAnnotation(RequestMapping.class).value())); | ||||
|         } else if (method.isAnnotationPresent(PatchMapping.class)) { | ||||
|             methodPaths.addAll(Arrays.asList(method.getAnnotation(PatchMapping.class).value())); | ||||
|         } | ||||
|  | ||||
|         return methodPaths; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 组合类路径和方法路径 | ||||
|      * | ||||
|      * @param classPaths  类路径列表 | ||||
|      * @param methodPaths 方法路径列表 | ||||
|      * @return 完整路径集合 | ||||
|      */ | ||||
|     private Set<String> combinePaths(List<String> classPaths, List<String> methodPaths) { | ||||
|         return classPaths.stream() | ||||
|             .flatMap(classPath -> methodPaths.stream().map(methodPath -> classPath + methodPath)) | ||||
|             .collect(Collectors.toSet()); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查路径是否在排除列表中 | ||||
|      * | ||||
|      * @param path          当前路径 | ||||
|      * @param excludedPaths 排除路径集合,支持通配符 | ||||
|      * @return 是否匹配排除规则 | ||||
|      */ | ||||
|     private boolean isPathExcluded(String path, Set<String> excludedPaths) { | ||||
|         return excludedPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path)); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,65 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.common.config.doc; | ||||
|  | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import io.swagger.v3.oas.models.Operation; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| import org.springdoc.core.customizers.GlobalOperationCustomizer; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.springframework.web.method.HandlerMethod; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * 全局描述定制器 - 处理 sa-token 的注解权限码 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2025/01/24 14:59 | ||||
|  */ | ||||
| @Slf4j | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public class GlobalDescriptionCustomizer implements GlobalOperationCustomizer { | ||||
|  | ||||
|     @Override | ||||
|     public Operation customize(Operation operation, HandlerMethod handlerMethod) { | ||||
|         // 将 sa-token 注解数据添加到 operation 的描述中 | ||||
|         // 权限 | ||||
|         List<String> noteList = new ArrayList<>(new OperationDescriptionCustomizer().getPermission(handlerMethod)); | ||||
|  | ||||
|         // 如果注解数据列表为空,直接返回原 operation | ||||
|         if (noteList.isEmpty()) { | ||||
|             return operation; | ||||
|         } | ||||
|         // 拼接注解数据为字符串 | ||||
|         String noteStr = StrUtil.join("<br/>", noteList); | ||||
|         // 获取原描述 | ||||
|         String originalDescription = operation.getDescription(); | ||||
|         // 根据原描述是否为空,更新描述 | ||||
|         String newDescription = StringUtils.isNotEmpty(originalDescription) | ||||
|             ? originalDescription + "<br/>" + noteStr | ||||
|             : noteStr; | ||||
|  | ||||
|         // 设置新描述 | ||||
|         operation.setDescription(newDescription); | ||||
|         return operation; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,180 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.common.config.doc; | ||||
|  | ||||
| import cn.dev33.satoken.annotation.SaCheckPermission; | ||||
| import cn.dev33.satoken.annotation.SaCheckRole; | ||||
| import cn.dev33.satoken.annotation.SaMode; | ||||
| import cn.hutool.core.text.CharSequenceUtil; | ||||
| import org.springframework.web.method.HandlerMethod; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.extension.crud.annotation.CrudApi; | ||||
| import top.continew.starter.extension.crud.annotation.CrudRequestMapping; | ||||
| import top.continew.starter.extension.crud.enums.Api; | ||||
|  | ||||
| import java.lang.annotation.Annotation; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * Operation 描述定制器 处理 sa-token 鉴权标识符 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @since 2024/06/14 11:18 | ||||
|  */ | ||||
| public class OperationDescriptionCustomizer { | ||||
|  | ||||
|     /** | ||||
|      * 获取 sa-token 注解信息 | ||||
|      * | ||||
|      * @param handlerMethod 处理程序方法 | ||||
|      * @return 包含权限和角色校验信息的列表 | ||||
|      */ | ||||
|     public List<String> getPermission(HandlerMethod handlerMethod) { | ||||
|         List<String> values = new ArrayList<>(); | ||||
|  | ||||
|         // 获取权限校验信息 | ||||
|         String permissionInfo = getAnnotationInfo(handlerMethod, SaCheckPermission.class, "权限校验:"); | ||||
|         if (!permissionInfo.isEmpty()) { | ||||
|             values.add(permissionInfo); | ||||
|         } | ||||
|  | ||||
|         // 获取角色校验信息 | ||||
|         String roleInfo = getAnnotationInfo(handlerMethod, SaCheckRole.class, "角色校验:"); | ||||
|         if (!roleInfo.isEmpty()) { | ||||
|             values.add(roleInfo); | ||||
|         } | ||||
|  | ||||
|         // 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息 | ||||
|         String crudPermissionInfo = getCrudPermissionInfo(handlerMethod); | ||||
|         if (!crudPermissionInfo.isEmpty()) { | ||||
|             values.add(crudPermissionInfo); | ||||
|         } | ||||
|         return values; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 获取类和方法上指定注解的信息 | ||||
|      * | ||||
|      * @param handlerMethod   处理程序方法 | ||||
|      * @param annotationClass 注解类 | ||||
|      * @param title           信息标题 | ||||
|      * @param <A>             注解类型 | ||||
|      * @return 拼接好的注解信息字符串 | ||||
|      */ | ||||
|     @SuppressWarnings("unchecked") | ||||
|     private <A extends Annotation> String getAnnotationInfo(HandlerMethod handlerMethod, | ||||
|                                                             Class<A> annotationClass, | ||||
|                                                             String title) { | ||||
|         StringBuilder infoBuilder = new StringBuilder(); | ||||
|  | ||||
|         // 获取类上的注解 | ||||
|         A classAnnotation = handlerMethod.getBeanType().getAnnotation(annotationClass); | ||||
|         if (classAnnotation != null) { | ||||
|             appendAnnotationInfo(infoBuilder, "类:", classAnnotation); | ||||
|         } | ||||
|  | ||||
|         // 获取方法上的注解 | ||||
|         A methodAnnotation = handlerMethod.getMethodAnnotation(annotationClass); | ||||
|         if (methodAnnotation != null) { | ||||
|             appendAnnotationInfo(infoBuilder, "方法:", methodAnnotation); | ||||
|         } | ||||
|  | ||||
|         // 如果有注解信息,添加标题 | ||||
|         if (!infoBuilder.isEmpty()) { | ||||
|             infoBuilder.insert(0, "<font style=\"color:red\" class=\"light-red\">" + title + "</font></br>"); | ||||
|         } | ||||
|  | ||||
|         return infoBuilder.toString(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 拼接注解信息到 StringBuilder 中 | ||||
|      * | ||||
|      * @param builder    用于拼接信息的 StringBuilder | ||||
|      * @param prefix     前缀信息,如 "类:" 或 "方法:" | ||||
|      * @param annotation 注解对象 | ||||
|      */ | ||||
|     private void appendAnnotationInfo(StringBuilder builder, String prefix, Annotation annotation) { | ||||
|         String[] values = null; | ||||
|         SaMode mode = null; | ||||
|         String type = ""; | ||||
|         String[] orRole = new String[0]; | ||||
|  | ||||
|         if (annotation instanceof SaCheckPermission checkPermission) { | ||||
|             values = checkPermission.value(); | ||||
|             mode = checkPermission.mode(); | ||||
|             type = checkPermission.type(); | ||||
|             orRole = checkPermission.orRole(); | ||||
|         } else if (annotation instanceof SaCheckRole checkRole) { | ||||
|             values = checkRole.value(); | ||||
|             mode = checkRole.mode(); | ||||
|             type = checkRole.type(); | ||||
|         } | ||||
|  | ||||
|         if (values != null && mode != null) { | ||||
|             builder.append("<font style=\"color:red\" class=\"light-red\">"); | ||||
|             builder.append(prefix); | ||||
|             if (!type.isEmpty()) { | ||||
|                 builder.append("(类型:").append(type).append(")"); | ||||
|             } | ||||
|             builder.append(getAnnotationNote(values, mode)); | ||||
|             if (orRole.length > 0) { | ||||
|                 builder.append(" 或 角色校验(").append(getAnnotationNote(orRole, mode)).append(")"); | ||||
|             } | ||||
|             builder.append("</font></br>"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 根据注解的模式拼接注解值 | ||||
|      * | ||||
|      * @param values 注解的值数组 | ||||
|      * @param mode   注解的模式(AND 或 OR) | ||||
|      * @return 拼接好的注解值字符串 | ||||
|      */ | ||||
|     private String getAnnotationNote(String[] values, SaMode mode) { | ||||
|         if (mode.equals(SaMode.AND)) { | ||||
|             return String.join(" 且 ", values); | ||||
|         } else { | ||||
|             return String.join(" 或 ", values); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 处理 CrudRequestMapping 和 CrudApi 注解生成的权限信息 | ||||
|      * | ||||
|      * @param handlerMethod 处理程序方法 | ||||
|      * @return 拼接好的权限信息字符串 | ||||
|      */ | ||||
|     private String getCrudPermissionInfo(HandlerMethod handlerMethod) { | ||||
|         CrudRequestMapping crudRequestMapping = handlerMethod.getBeanType().getAnnotation(CrudRequestMapping.class); | ||||
|         CrudApi crudApi = handlerMethod.getMethodAnnotation(CrudApi.class); | ||||
|  | ||||
|         if (crudRequestMapping == null || crudApi == null) { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         String path = crudRequestMapping.value(); | ||||
|         String prefix = String.join(StringConstants.COLON, CharSequenceUtil.splitTrim(path, StringConstants.SLASH)); | ||||
|         Api api = crudApi.value(); | ||||
|         String apiName = Api.PAGE.equals(api) || Api.TREE.equals(api) ? Api.LIST.name() : api.name(); | ||||
|         String permission = "%s:%s".formatted(prefix, apiName.toLowerCase()); | ||||
|  | ||||
|         return "<font style=\"color:red\" class=\"light-red\">Crud 权限校验:</font></br><font style=\"color:red\" class=\"light-red\">方法:</font><font style=\"color:red\" class=\"light-red\">" + permission + "</font>"; | ||||
|     } | ||||
| } | ||||
| @@ -17,15 +17,16 @@ | ||||
| package top.continew.admin.common.config.exception; | ||||
|  | ||||
| import cn.hutool.core.text.CharSequenceUtil; | ||||
| import cn.hutool.core.util.NumberUtil; | ||||
| import jakarta.servlet.http.HttpServletRequest; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.core.annotation.Order; | ||||
| import org.springframework.http.HttpStatus; | ||||
| import org.springframework.web.HttpRequestMethodNotSupportedException; | ||||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||||
| import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; | ||||
| import org.springframework.web.multipart.MultipartException; | ||||
| import org.springframework.web.servlet.NoHandlerFoundException; | ||||
| import top.continew.starter.core.exception.BadRequestException; | ||||
| import top.continew.starter.core.exception.BusinessException; | ||||
| import top.continew.starter.web.model.R; | ||||
| @@ -34,6 +35,7 @@ import top.continew.starter.web.model.R; | ||||
|  * 全局异常处理器 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @author echo | ||||
|  * @since 2024/8/7 20:21 | ||||
|  */ | ||||
| @Slf4j | ||||
| @@ -73,7 +75,7 @@ public class GlobalExceptionHandler { | ||||
|      * 拦截文件上传异常-超过上传大小限制 | ||||
|      */ | ||||
|     @ExceptionHandler(MultipartException.class) | ||||
|     public R handleRequestTooBigException(MultipartException e, HttpServletRequest request) { | ||||
|     public R handleMultipartException(MultipartException e, HttpServletRequest request) { | ||||
|         log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); | ||||
|         String msg = e.getMessage(); | ||||
|         R defaultFail = R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), msg); | ||||
| @@ -85,14 +87,33 @@ public class GlobalExceptionHandler { | ||||
|         if (null != cause) { | ||||
|             msg = msg.concat(cause.getMessage().toLowerCase()); | ||||
|         } | ||||
|         if (msg.contains("size") && msg.contains("exceed")) { | ||||
|             sizeLimit = CharSequenceUtil.subBetween(msg, "the maximum size ", " for"); | ||||
|         } else if (msg.contains("larger than")) { | ||||
|         if (msg.contains("larger than")) { | ||||
|             sizeLimit = CharSequenceUtil.subAfter(msg, "larger than ", true); | ||||
|         } else if (msg.contains("size") && msg.contains("exceed")) { | ||||
|             sizeLimit = CharSequenceUtil.subBetween(msg, "the maximum size ", " for"); | ||||
|         } else { | ||||
|             return defaultFail; | ||||
|         } | ||||
|         String errorMsg = "请上传小于 %sKB 的文件".formatted(NumberUtil.parseLong(sizeLimit) / 1024); | ||||
|         return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), errorMsg); | ||||
|         return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), "请上传小于 %s bytes 的文件".formatted(sizeLimit)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 拦截请求 URL 不存在异常 | ||||
|      */ | ||||
|     @ExceptionHandler(NoHandlerFoundException.class) | ||||
|     public R handleNoHandlerFoundException(NoHandlerFoundException e, HttpServletRequest request) { | ||||
|         log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); | ||||
|         return R.fail(String.valueOf(HttpStatus.NOT_FOUND.value()), "请求 URL '%s' 不存在".formatted(request | ||||
|             .getRequestURI())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 拦截不支持的 HTTP 请求方法异常 | ||||
|      */ | ||||
|     @ExceptionHandler(HttpRequestMethodNotSupportedException.class) | ||||
|     public R handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e, | ||||
|                                                           HttpServletRequest request) { | ||||
|         log.error("[{}] {}", request.getMethod(), request.getRequestURI(), e); | ||||
|         return R.fail(String.valueOf(HttpStatus.METHOD_NOT_ALLOWED.value()), "请求方式 '%s' 不支持".formatted(e.getMethod())); | ||||
|     } | ||||
| } | ||||
| @@ -110,7 +110,8 @@ public class AccountLoginHandler extends AbstractLoginHandler<AccountLoginReq> { | ||||
|             .getClientIP(request)); | ||||
|         int lockMinutes = optionService.getValueByCode2Int(PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.name()); | ||||
|         Integer currentErrorCount = ObjectUtil.defaultIfNull(RedisUtils.get(key), 0); | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes); | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.getMsg() | ||||
|             .formatted(lockMinutes)); | ||||
|         // 登录成功清除计数 | ||||
|         if (!isError) { | ||||
|             RedisUtils.delete(key); | ||||
| @@ -119,6 +120,7 @@ public class AccountLoginHandler extends AbstractLoginHandler<AccountLoginReq> { | ||||
|         // 登录失败递增计数 | ||||
|         currentErrorCount++; | ||||
|         RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes)); | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", maxErrorCount, lockMinutes); | ||||
|         CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.getMsg() | ||||
|             .formatted(maxErrorCount, lockMinutes)); | ||||
|     } | ||||
| } | ||||
| @@ -1,119 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.config.file; | ||||
|  | ||||
| import cn.hutool.core.date.DateUtil; | ||||
| import cn.hutool.core.util.ClassUtil; | ||||
| import cn.hutool.core.util.EscapeUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.x.file.storage.core.FileInfo; | ||||
| import org.dromara.x.file.storage.core.recorder.FileRecorder; | ||||
| import org.dromara.x.file.storage.core.upload.FilePartInfo; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.common.context.UserContextHolder; | ||||
| import top.continew.admin.system.enums.FileTypeEnum; | ||||
| import top.continew.admin.system.mapper.FileMapper; | ||||
| import top.continew.admin.system.mapper.StorageMapper; | ||||
| import top.continew.admin.system.model.entity.FileDO; | ||||
| import top.continew.admin.system.model.entity.StorageDO; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| /** | ||||
|  * 文件记录实现类 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/24 22:31 | ||||
|  */ | ||||
| @Slf4j | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public class FileRecorderImpl implements FileRecorder { | ||||
|  | ||||
|     private final FileMapper fileMapper; | ||||
|     private final StorageMapper storageMapper; | ||||
|  | ||||
|     @Override | ||||
|     public boolean save(FileInfo fileInfo) { | ||||
|         FileDO file = new FileDO(); | ||||
|         String originalFilename = EscapeUtil.unescape(fileInfo.getOriginalFilename()); | ||||
|         file.setName(StrUtil.contains(originalFilename, StringConstants.DOT) | ||||
|             ? StrUtil.subBefore(originalFilename, StringConstants.DOT, true) | ||||
|             : originalFilename); | ||||
|         file.setUrl(fileInfo.getUrl()); | ||||
|         file.setSize(fileInfo.getSize()); | ||||
|         file.setExtension(fileInfo.getExt()); | ||||
|         file.setType(FileTypeEnum.getByExtension(file.getExtension())); | ||||
|         file.setThumbnailUrl(fileInfo.getThUrl()); | ||||
|         file.setThumbnailSize(fileInfo.getThSize()); | ||||
|         StorageDO storage = (StorageDO)fileInfo.getAttr().get(ClassUtil.getClassName(StorageDO.class, false)); | ||||
|         file.setStorageId(storage.getId()); | ||||
|         file.setCreateTime(DateUtil.toLocalDateTime(fileInfo.getCreateTime())); | ||||
|         file.setUpdateUser(UserContextHolder.getUserId()); | ||||
|         file.setUpdateTime(file.getCreateTime()); | ||||
|         fileMapper.insert(file); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public FileInfo getByUrl(String url) { | ||||
|         FileDO file = this.getFileByUrl(url); | ||||
|         if (null == file) { | ||||
|             return null; | ||||
|         } | ||||
|         StorageDO storageDO = storageMapper.lambdaQuery().eq(StorageDO::getId, file.getStorageId()).one(); | ||||
|         return file.toFileInfo(storageDO); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean delete(String url) { | ||||
|         FileDO file = this.getFileByUrl(url); | ||||
|         return fileMapper.lambdaUpdate().eq(FileDO::getUrl, file.getUrl()).remove(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void update(FileInfo fileInfo) { | ||||
|         /* 不使用分片功能则无需重写 */ | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void saveFilePart(FilePartInfo filePartInfo) { | ||||
|         /* 不使用分片功能则无需重写 */ | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void deleteFilePartByUploadId(String s) { | ||||
|         /* 不使用分片功能则无需重写 */ | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 根据 URL 查询文件 | ||||
|      * | ||||
|      * @param url URL | ||||
|      * @return 文件信息 | ||||
|      */ | ||||
|     private FileDO getFileByUrl(String url) { | ||||
|         Optional<FileDO> fileOptional = fileMapper.lambdaQuery().eq(FileDO::getUrl, url).oneOpt(); | ||||
|         return fileOptional.orElseGet(() -> fileMapper.lambdaQuery() | ||||
|             .likeLeft(FileDO::getUrl, StrUtil.subAfter(url, StringConstants.SLASH, true)) | ||||
|             .oneOpt() | ||||
|             .orElse(null)); | ||||
|     } | ||||
| } | ||||
| @@ -16,25 +16,24 @@ | ||||
|  | ||||
| package top.continew.admin.system.config.file; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.boot.ApplicationArguments; | ||||
| import org.springframework.boot.ApplicationRunner; | ||||
| import org.springframework.context.annotation.Bean; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.system.model.query.StorageQuery; | ||||
| import top.continew.admin.system.model.req.StorageReq; | ||||
| import top.continew.admin.system.model.resp.StorageResp; | ||||
| import top.continew.admin.system.service.StorageService; | ||||
| import top.continew.admin.system.enums.OptionCategoryEnum; | ||||
| import top.continew.admin.system.mapper.FileMapper; | ||||
| import top.continew.admin.system.service.OptionService; | ||||
| import top.continew.starter.storage.dao.StorageDao; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 文件存储配置加载器 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @author echo | ||||
|  * @since 2023/12/24 22:31 | ||||
|  */ | ||||
| @Slf4j | ||||
| @@ -42,16 +41,22 @@ import java.util.List; | ||||
| @RequiredArgsConstructor | ||||
| public class FileStorageConfigLoader implements ApplicationRunner { | ||||
|  | ||||
|     private final StorageService storageService; | ||||
|     private final OptionService optionService; | ||||
|     private final FileStorageInit fileStorageInit; | ||||
|  | ||||
|     @Override | ||||
|     public void run(ApplicationArguments args) { | ||||
|         StorageQuery query = new StorageQuery(); | ||||
|         query.setStatus(DisEnableStatusEnum.ENABLE); | ||||
|         List<StorageResp> storageList = storageService.list(query, null); | ||||
|         if (CollUtil.isEmpty(storageList)) { | ||||
|             return; | ||||
|         } | ||||
|         storageList.forEach(s -> storageService.load(BeanUtil.copyProperties(s, StorageReq.class))); | ||||
|         // 查询存储配置 | ||||
|         Map<String, String> map = optionService.getByCategory(OptionCategoryEnum.STORAGE); | ||||
|         // 加载存储配置 | ||||
|         fileStorageInit.load(map); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 存储持久层接口本地实现类 | ||||
|      */ | ||||
|     @Bean | ||||
|     public StorageDao storageDao(FileMapper fileMapper) { | ||||
|         return new StorageDaoImpl(fileMapper); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,146 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.config.file; | ||||
|  | ||||
| import cn.hutool.core.map.MapUtil; | ||||
| import cn.hutool.extra.spring.SpringUtil; | ||||
| import org.springframework.stereotype.Component; | ||||
| import top.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.continew.starter.cache.redisson.util.RedisUtils; | ||||
| import top.continew.starter.storage.client.LocalClient; | ||||
| import top.continew.starter.storage.client.OssClient; | ||||
| import top.continew.starter.storage.constant.StorageConstant; | ||||
| import top.continew.starter.storage.dao.StorageDao; | ||||
| import top.continew.starter.storage.manger.StorageManager; | ||||
| import top.continew.starter.storage.model.req.StorageProperties; | ||||
| import top.continew.starter.storage.strategy.LocalStorageStrategy; | ||||
| import top.continew.starter.storage.strategy.OssStorageStrategy; | ||||
| import top.continew.starter.storage.util.StorageUtils; | ||||
| import top.continew.starter.web.util.SpringWebUtils; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| /** | ||||
|  * 文件存储初始化 | ||||
|  * | ||||
|  * @author echo | ||||
|  * @author Charles7c | ||||
|  * @since 2024/12/20 11:10 | ||||
|  */ | ||||
| @Component | ||||
| public class FileStorageInit { | ||||
|  | ||||
|     /** | ||||
|      * 加载文件存储 | ||||
|      * | ||||
|      * @param map 存储配置 | ||||
|      */ | ||||
|     public void load(Map<String, String> map) { | ||||
|         StorageManager.unload(StorageTypeEnum.OSS.name()); | ||||
|         StorageManager.unload(StorageTypeEnum.LOCAL.name()); | ||||
|         // 获取默认存储值并缓存 | ||||
|         String storageDefault = cacheDefaultStorage(map); | ||||
|         if (StorageTypeEnum.LOCAL.name().equals(storageDefault)) { | ||||
|             // 获取本地终端地址 和桶地址 | ||||
|             String localEndpoint = map.get("STORAGE_LOCAL_ENDPOINT"); | ||||
|             String localBucket = map.get("STORAGE_LOCAL_BUCKET"); | ||||
|             // 构建并加载本地存储配置 | ||||
|             StorageProperties localProperties = buildStorageProperties(StorageTypeEnum.LOCAL | ||||
|                 .name(), localBucket, storageDefault, localEndpoint); | ||||
|             // 本地静态资源映射 | ||||
|             SpringWebUtils.registerResourceHandler(MapUtil.of(StorageUtils.createUriWithProtocol(localEndpoint) | ||||
|                 .getPath(), localBucket)); | ||||
|             StorageManager.load(localProperties | ||||
|                 .getCode(), new LocalStorageStrategy(new LocalClient(localProperties), SpringUtil | ||||
|                     .getBean(StorageDao.class))); | ||||
|         } else if (StorageTypeEnum.OSS.name().equals(storageDefault)) { | ||||
|             // 构建并加载对象存储配置 | ||||
|             StorageProperties ossProperties = buildStorageProperties(StorageTypeEnum.OSS.name(), map | ||||
|                 .get("STORAGE_OSS_BUCKET"), storageDefault, map.get("STORAGE_OSS_ACCESS_KEY"), map | ||||
|                     .get("STORAGE_OSS_SECRET_KEY"), map.get("STORAGE_OSS_ENDPOINT"), map.get("STORAGE_OSS_REGION")); | ||||
|             StorageManager.load(ossProperties.getCode(), new OssStorageStrategy(new OssClient(ossProperties), SpringUtil | ||||
|                 .getBean(StorageDao.class))); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 卸载文件存储 | ||||
|      * | ||||
|      * @param code 存储编码 | ||||
|      */ | ||||
|     public void unLoad(String code) { | ||||
|         StorageManager.unload(code); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 将默认存储值放入缓存 | ||||
|      * | ||||
|      * @param map 存储配置 | ||||
|      * @return {@link String } | ||||
|      */ | ||||
|     private String cacheDefaultStorage(Map<String, String> map) { | ||||
|         String storageDefault = MapUtil.getStr(map, "STORAGE_DEFAULT"); | ||||
|         RedisUtils.set(StorageConstant.DEFAULT_KEY, storageDefault); | ||||
|         return storageDefault; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 构建本地存储配置属性 | ||||
|      * | ||||
|      * @param code        存储码 | ||||
|      * @param bucketName  桶名称 | ||||
|      * @param defaultCode 默认存储码 | ||||
|      * @return {@link StorageProperties } | ||||
|      */ | ||||
|     private StorageProperties buildStorageProperties(String code, | ||||
|                                                      String bucketName, | ||||
|                                                      String defaultCode, | ||||
|                                                      String endpoint) { | ||||
|         StorageProperties properties = new StorageProperties(); | ||||
|         properties.setCode(code); | ||||
|         properties.setBucketName(bucketName); | ||||
|         properties.setEndpoint(endpoint); | ||||
|         properties.setIsDefault(code.equals(defaultCode)); | ||||
|         return properties; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 构建对象存储配置属性 | ||||
|      * | ||||
|      * @param code        存储码 | ||||
|      * @param bucketName  桶名称 | ||||
|      * @param defaultCode 默认存储码 | ||||
|      * @param accessKey   访问密钥 | ||||
|      * @param secretKey   秘密密钥 | ||||
|      * @param endpoint    端点 | ||||
|      * @param region      区域 | ||||
|      * @return {@link StorageProperties } | ||||
|      */ | ||||
|     private StorageProperties buildStorageProperties(String code, | ||||
|                                                      String bucketName, | ||||
|                                                      String defaultCode, | ||||
|                                                      String accessKey, | ||||
|                                                      String secretKey, | ||||
|                                                      String endpoint, | ||||
|                                                      String region) { | ||||
|         StorageProperties properties = buildStorageProperties(code, bucketName, defaultCode, endpoint); | ||||
|         properties.setAccessKey(accessKey); | ||||
|         properties.setSecretKey(secretKey); | ||||
|         properties.setRegion(region); | ||||
|         return properties; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,66 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.config.file; | ||||
|  | ||||
| import cn.hutool.core.util.EscapeUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import top.continew.admin.common.context.UserContextHolder; | ||||
| import top.continew.admin.system.enums.FileTypeEnum; | ||||
| import top.continew.admin.system.mapper.FileMapper; | ||||
| import top.continew.admin.system.model.entity.FileDO; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.storage.dao.StorageDao; | ||||
| import top.continew.starter.storage.model.resp.UploadResp; | ||||
|  | ||||
| /** | ||||
|  * 存储持久层接口本地实现类 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @author echo | ||||
|  * @since 2023/12/24 22:31 | ||||
|  */ | ||||
| @Slf4j | ||||
| @RequiredArgsConstructor | ||||
| public class StorageDaoImpl implements StorageDao { | ||||
|  | ||||
|     private final FileMapper fileMapper; | ||||
|  | ||||
|     @Override | ||||
|     public void add(UploadResp uploadResp) { | ||||
|         FileDO file = new FileDO(); | ||||
|         file.setStorageCode(uploadResp.getCode()); | ||||
|         String originalFilename = EscapeUtil.unescape(uploadResp.getOriginalFilename()); | ||||
|         file.setName(StrUtil.contains(originalFilename, StringConstants.DOT) | ||||
|             ? StrUtil.subBefore(originalFilename, StringConstants.DOT, true) | ||||
|             : originalFilename); | ||||
|         file.setUrl(uploadResp.getUrl()); | ||||
|         file.setPath(uploadResp.getBasePath()); | ||||
|         file.setSize(uploadResp.getSize()); | ||||
|         file.setThumbnailUrl(uploadResp.getThumbnailUrl()); | ||||
|         file.setThumbnailSize(uploadResp.getThumbnailSize()); | ||||
|         file.setExtension(uploadResp.getExt()); | ||||
|         file.setType(FileTypeEnum.getByExtension(file.getExtension())); | ||||
|         file.setETag(uploadResp.geteTag()); | ||||
|         file.setBucketName(uploadResp.getBucketName()); | ||||
|         file.setCreateTime(uploadResp.getCreateTime()); | ||||
|         file.setUpdateUser(UserContextHolder.getUserId()); | ||||
|         file.setUpdateTime(file.getCreateTime()); | ||||
|         fileMapper.insert(file); | ||||
|     } | ||||
| } | ||||
| @@ -43,4 +43,9 @@ public enum OptionCategoryEnum { | ||||
|      * 登录配置 | ||||
|      */ | ||||
|     LOGIN, | ||||
|  | ||||
|     /** | ||||
|      * 存储配置 | ||||
|      */ | ||||
|     STORAGE,; | ||||
| } | ||||
|   | ||||
| @@ -45,14 +45,14 @@ import java.util.Map; | ||||
| public enum PasswordPolicyEnum { | ||||
|  | ||||
|     /** | ||||
|      * 登录密码错误锁定账号的次数 | ||||
|      * 密码错误锁定阈值 | ||||
|      */ | ||||
|     PASSWORD_ERROR_LOCK_COUNT("登录密码错误锁定账号的次数取值范围为 %d-%d", SysConstants.NO, 10, null), | ||||
|     PASSWORD_ERROR_LOCK_COUNT("密码错误锁定阈值取值范围为 %d-%d", SysConstants.NO, 10, "密码错误已达 %d 次,账号锁定 %d 分钟"), | ||||
|  | ||||
|     /** | ||||
|      * 登录密码错误锁定账号的时间(min) | ||||
|      * 账号锁定时长(分钟) | ||||
|      */ | ||||
|     PASSWORD_ERROR_LOCK_MINUTES("登录密码错误锁定账号的时间取值范围为 %d-%d 分钟", 1, 1440, null), | ||||
|     PASSWORD_ERROR_LOCK_MINUTES("账号锁定时长取值范围为 %d-%d 分钟", 1, 1440, "账号锁定 %d 分钟,请稍后再试"), | ||||
|  | ||||
|     /** | ||||
|      * 密码有效期(天) | ||||
| @@ -60,9 +60,9 @@ public enum PasswordPolicyEnum { | ||||
|     PASSWORD_EXPIRATION_DAYS("密码有效期取值范围为 %d-%d 天", SysConstants.NO, 999, null), | ||||
|  | ||||
|     /** | ||||
|      * 密码到期提前提示(天) | ||||
|      * 密码到期提醒(天) | ||||
|      */ | ||||
|     PASSWORD_EXPIRATION_WARNING_DAYS("密码到期提前提示取值范围为 %d-%d 天", SysConstants.NO, 998, null) { | ||||
|     PASSWORD_EXPIRATION_WARNING_DAYS("密码到期提醒取值范围为 %d-%d 天", SysConstants.NO, 998, null) { | ||||
|         @Override | ||||
|         public void validateRange(int value, Map<String, String> policyMap) { | ||||
|             if (CollUtil.isEmpty(policyMap)) { | ||||
| @@ -73,7 +73,7 @@ public enum PasswordPolicyEnum { | ||||
|                 .get(PASSWORD_EXPIRATION_DAYS.name())), SpringUtil.getBean(OptionService.class) | ||||
|                     .getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name())); | ||||
|             if (passwordExpirationDays > SysConstants.NO) { | ||||
|                 ValidationUtils.throwIf(value >= passwordExpirationDays, "密码到期前的提示时间应小于密码有效期"); | ||||
|                 ValidationUtils.throwIf(value >= passwordExpirationDays, "密码到期提醒时间应小于密码有效期"); | ||||
|                 return; | ||||
|             } | ||||
|             super.validateRange(value, policyMap); | ||||
| @@ -113,9 +113,9 @@ public enum PasswordPolicyEnum { | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 密码是否允许包含正反序账号名 | ||||
|      * 密码是否允许包含用户名 | ||||
|      */ | ||||
|     PASSWORD_ALLOW_CONTAIN_USERNAME("密码是否允许包含正反序账号名取值只能为是(%d)或否(%d)", SysConstants.NO, SysConstants.YES, "密码不允许包含正反序账号名") { | ||||
|     PASSWORD_ALLOW_CONTAIN_USERNAME("密码是否允许包含用户名取值只能为是(%d)或否(%d)", SysConstants.NO, SysConstants.YES, "密码不允许包含正反序用户名") { | ||||
|         @Override | ||||
|         public void validateRange(int value, Map<String, String> policyMap) { | ||||
|             ValidationUtils.throwIf(value != SysConstants.YES && value != SysConstants.NO, this.getDescription() | ||||
| @@ -133,9 +133,9 @@ public enum PasswordPolicyEnum { | ||||
|     }, | ||||
|  | ||||
|     /** | ||||
|      * 密码重复使用次数 | ||||
|      * 历史密码重复校验次数 | ||||
|      */ | ||||
|     PASSWORD_REPETITION_TIMES("密码重复使用规则取值范围为 %d-%d", 3, 32, "新密码不得与历史前 %d 次密码重复") { | ||||
|     PASSWORD_REPETITION_TIMES("历史密码重复校验次数取值范围为 %d-%d", 3, 32, "新密码不得与历史前 %d 次密码重复") { | ||||
|         @Override | ||||
|         public void validate(String password, int value, UserDO user) { | ||||
|             UserPasswordHistoryService userPasswordHistoryService = SpringUtil | ||||
|   | ||||
| @@ -31,9 +31,9 @@ import top.continew.starter.core.enums.BaseEnum; | ||||
| public enum StorageTypeEnum implements BaseEnum<Integer> { | ||||
|  | ||||
|     /** | ||||
|      * 兼容S3协议存储 | ||||
|      * 对象存储 | ||||
|      */ | ||||
|     S3(1, "兼容S3协议存储"), | ||||
|     OSS(1, "对象存储"), | ||||
|  | ||||
|     /** | ||||
|      * 本地存储 | ||||
|   | ||||
| @@ -1,29 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.mapper; | ||||
|  | ||||
| import top.continew.admin.system.model.entity.StorageDO; | ||||
| import top.continew.starter.data.mp.base.BaseMapper; | ||||
|  | ||||
| /** | ||||
|  * 存储 Mapper | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/26 22:09 | ||||
|  */ | ||||
| public interface StorageMapper extends BaseMapper<StorageDO> { | ||||
| } | ||||
| @@ -16,19 +16,12 @@ | ||||
|  | ||||
| package top.continew.admin.system.model.entity; | ||||
|  | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import com.baomidou.mybatisplus.annotation.TableName; | ||||
| import lombok.Data; | ||||
| import lombok.SneakyThrows; | ||||
| import org.dromara.x.file.storage.core.FileInfo; | ||||
| import top.continew.admin.system.enums.FileTypeEnum; | ||||
| import top.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.core.util.StrUtils; | ||||
| import top.continew.admin.common.model.entity.BaseDO; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.net.URL; | ||||
|  | ||||
| /** | ||||
|  * 文件实体 | ||||
| @@ -79,64 +72,23 @@ public class FileDO extends BaseDO { | ||||
|     private String thumbnailUrl; | ||||
|  | ||||
|     /** | ||||
|      * 存储 ID | ||||
|      * 存储code | ||||
|      */ | ||||
|     private Long storageId; | ||||
|     private String storageCode; | ||||
|  | ||||
|     /** | ||||
|      * 转换为 X-File-Storage 文件信息对象 | ||||
|      * | ||||
|      * @param storageDO 存储桶信息 | ||||
|      * @return X-File-Storage 文件信息对象 | ||||
|      * 基础路径 | ||||
|      */ | ||||
|     public FileInfo toFileInfo(StorageDO storageDO) { | ||||
|         FileInfo fileInfo = new FileInfo(); | ||||
|         fileInfo.setUrl(this.url); | ||||
|         fileInfo.setSize(this.size); | ||||
|         fileInfo.setFilename(StrUtil.contains(this.url, StringConstants.SLASH) | ||||
|             ? StrUtil.subAfter(this.url, StringConstants.SLASH, true) | ||||
|             : this.url); | ||||
|         fileInfo.setOriginalFilename(StrUtils | ||||
|             .blankToDefault(this.extension, this.name, ex -> this.name + StringConstants.DOT + ex)); | ||||
|         fileInfo.setBasePath(StringConstants.EMPTY); | ||||
|         // 优化 path 处理 | ||||
|         fileInfo.setPath(extractRelativePath(this.url, storageDO)); | ||||
|  | ||||
|         fileInfo.setExt(this.extension); | ||||
|         fileInfo.setPlatform(storageDO.getCode()); | ||||
|         fileInfo.setThUrl(this.thumbnailUrl); | ||||
|         fileInfo.setThFilename(StrUtil.contains(this.thumbnailUrl, StringConstants.SLASH) | ||||
|             ? StrUtil.subAfter(this.thumbnailUrl, StringConstants.SLASH, true) | ||||
|             : this.thumbnailUrl); | ||||
|         fileInfo.setThSize(this.thumbnailSize); | ||||
|         return fileInfo; | ||||
|     } | ||||
|     private String path; | ||||
|  | ||||
|     /** | ||||
|      * 将文件路径处理成资源路径 | ||||
|      * 例如: | ||||
|      * http://domain.cn/bucketName/2024/11/27/6746ec3b2907f0de80afdd70.png => 2024/11/27/ | ||||
|      * http://bucketName.domain.cn/2024/11/27/6746ec3b2907f0de80afdd70.png => 2024/11/27/ | ||||
|      * | ||||
|      * @param url       文件路径 | ||||
|      * @param storageDO 存储桶信息 | ||||
|      * @return | ||||
|      * 存储桶 | ||||
|      */ | ||||
|     @SneakyThrows | ||||
|     private static String extractRelativePath(String url, StorageDO storageDO) { | ||||
|         url = StrUtil.subBefore(url, StringConstants.SLASH, true) + StringConstants.SLASH; | ||||
|         if (storageDO.getType().equals(StorageTypeEnum.LOCAL)) { | ||||
|             return url; | ||||
|         } | ||||
|         // 提取 URL 中的路径部分 | ||||
|         String fullPath = new URL(url).getPath(); | ||||
|         // 移除开头的斜杠 | ||||
|         String relativePath = fullPath.startsWith(StringConstants.SLASH) ? fullPath.substring(1) : fullPath; | ||||
|         // 如果路径以 bucketName 开头,则移除 bucketName 例如: bucketName/2024/11/27/ -> 2024/11/27/ | ||||
|         if (relativePath.startsWith(storageDO.getBucketName())) { | ||||
|             return StrUtil.split(relativePath, storageDO.getBucketName()).get(1); | ||||
|         } | ||||
|         return relativePath; | ||||
|     } | ||||
|     private String bucketName; | ||||
|  | ||||
|     /** | ||||
|      * 文件标识 | ||||
|      */ | ||||
|     private String eTag; | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,102 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.model.entity; | ||||
|  | ||||
| import com.baomidou.mybatisplus.annotation.TableName; | ||||
| import lombok.Data; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.continew.admin.common.model.entity.BaseDO; | ||||
| import top.continew.starter.security.crypto.annotation.FieldEncrypt; | ||||
|  | ||||
| import java.io.Serial; | ||||
|  | ||||
| /** | ||||
|  * 存储实体 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/26 22:09 | ||||
|  */ | ||||
| @Data | ||||
| @TableName("sys_storage") | ||||
| public class StorageDO extends BaseDO { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 名称 | ||||
|      */ | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * 编码 | ||||
|      */ | ||||
|     private String code; | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     private StorageTypeEnum type; | ||||
|  | ||||
|     /** | ||||
|      * Access Key(访问密钥) | ||||
|      */ | ||||
|     @FieldEncrypt | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * Secret Key(私有密钥) | ||||
|      */ | ||||
|     @FieldEncrypt | ||||
|     private String secretKey; | ||||
|  | ||||
|     /** | ||||
|      * Endpoint(终端节点) | ||||
|      */ | ||||
|     private String endpoint; | ||||
|  | ||||
|     /** | ||||
|      * 桶名称 | ||||
|      */ | ||||
|     private String bucketName; | ||||
|  | ||||
|     /** | ||||
|      * 域名 | ||||
|      */ | ||||
|     private String domain; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 是否为默认存储 | ||||
|      */ | ||||
|     private Boolean isDefault; | ||||
|  | ||||
|     /** | ||||
|      * 排序 | ||||
|      */ | ||||
|     private Integer sort; | ||||
|  | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     private DisEnableStatusEnum status; | ||||
| } | ||||
| @@ -1,53 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.model.query; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.Data; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.starter.data.core.annotation.Query; | ||||
| import top.continew.starter.data.core.enums.QueryType; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 存储查询条件 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/26 22:09 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "存储查询条件") | ||||
| public class StorageQuery implements Serializable { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 关键词 | ||||
|      */ | ||||
|     @Schema(description = "关键词", example = "本地存储") | ||||
|     @Query(columns = {"name", "code", "description"}, type = QueryType.LIKE) | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     private DisEnableStatusEnum status; | ||||
| } | ||||
| @@ -1,134 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.model.req; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import jakarta.validation.constraints.NotNull; | ||||
| import jakarta.validation.constraints.Pattern; | ||||
| import lombok.Data; | ||||
| import org.hibernate.validator.constraints.Length; | ||||
| import top.continew.admin.common.constant.RegexConstants; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.continew.admin.system.validation.ValidationGroup; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 存储请求参数 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/26 22:09 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "存储请求参数") | ||||
| public class StorageReq implements Serializable { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "名称", example = "存储1") | ||||
|     @NotBlank(message = "名称不能为空") | ||||
|     @Length(max = 100, message = "名称长度不能超过 {max} 个字符") | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * 编码 | ||||
|      */ | ||||
|     @Schema(description = "编码", example = "local") | ||||
|     @NotBlank(message = "编码不能为空") | ||||
|     @Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头") | ||||
|     private String code; | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     @Schema(description = "类型", example = "2") | ||||
|     @NotNull(message = "类型非法") | ||||
|     private StorageTypeEnum type; | ||||
|  | ||||
|     /** | ||||
|      * 访问密钥 | ||||
|      */ | ||||
|     @Schema(description = "访问密钥", example = "") | ||||
|     @Length(max = 255, message = "访问密钥长度不能超过 {max} 个字符") | ||||
|     @NotBlank(message = "访问密钥不能为空", groups = ValidationGroup.Storage.S3.class) | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * 私有密钥 | ||||
|      */ | ||||
|     @Schema(description = "私有密钥", example = "") | ||||
|     @NotBlank(message = "私有密钥不能为空", groups = ValidationGroup.Storage.S3.class) | ||||
|     private String secretKey; | ||||
|  | ||||
|     /** | ||||
|      * 终端节点 | ||||
|      */ | ||||
|     @Schema(description = "终端节点", example = "") | ||||
|     @Length(max = 255, message = "终端节点长度不能超过 {max} 个字符") | ||||
|     @NotBlank(message = "终端节点不能为空", groups = ValidationGroup.Storage.S3.class) | ||||
|     private String endpoint; | ||||
|  | ||||
|     /** | ||||
|      * 桶名称 | ||||
|      */ | ||||
|     @Schema(description = "桶名称", example = "C:/continew-admin/data/file/") | ||||
|     @Length(max = 255, message = "桶名称长度不能超过 {max} 个字符") | ||||
|     @NotBlank(message = "桶名称不能为空", groups = ValidationGroup.Storage.S3.class) | ||||
|     @NotBlank(message = "存储路径不能为空", groups = ValidationGroup.Storage.Local.class) | ||||
|     private String bucketName; | ||||
|  | ||||
|     /** | ||||
|      * 域名 | ||||
|      */ | ||||
|     @Schema(description = "域名", example = "http://localhost:8000/file") | ||||
|     @Length(max = 255, message = "域名长度不能超过 {max} 个字符") | ||||
|     @NotBlank(message = "域名不能为空") | ||||
|     private String domain; | ||||
|  | ||||
|     /** | ||||
|      * 排序 | ||||
|      */ | ||||
|     @Schema(description = "排序", example = "1") | ||||
|     private Integer sort; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     @Schema(description = "描述", example = "存储描述") | ||||
|     @Length(max = 200, message = "描述长度不能超过 {max} 个字符") | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 是否为默认存储 | ||||
|      */ | ||||
|     @Schema(description = "是否为默认存储", example = "true") | ||||
|     @NotNull(message = "是否为默认存储不能为空") | ||||
|     private Boolean isDefault; | ||||
|  | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     private DisEnableStatusEnum status; | ||||
| } | ||||
| @@ -1,119 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.model.resp; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.Data; | ||||
| import top.continew.admin.common.model.resp.BaseDetailResp; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.continew.starter.security.mask.annotation.JsonMask; | ||||
|  | ||||
| import java.io.Serial; | ||||
|  | ||||
| /** | ||||
|  * 存储响应信息 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/26 22:09 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "存储响应信息") | ||||
| public class StorageResp extends BaseDetailResp { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "名称", example = "存储1") | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * 编码 | ||||
|      */ | ||||
|     @Schema(description = "编码", example = "local") | ||||
|     private String code; | ||||
|  | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     private DisEnableStatusEnum status; | ||||
|  | ||||
|     /** | ||||
|      * 类型 | ||||
|      */ | ||||
|     @Schema(description = "类型", example = "2") | ||||
|     private StorageTypeEnum type; | ||||
|  | ||||
|     /** | ||||
|      * 访问密钥 | ||||
|      */ | ||||
|     @Schema(description = "访问密钥", example = "") | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * 私有密钥 | ||||
|      */ | ||||
|     @Schema(description = "私有密钥", example = "") | ||||
|     @JsonMask(left = 4, right = 3) | ||||
|     private String secretKey; | ||||
|  | ||||
|     /** | ||||
|      * 终端节点 | ||||
|      */ | ||||
|     @Schema(description = "终端节点", example = "") | ||||
|     private String endpoint; | ||||
|  | ||||
|     /** | ||||
|      * 桶名称 | ||||
|      */ | ||||
|     @Schema(description = "桶名称", example = "C:/continew-admin/data/file/") | ||||
|     private String bucketName; | ||||
|  | ||||
|     /** | ||||
|      * 域名 | ||||
|      */ | ||||
|     @Schema(description = "域名", example = "http://localhost:8000/file") | ||||
|     private String domain; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     @Schema(description = "描述", example = "存储描述") | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 是否为默认存储 | ||||
|      */ | ||||
|     @Schema(description = "是否为默认存储", example = "true") | ||||
|     private Boolean isDefault; | ||||
|  | ||||
|     /** | ||||
|      * 排序 | ||||
|      */ | ||||
|     @Schema(description = "排序", example = "1") | ||||
|     private Integer sort; | ||||
|  | ||||
|     @Override | ||||
|     public Boolean getDisabled() { | ||||
|         return this.getIsDefault(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -16,13 +16,13 @@ | ||||
|  | ||||
| package top.continew.admin.system.service; | ||||
|  | ||||
| import org.dromara.x.file.storage.core.FileInfo; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.admin.system.model.entity.FileDO; | ||||
| import top.continew.admin.system.model.query.FileQuery; | ||||
| import top.continew.admin.system.model.req.FileReq; | ||||
| import top.continew.admin.system.model.resp.FileResp; | ||||
| import top.continew.admin.system.model.resp.FileStatisticsResp; | ||||
| import top.continew.admin.system.model.resp.FileUploadResp; | ||||
| import top.continew.starter.data.mp.service.IService; | ||||
| import top.continew.starter.extension.crud.service.BaseService; | ||||
|  | ||||
| @@ -42,7 +42,7 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery, | ||||
|      * @param file 文件信息 | ||||
|      * @return 文件信息 | ||||
|      */ | ||||
|     default FileInfo upload(MultipartFile file) { | ||||
|     default FileUploadResp upload(MultipartFile file) { | ||||
|         return upload(file, null); | ||||
|     } | ||||
|  | ||||
| @@ -53,7 +53,7 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery, | ||||
|      * @param storageCode 存储编码 | ||||
|      * @return 文件信息 | ||||
|      */ | ||||
|     FileInfo upload(MultipartFile file, String storageCode); | ||||
|     FileUploadResp upload(MultipartFile file, String storageCode); | ||||
|  | ||||
|     /** | ||||
|      * 根据存储 ID 列表查询 | ||||
|   | ||||
| @@ -1,62 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.service; | ||||
|  | ||||
| import top.continew.admin.system.model.entity.StorageDO; | ||||
| import top.continew.admin.system.model.query.StorageQuery; | ||||
| import top.continew.admin.system.model.req.StorageReq; | ||||
| import top.continew.admin.system.model.resp.StorageResp; | ||||
| import top.continew.starter.data.mp.service.IService; | ||||
| import top.continew.starter.extension.crud.service.BaseService; | ||||
|  | ||||
| /** | ||||
|  * 存储业务接口 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/26 22:09 | ||||
|  */ | ||||
| public interface StorageService extends BaseService<StorageResp, StorageResp, StorageQuery, StorageReq>, IService<StorageDO> { | ||||
|  | ||||
|     /** | ||||
|      * 查询默认存储 | ||||
|      * | ||||
|      * @return 存储信息 | ||||
|      */ | ||||
|     StorageDO getDefaultStorage(); | ||||
|  | ||||
|     /** | ||||
|      * 根据编码查询 | ||||
|      * | ||||
|      * @param code 编码 | ||||
|      * @return 存储信息 | ||||
|      */ | ||||
|     StorageDO getByCode(String code); | ||||
|  | ||||
|     /** | ||||
|      * 加载存储 | ||||
|      * | ||||
|      * @param req 存储信息 | ||||
|      */ | ||||
|     void load(StorageReq req); | ||||
|  | ||||
|     /** | ||||
|      * 卸载存储 | ||||
|      * | ||||
|      * @param req 存储信息 | ||||
|      */ | ||||
|     void unload(StorageReq req); | ||||
| } | ||||
| @@ -17,39 +17,27 @@ | ||||
| package top.continew.admin.system.service.impl; | ||||
|  | ||||
| import cn.hutool.core.collection.CollUtil; | ||||
| import cn.hutool.core.io.file.FileNameUtil; | ||||
| import cn.hutool.core.util.ClassUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.hutool.core.util.URLUtil; | ||||
| import jakarta.annotation.Resource; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.x.file.storage.core.FileInfo; | ||||
| import org.dromara.x.file.storage.core.FileStorageService; | ||||
| import org.dromara.x.file.storage.core.ProgressListener; | ||||
| import org.dromara.x.file.storage.core.upload.UploadPretreatment; | ||||
| import org.springframework.stereotype.Service; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| import top.continew.admin.system.enums.FileTypeEnum; | ||||
| import top.continew.admin.system.mapper.FileMapper; | ||||
| import top.continew.admin.system.model.entity.FileDO; | ||||
| import top.continew.admin.system.model.entity.StorageDO; | ||||
| import top.continew.admin.system.model.query.FileQuery; | ||||
| import top.continew.admin.system.model.req.FileReq; | ||||
| import top.continew.admin.system.model.resp.FileResp; | ||||
| import top.continew.admin.system.model.resp.FileStatisticsResp; | ||||
| import top.continew.admin.system.model.resp.FileUploadResp; | ||||
| import top.continew.admin.system.service.FileService; | ||||
| import top.continew.admin.system.service.StorageService; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.core.util.StrUtils; | ||||
| import top.continew.starter.core.util.URLUtils; | ||||
| import top.continew.starter.core.validation.CheckUtils; | ||||
| import top.continew.starter.core.exception.BusinessException; | ||||
| import top.continew.starter.extension.crud.service.BaseServiceImpl; | ||||
| import top.continew.starter.storage.manger.StorageManager; | ||||
| import top.continew.starter.storage.model.resp.UploadResp; | ||||
| import top.continew.starter.storage.strategy.StorageStrategy; | ||||
|  | ||||
| import java.time.LocalDate; | ||||
| import java.io.IOException; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * 文件业务实现 | ||||
| @@ -62,65 +50,31 @@ import java.util.stream.Collectors; | ||||
| @RequiredArgsConstructor | ||||
| public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileResp, FileResp, FileQuery, FileReq> implements FileService { | ||||
|  | ||||
|     private final FileStorageService fileStorageService; | ||||
|     @Resource | ||||
|     private StorageService storageService; | ||||
|  | ||||
|     @Override | ||||
|     protected void beforeDelete(List<Long> ids) { | ||||
|         List<FileDO> fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list(); | ||||
|         Map<Long, List<FileDO>> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId)); | ||||
|         for (Map.Entry<Long, List<FileDO>> entry : fileListGroup.entrySet()) { | ||||
|             StorageDO storage = storageService.getById(entry.getKey()); | ||||
|             for (FileDO file : entry.getValue()) { | ||||
|                 FileInfo fileInfo = file.toFileInfo(storage); | ||||
|                 fileStorageService.delete(fileInfo); | ||||
|             } | ||||
|         } | ||||
|         fileList.forEach(file -> { | ||||
|             StorageStrategy<?> instance = StorageManager.instance(file.getStorageCode()); | ||||
|             instance.delete(file.getBucketName(), file.getPath()); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public FileInfo upload(MultipartFile file, String storageCode) { | ||||
|         StorageDO storage; | ||||
|     public FileUploadResp upload(MultipartFile file, String storageCode) { | ||||
|         StorageStrategy<?> instance; | ||||
|         if (StrUtil.isBlank(storageCode)) { | ||||
|             storage = storageService.getDefaultStorage(); | ||||
|             CheckUtils.throwIfNull(storage, "请先指定默认存储"); | ||||
|             instance = StorageManager.instance(); | ||||
|         } else { | ||||
|             storage = storageService.getByCode(storageCode); | ||||
|             CheckUtils.throwIfNotExists(storage, "StorageDO", "Code", storageCode); | ||||
|             instance = StorageManager.instance(storageCode); | ||||
|         } | ||||
|         LocalDate today = LocalDate.now(); | ||||
|         String path = today.getYear() + StringConstants.SLASH + today.getMonthValue() + StringConstants.SLASH + today | ||||
|             .getDayOfMonth() + StringConstants.SLASH; | ||||
|         UploadPretreatment uploadPretreatment = fileStorageService.of(file) | ||||
|             .setPlatform(storage.getCode()) | ||||
|             .putAttr(ClassUtil.getClassName(StorageDO.class, false), storage) | ||||
|             .setPath(path); | ||||
|         // 图片文件生成缩略图 | ||||
|         if (FileTypeEnum.IMAGE.getExtensions().contains(FileNameUtil.extName(file.getOriginalFilename()))) { | ||||
|             uploadPretreatment.thumbnail(img -> img.size(100, 100)); | ||||
|         UploadResp uploadResp; | ||||
|         try { | ||||
|             uploadResp = instance.upload(file.getOriginalFilename(), null, file.getInputStream(), file | ||||
|                 .getContentType(), true); | ||||
|         } catch (IOException e) { | ||||
|             throw new BusinessException("文件上传失败", e); | ||||
|         } | ||||
|         uploadPretreatment.setProgressMonitor(new ProgressListener() { | ||||
|             @Override | ||||
|             public void start() { | ||||
|                 log.info("开始上传"); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void progress(long progressSize, Long allSize) { | ||||
|                 log.info("已上传 [{}],总大小 [{}]", progressSize, allSize); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public void finish() { | ||||
|                 log.info("上传结束"); | ||||
|             } | ||||
|         }); | ||||
|         // 处理本地存储文件 URL | ||||
|         FileInfo fileInfo = uploadPretreatment.upload(); | ||||
|         String domain = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH); | ||||
|         fileInfo.setUrl(URLUtil.normalize(domain + fileInfo.getPath() + fileInfo.getFilename())); | ||||
|         return fileInfo; | ||||
|         return FileUploadResp.builder().url(uploadResp.getUrl()).build(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -128,7 +82,7 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | ||||
|         if (CollUtil.isEmpty(storageIds)) { | ||||
|             return 0L; | ||||
|         } | ||||
|         return baseMapper.lambdaQuery().in(FileDO::getStorageId, storageIds).count(); | ||||
|         return baseMapper.lambdaQuery().in(FileDO::getStorageCode, storageIds).count(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -143,19 +97,4 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes | ||||
|         resp.setNumber(statisticsList.stream().mapToLong(FileStatisticsResp::getNumber).sum()); | ||||
|         return resp; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     protected void fill(Object obj) { | ||||
|         super.fill(obj); | ||||
|         if (obj instanceof FileResp fileResp && !URLUtils.isHttpUrl(fileResp.getUrl())) { | ||||
|             StorageDO storage = storageService.getById(fileResp.getStorageId()); | ||||
|             String prefix = StrUtil.appendIfMissing(storage.getDomain(), StringConstants.SLASH); | ||||
|             String url = URLUtil.normalize(prefix + fileResp.getUrl()); | ||||
|             fileResp.setUrl(url); | ||||
|             String thumbnailUrl = StrUtils.blankToDefault(fileResp.getThumbnailUrl(), url, thUrl -> URLUtil | ||||
|                 .normalize(prefix + thUrl)); | ||||
|             fileResp.setThumbnailUrl(thumbnailUrl); | ||||
|             fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode())); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWra | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.stereotype.Service; | ||||
| import top.continew.admin.common.constant.CacheConstants; | ||||
| import top.continew.admin.system.config.file.FileStorageInit; | ||||
| import top.continew.admin.system.enums.OptionCategoryEnum; | ||||
| import top.continew.admin.system.enums.PasswordPolicyEnum; | ||||
| import top.continew.admin.system.mapper.OptionMapper; | ||||
| @@ -57,6 +58,7 @@ import java.util.stream.Collectors; | ||||
| public class OptionServiceImpl implements OptionService { | ||||
|  | ||||
|     private final OptionMapper baseMapper; | ||||
|     private final FileStorageInit fileStorageInit; | ||||
|  | ||||
|     @Override | ||||
|     public List<OptionResp> list(OptionQuery query) { | ||||
| @@ -98,6 +100,7 @@ public class OptionServiceImpl implements OptionService { | ||||
|             PasswordPolicyEnum passwordPolicy = PasswordPolicyEnum.valueOf(code); | ||||
|             passwordPolicy.validateRange(Integer.parseInt(value), passwordPolicyOptionMap); | ||||
|         } | ||||
|         storageReload(options); | ||||
|         RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK); | ||||
|         baseMapper.updateById(BeanUtil.copyToList(options, OptionDO.class)); | ||||
|     } | ||||
| @@ -138,4 +141,18 @@ public class OptionServiceImpl implements OptionService { | ||||
|         RedisUtils.set(CacheConstants.OPTION_KEY_PREFIX + code, value); | ||||
|         return mapper.apply(value); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 存储重新加载 | ||||
|      * | ||||
|      * @param options 选项 | ||||
|      */ | ||||
|     private void storageReload(List<OptionReq> options) { | ||||
|         Map<String, String> storage = options.stream() | ||||
|             .filter(option -> option.getCode() != null && option.getCode().startsWith("STORAGE_")) | ||||
|             .collect(Collectors.toMap(OptionReq::getCode, OptionReq::getValue)); | ||||
|         if (ObjectUtil.isNotEmpty(storage)) { | ||||
|             fileStorageInit.load(storage); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,209 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.service.impl; | ||||
|  | ||||
| import cn.hutool.core.bean.BeanUtil; | ||||
| import cn.hutool.core.map.MapUtil; | ||||
| import cn.hutool.core.util.StrUtil; | ||||
| import cn.hutool.core.util.URLUtil; | ||||
| import jakarta.annotation.Resource; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.dromara.x.file.storage.core.FileStorageProperties; | ||||
| import org.dromara.x.file.storage.core.FileStorageService; | ||||
| import org.dromara.x.file.storage.core.FileStorageServiceBuilder; | ||||
| import org.dromara.x.file.storage.core.platform.FileStorage; | ||||
| import org.springframework.stereotype.Service; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.admin.common.util.SecureUtils; | ||||
| import top.continew.admin.system.enums.StorageTypeEnum; | ||||
| import top.continew.admin.system.mapper.StorageMapper; | ||||
| import top.continew.admin.system.model.entity.StorageDO; | ||||
| import top.continew.admin.system.model.query.StorageQuery; | ||||
| import top.continew.admin.system.model.req.StorageReq; | ||||
| import top.continew.admin.system.model.resp.StorageResp; | ||||
| import top.continew.admin.system.service.FileService; | ||||
| import top.continew.admin.system.service.StorageService; | ||||
| import top.continew.admin.system.validation.ValidationGroup; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.core.util.ExceptionUtils; | ||||
| import top.continew.starter.core.util.URLUtils; | ||||
| import top.continew.starter.core.validation.CheckUtils; | ||||
| import top.continew.starter.core.validation.ValidationUtils; | ||||
| import top.continew.starter.extension.crud.service.BaseServiceImpl; | ||||
| import top.continew.starter.web.util.SpringWebUtils; | ||||
|  | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.concurrent.CopyOnWriteArrayList; | ||||
|  | ||||
| /** | ||||
|  * 存储业务实现 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/26 22:09 | ||||
|  */ | ||||
| @Service | ||||
| @RequiredArgsConstructor | ||||
| public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO, StorageResp, StorageResp, StorageQuery, StorageReq> implements StorageService { | ||||
|  | ||||
|     private final FileStorageService fileStorageService; | ||||
|     @Resource | ||||
|     private FileService fileService; | ||||
|  | ||||
|     @Override | ||||
|     public void beforeAdd(StorageReq req) { | ||||
|         this.decodeSecretKey(req, null); | ||||
|         CheckUtils.throwIf(Boolean.TRUE.equals(req.getIsDefault()) && this.isDefaultExists(null), "请先取消原有默认存储"); | ||||
|         String code = req.getCode(); | ||||
|         CheckUtils.throwIf(this.isCodeExists(code, null), "新增失败,[{}] 已存在", code); | ||||
|         this.load(req); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void beforeUpdate(StorageReq req, Long id) { | ||||
|         StorageDO oldStorage = super.getById(id); | ||||
|         CheckUtils.throwIfNotEqual(req.getCode(), oldStorage.getCode(), "不允许修改存储编码"); | ||||
|         CheckUtils.throwIfNotEqual(req.getType(), oldStorage.getType(), "不允许修改存储类型"); | ||||
|         DisEnableStatusEnum newStatus = req.getStatus(); | ||||
|         CheckUtils.throwIf(Boolean.TRUE.equals(oldStorage.getIsDefault()) && DisEnableStatusEnum.DISABLE | ||||
|             .equals(newStatus), "[{}] 是默认存储,不允许禁用", oldStorage.getName()); | ||||
|         this.decodeSecretKey(req, oldStorage); | ||||
|         DisEnableStatusEnum oldStatus = oldStorage.getStatus(); | ||||
|         if (Boolean.TRUE.equals(req.getIsDefault())) { | ||||
|             CheckUtils.throwIf(this.isDefaultExists(id), "请先取消原有默认存储"); | ||||
|             CheckUtils.throwIf(!DisEnableStatusEnum.ENABLE.equals(oldStatus) && !DisEnableStatusEnum.ENABLE | ||||
|                 .equals(newStatus), "请先启用该存储"); | ||||
|         } | ||||
|         // 先卸载 | ||||
|         if (DisEnableStatusEnum.ENABLE.equals(oldStatus)) { | ||||
|             this.unload(BeanUtil.copyProperties(oldStorage, StorageReq.class)); | ||||
|         } | ||||
|         // 再加载 | ||||
|         if (DisEnableStatusEnum.ENABLE.equals(newStatus)) { | ||||
|             this.load(req); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void beforeDelete(List<Long> ids) { | ||||
|         CheckUtils.throwIf(fileService.countByStorageIds(ids) > 0, "所选存储存在文件关联,请删除文件后重试"); | ||||
|         List<StorageDO> storageList = baseMapper.lambdaQuery().in(StorageDO::getId, ids).list(); | ||||
|         storageList.forEach(s -> { | ||||
|             CheckUtils.throwIfEqual(Boolean.TRUE, s.getIsDefault(), "[{}] 是默认存储,不允许禁用", s.getName()); | ||||
|             // 卸载启用状态的存储 | ||||
|             if (DisEnableStatusEnum.ENABLE.equals(s.getStatus())) { | ||||
|                 this.unload(BeanUtil.copyProperties(s, StorageReq.class)); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public StorageDO getDefaultStorage() { | ||||
|         return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).one(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public StorageDO getByCode(String code) { | ||||
|         return baseMapper.lambdaQuery().eq(StorageDO::getCode, code).one(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void load(StorageReq req) { | ||||
|         CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList(); | ||||
|         String domain = req.getDomain(); | ||||
|         ValidationUtils.throwIf(!URLUtils.isHttpUrl(domain), "域名格式错误"); | ||||
|         String bucketName = req.getBucketName(); | ||||
|         StorageTypeEnum type = req.getType(); | ||||
|         if (StorageTypeEnum.LOCAL.equals(type)) { | ||||
|             ValidationUtils.validate(req, ValidationGroup.Storage.Local.class); | ||||
|             req.setBucketName(StrUtil.appendIfMissing(bucketName | ||||
|                 .replace(StringConstants.BACKSLASH, StringConstants.SLASH), StringConstants.SLASH)); | ||||
|             FileStorageProperties.LocalPlusConfig config = new FileStorageProperties.LocalPlusConfig(); | ||||
|             config.setPlatform(req.getCode()); | ||||
|             config.setStoragePath(bucketName); | ||||
|             fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections | ||||
|                 .singletonList(config))); | ||||
|             SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), bucketName)); | ||||
|         } else if (StorageTypeEnum.S3.equals(type)) { | ||||
|             ValidationUtils.validate(req, ValidationGroup.Storage.S3.class); | ||||
|             FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config(); | ||||
|             config.setPlatform(req.getCode()); | ||||
|             config.setAccessKey(req.getAccessKey()); | ||||
|             config.setSecretKey(req.getSecretKey()); | ||||
|             config.setEndPoint(req.getEndpoint()); | ||||
|             config.setBucketName(bucketName); | ||||
|             config.setDomain(domain); | ||||
|             fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections | ||||
|                 .singletonList(config), null)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void unload(StorageReq req) { | ||||
|         CopyOnWriteArrayList<FileStorage> fileStorageList = fileStorageService.getFileStorageList(); | ||||
|         FileStorage fileStorage = fileStorageService.getFileStorage(req.getCode()); | ||||
|         fileStorageList.remove(fileStorage); | ||||
|         fileStorage.close(); | ||||
|         SpringWebUtils.deRegisterResourceHandler(MapUtil.of(URLUtil.url(req.getDomain()).getPath(), req | ||||
|             .getBucketName())); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 解密 SecretKey | ||||
|      * | ||||
|      * @param req     请求参数 | ||||
|      * @param storage 存储信息 | ||||
|      */ | ||||
|     private void decodeSecretKey(StorageReq req, StorageDO storage) { | ||||
|         if (!StorageTypeEnum.S3.equals(req.getType())) { | ||||
|             return; | ||||
|         } | ||||
|         // 修改时,如果 SecretKey 不修改,需要手动修正 | ||||
|         String newSecretKey = req.getSecretKey(); | ||||
|         boolean isSecretKeyNotUpdate = StrUtil.isBlank(newSecretKey) || newSecretKey.contains(StringConstants.ASTERISK); | ||||
|         if (null != storage && isSecretKeyNotUpdate) { | ||||
|             req.setSecretKey(storage.getSecretKey()); | ||||
|             return; | ||||
|         } | ||||
|         // 新增时或修改了 SecretKey | ||||
|         String secretKey = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(newSecretKey)); | ||||
|         ValidationUtils.throwIfNull(secretKey, "私有密钥解密失败"); | ||||
|         ValidationUtils.throwIf(secretKey.length() > 255, "私有密钥长度不能超过 255 个字符"); | ||||
|         req.setSecretKey(secretKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 默认存储是否存在 | ||||
|      * | ||||
|      * @param id ID | ||||
|      * @return 是否存在 | ||||
|      */ | ||||
|     private boolean isDefaultExists(Long id) { | ||||
|         return baseMapper.lambdaQuery().eq(StorageDO::getIsDefault, true).ne(null != id, StorageDO::getId, id).exists(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 编码是否存在 | ||||
|      * | ||||
|      * @param code 编码 | ||||
|      * @param id   ID | ||||
|      * @return 是否存在 | ||||
|      */ | ||||
|     private boolean isCodeExists(String code, Long id) { | ||||
|         return baseMapper.lambdaQuery().eq(StorageDO::getCode, code).ne(null != id, StorageDO::getId, id).exists(); | ||||
|     } | ||||
| } | ||||
| @@ -1,45 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.system.validation; | ||||
|  | ||||
| import jakarta.validation.groups.Default; | ||||
|  | ||||
| /** | ||||
|  * 分组校验 | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2024/7/3 22:01 | ||||
|  */ | ||||
| public interface ValidationGroup extends Default { | ||||
|  | ||||
|     /** | ||||
|      * 分组校验-增删改查 | ||||
|      */ | ||||
|     interface Storage extends ValidationGroup { | ||||
|         /** | ||||
|          * 本地存储 | ||||
|          */ | ||||
|         interface Local extends Storage { | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * 兼容S3协议存储 | ||||
|          */ | ||||
|         interface S3 extends Storage { | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -25,7 +25,6 @@ import com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties; | ||||
| import io.swagger.v3.oas.annotations.Hidden; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.dromara.x.file.storage.spring.EnableFileStorage; | ||||
| import org.springframework.boot.ApplicationArguments; | ||||
| import org.springframework.boot.ApplicationRunner; | ||||
| import org.springframework.boot.SpringApplication; | ||||
| @@ -45,7 +44,6 @@ import top.continew.starter.web.model.R; | ||||
|  * @since 2022/12/8 23:15 | ||||
|  */ | ||||
| @Slf4j | ||||
| @EnableFileStorage | ||||
| @EnableMethodCache(basePackages = "top.continew.admin") | ||||
| @EnableGlobalResponse | ||||
| @EnableCrudRestController | ||||
|   | ||||
| @@ -26,7 +26,6 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import jakarta.validation.constraints.NotNull; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.dromara.x.file.storage.core.FileInfo; | ||||
| import org.springframework.validation.annotation.Validated; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
| @@ -66,9 +65,9 @@ public class CommonController { | ||||
|  | ||||
|     @Operation(summary = "上传文件", description = "上传文件") | ||||
|     @PostMapping("/file") | ||||
|     public FileUploadResp upload(@NotNull(message = "文件不能为空") MultipartFile file) { | ||||
|     public FileUploadResp upload(@NotNull(message = "文件不能为空") @RequestPart("file") MultipartFile file) { | ||||
|         ValidationUtils.throwIf(file::isEmpty, "文件不能为空"); | ||||
|         FileInfo fileInfo = fileService.upload(file); | ||||
|         FileUploadResp fileInfo = fileService.upload(file); | ||||
|         return FileUploadResp.builder().url(fileInfo.getUrl()).build(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -47,7 +47,6 @@ public class DemoEnvironmentJob { | ||||
|  | ||||
|     private final DictItemMapper dictItemMapper; | ||||
|     private final DictMapper dictMapper; | ||||
|     private final StorageMapper storageMapper; | ||||
|     private final NoticeMapper noticeMapper; | ||||
|     private final MessageMapper messageMapper; | ||||
|     private final MessageUserMapper messageUserMapper; | ||||
| @@ -84,8 +83,6 @@ public class DemoEnvironmentJob { | ||||
|             this.log(dictItemCount, "字典项"); | ||||
|             Long dictCount = dictMapper.lambdaQuery().gt(DictDO::getId, DELETE_FLAG).count(); | ||||
|             this.log(dictCount, "字典"); | ||||
|             Long storageCount = storageMapper.lambdaQuery().gt(StorageDO::getId, DELETE_FLAG).count(); | ||||
|             this.log(storageCount, "存储"); | ||||
|             Long noticeCount = noticeMapper.lambdaQuery().gt(NoticeDO::getId, DELETE_FLAG).count(); | ||||
|             this.log(noticeCount, "公告"); | ||||
|             Long messageCount = messageMapper.lambdaQuery().count(); | ||||
| @@ -111,9 +108,6 @@ public class DemoEnvironmentJob { | ||||
|             this.clean(dictCount, "字典", CacheConstants.DICT_KEY_PREFIX, () -> dictMapper.lambdaUpdate() | ||||
|                 .gt(DictDO::getId, DELETE_FLAG) | ||||
|                 .remove()); | ||||
|             this.clean(storageCount, "存储", null, () -> storageMapper.lambdaUpdate() | ||||
|                 .gt(StorageDO::getId, DELETE_FLAG) | ||||
|                 .remove()); | ||||
|             this.clean(noticeCount, "公告", null, () -> noticeMapper.lambdaUpdate() | ||||
|                 .gt(NoticeDO::getId, DELETE_FLAG) | ||||
|                 .remove()); | ||||
|   | ||||
| @@ -1,39 +0,0 @@ | ||||
| /* | ||||
|  * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
|  | ||||
| package top.continew.admin.controller.system; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
| import top.continew.admin.common.controller.BaseController; | ||||
| import top.continew.admin.system.model.query.StorageQuery; | ||||
| import top.continew.admin.system.model.req.StorageReq; | ||||
| import top.continew.admin.system.model.resp.StorageResp; | ||||
| import top.continew.admin.system.service.StorageService; | ||||
| import top.continew.starter.extension.crud.annotation.CrudRequestMapping; | ||||
| import top.continew.starter.extension.crud.enums.Api; | ||||
|  | ||||
| /** | ||||
|  * 存储管理 API | ||||
|  * | ||||
|  * @author Charles7c | ||||
|  * @since 2023/12/26 22:09 | ||||
|  */ | ||||
| @Tag(name = "存储管理 API") | ||||
| @RestController | ||||
| @CrudRequestMapping(value = "/system/storage", api = {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE}) | ||||
| public class StorageController extends BaseController<StorageService, StorageResp, StorageResp, StorageQuery, StorageReq> { | ||||
| } | ||||
| @@ -71,13 +71,6 @@ VALUES | ||||
| (1105, '删除', 1100, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:delete', 5, 1, 1, NOW()), | ||||
| (1106, '下载', 1100, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:download', 6, 1, 1, NOW()), | ||||
|  | ||||
| (1110, '存储管理', 1000, 2, '/system/storage', 'SystemStorage', 'system/storage/index', NULL, 'storage', b'0', b'0', b'0', NULL, 8, 1, 1, NOW()), | ||||
| (1111, '列表', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:list', 1, 1, 1, NOW()), | ||||
| (1112, '详情', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:detail', 2, 1, 1, NOW()), | ||||
| (1113, '新增', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:add', 3, 1, 1, NOW()), | ||||
| (1114, '修改', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:update', 4, 1, 1, NOW()), | ||||
| (1115, '删除', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:delete', 5, 1, 1, NOW()), | ||||
|  | ||||
| ( 1180, '客户端管理', 1000, 2, '/system/client', 'SystemClient', 'system/client/index', NULL, 'mobile', b'0', b'0', b'0', NULL, 9, 1, 1, NOW()), | ||||
| (1181, '列表', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:list', 1, 1, 1, NOW()), | ||||
| (1182, '详情', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:detail', 2, 1, 1, NOW()), | ||||
| @@ -161,28 +154,37 @@ VALUES | ||||
| INSERT INTO `sys_option` | ||||
| (`id`, `category`, `name`, `code`, `value`, `default_value`, `description`) | ||||
| VALUES | ||||
| (1, 'SITE', '系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。'), | ||||
| (2, 'SITE', '系统描述', 'SITE_DESCRIPTION', NULL, '持续迭代优化的前后端分离中后台管理系统框架', NULL), | ||||
| (3, 'SITE', '版权信息', 'SITE_COPYRIGHT', NULL, 'Copyright © 2022 - present ContiNew Admin 版权所有', '用于显示登录页面的底部版权信息。'), | ||||
| (4, 'SITE', '备案号', 'SITE_BEIAN', NULL, NULL, 'ICP备案号'), | ||||
| (5, 'SITE', 'favicon', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。'), | ||||
| (6, 'SITE', '系统LOGO', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。'), | ||||
| (7, 'PASSWORD', '登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。'), | ||||
| (8, 'PASSWORD', '登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。'), | ||||
| (9, 'PASSWORD', '密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。'), | ||||
| (10, 'PASSWORD', '密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。'), | ||||
| (11, 'PASSWORD', '密码重复使用次数', 'PASSWORD_REPETITION_TIMES', NULL, '3', '不允许使用最近 N 次密码,取值范围为 3-32。'), | ||||
| (12, 'PASSWORD', '密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。'), | ||||
| (13, 'PASSWORD', '密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', NULL), | ||||
| (14, 'PASSWORD', '密码是否必须包含特殊字符', 'PASSWORD_REQUIRE_SYMBOLS', NULL, '0', NULL), | ||||
| (15, 'MAIL', '发送协议', 'MAIL_PROTOCOL', NULL, 'smtp', NULL), | ||||
| (16, 'MAIL', '服务器地址', 'MAIL_HOST', NULL, 'smtp.126.com', NULL), | ||||
| (17, 'MAIL', '服务器端口', 'MAIL_PORT', NULL, '465', NULL), | ||||
| (18, 'MAIL', '用户名', 'MAIL_USERNAME', NULL, 'charles7c@126.com', NULL), | ||||
| (19, 'MAIL', '密码', 'MAIL_PASSWORD', NULL, NULL, NULL), | ||||
| (20, 'MAIL', '是否启用SSL', 'MAIL_SSL_ENABLED', NULL, '1', NULL), | ||||
| (21, 'MAIL', 'SSL端口', 'MAIL_SSL_PORT', NULL, '465', NULL), | ||||
| (22, 'LOGIN', '是否启用验证码', 'LOGIN_CAPTCHA_ENABLED', NULL, '1', '是否启用验证码(1:是;0:否)'); | ||||
| (1, 'SITE', '网站名称', 'SITE_TITLE', NULL, 'ContiNew Admin', '显示在浏览器标题栏和登录界面的系统名称'), | ||||
| (2, 'SITE', '网站描述', 'SITE_DESCRIPTION', NULL, '持续迭代优化的前后端分离中后台管理系统框架', '用于 SEO 的网站元描述'), | ||||
| (3, 'SITE', '版权声明', 'SITE_COPYRIGHT', NULL, 'Copyright © 2022 - present ContiNew Admin 版权所有', '显示在页面底部的版权声明文本'), | ||||
| (4, 'SITE', '网站域名', 'SITE_DOMAIN', NULL, 'https://admin.continew.top', '系统主域名,用于生成绝对链接和 CORS 配置'), | ||||
| (5, 'SITE', '网站备案号', 'SITE_BEIAN', NULL, NULL, '工信部 ICP 备案编号(如:京ICP备12345678号)'), | ||||
| (6, 'SITE', '网站图标', 'SITE_FAVICON', NULL, '/favicon.ico', '浏览器标签页显示的网站图标(建议 .ico 格式)'), | ||||
| (7, 'SITE', '网站LOGO', 'SITE_LOGO', NULL, '/logo.svg', '显示在登录页面和系统导航栏的网站图标(建议 .svg 格式)'), | ||||
| (10, 'PASSWORD', '密码错误锁定阈值', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '连续登录失败次数达到该值将锁定账号(0-10次,0表示禁用锁定)'), | ||||
| (11, 'PASSWORD', '账号锁定时长(分钟)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '账号锁定后自动解锁的时间(1-1440分钟,即24小时)'), | ||||
| (12, 'PASSWORD', '密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '密码强制修改周期(0-999天,0表示永不过期)'), | ||||
| (13, 'PASSWORD', '密码到期提醒(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码过期前的提前提醒天数(0表示不提醒)'), | ||||
| (14, 'PASSWORD', '历史密码重复校验次数', 'PASSWORD_REPETITION_TIMES', NULL, '3', '禁止使用最近 N 次的历史密码(3-32次)'), | ||||
| (15, 'PASSWORD', '密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '密码最小字符长度要求(8-32个字符)'), | ||||
| (16, 'PASSWORD', '是否允许密码包含用户名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', '是否允许密码包含正序或倒序的用户名字符'), | ||||
| (17, 'PASSWORD', '密码是否必须包含特殊字符', 'PASSWORD_REQUIRE_SYMBOLS', NULL, '0', '是否要求密码必须包含特殊字符(如:!@#$%)'), | ||||
| (20, 'MAIL', '邮件协议', 'MAIL_PROTOCOL', NULL, 'smtp', '邮件发送协议类型'), | ||||
| (21, 'MAIL', '服务器地址', 'MAIL_HOST', NULL, 'smtp.126.com', '邮件服务器地址'), | ||||
| (22, 'MAIL', '服务器端口', 'MAIL_PORT', NULL, '465', '邮件服务器连接端口'), | ||||
| (23, 'MAIL', '邮箱账号', 'MAIL_USERNAME', NULL, 'charles7c@126.com', '发件人邮箱地址'), | ||||
| (24, 'MAIL', '邮箱密码', 'MAIL_PASSWORD', NULL, NULL, '服务授权密码/客户端专用密码'), | ||||
| (25, 'MAIL', '启用SSL加密', 'MAIL_SSL_ENABLED', NULL, '1', '是否启用SSL/TLS加密连接'), | ||||
| (26, 'MAIL', 'SSL端口号', 'MAIL_SSL_PORT', NULL, '465', 'SSL加密连接的备用端口(通常与主端口一致)'), | ||||
| (30, 'STORAGE', '默认存储类型', 'STORAGE_DEFAULT', NULL, 'LOCAL', '系统文件存储方式(LOCAL:本地存储;OSS:对象存储)'), | ||||
| (31, 'STORAGE', '本地存储路径', 'STORAGE_LOCAL_BUCKET', NULL, 'C:/continew-admin/data/file/', '本地存储目录绝对路径(需以斜杠结尾,如:/data/uploads/)'), | ||||
| (32, 'STORAGE', '本地资源访问地址', 'STORAGE_LOCAL_ENDPOINT', NULL, 'localhost:8000/file', '通过 URL 访问本地文件的映射地址'), | ||||
| (33, 'STORAGE', 'Access Key', 'STORAGE_OSS_ACCESS_KEY', NULL, NULL, '对象存储访问密钥'), | ||||
| (34, 'STORAGE', 'Secret Key', 'STORAGE_OSS_SECRET_KEY', NULL, NULL, '对象存储私有密钥'), | ||||
| (35, 'STORAGE', '对象存储桶名称', 'STORAGE_OSS_BUCKET', NULL, 'continew', '对象存储 Bucket 名称(需预先创建)'), | ||||
| (36, 'STORAGE', '对象存储终端节点', 'STORAGE_OSS_ENDPOINT', NULL, NULL, '对象存储访问地址'), | ||||
| (37, 'STORAGE', '对象存储区域代码', 'STORAGE_OSS_REGION', NULL, 'cn-hangzhou', '对象存储数据中心区域标识(如:cn-hangzhou)'), | ||||
| (40, 'LOGIN', '是否启用验证码', 'LOGIN_CAPTCHA_ENABLED', NULL, '1', NULL); | ||||
|  | ||||
| -- 初始化默认字典 | ||||
| INSERT INTO `sys_dict` | ||||
| @@ -240,13 +242,6 @@ VALUES | ||||
| -- 初始化默认角色和部门关联数据 | ||||
| INSERT INTO `sys_role_dept` (`role_id`, `dept_id`) VALUES (547888897925840927, 547887852587843593); | ||||
|  | ||||
| -- 初始化默认存储 | ||||
| INSERT INTO `sys_storage` | ||||
| (`id`, `name`, `code`, `type`, `access_key`, `secret_key`, `endpoint`, `bucket_name`, `domain`, `description`, `is_default`, `sort`, `status`, `create_user`, `create_time`) | ||||
| VALUES | ||||
| (1, '开发环境', 'local_dev', 2, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file', '本地存储', b'1', 1, 1, 1, NOW()), | ||||
| (2, '生产环境', 'local_prod', 2, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file', '本地存储', b'0', 2, 2, 1, NOW()); | ||||
|  | ||||
| -- 初始化客户端数据 | ||||
| INSERT INTO `sys_client` | ||||
| (`id`, `client_id`, `client_key`, `client_secret`, `auth_type`, `client_type`, `active_timeout`, `timeout`, `status`, `create_user`, `create_time`) | ||||
|   | ||||
| @@ -191,7 +191,7 @@ CREATE TABLE IF NOT EXISTS `sys_log` ( | ||||
|     `id`               bigint(20)   NOT NULL AUTO_INCREMENT     COMMENT 'ID', | ||||
|     `trace_id`         varchar(255) DEFAULT NULL                COMMENT '链路ID', | ||||
|     `description`      varchar(255) NOT NULL                    COMMENT '日志描述', | ||||
|     `module`           varchar(50)  NOT NULL                    COMMENT '所属模块', | ||||
|     `module`           varchar(100) NOT NULL                    COMMENT '所属模块', | ||||
|     `request_url`      varchar(512) NOT NULL                    COMMENT '请求URL', | ||||
|     `request_method`   varchar(10)  NOT NULL                    COMMENT '请求方式', | ||||
|     `request_headers`  text         DEFAULT NULL                COMMENT '请求头', | ||||
| @@ -252,40 +252,19 @@ CREATE TABLE IF NOT EXISTS `sys_notice` ( | ||||
|     INDEX `idx_update_user`(`update_user`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告表'; | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS `sys_storage` ( | ||||
|     `id`          bigint(20)   NOT NULL AUTO_INCREMENT     COMMENT 'ID', | ||||
|     `name`        varchar(100) NOT NULL                    COMMENT '名称', | ||||
|     `code`        varchar(30)  NOT NULL                    COMMENT '编码', | ||||
|     `type`        tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:兼容S3协议存储;2:本地存储)', | ||||
|     `access_key`  varchar(255) DEFAULT NULL                COMMENT 'Access Key(访问密钥)', | ||||
|     `secret_key`  varchar(255) DEFAULT NULL                COMMENT 'Secret Key(私有密钥)', | ||||
|     `endpoint`    varchar(255) DEFAULT NULL                COMMENT 'Endpoint(终端节点)', | ||||
|     `bucket_name` varchar(255) DEFAULT NULL                COMMENT '桶名称', | ||||
|     `domain`      varchar(255) NOT NULL DEFAULT ''         COMMENT '域名', | ||||
|     `description` varchar(200) DEFAULT NULL                COMMENT '描述', | ||||
|     `is_default`  bit(1)       NOT NULL DEFAULT b'0'       COMMENT '是否为默认存储', | ||||
|     `sort`        int          NOT NULL DEFAULT 999        COMMENT '排序', | ||||
|     `status`      tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态(1:启用;2:禁用)', | ||||
|     `create_user` bigint(20)   NOT NULL                    COMMENT '创建人', | ||||
|     `create_time` datetime     NOT NULL                    COMMENT '创建时间', | ||||
|     `update_user` bigint(20)   DEFAULT NULL                COMMENT '修改人', | ||||
|     `update_time` datetime     DEFAULT NULL                COMMENT '修改时间', | ||||
|     PRIMARY KEY (`id`), | ||||
|     UNIQUE INDEX `uk_code`(`code`), | ||||
|     INDEX `idx_create_user`(`create_user`), | ||||
|     INDEX `idx_update_user`(`update_user`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='存储表'; | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS `sys_file` ( | ||||
|     `id`             bigint(20)   NOT NULL AUTO_INCREMENT     COMMENT 'ID', | ||||
|     `name`           varchar(255) NOT NULL                    COMMENT '名称', | ||||
|     `size`           bigint(20)   NOT NULL                    COMMENT '大小(字节)', | ||||
|     `url`            varchar(512) NOT NULL                    COMMENT 'URL', | ||||
|     `extension`      varchar(100) DEFAULT NULL                COMMENT '扩展名', | ||||
|     `e_tag`          varchar(100) DEFAULT NULL                COMMENT '文件唯一标识', | ||||
|     `thumbnail_size` bigint(20)   DEFAULT NULL                COMMENT '缩略图大小(字节)', | ||||
|     `thumbnail_url`  varchar(512) DEFAULT NULL                COMMENT '缩略图URL', | ||||
|     `type`           tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '类型(1:其他;2:图片;3:文档;4:视频;5:音频)', | ||||
|     `storage_id`     bigint(20)   NOT NULL                    COMMENT '存储ID', | ||||
|     `storage_code`   varchar(255) DEFAULT NULL                COMMENT '存储唯一标识', | ||||
|     `bucket_name`    varchar(255) DEFAULT NULL                COMMENT '存储桶名称', | ||||
|     `path`           varchar(512) DEFAULT NULL                COMMENT '基础路径', | ||||
|     `create_user`    bigint(20)   NOT NULL                    COMMENT '创建人', | ||||
|     `create_time`    datetime     NOT NULL                    COMMENT '创建时间', | ||||
|     `update_user`    bigint(20)   NOT NULL                    COMMENT '修改人', | ||||
| @@ -293,6 +272,7 @@ CREATE TABLE IF NOT EXISTS `sys_file` ( | ||||
|     PRIMARY KEY (`id`), | ||||
|     INDEX `idx_url`(`url`), | ||||
|     INDEX `idx_type`(`type`), | ||||
|     INDEX `idx_storage_code`(`storage_code`), | ||||
|     INDEX `idx_create_user`(`create_user`), | ||||
|     INDEX `idx_update_user`(`update_user`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件表'; | ||||
|   | ||||
| @@ -71,13 +71,6 @@ VALUES | ||||
| (1105, '删除', 1100, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:delete', 5, 1, 1, NOW()), | ||||
| (1106, '下载', 1100, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:file:download', 6, 1, 1, NOW()), | ||||
|  | ||||
| (1110, '存储管理', 1000, 2, '/system/storage', 'SystemStorage', 'system/storage/index', NULL, 'storage', false, false, false, NULL, 8, 1, 1, NOW()), | ||||
| (1111, '列表', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:list', 1, 1, 1, NOW()), | ||||
| (1112, '详情', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:detail', 2, 1, 1, NOW()), | ||||
| (1113, '新增', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:add', 3, 1, 1, NOW()), | ||||
| (1114, '修改', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:update', 4, 1, 1, NOW()), | ||||
| (1115, '删除', 1110, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:storage:delete', 5, 1, 1, NOW()), | ||||
|  | ||||
| ( 1180, '客户端管理', 1000, 2, '/system/client', 'SystemClient', 'system/client/index', NULL, 'mobile', false, false, false, NULL, 9, 1, 1, NOW()), | ||||
| (1181, '列表', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:list', 1, 1, 1, NOW()), | ||||
| (1182, '详情', 1180, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'system:client:detail', 2, 1, 1, NOW()), | ||||
| @@ -161,28 +154,37 @@ VALUES | ||||
| INSERT INTO "sys_option" | ||||
| ("id", "category", "name", "code", "value", "default_value", "description") | ||||
| VALUES | ||||
| (1, 'SITE', '系统标题', 'SITE_TITLE', NULL, 'ContiNew Admin', '用于显示登录页面的系统标题。'), | ||||
| (2, 'SITE', '系统描述', 'SITE_DESCRIPTION', NULL, '持续迭代优化的前后端分离中后台管理系统框架', NULL), | ||||
| (3, 'SITE', '版权信息', 'SITE_COPYRIGHT', NULL, 'Copyright © 2022 - present ContiNew Admin 版权所有', '用于显示登录页面的底部版权信息。'), | ||||
| (4, 'SITE', '备案号', 'SITE_BEIAN', NULL, '津ICP备2022005864号-3', 'ICP备案号'), | ||||
| (5, 'SITE', 'favicon', 'SITE_FAVICON', NULL, '/favicon.ico', '用于显示浏览器地址栏的系统LOGO。'), | ||||
| (6, 'SITE', '系统LOGO', 'SITE_LOGO', NULL, '/logo.svg', '用于显示登录页面的系统LOGO。'), | ||||
| (7, 'PASSWORD', '登录密码错误锁定账号的次数', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '取值范围为 0-10(0 表示不锁定)。'), | ||||
| (8, 'PASSWORD', '登录密码错误锁定账号的时间(min)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '取值范围为 1-1440(一天)。'), | ||||
| (9, 'PASSWORD', '密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '取值范围为 0-999(0 表示永久有效)。'), | ||||
| (10, 'PASSWORD', '密码到期提前提示(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码到期 N 天前进行提示(0 表示不提示)。'), | ||||
| (11, 'PASSWORD', '密码重复使用次数', 'PASSWORD_REPETITION_TIMES', NULL, '3', '不允许使用最近 N 次密码,取值范围为 3-32。'), | ||||
| (12, 'PASSWORD', '密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '取值范围为 8-32。'), | ||||
| (13, 'PASSWORD', '密码是否允许包含正反序账号名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', NULL), | ||||
| (14, 'PASSWORD', '密码是否必须包含特殊字符', 'PASSWORD_REQUIRE_SYMBOLS', NULL, '0', NULL), | ||||
| (15, 'MAIL', '发送协议', 'MAIL_PROTOCOL', NULL, 'smtp', NULL), | ||||
| (16, 'MAIL', '服务器地址', 'MAIL_HOST', NULL, 'smtp.126.com', NULL), | ||||
| (17, 'MAIL', '服务器端口', 'MAIL_PORT', NULL, '465', NULL), | ||||
| (18, 'MAIL', '用户名', 'MAIL_USERNAME', NULL, 'charles7c@126.com', NULL), | ||||
| (19, 'MAIL', '密码', 'MAIL_PASSWORD', NULL, NULL, NULL), | ||||
| (20, 'MAIL', '是否启用SSL', 'MAIL_SSL_ENABLED', NULL, '1', NULL), | ||||
| (21, 'MAIL', 'SSL端口', 'MAIL_SSL_PORT', NULL, '465', NULL), | ||||
| (22, 'LOGIN', '是否启用验证码', 'LOGIN_CAPTCHA_ENABLED', NULL, '1', '是否启用验证码(1:是;0:否)'); | ||||
| (1, 'SITE', '网站名称', 'SITE_TITLE', NULL, 'ContiNew Admin', '显示在浏览器标题栏和登录界面的系统名称'), | ||||
| (2, 'SITE', '网站描述', 'SITE_DESCRIPTION', NULL, '持续迭代优化的前后端分离中后台管理系统框架', '用于 SEO 的网站元描述'), | ||||
| (3, 'SITE', '版权声明', 'SITE_COPYRIGHT', NULL, 'Copyright © 2022 - present ContiNew Admin 版权所有', '显示在页面底部的版权声明文本'), | ||||
| (4, 'SITE', '网站域名', 'SITE_DOMAIN', NULL, 'https://admin.continew.top', '系统主域名,用于生成绝对链接和 CORS 配置'), | ||||
| (5, 'SITE', '网站备案号', 'SITE_BEIAN', NULL, NULL, '工信部 ICP 备案编号(如:京ICP备12345678号)'), | ||||
| (6, 'SITE', '网站图标', 'SITE_FAVICON', NULL, '/favicon.ico', '浏览器标签页显示的网站图标(建议 .ico 格式)'), | ||||
| (7, 'SITE', '网站LOGO', 'SITE_LOGO', NULL, '/logo.svg', '显示在登录页面和系统导航栏的网站图标(建议 .svg 格式)'), | ||||
| (10, 'PASSWORD', '密码错误锁定阈值', 'PASSWORD_ERROR_LOCK_COUNT', NULL, '5', '连续登录失败次数达到该值将锁定账号(0-10次,0表示禁用锁定)'), | ||||
| (11, 'PASSWORD', '账号锁定时长(分钟)', 'PASSWORD_ERROR_LOCK_MINUTES', NULL, '5', '账号锁定后自动解锁的时间(1-1440分钟,即24小时)'), | ||||
| (12, 'PASSWORD', '密码有效期(天)', 'PASSWORD_EXPIRATION_DAYS', NULL, '0', '密码强制修改周期(0-999天,0表示永不过期)'), | ||||
| (13, 'PASSWORD', '密码到期提醒(天)', 'PASSWORD_EXPIRATION_WARNING_DAYS', NULL, '0', '密码过期前的提前提醒天数(0表示不提醒)'), | ||||
| (14, 'PASSWORD', '历史密码重复校验次数', 'PASSWORD_REPETITION_TIMES', NULL, '3', '禁止使用最近 N 次的历史密码(3-32次)'), | ||||
| (15, 'PASSWORD', '密码最小长度', 'PASSWORD_MIN_LENGTH', NULL, '8', '密码最小字符长度要求(8-32个字符)'), | ||||
| (16, 'PASSWORD', '是否允许密码包含用户名', 'PASSWORD_ALLOW_CONTAIN_USERNAME', NULL, '1', '是否允许密码包含正序或倒序的用户名字符'), | ||||
| (17, 'PASSWORD', '密码是否必须包含特殊字符', 'PASSWORD_REQUIRE_SYMBOLS', NULL, '0', '是否要求密码必须包含特殊字符(如:!@#$%)'), | ||||
| (20, 'MAIL', '邮件协议', 'MAIL_PROTOCOL', NULL, 'smtp', '邮件发送协议类型'), | ||||
| (21, 'MAIL', '服务器地址', 'MAIL_HOST', NULL, 'smtp.126.com', '邮件服务器地址'), | ||||
| (22, 'MAIL', '服务器端口', 'MAIL_PORT', NULL, '465', '邮件服务器连接端口'), | ||||
| (23, 'MAIL', '邮箱账号', 'MAIL_USERNAME', NULL, 'charles7c@126.com', '发件人邮箱地址'), | ||||
| (24, 'MAIL', '邮箱密码', 'MAIL_PASSWORD', NULL, NULL, '服务授权密码/客户端专用密码'), | ||||
| (25, 'MAIL', '启用SSL加密', 'MAIL_SSL_ENABLED', NULL, '1', '是否启用SSL/TLS加密连接'), | ||||
| (26, 'MAIL', 'SSL端口号', 'MAIL_SSL_PORT', NULL, '465', 'SSL加密连接的备用端口(通常与主端口一致)'), | ||||
| (30, 'STORAGE', '默认存储类型', 'STORAGE_DEFAULT', NULL, 'LOCAL', '系统文件存储方式(LOCAL:本地存储;OSS:对象存储)'), | ||||
| (31, 'STORAGE', '本地存储路径', 'STORAGE_LOCAL_BUCKET', NULL, 'C:/continew-admin/data/file/', '本地存储目录绝对路径(需以斜杠结尾,如:/data/uploads/)'), | ||||
| (32, 'STORAGE', '本地资源访问地址', 'STORAGE_LOCAL_ENDPOINT', NULL, 'localhost:8000/file', '通过 URL 访问本地文件的映射地址'), | ||||
| (33, 'STORAGE', 'Access Key', 'STORAGE_OSS_ACCESS_KEY', NULL, NULL, '对象存储访问密钥'), | ||||
| (34, 'STORAGE', 'Secret Key', 'STORAGE_OSS_SECRET_KEY', NULL, NULL, '对象存储私有密钥'), | ||||
| (35, 'STORAGE', '对象存储桶名称', 'STORAGE_OSS_BUCKET', NULL, 'continew', '对象存储 Bucket 名称(需预先创建)'), | ||||
| (36, 'STORAGE', '对象存储终端节点', 'STORAGE_OSS_ENDPOINT', NULL, NULL, '对象存储访问地址'), | ||||
| (37, 'STORAGE', '对象存储区域代码', 'STORAGE_OSS_REGION', NULL, 'cn-hangzhou', '对象存储数据中心区域标识(如:cn-hangzhou)'), | ||||
| (40, 'LOGIN', '是否启用验证码', 'LOGIN_CAPTCHA_ENABLED', NULL, '1', NULL); | ||||
|  | ||||
| -- 初始化默认字典 | ||||
| INSERT INTO "sys_dict" | ||||
| @@ -240,13 +242,6 @@ VALUES | ||||
| -- 初始化默认角色和部门关联数据 | ||||
| INSERT INTO "sys_role_dept" ("role_id", "dept_id") VALUES (547888897925840927, 547887852587843593); | ||||
|  | ||||
| -- 初始化默认存储 | ||||
| INSERT INTO "sys_storage" | ||||
| ("id", "name", "code", "type", "access_key", "secret_key", "endpoint", "bucket_name", "domain", "description", "is_default", "sort", "status", "create_user", "create_time") | ||||
| VALUES | ||||
| (1, '开发环境', 'local_dev', 2, NULL, NULL, NULL, 'C:/continew-admin/data/file/', 'http://localhost:8000/file', '本地存储', true, 1, 1, 1, NOW()), | ||||
| (2, '生产环境', 'local_prod', 2, NULL, NULL, NULL, '../data/file/', 'http://api.continew.top/file', '本地存储', false, 2, 2, 1, NOW()); | ||||
|  | ||||
| -- 初始化客户端数据 | ||||
| INSERT INTO "sys_client" | ||||
| ("id", "client_id", "client_key", "client_secret", "auth_type", "client_type", "active_timeout", "timeout", "status", "create_user", "create_time") | ||||
|   | ||||
| @@ -312,7 +312,7 @@ CREATE TABLE IF NOT EXISTS "sys_log" ( | ||||
|     "id"               int8         NOT NULL, | ||||
|     "trace_id"         varchar(255) DEFAULT NULL, | ||||
|     "description"      varchar(255) NOT NULL, | ||||
|     "module"           varchar(50)  NOT NULL, | ||||
|     "module"           varchar(100) NOT NULL, | ||||
|     "request_url"      varchar(512) NOT NULL, | ||||
|     "request_method"   varchar(10)  NOT NULL, | ||||
|     "request_headers"  text         DEFAULT NULL, | ||||
| @@ -420,47 +420,6 @@ COMMENT ON COLUMN "sys_notice"."update_user"    IS '修改人'; | ||||
| COMMENT ON COLUMN "sys_notice"."update_time"    IS '修改时间'; | ||||
| COMMENT ON TABLE  "sys_notice"                  IS '公告表'; | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "sys_storage" ( | ||||
|     "id"          int8         NOT NULL, | ||||
|     "name"        varchar(100) NOT NULL, | ||||
|     "code"        varchar(30)  NOT NULL, | ||||
|     "type"        int2         NOT NULL DEFAULT 1, | ||||
|     "access_key"  varchar(255) DEFAULT NULL, | ||||
|     "secret_key"  varchar(255) DEFAULT NULL, | ||||
|     "endpoint"    varchar(255) DEFAULT NULL, | ||||
|     "bucket_name" varchar(255) DEFAULT NULL, | ||||
|     "domain"      varchar(255) NOT NULL DEFAULT '', | ||||
|     "description" varchar(200) DEFAULT NULL, | ||||
|     "is_default"  bool         NOT NULL DEFAULT false, | ||||
|     "sort"        int4         NOT NULL DEFAULT 999, | ||||
|     "status"      int2         NOT NULL DEFAULT 1, | ||||
|     "create_user" int8         NOT NULL, | ||||
|     "create_time" timestamp    NOT NULL, | ||||
|     "update_user" int8         DEFAULT NULL, | ||||
|     "update_time" timestamp    DEFAULT NULL, | ||||
|     PRIMARY KEY ("id") | ||||
| ); | ||||
| CREATE UNIQUE INDEX "uk_storage_code"  ON "sys_storage" ("code"); | ||||
| CREATE INDEX "idx_storage_create_user" ON "sys_storage" ("create_user"); | ||||
| CREATE INDEX "idx_storage_update_user" ON "sys_storage" ("update_user"); | ||||
| COMMENT ON COLUMN "sys_storage"."id"          IS 'ID'; | ||||
| COMMENT ON COLUMN "sys_storage"."name"        IS '名称'; | ||||
| COMMENT ON COLUMN "sys_storage"."code"        IS '编码'; | ||||
| COMMENT ON COLUMN "sys_storage"."type"        IS '类型(1:兼容S3协议存储;2:本地存储)'; | ||||
| COMMENT ON COLUMN "sys_storage"."access_key"  IS 'Access Key(访问密钥)'; | ||||
| COMMENT ON COLUMN "sys_storage"."secret_key"  IS 'Secret Key(私有密钥)'; | ||||
| COMMENT ON COLUMN "sys_storage"."endpoint"    IS 'Endpoint(终端节点)'; | ||||
| COMMENT ON COLUMN "sys_storage"."bucket_name" IS '桶名称'; | ||||
| COMMENT ON COLUMN "sys_storage"."domain"      IS '域名'; | ||||
| COMMENT ON COLUMN "sys_storage"."description" IS '描述'; | ||||
| COMMENT ON COLUMN "sys_storage"."is_default"  IS '是否为默认存储'; | ||||
| COMMENT ON COLUMN "sys_storage"."sort"        IS '排序'; | ||||
| COMMENT ON COLUMN "sys_storage"."status"      IS '状态(1:启用;2:禁用)'; | ||||
| COMMENT ON COLUMN "sys_storage"."create_user" IS '创建人'; | ||||
| COMMENT ON COLUMN "sys_storage"."create_time" IS '创建时间'; | ||||
| COMMENT ON COLUMN "sys_storage"."update_user" IS '修改人'; | ||||
| COMMENT ON COLUMN "sys_storage"."update_time" IS '修改时间'; | ||||
| COMMENT ON TABLE  "sys_storage"               IS '存储表'; | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "sys_file" ( | ||||
|     "id"             int8         NOT NULL, | ||||
| @@ -471,15 +430,19 @@ CREATE TABLE IF NOT EXISTS "sys_file" ( | ||||
|     "thumbnail_size" int8         DEFAULT NULL, | ||||
|     "thumbnail_url"  varchar(512) DEFAULT NULL, | ||||
|     "type"           int2         NOT NULL DEFAULT 1, | ||||
|     "storage_id"     int8         NOT NULL, | ||||
|     "create_user"    int8         NOT NULL, | ||||
|     "create_time"    timestamp    NOT NULL, | ||||
|     "update_user"    int8         NOT NULL, | ||||
|     "update_time"    timestamp    NOT NULL, | ||||
|     "e_tag"          varchar(100) DEFAULT NULL, | ||||
|     "storage_code"   varchar(255) DEFAULT NULL, | ||||
|     "bucket_name"    varchar(255) DEFAULT NULL, | ||||
|     "path"           varchar(512) DEFAULT NULL, | ||||
|     PRIMARY KEY ("id") | ||||
| ); | ||||
| CREATE INDEX "idx_file_url"  ON "sys_file" ("url"); | ||||
| CREATE INDEX "idx_file_type" ON "sys_file" ("type"); | ||||
| CREATE INDEX "idx_file_storage_code" ON "sys_file" ("storage_code"); | ||||
| CREATE INDEX "idx_file_create_user" ON "sys_file" ("create_user"); | ||||
| CREATE INDEX "idx_file_update_user" ON "sys_file" ("update_user"); | ||||
| COMMENT ON COLUMN "sys_file"."id"             IS 'ID'; | ||||
| @@ -490,11 +453,14 @@ COMMENT ON COLUMN "sys_file"."extension"      IS '扩展名'; | ||||
| COMMENT ON COLUMN "sys_file"."thumbnail_size" IS '缩略图大小(字节)'; | ||||
| COMMENT ON COLUMN "sys_file"."thumbnail_url"  IS '缩略图URL'; | ||||
| COMMENT ON COLUMN "sys_file"."type"           IS '类型(1:其他;2:图片;3:文档;4:视频;5:音频)'; | ||||
| COMMENT ON COLUMN "sys_file"."storage_id"     IS '存储ID'; | ||||
| COMMENT ON COLUMN "sys_file"."create_user"    IS '创建人'; | ||||
| COMMENT ON COLUMN "sys_file"."create_time"    IS '创建时间'; | ||||
| COMMENT ON COLUMN "sys_file"."update_user"    IS '修改人'; | ||||
| COMMENT ON COLUMN "sys_file"."update_time"    IS '修改时间'; | ||||
| COMMENT ON COLUMN "sys_file"."e_tag"          IS '文件唯一标识'; | ||||
| COMMENT ON COLUMN "sys_file"."storage_code"   IS '存储唯一标识'; | ||||
| COMMENT ON COLUMN "sys_file"."bucket_name"    IS '存储桶名称'; | ||||
| COMMENT ON COLUMN "sys_file"."path"           IS '基础路径'; | ||||
| COMMENT ON TABLE  "sys_file"                  IS '文件表'; | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "sys_client" ( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user