From 6d64f47d3e90c020b7aa0f44f8aebeb959d22d4c Mon Sep 17 00:00:00 2001 From: Charles7c Date: Tue, 25 Feb 2025 14:17:53 +0000 Subject: [PATCH] =?UTF-8?q?feat(system/config):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- continew-common/pom.xml | 27 +-- .../doc/GlobalAuthenticationCustomizer.java | 216 ++++++++++++++++++ .../doc/GlobalDescriptionCustomizer.java | 65 ++++++ .../doc/OperationDescriptionCustomizer.java | 180 +++++++++++++++ .../exception/GlobalExceptionHandler.java | 35 ++- .../auth/handler/AccountLoginHandler.java | 6 +- .../system/config/file/FileRecorderImpl.java | 119 ---------- .../config/file/FileStorageConfigLoader.java | 37 +-- .../system/config/file/FileStorageInit.java | 146 ++++++++++++ .../system/config/file/StorageDaoImpl.java | 66 ++++++ .../system/enums/OptionCategoryEnum.java | 5 + .../system/enums/PasswordPolicyEnum.java | 22 +- .../admin/system/enums/StorageTypeEnum.java | 4 +- .../admin/system/mapper/StorageMapper.java | 29 --- .../admin/system/model/entity/FileDO.java | 70 +----- .../admin/system/model/entity/StorageDO.java | 102 --------- .../system/model/query/StorageQuery.java | 53 ----- .../admin/system/model/req/StorageReq.java | 134 ----------- .../admin/system/model/resp/StorageResp.java | 119 ---------- .../admin/system/service/FileService.java | 6 +- .../admin/system/service/StorageService.java | 62 ----- .../system/service/impl/FileServiceImpl.java | 105 ++------- .../service/impl/OptionServiceImpl.java | 17 ++ .../service/impl/StorageServiceImpl.java | 209 ----------------- .../system/validation/ValidationGroup.java | 45 ---- .../admin/ContiNewAdminApplication.java | 2 - .../controller/common/CommonController.java | 5 +- .../schedule/DemoEnvironmentJob.java | 6 - .../controller/system/StorageController.java | 39 ---- .../db/changelog/mysql/main_data.sql | 67 +++--- .../db/changelog/mysql/main_table.sql | 32 +-- .../db/changelog/postgresql/main_data.sql | 67 +++--- .../db/changelog/postgresql/main_table.sql | 54 +---- 33 files changed, 891 insertions(+), 1260 deletions(-) create mode 100644 continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalAuthenticationCustomizer.java create mode 100644 continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalDescriptionCustomizer.java create mode 100644 continew-common/src/main/java/top/continew/admin/common/config/doc/OperationDescriptionCustomizer.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java create mode 100644 continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageInit.java create mode 100644 continew-module-system/src/main/java/top/continew/admin/system/config/file/StorageDaoImpl.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/mapper/StorageMapper.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/model/query/StorageQuery.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/service/StorageService.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java delete mode 100644 continew-module-system/src/main/java/top/continew/admin/system/validation/ValidationGroup.java delete mode 100644 continew-webapi/src/main/java/top/continew/admin/controller/system/StorageController.java diff --git a/continew-common/pom.xml b/continew-common/pom.xml index a6ce2e87..d9ef7094 100644 --- a/continew-common/pom.xml +++ b/continew-common/pom.xml @@ -29,19 +29,6 @@ sms4j-spring-boot-starter - - - org.dromara.x-file-storage - x-file-storage-spring - 2.2.1 - - - - com.amazonaws - aws-java-sdk-s3 - 1.12.780 - - org.freemarker @@ -149,5 +136,19 @@ top.continew continew-starter-json-jackson + + + + + top.continew + continew-starter-storage-local + + + + + + top.continew + continew-starter-storage-oss + \ No newline at end of file diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalAuthenticationCustomizer.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalAuthenticationCustomizer.java new file mode 100644 index 00000000..5464f36a --- /dev/null +++ b/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalAuthenticationCustomizer.java @@ -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 excludedPaths = collectExcludedPaths(); + + // 遍历所有路径,为需要鉴权的路径添加安全认证配置 + openApi.getPaths().forEach((path, pathItem) -> { + if (isPathExcluded(path, excludedPaths)) { + // 路径在排除列表中,跳过处理 + return; + } + // 为路径添加安全认证参数 + addAuthenticationParameters(pathItem); + }); + } + + /** + * 收集所有需要排除的路径 + * + * @return 排除路径集合 + */ + private Set collectExcludedPaths() { + Set 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 securitySchemes = components.getSecuritySchemes(); + List 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 resolveSaIgnorePaths() { + // 获取所有标注 @RestController 的 Bean + Map controllers = context.getBeansWithAnnotation(RestController.class); + Set ignoredPaths = new HashSet<>(); + + // 遍历所有控制器,解析 @SaIgnore 注解路径 + controllers.values().forEach(controllerBean -> { + Class controllerClass = AopUtils.getTargetClass(controllerBean); + List 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 getClassPaths(Class controller) { + List 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 getMethodPaths(Method method) { + List 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 combinePaths(List classPaths, List 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 excludedPaths) { + return excludedPaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path)); + } +} diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalDescriptionCustomizer.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalDescriptionCustomizer.java new file mode 100644 index 00000000..52d14a01 --- /dev/null +++ b/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalDescriptionCustomizer.java @@ -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 noteList = new ArrayList<>(new OperationDescriptionCustomizer().getPermission(handlerMethod)); + + // 如果注解数据列表为空,直接返回原 operation + if (noteList.isEmpty()) { + return operation; + } + // 拼接注解数据为字符串 + String noteStr = StrUtil.join("
", noteList); + // 获取原描述 + String originalDescription = operation.getDescription(); + // 根据原描述是否为空,更新描述 + String newDescription = StringUtils.isNotEmpty(originalDescription) + ? originalDescription + "
" + noteStr + : noteStr; + + // 设置新描述 + operation.setDescription(newDescription); + return operation; + } +} diff --git a/continew-common/src/main/java/top/continew/admin/common/config/doc/OperationDescriptionCustomizer.java b/continew-common/src/main/java/top/continew/admin/common/config/doc/OperationDescriptionCustomizer.java new file mode 100644 index 00000000..7625640c --- /dev/null +++ b/continew-common/src/main/java/top/continew/admin/common/config/doc/OperationDescriptionCustomizer.java @@ -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 getPermission(HandlerMethod handlerMethod) { + List 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 注解类型 + * @return 拼接好的注解信息字符串 + */ + @SuppressWarnings("unchecked") + private String getAnnotationInfo(HandlerMethod handlerMethod, + Class 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, "" + title + "
"); + } + + 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(""); + 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("
"); + } + } + + /** + * 根据注解的模式拼接注解值 + * + * @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 "Crud 权限校验:
方法:" + permission + ""; + } +} diff --git a/continew-common/src/main/java/top/continew/admin/common/config/exception/GlobalExceptionHandler.java b/continew-common/src/main/java/top/continew/admin/common/config/exception/GlobalExceptionHandler.java index f2f7562d..567b8b58 100644 --- a/continew-common/src/main/java/top/continew/admin/common/config/exception/GlobalExceptionHandler.java +++ b/continew-common/src/main/java/top/continew/admin/common/config/exception/GlobalExceptionHandler.java @@ -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())); } } \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/auth/handler/AccountLoginHandler.java b/continew-module-system/src/main/java/top/continew/admin/auth/handler/AccountLoginHandler.java index 0a677d7c..f586f958 100644 --- a/continew-module-system/src/main/java/top/continew/admin/auth/handler/AccountLoginHandler.java +++ b/continew-module-system/src/main/java/top/continew/admin/auth/handler/AccountLoginHandler.java @@ -110,7 +110,8 @@ public class AccountLoginHandler extends AbstractLoginHandler { .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 { // 登录失败递增计数 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)); } } \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java b/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java deleted file mode 100644 index b15681d7..00000000 --- a/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java +++ /dev/null @@ -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 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)); - } -} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageConfigLoader.java b/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageConfigLoader.java index 6e0d5b4e..77108645 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageConfigLoader.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageConfigLoader.java @@ -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 storageList = storageService.list(query, null); - if (CollUtil.isEmpty(storageList)) { - return; - } - storageList.forEach(s -> storageService.load(BeanUtil.copyProperties(s, StorageReq.class))); + // 查询存储配置 + Map map = optionService.getByCategory(OptionCategoryEnum.STORAGE); + // 加载存储配置 + fileStorageInit.load(map); + } + + /** + * 存储持久层接口本地实现类 + */ + @Bean + public StorageDao storageDao(FileMapper fileMapper) { + return new StorageDaoImpl(fileMapper); } } diff --git a/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageInit.java b/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageInit.java new file mode 100644 index 00000000..030896aa --- /dev/null +++ b/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageInit.java @@ -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 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 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; + } +} diff --git a/continew-module-system/src/main/java/top/continew/admin/system/config/file/StorageDaoImpl.java b/continew-module-system/src/main/java/top/continew/admin/system/config/file/StorageDaoImpl.java new file mode 100644 index 00000000..2c87ffbd --- /dev/null +++ b/continew-module-system/src/main/java/top/continew/admin/system/config/file/StorageDaoImpl.java @@ -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); + } +} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/enums/OptionCategoryEnum.java b/continew-module-system/src/main/java/top/continew/admin/system/enums/OptionCategoryEnum.java index 2533d112..681d9feb 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/enums/OptionCategoryEnum.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/enums/OptionCategoryEnum.java @@ -43,4 +43,9 @@ public enum OptionCategoryEnum { * 登录配置 */ LOGIN, + + /** + * 存储配置 + */ + STORAGE,; } diff --git a/continew-module-system/src/main/java/top/continew/admin/system/enums/PasswordPolicyEnum.java b/continew-module-system/src/main/java/top/continew/admin/system/enums/PasswordPolicyEnum.java index cde757e5..7a90c350 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/enums/PasswordPolicyEnum.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/enums/PasswordPolicyEnum.java @@ -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 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 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 diff --git a/continew-module-system/src/main/java/top/continew/admin/system/enums/StorageTypeEnum.java b/continew-module-system/src/main/java/top/continew/admin/system/enums/StorageTypeEnum.java index 45a3c8c6..1dfaed59 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/enums/StorageTypeEnum.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/enums/StorageTypeEnum.java @@ -31,9 +31,9 @@ import top.continew.starter.core.enums.BaseEnum; public enum StorageTypeEnum implements BaseEnum { /** - * 兼容S3协议存储 + * 对象存储 */ - S3(1, "兼容S3协议存储"), + OSS(1, "对象存储"), /** * 本地存储 diff --git a/continew-module-system/src/main/java/top/continew/admin/system/mapper/StorageMapper.java b/continew-module-system/src/main/java/top/continew/admin/system/mapper/StorageMapper.java deleted file mode 100644 index 06d8c5f6..00000000 --- a/continew-module-system/src/main/java/top/continew/admin/system/mapper/StorageMapper.java +++ /dev/null @@ -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 { -} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/model/entity/FileDO.java b/continew-module-system/src/main/java/top/continew/admin/system/model/entity/FileDO.java index 95ce396a..b15a0105 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/model/entity/FileDO.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/model/entity/FileDO.java @@ -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; } diff --git a/continew-module-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java b/continew-module-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java deleted file mode 100644 index e6109c43..00000000 --- a/continew-module-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/model/query/StorageQuery.java b/continew-module-system/src/main/java/top/continew/admin/system/model/query/StorageQuery.java deleted file mode 100644 index 7dd6f592..00000000 --- a/continew-module-system/src/main/java/top/continew/admin/system/model/query/StorageQuery.java +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java b/continew-module-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java deleted file mode 100644 index 9a529cac..00000000 --- a/continew-module-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java b/continew-module-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java deleted file mode 100644 index 439b15bf..00000000 --- a/continew-module-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java +++ /dev/null @@ -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(); - } - -} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/service/FileService.java b/continew-module-system/src/main/java/top/continew/admin/system/service/FileService.java index a87cbfde..5e650ed1 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/service/FileService.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/service/FileService.java @@ -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, IService { - - /** - * 查询默认存储 - * - * @return 存储信息 - */ - StorageDO getDefaultStorage(); - - /** - * 根据编码查询 - * - * @param code 编码 - * @return 存储信息 - */ - StorageDO getByCode(String code); - - /** - * 加载存储 - * - * @param req 存储信息 - */ - void load(StorageReq req); - - /** - * 卸载存储 - * - * @param req 存储信息 - */ - void unload(StorageReq req); -} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java b/continew-module-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java index 42419d25..46340e3a 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java @@ -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 implements FileService { - private final FileStorageService fileStorageService; - @Resource - private StorageService storageService; - @Override protected void beforeDelete(List ids) { List fileList = baseMapper.lambdaQuery().in(FileDO::getId, ids).list(); - Map> fileListGroup = fileList.stream().collect(Collectors.groupingBy(FileDO::getStorageId)); - for (Map.Entry> 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 URLUtil - .normalize(prefix + thUrl)); - fileResp.setThumbnailUrl(thumbnailUrl); - fileResp.setStorageName("%s (%s)".formatted(storage.getName(), storage.getCode())); - } - } } \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java b/continew-module-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java index 3ac3c472..14ffe9b3 100644 --- a/continew-module-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java +++ b/continew-module-system/src/main/java/top/continew/admin/system/service/impl/OptionServiceImpl.java @@ -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 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 options) { + Map 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); + } + } } \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java b/continew-module-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java deleted file mode 100644 index 3aa01447..00000000 --- a/continew-module-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java +++ /dev/null @@ -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 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 ids) { - CheckUtils.throwIf(fileService.countByStorageIds(ids) > 0, "所选存储存在文件关联,请删除文件后重试"); - List 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 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 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(); - } -} \ No newline at end of file diff --git a/continew-module-system/src/main/java/top/continew/admin/system/validation/ValidationGroup.java b/continew-module-system/src/main/java/top/continew/admin/system/validation/ValidationGroup.java deleted file mode 100644 index 072b006f..00000000 --- a/continew-module-system/src/main/java/top/continew/admin/system/validation/ValidationGroup.java +++ /dev/null @@ -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 { - } - } -} \ No newline at end of file diff --git a/continew-webapi/src/main/java/top/continew/admin/ContiNewAdminApplication.java b/continew-webapi/src/main/java/top/continew/admin/ContiNewAdminApplication.java index 5c7fa8f5..17d9ffa1 100644 --- a/continew-webapi/src/main/java/top/continew/admin/ContiNewAdminApplication.java +++ b/continew-webapi/src/main/java/top/continew/admin/ContiNewAdminApplication.java @@ -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 diff --git a/continew-webapi/src/main/java/top/continew/admin/controller/common/CommonController.java b/continew-webapi/src/main/java/top/continew/admin/controller/common/CommonController.java index 07301ef8..81a9293b 100644 --- a/continew-webapi/src/main/java/top/continew/admin/controller/common/CommonController.java +++ b/continew-webapi/src/main/java/top/continew/admin/controller/common/CommonController.java @@ -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(); } diff --git a/continew-webapi/src/main/java/top/continew/admin/controller/schedule/DemoEnvironmentJob.java b/continew-webapi/src/main/java/top/continew/admin/controller/schedule/DemoEnvironmentJob.java index 6c5dd4f9..45876c27 100644 --- a/continew-webapi/src/main/java/top/continew/admin/controller/schedule/DemoEnvironmentJob.java +++ b/continew-webapi/src/main/java/top/continew/admin/controller/schedule/DemoEnvironmentJob.java @@ -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()); diff --git a/continew-webapi/src/main/java/top/continew/admin/controller/system/StorageController.java b/continew-webapi/src/main/java/top/continew/admin/controller/system/StorageController.java deleted file mode 100644 index e6d5f3c4..00000000 --- a/continew-webapi/src/main/java/top/continew/admin/controller/system/StorageController.java +++ /dev/null @@ -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 { -} \ No newline at end of file diff --git a/continew-webapi/src/main/resources/db/changelog/mysql/main_data.sql b/continew-webapi/src/main/resources/db/changelog/mysql/main_data.sql index 98167bf7..700f1052 100644 --- a/continew-webapi/src/main/resources/db/changelog/mysql/main_data.sql +++ b/continew-webapi/src/main/resources/db/changelog/mysql/main_data.sql @@ -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`) diff --git a/continew-webapi/src/main/resources/db/changelog/mysql/main_table.sql b/continew-webapi/src/main/resources/db/changelog/mysql/main_table.sql index 7e89ce1b..7f5d55e2 100644 --- a/continew-webapi/src/main/resources/db/changelog/mysql/main_table.sql +++ b/continew-webapi/src/main/resources/db/changelog/mysql/main_table.sql @@ -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='文件表'; diff --git a/continew-webapi/src/main/resources/db/changelog/postgresql/main_data.sql b/continew-webapi/src/main/resources/db/changelog/postgresql/main_data.sql index 47185859..d1950b2c 100644 --- a/continew-webapi/src/main/resources/db/changelog/postgresql/main_data.sql +++ b/continew-webapi/src/main/resources/db/changelog/postgresql/main_data.sql @@ -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") diff --git a/continew-webapi/src/main/resources/db/changelog/postgresql/main_table.sql b/continew-webapi/src/main/resources/db/changelog/postgresql/main_table.sql index 246cbb51..cf7ad13a 100644 --- a/continew-webapi/src/main/resources/db/changelog/postgresql/main_table.sql +++ b/continew-webapi/src/main/resources/db/changelog/postgresql/main_table.sql @@ -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" (