diff --git a/continew-common/pom.xml b/continew-common/pom.xml
index d9ef7094..a6ce2e87 100644
--- a/continew-common/pom.xml
+++ b/continew-common/pom.xml
@@ -29,6 +29,19 @@
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
@@ -136,19 +149,5 @@
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
deleted file mode 100644
index 5464f36a..00000000
--- a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalAuthenticationCustomizer.java
+++ /dev/null
@@ -1,216 +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.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
deleted file mode 100644
index 52d14a01..00000000
--- a/continew-common/src/main/java/top/continew/admin/common/config/doc/GlobalDescriptionCustomizer.java
+++ /dev/null
@@ -1,65 +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.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
deleted file mode 100644
index 7625640c..00000000
--- a/continew-common/src/main/java/top/continew/admin/common/config/doc/OperationDescriptionCustomizer.java
+++ /dev/null
@@ -1,180 +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.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 567b8b58..f2f7562d 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,16 +17,15 @@
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;
@@ -35,7 +34,6 @@ import top.continew.starter.web.model.R;
* 全局异常处理器
*
* @author Charles7c
- * @author echo
* @since 2024/8/7 20:21
*/
@Slf4j
@@ -75,7 +73,7 @@ public class GlobalExceptionHandler {
* 拦截文件上传异常-超过上传大小限制
*/
@ExceptionHandler(MultipartException.class)
- public R handleMultipartException(MultipartException e, HttpServletRequest request) {
+ public R handleRequestTooBigException(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);
@@ -87,33 +85,14 @@ public class GlobalExceptionHandler {
if (null != cause) {
msg = msg.concat(cause.getMessage().toLowerCase());
}
- if (msg.contains("larger than")) {
- sizeLimit = CharSequenceUtil.subAfter(msg, "larger than ", true);
- } else if (msg.contains("size") && msg.contains("exceed")) {
+ if (msg.contains("size") && msg.contains("exceed")) {
sizeLimit = CharSequenceUtil.subBetween(msg, "the maximum size ", " for");
+ } else if (msg.contains("larger than")) {
+ sizeLimit = CharSequenceUtil.subAfter(msg, "larger than ", true);
} else {
return defaultFail;
}
- 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()));
+ String errorMsg = "请上传小于 %sKB 的文件".formatted(NumberUtil.parseLong(sizeLimit) / 1024);
+ return R.fail(String.valueOf(HttpStatus.BAD_REQUEST.value()), errorMsg);
}
}
\ 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 f586f958..0a677d7c 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,8 +110,7 @@ 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, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_MINUTES.getMsg()
- .formatted(lockMinutes));
+ CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "账号锁定 {} 分钟,请稍后再试", lockMinutes);
// 登录成功清除计数
if (!isError) {
RedisUtils.delete(key);
@@ -120,7 +119,6 @@ public class AccountLoginHandler extends AbstractLoginHandler {
// 登录失败递增计数
currentErrorCount++;
RedisUtils.set(key, currentErrorCount, Duration.ofMinutes(lockMinutes));
- CheckUtils.throwIf(currentErrorCount >= maxErrorCount, PasswordPolicyEnum.PASSWORD_ERROR_LOCK_COUNT.getMsg()
- .formatted(maxErrorCount, lockMinutes));
+ CheckUtils.throwIf(currentErrorCount >= maxErrorCount, "密码错误已达 {} 次,账号锁定 {} 分钟", 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
new file mode 100644
index 00000000..b15681d7
--- /dev/null
+++ b/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileRecorderImpl.java
@@ -0,0 +1,119 @@
+/*
+ * 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 77108645..6e0d5b4e 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,24 +16,25 @@
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.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 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 java.util.Map;
+import java.util.List;
/**
* 文件存储配置加载器
*
* @author Charles7c
- * @author echo
* @since 2023/12/24 22:31
*/
@Slf4j
@@ -41,22 +42,16 @@ import java.util.Map;
@RequiredArgsConstructor
public class FileStorageConfigLoader implements ApplicationRunner {
- private final OptionService optionService;
- private final FileStorageInit fileStorageInit;
+ private final StorageService storageService;
@Override
public void run(ApplicationArguments args) {
- // 查询存储配置
- Map map = optionService.getByCategory(OptionCategoryEnum.STORAGE);
- // 加载存储配置
- fileStorageInit.load(map);
- }
-
- /**
- * 存储持久层接口本地实现类
- */
- @Bean
- public StorageDao storageDao(FileMapper fileMapper) {
- return new StorageDaoImpl(fileMapper);
+ 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)));
}
}
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
deleted file mode 100644
index 030896aa..00000000
--- a/continew-module-system/src/main/java/top/continew/admin/system/config/file/FileStorageInit.java
+++ /dev/null
@@ -1,146 +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.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
deleted file mode 100644
index 2c87ffbd..00000000
--- a/continew-module-system/src/main/java/top/continew/admin/system/config/file/StorageDaoImpl.java
+++ /dev/null
@@ -1,66 +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.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 681d9feb..2533d112 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,9 +43,4 @@ 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 7a90c350..cde757e5 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, "密码错误已达 %d 次,账号锁定 %d 分钟"),
+ PASSWORD_ERROR_LOCK_COUNT("登录密码错误锁定账号的次数取值范围为 %d-%d", SysConstants.NO, 10, null),
/**
- * 账号锁定时长(分钟)
+ * 登录密码错误锁定账号的时间(min)
*/
- PASSWORD_ERROR_LOCK_MINUTES("账号锁定时长取值范围为 %d-%d 分钟", 1, 1440, "账号锁定 %d 分钟,请稍后再试"),
+ PASSWORD_ERROR_LOCK_MINUTES("登录密码错误锁定账号的时间取值范围为 %d-%d 分钟", 1, 1440, null),
/**
* 密码有效期(天)
@@ -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 1dfaed59..45a3c8c6 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协议存储
*/
- OSS(1, "对象存储"),
+ S3(1, "兼容S3协议存储"),
/**
* 本地存储
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
new file mode 100644
index 00000000..06d8c5f6
--- /dev/null
+++ b/continew-module-system/src/main/java/top/continew/admin/system/mapper/StorageMapper.java
@@ -0,0 +1,29 @@
+/*
+ * 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 b15a0105..95ce396a 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,12 +16,19 @@
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;
/**
* 文件实体
@@ -72,23 +79,64 @@ public class FileDO extends BaseDO {
private String thumbnailUrl;
/**
- * 存储code
+ * 存储 ID
*/
- private String storageCode;
+ private Long storageId;
/**
- * 基础路径
+ * 转换为 X-File-Storage 文件信息对象
+ *
+ * @param storageDO 存储桶信息
+ * @return X-File-Storage 文件信息对象
*/
- private String path;
+ 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;
+ }
/**
- * 存储桶
+ * 将文件路径处理成资源路径
+ * 例如:
+ * 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
*/
- private String bucketName;
-
- /**
- * 文件标识
- */
- private String eTag;
+ @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;
+ }
}
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
new file mode 100644
index 00000000..e6109c43
--- /dev/null
+++ b/continew-module-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java
@@ -0,0 +1,102 @@
+/*
+ * 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
new file mode 100644
index 00000000..7dd6f592
--- /dev/null
+++ b/continew-module-system/src/main/java/top/continew/admin/system/model/query/StorageQuery.java
@@ -0,0 +1,53 @@
+/*
+ * 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
new file mode 100644
index 00000000..9a529cac
--- /dev/null
+++ b/continew-module-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java
@@ -0,0 +1,134 @@
+/*
+ * 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
new file mode 100644
index 00000000..439b15bf
--- /dev/null
+++ b/continew-module-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java
@@ -0,0 +1,119 @@
+/*
+ * 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 5e650ed1..a87cbfde 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 46340e3a..42419d25 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,27 +17,39 @@
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.starter.core.exception.BusinessException;
+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.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.io.IOException;
+import java.time.LocalDate;
import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
/**
* 文件业务实现
@@ -50,31 +62,65 @@ import java.util.List;
@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();
- fileList.forEach(file -> {
- StorageStrategy> instance = StorageManager.instance(file.getStorageCode());
- instance.delete(file.getBucketName(), file.getPath());
- });
+ 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);
+ }
+ }
}
@Override
- public FileUploadResp upload(MultipartFile file, String storageCode) {
- StorageStrategy> instance;
+ public FileInfo upload(MultipartFile file, String storageCode) {
+ StorageDO storage;
if (StrUtil.isBlank(storageCode)) {
- instance = StorageManager.instance();
+ storage = storageService.getDefaultStorage();
+ CheckUtils.throwIfNull(storage, "请先指定默认存储");
} else {
- instance = StorageManager.instance(storageCode);
+ storage = storageService.getByCode(storageCode);
+ CheckUtils.throwIfNotExists(storage, "StorageDO", "Code", storageCode);
}
- UploadResp uploadResp;
- try {
- uploadResp = instance.upload(file.getOriginalFilename(), null, file.getInputStream(), file
- .getContentType(), true);
- } catch (IOException e) {
- throw new BusinessException("文件上传失败", e);
+ 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));
}
- return FileUploadResp.builder().url(uploadResp.getUrl()).build();
+ 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;
}
@Override
@@ -82,7 +128,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 14ffe9b3..3ac3c472 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,7 +26,6 @@ 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;
@@ -58,7 +57,6 @@ import java.util.stream.Collectors;
public class OptionServiceImpl implements OptionService {
private final OptionMapper baseMapper;
- private final FileStorageInit fileStorageInit;
@Override
public List list(OptionQuery query) {
@@ -100,7 +98,6 @@ 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));
}
@@ -141,18 +138,4 @@ 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
new file mode 100644
index 00000000..3aa01447
--- /dev/null
+++ b/continew-module-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java
@@ -0,0 +1,209 @@
+/*
+ * 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
new file mode 100644
index 00000000..072b006f
--- /dev/null
+++ b/continew-module-system/src/main/java/top/continew/admin/system/validation/ValidationGroup.java
@@ -0,0 +1,45 @@
+/*
+ * 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 17d9ffa1..5c7fa8f5 100644
--- a/continew-webapi/src/main/java/top/continew/admin/ContiNewAdminApplication.java
+++ b/continew-webapi/src/main/java/top/continew/admin/ContiNewAdminApplication.java
@@ -25,6 +25,7 @@ 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;
@@ -44,6 +45,7 @@ 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 81a9293b..07301ef8 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,6 +26,7 @@ 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;
@@ -65,9 +66,9 @@ public class CommonController {
@Operation(summary = "上传文件", description = "上传文件")
@PostMapping("/file")
- public FileUploadResp upload(@NotNull(message = "文件不能为空") @RequestPart("file") MultipartFile file) {
+ public FileUploadResp upload(@NotNull(message = "文件不能为空") MultipartFile file) {
ValidationUtils.throwIf(file::isEmpty, "文件不能为空");
- FileUploadResp fileInfo = fileService.upload(file);
+ FileInfo 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 45876c27..6c5dd4f9 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,6 +47,7 @@ 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;
@@ -83,6 +84,8 @@ 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();
@@ -108,6 +111,9 @@ 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
new file mode 100644
index 00000000..e6d5f3c4
--- /dev/null
+++ b/continew-webapi/src/main/java/top/continew/admin/controller/system/StorageController.java
@@ -0,0 +1,39 @@
+/*
+ * 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 700f1052..98167bf7 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,6 +71,13 @@ 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()),
@@ -154,37 +161,28 @@ 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, '持续迭代优化的前后端分离中后台管理系统框架', '用于 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);
+(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:否)');
-- 初始化默认字典
INSERT INTO `sys_dict`
@@ -242,6 +240,13 @@ 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 7f5d55e2..7e89ce1b 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(100) NOT NULL COMMENT '所属模块',
+ `module` varchar(50) 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,19 +252,40 @@ 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_code` varchar(255) DEFAULT NULL COMMENT '存储唯一标识',
- `bucket_name` varchar(255) DEFAULT NULL COMMENT '存储桶名称',
- `path` varchar(512) DEFAULT NULL COMMENT '基础路径',
+ `storage_id` bigint(20) NOT NULL COMMENT '存储ID',
`create_user` bigint(20) NOT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_user` bigint(20) NOT NULL COMMENT '修改人',
@@ -272,7 +293,6 @@ 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 d1950b2c..47185859 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,6 +71,13 @@ 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()),
@@ -154,37 +161,28 @@ 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, '持续迭代优化的前后端分离中后台管理系统框架', '用于 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);
+(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:否)');
-- 初始化默认字典
INSERT INTO "sys_dict"
@@ -242,6 +240,13 @@ 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 cf7ad13a..246cbb51 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(100) NOT NULL,
+ "module" varchar(50) NOT NULL,
"request_url" varchar(512) NOT NULL,
"request_method" varchar(10) NOT NULL,
"request_headers" text DEFAULT NULL,
@@ -420,6 +420,47 @@ 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,
@@ -430,19 +471,15 @@ 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';
@@ -453,14 +490,11 @@ 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" (