feat(storage): 添加MinIO对象存储支持

实现基于MinIO的对象存储功能,包括:
1. 新增MinioProperties配置类
2. 创建UploadStorage接口及Minio/Local实现
3. 重构MediaLibraryService使用统一存储接口
4. 添加MinIO模式下文件服务控制器
5. 提供docker-compose.minio.yml部署配置
6. 更新文档说明MinIO集成方式

支持本地存储和MinIO对象存储两种模式,可通过upload.storage配置切换
This commit is contained in:
2026-05-07 14:52:06 +08:00
parent 687cf0ba07
commit 4d347fc108
51 changed files with 422 additions and 34 deletions

View File

@@ -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
| 文件 | 用途 |

19
docker-compose.minio.yml Normal file
View File

@@ -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:

View File

@@ -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
# endpointSpring 所在机器能访问的 MinIO API9000。同机 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

View File

@@ -84,6 +84,12 @@
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.13</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>

View File

@@ -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) {

View File

@@ -0,0 +1,24 @@
package com.wuhansaga.server.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* MinIOS3 兼容)接入;仅在 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";
}
}
}

View File

@@ -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);

View File

@@ -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<org.springframework.core.io.InputStreamResource> serve(HttpServletRequest request)
throws IOException {
String rel = requestUriWithoutContext(request);
if (!rel.startsWith("/uploads/")) {
return ResponseEntity.notFound().build();
}
Optional<UploadStorage.StoredObject> 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;
}
}

View File

@@ -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<MediaLibrary> 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);

View File

@@ -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<StoredObject> 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));
}
}

View File

@@ -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<StoredObject> 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);
}
}
}

View File

@@ -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<StoredObject> open(String relativePath) throws IOException;
record StoredObject(InputStream inputStream, long size, String contentType) {}
}

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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