diff --git a/database/SQL_INDEX.md b/database/SQL_INDEX.md
index 9833c8d3..86e43433 100644
--- a/database/SQL_INDEX.md
+++ b/database/SQL_INDEX.md
@@ -5,6 +5,10 @@
- **用于生产一次性导入**:`wuhan_saga_prod_YYYYMMDD.sql`(全库 `mysqldump`,含结构与数据)。
- 导入后**不要**再按顺序重放本目录下历史 `patch_*.sql` / `migration_*.sql`(除非你在维护很老的库且确认缺列)。
+## 附件与 MinIO
+
+- 图片路径在库中仍为 `/uploads/{分类}/{文件名}`。`upload.storage=minio` 时文件在对象存储桶内键为 `uploads/...`,需把原 `uploads/` 目录同步进桶(见仓库 `docker-compose.minio.yml` 与后端 `application.yml` 中 `minio.*`)。
+
## 日常开发以代码为准的 DDL
| 文件 | 用途 |
diff --git a/docker-compose.minio.yml b/docker-compose.minio.yml
new file mode 100644
index 00000000..36f9f84b
--- /dev/null
+++ b/docker-compose.minio.yml
@@ -0,0 +1,19 @@
+# 本地/内网 MinIO(与 Spring upload.storage=minio 配套)
+# 启动:docker compose -f docker-compose.minio.yml up -d
+# 控制台:http://127.0.0.1:9001 (默认账号 minioadmin / minioadmin,生产务必修改)
+
+services:
+ minio:
+ image: minio/minio:RELEASE.2024-11-07T00-52-20Z
+ command: server /data --console-address ":9001"
+ ports:
+ - "9000:9000"
+ - "9001:9001"
+ environment:
+ MINIO_ROOT_USER: minioadmin
+ MINIO_ROOT_PASSWORD: minioadmin
+ volumes:
+ - minio-data:/data
+
+volumes:
+ minio-data:
diff --git a/server/deploy/minio-spring-config.example.yml b/server/deploy/minio-spring-config.example.yml
new file mode 100644
index 00000000..b9fe01de
--- /dev/null
+++ b/server/deploy/minio-spring-config.example.yml
@@ -0,0 +1,27 @@
+# Spring Boot 使用 MinIO 时,在「生产配置」里增加或改成以下内容。
+# 不要提交含真实生产密码的副本;交给运维时可另存为服务器上的 application-prod.yml。
+#
+# 启动示例:java -jar wuhan-saga-server.jar --spring.profiles.active=prod
+#
+# 与运维 Docker 中环境变量的对应:
+# MINIO_ROOT_USER -> minio.access-key
+# MINIO_ROOT_PASSWORD -> minio.secret-key
+# endpoint:Spring 所在机器能访问的 MinIO API(9000)。同机 Docker host 网络一般用 http://127.0.0.1:9000
+
+# --- 下面粘贴进 application-prod.yml(或通过 Nacos/环境变量等价提供)---
+
+spring:
+ config:
+ activate:
+ on-profile: prod
+
+upload:
+ storage: minio
+ path: uploads/
+
+minio:
+ endpoint: http://127.0.0.1:9000
+ access-key: klp
+ secret-key: ruoyi123
+ bucket: wuhan-saga
+ region: us-east-1
diff --git a/server/pom.xml b/server/pom.xml
index d0a4bd18..f3a918fd 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -84,6 +84,12 @@
${hutool.version}
+
+ io.minio
+ minio
+ 8.5.13
+
+
org.projectlombok
lombok
diff --git a/server/src/main/java/com/wuhansaga/server/WuhanSagaApplication.java b/server/src/main/java/com/wuhansaga/server/WuhanSagaApplication.java
index 94c211da..09a7957e 100644
--- a/server/src/main/java/com/wuhansaga/server/WuhanSagaApplication.java
+++ b/server/src/main/java/com/wuhansaga/server/WuhanSagaApplication.java
@@ -1,5 +1,6 @@
package com.wuhansaga.server;
+import com.wuhansaga.server.config.MinioProperties;
import com.wuhansaga.server.config.PortalSiteProperties;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
@@ -8,7 +9,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
@SpringBootApplication
@MapperScan("com.wuhansaga.server.mapper")
-@EnableConfigurationProperties(PortalSiteProperties.class)
+@EnableConfigurationProperties({ PortalSiteProperties.class, MinioProperties.class })
public class WuhanSagaApplication {
public static void main(String[] args) {
diff --git a/server/src/main/java/com/wuhansaga/server/config/MinioProperties.java b/server/src/main/java/com/wuhansaga/server/config/MinioProperties.java
new file mode 100644
index 00000000..3ab1adb5
--- /dev/null
+++ b/server/src/main/java/com/wuhansaga/server/config/MinioProperties.java
@@ -0,0 +1,24 @@
+package com.wuhansaga.server.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * MinIO(S3 兼容)接入;仅在 upload.storage=minio 时使用。
+ */
+@ConfigurationProperties(prefix = "minio")
+public record MinioProperties(
+ /** 例:http://127.0.0.1:9000 或 https://minio.example.com(无尾斜杠) */
+ String endpoint,
+ String accessKey,
+ String secretKey,
+ /** 桶名,需有读权限;上传会 put 到此桶 */
+ String bucket,
+ /** 区域,MinIO 单机常用 us-east-1 */
+ String region
+) {
+ public MinioProperties {
+ if (region == null || region.isBlank()) {
+ region = "us-east-1";
+ }
+ }
+}
diff --git a/server/src/main/java/com/wuhansaga/server/config/WebMvcConfig.java b/server/src/main/java/com/wuhansaga/server/config/WebMvcConfig.java
index ef1f0c13..c482ab9a 100644
--- a/server/src/main/java/com/wuhansaga/server/config/WebMvcConfig.java
+++ b/server/src/main/java/com/wuhansaga/server/config/WebMvcConfig.java
@@ -14,6 +14,9 @@ public class WebMvcConfig implements WebMvcConfigurer {
@Value("${upload.path:uploads/}")
private String uploadPath;
+ @Value("${upload.storage:local}")
+ private String uploadStorage;
+
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
@@ -26,6 +29,9 @@ public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ if ("minio".equalsIgnoreCase(uploadStorage)) {
+ return;
+ }
File dir = new File(uploadPath);
if (!dir.isAbsolute()) {
dir = new File(System.getProperty("user.dir"), uploadPath);
diff --git a/server/src/main/java/com/wuhansaga/server/controller/portal/UploadServeController.java b/server/src/main/java/com/wuhansaga/server/controller/portal/UploadServeController.java
new file mode 100644
index 00000000..ab134cbd
--- /dev/null
+++ b/server/src/main/java/com/wuhansaga/server/controller/portal/UploadServeController.java
@@ -0,0 +1,88 @@
+package com.wuhansaga.server.controller.portal;
+
+import com.wuhansaga.server.storage.UploadStorage;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+/**
+ * MinIO 模式下由应用流式输出 /uploads/**;对象不存在时回退到 classpath bundled-uploads。
+ */
+@RestController
+@RequiredArgsConstructor
+@ConditionalOnProperty(name = "upload.storage", havingValue = "minio")
+public class UploadServeController {
+
+ private final UploadStorage uploadStorage;
+ private final ResourceLoader resourceLoader;
+
+ @GetMapping("/uploads/**")
+ public ResponseEntity serve(HttpServletRequest request)
+ throws IOException {
+ String rel = requestUriWithoutContext(request);
+ if (!rel.startsWith("/uploads/")) {
+ return ResponseEntity.notFound().build();
+ }
+
+ Optional fromStore = uploadStorage.open(rel);
+ if (fromStore.isPresent()) {
+ UploadStorage.StoredObject o = fromStore.get();
+ MediaType mt = MediaType.parseMediaType(o.contentType());
+ var body = new org.springframework.core.io.InputStreamResource(o.inputStream()) {
+ @Override
+ public long contentLength() {
+ return o.size();
+ }
+ };
+ return ResponseEntity.ok().contentType(mt).contentLength(o.size()).body(body);
+ }
+
+ String tail = rel.substring("/uploads/".length());
+ Resource bundled = resourceLoader.getResource("classpath:/static/bundled-uploads/" + tail);
+ if (!bundled.exists() || !bundled.isReadable()) {
+ return ResponseEntity.notFound().build();
+ }
+ InputStream in = bundled.getInputStream();
+ long len = bundled.contentLength();
+ MediaType mt = MediaType.APPLICATION_OCTET_STREAM;
+ String name = bundled.getFilename();
+ if (name != null && (name.endsWith(".jpg") || name.endsWith(".jpeg"))) {
+ mt = MediaType.IMAGE_JPEG;
+ } else if (name != null && name.endsWith(".png")) {
+ mt = MediaType.IMAGE_PNG;
+ } else if (name != null && name.endsWith(".gif")) {
+ mt = MediaType.IMAGE_GIF;
+ } else if (name != null && name.endsWith(".webp")) {
+ mt = MediaType.parseMediaType("image/webp");
+ }
+ var body = new org.springframework.core.io.InputStreamResource(in) {
+ @Override
+ public long contentLength() {
+ return len > 0 ? len : -1;
+ }
+ };
+ if (len > 0) {
+ return ResponseEntity.ok().contentType(mt).contentLength(len).body(body);
+ }
+ return ResponseEntity.ok().contentType(mt).body(body);
+ }
+
+ private static String requestUriWithoutContext(HttpServletRequest request) {
+ String uri = request.getRequestURI();
+ String cp = request.getContextPath();
+ if (cp != null && !cp.isEmpty() && uri.startsWith(cp)) {
+ return uri.substring(cp.length());
+ }
+ return uri;
+ }
+}
diff --git a/server/src/main/java/com/wuhansaga/server/service/MediaLibraryService.java b/server/src/main/java/com/wuhansaga/server/service/MediaLibraryService.java
index ddf79d10..e4701500 100644
--- a/server/src/main/java/com/wuhansaga/server/service/MediaLibraryService.java
+++ b/server/src/main/java/com/wuhansaga/server/service/MediaLibraryService.java
@@ -4,13 +4,11 @@ import com.wuhansaga.server.common.PageQuery;
import com.wuhansaga.server.common.PageResult;
import com.wuhansaga.server.entity.MediaLibrary;
import com.wuhansaga.server.mapper.MediaLibraryMapper;
+import com.wuhansaga.server.storage.UploadStorage;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
-import jakarta.annotation.PostConstruct;
-import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
@@ -20,29 +18,11 @@ import java.util.UUID;
public class MediaLibraryService {
private final MediaLibraryMapper mediaLibraryMapper;
+ private final UploadStorage uploadStorage;
- @Value("${upload.path}")
- private String uploadPath;
-
- private File uploadDir;
-
- public MediaLibraryService(MediaLibraryMapper mediaLibraryMapper) {
+ public MediaLibraryService(MediaLibraryMapper mediaLibraryMapper, UploadStorage uploadStorage) {
this.mediaLibraryMapper = mediaLibraryMapper;
- }
-
- @PostConstruct
- public void init() {
- uploadDir = new File(uploadPath);
- if (!uploadDir.isAbsolute()) {
- String userDir = System.getProperty("user.dir");
- uploadDir = new File(userDir, uploadPath);
- }
- if (!uploadDir.exists()) {
- boolean created = uploadDir.mkdirs();
- log.info("初始化上传目录: {} => {}", uploadDir.getAbsolutePath(), created);
- } else {
- log.info("上传目录已存在: {}", uploadDir.getAbsolutePath());
- }
+ this.uploadStorage = uploadStorage;
}
public PageResult page(String category, String keyword, PageQuery query) {
@@ -63,15 +43,7 @@ public class MediaLibraryService {
String newFileName = UUID.randomUUID().toString().replace("-", "") + ext;
String relativePath = "/uploads/" + category + "/" + newFileName;
- File categoryDir = new File(uploadDir, category);
- if (!categoryDir.exists()) {
- boolean created = categoryDir.mkdirs();
- log.info("创建分类目录: {} => {}", categoryDir.getAbsolutePath(), created);
- }
-
- File dest = new File(categoryDir, newFileName);
- file.transferTo(dest);
- log.info("文件已保存: {}", dest.getAbsolutePath());
+ uploadStorage.store(file, relativePath);
MediaLibrary media = new MediaLibrary();
media.setFilePath(relativePath);
diff --git a/server/src/main/java/com/wuhansaga/server/storage/LocalUploadStorage.java b/server/src/main/java/com/wuhansaga/server/storage/LocalUploadStorage.java
new file mode 100644
index 00000000..157d6acf
--- /dev/null
+++ b/server/src/main/java/com/wuhansaga/server/storage/LocalUploadStorage.java
@@ -0,0 +1,77 @@
+package com.wuhansaga.server.storage;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.annotation.PostConstruct;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Optional;
+
+@Slf4j
+@Component
+@ConditionalOnProperty(name = "upload.storage", havingValue = "local", matchIfMissing = true)
+public class LocalUploadStorage implements UploadStorage {
+
+ @Value("${upload.path}")
+ private String uploadPath;
+
+ private File uploadDir;
+
+ @PostConstruct
+ public void init() {
+ uploadDir = new File(uploadPath);
+ if (!uploadDir.isAbsolute()) {
+ uploadDir = new File(System.getProperty("user.dir"), uploadPath);
+ }
+ if (!uploadDir.exists()) {
+ boolean created = uploadDir.mkdirs();
+ log.info("初始化本地上传目录: {} => {}", uploadDir.getAbsolutePath(), created);
+ } else {
+ log.info("本地上传目录已存在: {}", uploadDir.getAbsolutePath());
+ }
+ }
+
+ @Override
+ public void store(MultipartFile file, String relativePath) throws IOException {
+ if (relativePath == null || !relativePath.startsWith("/uploads/")) {
+ throw new IllegalArgumentException("relativePath 须以 /uploads/ 开头: " + relativePath);
+ }
+ String withoutPrefix = relativePath.substring("/uploads/".length());
+ int slash = withoutPrefix.lastIndexOf('/');
+ String category = slash >= 0 ? withoutPrefix.substring(0, slash) : "";
+ String fileName = slash >= 0 ? withoutPrefix.substring(slash + 1) : withoutPrefix;
+
+ File categoryDir = category.isEmpty() ? uploadDir : new File(uploadDir, category);
+ if (!categoryDir.exists()) {
+ boolean created = categoryDir.mkdirs();
+ log.info("创建分类目录: {} => {}", categoryDir.getAbsolutePath(), created);
+ }
+ File dest = new File(categoryDir, fileName);
+ file.transferTo(dest);
+ log.info("文件已保存(本地): {}", dest.getAbsolutePath());
+ }
+
+ @Override
+ public Optional open(String relativePath) throws IOException {
+ if (relativePath == null || !relativePath.startsWith("/uploads/")) {
+ return Optional.empty();
+ }
+ String withoutPrefix = relativePath.substring("/uploads/".length());
+ File f = new File(uploadDir, withoutPrefix.replace('/', File.separatorChar));
+ if (!f.isFile()) {
+ return Optional.empty();
+ }
+ String ct = Files.probeContentType(f.toPath());
+ if (ct == null) {
+ ct = "application/octet-stream";
+ }
+ InputStream in = Files.newInputStream(f.toPath());
+ return Optional.of(new StoredObject(in, f.length(), ct));
+ }
+}
diff --git a/server/src/main/java/com/wuhansaga/server/storage/MinioUploadStorage.java b/server/src/main/java/com/wuhansaga/server/storage/MinioUploadStorage.java
new file mode 100644
index 00000000..180a54b6
--- /dev/null
+++ b/server/src/main/java/com/wuhansaga/server/storage/MinioUploadStorage.java
@@ -0,0 +1,112 @@
+package com.wuhansaga.server.storage;
+
+import com.wuhansaga.server.config.MinioProperties;
+import io.minio.BucketExistsArgs;
+import io.minio.GetObjectArgs;
+import io.minio.MakeBucketArgs;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
+import io.minio.StatObjectArgs;
+import io.minio.StatObjectResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import jakarta.annotation.PostConstruct;
+import java.io.InputStream;
+import java.util.Optional;
+
+@Slf4j
+@Component
+@ConditionalOnProperty(name = "upload.storage", havingValue = "minio")
+public class MinioUploadStorage implements UploadStorage {
+
+ private final MinioProperties props;
+ private MinioClient client;
+
+ public MinioUploadStorage(MinioProperties props) {
+ this.props = props;
+ }
+
+ @PostConstruct
+ public void init() throws java.io.IOException {
+ if (props.endpoint() == null || props.endpoint().isBlank()) {
+ throw new IllegalStateException("minio.endpoint 未配置");
+ }
+ if (props.accessKey() == null || props.secretKey() == null || props.bucket() == null
+ || props.accessKey().isBlank() || props.secretKey().isBlank() || props.bucket().isBlank()) {
+ throw new IllegalStateException("minio.access-key、secret-key、bucket 均需配置");
+ }
+ client = MinioClient.builder()
+ .endpoint(props.endpoint())
+ .region(props.region())
+ .credentials(props.accessKey(), props.secretKey())
+ .build();
+ try {
+ boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(props.bucket()).build());
+ if (!exists) {
+ client.makeBucket(MakeBucketArgs.builder().bucket(props.bucket()).build());
+ log.info("已创建 MinIO 桶: {}", props.bucket());
+ } else {
+ log.info("MinIO 桶已存在: {}", props.bucket());
+ }
+ } catch (Exception e) {
+ throw new java.io.IOException("初始化 MinIO 失败: " + e.getMessage(), e);
+ }
+ }
+
+ private static String objectName(String relativePath) {
+ if (relativePath == null || !relativePath.startsWith("/uploads/")) {
+ throw new IllegalArgumentException("relativePath 须以 /uploads/ 开头: " + relativePath);
+ }
+ return relativePath.substring(1);
+ }
+
+ @Override
+ public void store(MultipartFile file, String relativePath) throws java.io.IOException {
+ String name = objectName(relativePath);
+ String ct = file.getContentType();
+ if (ct == null || ct.isBlank()) {
+ ct = "application/octet-stream";
+ }
+ try (InputStream in = file.getInputStream()) {
+ client.putObject(
+ PutObjectArgs.builder()
+ .bucket(props.bucket())
+ .object(name)
+ .stream(in, file.getSize(), -1)
+ .contentType(ct)
+ .build());
+ } catch (Exception e) {
+ throw new java.io.IOException("MinIO 上传失败: " + e.getMessage(), e);
+ }
+ log.info("文件已写入 MinIO: {}/{}", props.bucket(), name);
+ }
+
+ @Override
+ public Optional open(String relativePath) throws java.io.IOException {
+ if (relativePath == null || !relativePath.startsWith("/uploads/")) {
+ return Optional.empty();
+ }
+ String name = objectName(relativePath);
+ try {
+ StatObjectResponse stat = client.statObject(
+ StatObjectArgs.builder().bucket(props.bucket()).object(name).build());
+ InputStream in = client.getObject(
+ GetObjectArgs.builder().bucket(props.bucket()).object(name).build());
+ String ct = stat.contentType();
+ if (ct == null || ct.isBlank()) {
+ ct = "application/octet-stream";
+ }
+ return Optional.of(new StoredObject(in, stat.size(), ct));
+ } catch (io.minio.errors.ErrorResponseException e) {
+ if ("NoSuchKey".equals(e.errorResponse().code()) || "NoSuchObject".equals(e.errorResponse().code())) {
+ return Optional.empty();
+ }
+ throw new java.io.IOException("MinIO 读取失败: " + e.getMessage(), e);
+ } catch (Exception e) {
+ throw new java.io.IOException("MinIO 读取失败: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/server/src/main/java/com/wuhansaga/server/storage/UploadStorage.java b/server/src/main/java/com/wuhansaga/server/storage/UploadStorage.java
new file mode 100644
index 00000000..26445c1a
--- /dev/null
+++ b/server/src/main/java/com/wuhansaga/server/storage/UploadStorage.java
@@ -0,0 +1,19 @@
+package com.wuhansaga.server.storage;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+
+/**
+ * 统一上传存储;相对路径与库存一致,如 /uploads/banner/xxx.jpg
+ */
+public interface UploadStorage {
+
+ void store(MultipartFile file, String relativePath) throws IOException;
+
+ Optional open(String relativePath) throws IOException;
+
+ record StoredObject(InputStream inputStream, long size, String contentType) {}
+}
diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml
index 838b6190..be64af1e 100644
--- a/server/src/main/resources/application.yml
+++ b/server/src/main/resources/application.yml
@@ -56,11 +56,21 @@ knife4j:
setting:
language: zh_cn
+# 存储:local=本机 uploads | minio=对象存储,见 server/deploy/minio-spring-config.example.yml
upload:
+ storage: local
path: uploads/
allowed-types: image/jpeg,image/png,image/gif,image/webp,image/svg+xml,video/mp4,video/webm
max-size: 52428800
+# 仅 upload.storage=minio 时生效;密钥用环境变量注入,勿提交生产明文
+minio:
+ endpoint: http://127.0.0.1:9000
+ access-key: minioadmin
+ secret-key: minioadmin
+ bucket: wuhan-saga
+ region: us-east-1
+
# 新闻中心多站点:单部署实例默认站点;扩展编码时改 allowed-site-codes 与库内数据
app:
portal:
diff --git a/server/target/classes/application.yml b/server/target/classes/application.yml
index 838b6190..be64af1e 100644
--- a/server/target/classes/application.yml
+++ b/server/target/classes/application.yml
@@ -56,11 +56,21 @@ knife4j:
setting:
language: zh_cn
+# 存储:local=本机 uploads | minio=对象存储,见 server/deploy/minio-spring-config.example.yml
upload:
+ storage: local
path: uploads/
allowed-types: image/jpeg,image/png,image/gif,image/webp,image/svg+xml,video/mp4,video/webm
max-size: 52428800
+# 仅 upload.storage=minio 时生效;密钥用环境变量注入,勿提交生产明文
+minio:
+ endpoint: http://127.0.0.1:9000
+ access-key: minioadmin
+ secret-key: minioadmin
+ bucket: wuhan-saga
+ region: us-east-1
+
# 新闻中心多站点:单部署实例默认站点;扩展编码时改 allowed-site-codes 与库内数据
app:
portal:
diff --git a/server/target/classes/com/wuhansaga/server/WuhanSagaApplication.class b/server/target/classes/com/wuhansaga/server/WuhanSagaApplication.class
index 67441d3d..19d133e6 100644
Binary files a/server/target/classes/com/wuhansaga/server/WuhanSagaApplication.class and b/server/target/classes/com/wuhansaga/server/WuhanSagaApplication.class differ
diff --git a/server/target/classes/com/wuhansaga/server/config/MinioProperties.class b/server/target/classes/com/wuhansaga/server/config/MinioProperties.class
new file mode 100644
index 00000000..8e4569c5
Binary files /dev/null and b/server/target/classes/com/wuhansaga/server/config/MinioProperties.class differ
diff --git a/server/target/classes/com/wuhansaga/server/config/PortalSiteProperties.class b/server/target/classes/com/wuhansaga/server/config/PortalSiteProperties.class
index 016eaee3..b5e8cf9d 100644
Binary files a/server/target/classes/com/wuhansaga/server/config/PortalSiteProperties.class and b/server/target/classes/com/wuhansaga/server/config/PortalSiteProperties.class differ
diff --git a/server/target/classes/com/wuhansaga/server/config/PortalSiteResolver.class b/server/target/classes/com/wuhansaga/server/config/PortalSiteResolver.class
index c9129e37..eaee7108 100644
Binary files a/server/target/classes/com/wuhansaga/server/config/PortalSiteResolver.class and b/server/target/classes/com/wuhansaga/server/config/PortalSiteResolver.class differ
diff --git a/server/target/classes/com/wuhansaga/server/config/WebMvcConfig.class b/server/target/classes/com/wuhansaga/server/config/WebMvcConfig.class
index fdaa3e75..4f18a6ea 100644
Binary files a/server/target/classes/com/wuhansaga/server/config/WebMvcConfig.class and b/server/target/classes/com/wuhansaga/server/config/WebMvcConfig.class differ
diff --git a/server/target/classes/com/wuhansaga/server/constant/SiteCodes.class b/server/target/classes/com/wuhansaga/server/constant/SiteCodes.class
index 1f331a17..e003ff80 100644
Binary files a/server/target/classes/com/wuhansaga/server/constant/SiteCodes.class and b/server/target/classes/com/wuhansaga/server/constant/SiteCodes.class differ
diff --git a/server/target/classes/com/wuhansaga/server/controller/admin/AdminNewsController.class b/server/target/classes/com/wuhansaga/server/controller/admin/AdminNewsController.class
index 2ba8543e..4260f6b8 100644
Binary files a/server/target/classes/com/wuhansaga/server/controller/admin/AdminNewsController.class and b/server/target/classes/com/wuhansaga/server/controller/admin/AdminNewsController.class differ
diff --git a/server/target/classes/com/wuhansaga/server/controller/admin/AdminProductLineController.class b/server/target/classes/com/wuhansaga/server/controller/admin/AdminProductLineController.class
index 27b79356..4954035d 100644
Binary files a/server/target/classes/com/wuhansaga/server/controller/admin/AdminProductLineController.class and b/server/target/classes/com/wuhansaga/server/controller/admin/AdminProductLineController.class differ
diff --git a/server/target/classes/com/wuhansaga/server/controller/portal/PortalNewsController.class b/server/target/classes/com/wuhansaga/server/controller/portal/PortalNewsController.class
index d2accee0..26c39ea4 100644
Binary files a/server/target/classes/com/wuhansaga/server/controller/portal/PortalNewsController.class and b/server/target/classes/com/wuhansaga/server/controller/portal/PortalNewsController.class differ
diff --git a/server/target/classes/com/wuhansaga/server/controller/portal/PortalProductController.class b/server/target/classes/com/wuhansaga/server/controller/portal/PortalProductController.class
index f3fe82e0..c9a5f397 100644
Binary files a/server/target/classes/com/wuhansaga/server/controller/portal/PortalProductController.class and b/server/target/classes/com/wuhansaga/server/controller/portal/PortalProductController.class differ
diff --git a/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController$1.class b/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController$1.class
new file mode 100644
index 00000000..19ee0831
Binary files /dev/null and b/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController$1.class differ
diff --git a/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController$2.class b/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController$2.class
new file mode 100644
index 00000000..0cf42869
Binary files /dev/null and b/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController$2.class differ
diff --git a/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController.class b/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController.class
new file mode 100644
index 00000000..61f40d53
Binary files /dev/null and b/server/target/classes/com/wuhansaga/server/controller/portal/UploadServeController.class differ
diff --git a/server/target/classes/com/wuhansaga/server/entity/MediaLibrary.class b/server/target/classes/com/wuhansaga/server/entity/MediaLibrary.class
index 8fb2772e..7ba4dc54 100644
Binary files a/server/target/classes/com/wuhansaga/server/entity/MediaLibrary.class and b/server/target/classes/com/wuhansaga/server/entity/MediaLibrary.class differ
diff --git a/server/target/classes/com/wuhansaga/server/entity/News.class b/server/target/classes/com/wuhansaga/server/entity/News.class
index af0abdcc..16a122d5 100644
Binary files a/server/target/classes/com/wuhansaga/server/entity/News.class and b/server/target/classes/com/wuhansaga/server/entity/News.class differ
diff --git a/server/target/classes/com/wuhansaga/server/entity/NewsCategory.class b/server/target/classes/com/wuhansaga/server/entity/NewsCategory.class
index 00ee26b7..2062066a 100644
Binary files a/server/target/classes/com/wuhansaga/server/entity/NewsCategory.class and b/server/target/classes/com/wuhansaga/server/entity/NewsCategory.class differ
diff --git a/server/target/classes/com/wuhansaga/server/entity/ProductCategory.class b/server/target/classes/com/wuhansaga/server/entity/ProductCategory.class
index f7b39202..18f2692c 100644
Binary files a/server/target/classes/com/wuhansaga/server/entity/ProductCategory.class and b/server/target/classes/com/wuhansaga/server/entity/ProductCategory.class differ
diff --git a/server/target/classes/com/wuhansaga/server/entity/ProductLine.class b/server/target/classes/com/wuhansaga/server/entity/ProductLine.class
index b893108f..479065c3 100644
Binary files a/server/target/classes/com/wuhansaga/server/entity/ProductLine.class and b/server/target/classes/com/wuhansaga/server/entity/ProductLine.class differ
diff --git a/server/target/classes/com/wuhansaga/server/entity/SingleEquipment.class b/server/target/classes/com/wuhansaga/server/entity/SingleEquipment.class
index 81f48732..9214ad43 100644
Binary files a/server/target/classes/com/wuhansaga/server/entity/SingleEquipment.class and b/server/target/classes/com/wuhansaga/server/entity/SingleEquipment.class differ
diff --git a/server/target/classes/com/wuhansaga/server/entity/SparePart.class b/server/target/classes/com/wuhansaga/server/entity/SparePart.class
index 7e114c4a..3b163198 100644
Binary files a/server/target/classes/com/wuhansaga/server/entity/SparePart.class and b/server/target/classes/com/wuhansaga/server/entity/SparePart.class differ
diff --git a/server/target/classes/com/wuhansaga/server/mapper/NewsCategoryMapper.class b/server/target/classes/com/wuhansaga/server/mapper/NewsCategoryMapper.class
index bf5b04ab..780730f2 100644
Binary files a/server/target/classes/com/wuhansaga/server/mapper/NewsCategoryMapper.class and b/server/target/classes/com/wuhansaga/server/mapper/NewsCategoryMapper.class differ
diff --git a/server/target/classes/com/wuhansaga/server/mapper/NewsMapper.class b/server/target/classes/com/wuhansaga/server/mapper/NewsMapper.class
index e808518d..2108c7ca 100644
Binary files a/server/target/classes/com/wuhansaga/server/mapper/NewsMapper.class and b/server/target/classes/com/wuhansaga/server/mapper/NewsMapper.class differ
diff --git a/server/target/classes/com/wuhansaga/server/mapper/ProductCategoryMapper.class b/server/target/classes/com/wuhansaga/server/mapper/ProductCategoryMapper.class
index fad989a8..fa83f437 100644
Binary files a/server/target/classes/com/wuhansaga/server/mapper/ProductCategoryMapper.class and b/server/target/classes/com/wuhansaga/server/mapper/ProductCategoryMapper.class differ
diff --git a/server/target/classes/com/wuhansaga/server/mapper/ProductLineEquipmentMapper.class b/server/target/classes/com/wuhansaga/server/mapper/ProductLineEquipmentMapper.class
index 83813a9c..73662ba3 100644
Binary files a/server/target/classes/com/wuhansaga/server/mapper/ProductLineEquipmentMapper.class and b/server/target/classes/com/wuhansaga/server/mapper/ProductLineEquipmentMapper.class differ
diff --git a/server/target/classes/com/wuhansaga/server/mapper/SingleEquipmentMapper.class b/server/target/classes/com/wuhansaga/server/mapper/SingleEquipmentMapper.class
index 8a43999c..d391a110 100644
Binary files a/server/target/classes/com/wuhansaga/server/mapper/SingleEquipmentMapper.class and b/server/target/classes/com/wuhansaga/server/mapper/SingleEquipmentMapper.class differ
diff --git a/server/target/classes/com/wuhansaga/server/service/BannerService.class b/server/target/classes/com/wuhansaga/server/service/BannerService.class
index 7eb3ebd5..08654da1 100644
Binary files a/server/target/classes/com/wuhansaga/server/service/BannerService.class and b/server/target/classes/com/wuhansaga/server/service/BannerService.class differ
diff --git a/server/target/classes/com/wuhansaga/server/service/MediaLibraryService.class b/server/target/classes/com/wuhansaga/server/service/MediaLibraryService.class
index c1a06642..9d05baea 100644
Binary files a/server/target/classes/com/wuhansaga/server/service/MediaLibraryService.class and b/server/target/classes/com/wuhansaga/server/service/MediaLibraryService.class differ
diff --git a/server/target/classes/com/wuhansaga/server/service/NewsCategoryService.class b/server/target/classes/com/wuhansaga/server/service/NewsCategoryService.class
index 2931e821..59ec1f5e 100644
Binary files a/server/target/classes/com/wuhansaga/server/service/NewsCategoryService.class and b/server/target/classes/com/wuhansaga/server/service/NewsCategoryService.class differ
diff --git a/server/target/classes/com/wuhansaga/server/service/NewsService.class b/server/target/classes/com/wuhansaga/server/service/NewsService.class
index b84ff8cb..c61a0c92 100644
Binary files a/server/target/classes/com/wuhansaga/server/service/NewsService.class and b/server/target/classes/com/wuhansaga/server/service/NewsService.class differ
diff --git a/server/target/classes/com/wuhansaga/server/service/ProductCategoryService.class b/server/target/classes/com/wuhansaga/server/service/ProductCategoryService.class
index 527fbefc..b313ecbd 100644
Binary files a/server/target/classes/com/wuhansaga/server/service/ProductCategoryService.class and b/server/target/classes/com/wuhansaga/server/service/ProductCategoryService.class differ
diff --git a/server/target/classes/com/wuhansaga/server/service/ProductLineService.class b/server/target/classes/com/wuhansaga/server/service/ProductLineService.class
index 65e2b022..6469f8ca 100644
Binary files a/server/target/classes/com/wuhansaga/server/service/ProductLineService.class and b/server/target/classes/com/wuhansaga/server/service/ProductLineService.class differ
diff --git a/server/target/classes/com/wuhansaga/server/storage/LocalUploadStorage.class b/server/target/classes/com/wuhansaga/server/storage/LocalUploadStorage.class
new file mode 100644
index 00000000..eee84b43
Binary files /dev/null and b/server/target/classes/com/wuhansaga/server/storage/LocalUploadStorage.class differ
diff --git a/server/target/classes/com/wuhansaga/server/storage/MinioUploadStorage.class b/server/target/classes/com/wuhansaga/server/storage/MinioUploadStorage.class
new file mode 100644
index 00000000..13a71187
Binary files /dev/null and b/server/target/classes/com/wuhansaga/server/storage/MinioUploadStorage.class differ
diff --git a/server/target/classes/com/wuhansaga/server/storage/UploadStorage$StoredObject.class b/server/target/classes/com/wuhansaga/server/storage/UploadStorage$StoredObject.class
new file mode 100644
index 00000000..a0506d22
Binary files /dev/null and b/server/target/classes/com/wuhansaga/server/storage/UploadStorage$StoredObject.class differ
diff --git a/server/target/classes/com/wuhansaga/server/storage/UploadStorage.class b/server/target/classes/com/wuhansaga/server/storage/UploadStorage.class
new file mode 100644
index 00000000..a4f5d590
Binary files /dev/null and b/server/target/classes/com/wuhansaga/server/storage/UploadStorage.class differ
diff --git a/server/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/server/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
index bfd4da2c..c6e297ee 100644
--- a/server/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
+++ b/server/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -7,10 +7,12 @@ com\wuhansaga\server\mapper\SingleEquipmentMapper.class
com\wuhansaga\server\mapper\ProductCategoryMapper.class
com\wuhansaga\server\service\WorkshopService.class
com\wuhansaga\server\controller\portal\PortalCaseController.class
+com\wuhansaga\server\config\MinioProperties.class
com\wuhansaga\server\service\CompanyInfoService.class
com\wuhansaga\server\controller\admin\AdminBannerController.class
com\wuhansaga\server\controller\admin\AdminCaseController.class
com\wuhansaga\server\mapper\MediaLibraryMapper.class
+com\wuhansaga\server\controller\portal\UploadServeController$1.class
com\wuhansaga\server\common\PageQuery.class
com\wuhansaga\server\entity\MediaLibrary.class
com\wuhansaga\server\controller\portal\PortalWorkshopController.class
@@ -23,11 +25,15 @@ com\wuhansaga\server\service\MediaLibraryService.class
com\wuhansaga\server\mapper\CaseMediaMapper.class
com\wuhansaga\server\config\OpenApiConfig.class
com\wuhansaga\server\mapper\CompanyInfoMapper.class
+com\wuhansaga\server\controller\portal\UploadServeController.class
com\wuhansaga\server\entity\CaseMedia.class
com\wuhansaga\server\controller\admin\AdminAuthController.class
com\wuhansaga\server\entity\ProductMedia.class
+com\wuhansaga\server\controller\portal\UploadServeController$2.class
com\wuhansaga\server\mapper\SysUserMapper.class
+com\wuhansaga\server\storage\UploadStorage$StoredObject.class
com\wuhansaga\server\mapper\AboutMapper.class
+com\wuhansaga\server\storage\LocalUploadStorage.class
com\wuhansaga\server\controller\admin\AdminProductCategoryController.class
com\wuhansaga\server\controller\portal\PortalProductController.class
com\wuhansaga\server\common\GlobalExceptionHandler.class
@@ -48,6 +54,7 @@ com\wuhansaga\server\controller\admin\AdminCompanyController.class
com\wuhansaga\server\entity\SysUser.class
com\wuhansaga\server\service\AboutService.class
com\wuhansaga\server\mapper\CaseStudyMapper.class
+com\wuhansaga\server\storage\UploadStorage.class
com\wuhansaga\server\config\WebMvcConfig.class
com\wuhansaga\server\entity\News.class
com\wuhansaga\server\common\PageResult.class
@@ -79,6 +86,7 @@ com\wuhansaga\server\controller\admin\AdminWorkshopController.class
com\wuhansaga\server\mapper\CaseCategoryMapper.class
com\wuhansaga\server\entity\Banner.class
com\wuhansaga\server\entity\ProductCategory.class
+com\wuhansaga\server\storage\MinioUploadStorage.class
com\wuhansaga\server\entity\Contact.class
com\wuhansaga\server\config\SaTokenConfig.class
com\wuhansaga\server\entity\SparePart.class
diff --git a/server/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/server/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
index db7fbce4..d75e8b1c 100644
--- a/server/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
+++ b/server/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -3,6 +3,7 @@ D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\common\PageQuery.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\common\PageResult.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\common\R.java
+D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\config\MinioProperties.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\config\OpenApiConfig.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\config\PortalSiteProperties.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\config\PortalSiteResolver.java
@@ -31,6 +32,7 @@ D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\controller\portal\PortalProductController.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\controller\portal\PortalTechnologyController.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\controller\portal\PortalWorkshopController.java
+D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\controller\portal\UploadServeController.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\entity\About.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\entity\Banner.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\entity\CaseCategory.java
@@ -83,4 +85,7 @@ D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\service\SingleEquipmentService.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\service\SparePartService.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\service\WorkshopService.java
+D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\storage\LocalUploadStorage.java
+D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\storage\MinioUploadStorage.java
+D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\storage\UploadStorage.java
D:\DeXun_workspace\projects\wuhan-saga-official-website\server\src\main\java\com\wuhansaga\server\WuhanSagaApplication.java