org.springframework.boot
diff --git a/src/main/java/com/gxwebsoft/common/core/config/ConfigProperties.java b/src/main/java/com/gxwebsoft/common/core/config/ConfigProperties.java
index 29b2096..a887d0c 100644
--- a/src/main/java/com/gxwebsoft/common/core/config/ConfigProperties.java
+++ b/src/main/java/com/gxwebsoft/common/core/config/ConfigProperties.java
@@ -84,4 +84,39 @@ public class ConfigProperties {
// 网关地址
private String serverUrl;
+ // 文件存储(本地磁盘/对象存储)
+ private Storage storage = new Storage();
+
+ /**
+ * 对象存储配置。type=local 时行为与改造前完全一致,可随时回滚。
+ */
+ @Data
+ public static class Storage {
+ /**
+ * local | s3
+ */
+ private String type = "local";
+
+ /**
+ * S3 兼容端点,如 http://127.0.0.1:9000(RustFS/MinIO)
+ */
+ private String endpoint;
+
+ private String accessKey;
+
+ private String secretKey;
+
+ /**
+ * 存储桶,不存在时启动首次写入会自动创建
+ */
+ private String bucket = "tuanwei";
+
+ private String region = "us-east-1";
+
+ /**
+ * 对象存储文件的本地物化缓存目录,默认 uploadPath + filecache/
+ */
+ private String cachePath;
+ }
+
}
diff --git a/src/main/java/com/gxwebsoft/common/core/storage/FileStorageService.java b/src/main/java/com/gxwebsoft/common/core/storage/FileStorageService.java
new file mode 100644
index 0000000..92625cd
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/storage/FileStorageService.java
@@ -0,0 +1,316 @@
+package com.gxwebsoft.common.core.storage;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.StrUtil;
+import com.gxwebsoft.common.core.config.ConfigProperties;
+import com.gxwebsoft.common.core.utils.FileServerUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Base64;
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * 文件存取服务:上传、读取、删除的唯一入口。
+ *
+ * 启用对象存储时文件只存在于对象存储里(本地缓存目录仅为读取时的物化副本,可随时清空);
+ * 未启用时退回本地磁盘,仅供没有对象存储的开发环境使用。
+ * 键与磁盘相对路径对齐:原图 {@code file/20261021/xxx.jpg},
+ * 缩略图 {@code file/thumbnail/20261021/xxx.jpg}。
+ *
+ * @author WebSoft
+ */
+@Service
+public class FileStorageService {
+
+ private static final Logger log = LoggerFactory.getLogger(FileStorageService.class);
+
+ /**
+ * 对象存储中文件前缀,与磁盘上的 file/ 目录对应
+ */
+ private static final String KEY_PREFIX = "file/";
+
+ @Resource
+ private ConfigProperties config;
+
+ @Resource
+ private ObjectStorage objectStorage;
+
+ /**
+ * 是否启用了对象存储
+ */
+ public boolean isRemote() {
+ return objectStorage.enabled();
+ }
+
+ /**
+ * 本地文件目录:未启用对象存储时作为存储位置,启用后只用于临时文件
+ */
+ public File getUploadDir() {
+ return new File(config.getUploadPath(), "file");
+ }
+
+ /**
+ * 对象存储物化缓存目录:对象存储里的文件首次被读取时落在这里,后续直接命中本地。
+ * 该目录可随时清空,不承载唯一副本。
+ */
+ public File getCacheDir() {
+ String path = config.getStorage() == null ? null : config.getStorage().getCachePath();
+ if (StrUtil.isBlank(path)) {
+ path = config.getUploadPath() + "filecache/";
+ }
+ return new File(path);
+ }
+
+ // ------------------------------------------------------------------ 写入
+
+ /**
+ * 保存上传文件
+ */
+ public StoredFile store(MultipartFile file) throws IOException {
+ String originalName = file.getOriginalFilename();
+ String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date());
+ String suffix = suffixOf(originalName);
+ boolean uuidName = isUuidName();
+ String name = uuidName ? UUID.randomUUID().toString().replaceAll("-", "") + suffix : originalName;
+
+ if (!isRemote()) {
+ File out = FileServerUtil.upload(file, getUploadDir().getAbsolutePath() + "/", uuidName);
+ String path = "/" + dateDir + "/" + out.getName();
+ return new StoredFile(path, out.length(), FileServerUtil.getContentType(out),
+ StrUtil.isBlank(originalName) ? out.getName() : originalName);
+ }
+
+ String path = "/" + dateDir + "/" + name;
+ if (!uuidName) {
+ // 对象存储没有文件名冲突,这里手动避让,保持与原逻辑一致
+ String prefix = StrUtil.removeSuffix(originalName, suffix);
+ int sameSize = 2;
+ while (existsInStore(path)) {
+ path = "/" + dateDir + "/" + prefix + "(" + sameSize + ")" + suffix;
+ sameSize++;
+ }
+ }
+ File tmp = new File(new File(getCacheDir(), "tmp/" + dateDir), StrUtil.subAfter(path, "/", true));
+ FileUtil.mkParentDirs(tmp);
+ file.transferTo(tmp);
+ try {
+ String contentType = FileServerUtil.getContentType(tmp);
+ putObject(KEY_PREFIX + normalize(path), tmp, contentType);
+ return new StoredFile(path, tmp.length(), contentType,
+ StrUtil.isBlank(originalName) ? tmp.getName() : originalName);
+ } finally {
+ FileUtil.del(tmp);
+ }
+ }
+
+ /**
+ * 保存 base64 图片
+ */
+ public StoredFile storeBase64(String base64, String fileName) throws IOException {
+ if (StrUtil.isBlank(base64) || !base64.startsWith("data:image/") || !base64.contains(";base64,")) {
+ throw new IllegalArgumentException("base64 data error");
+ }
+ boolean uuidName = StrUtil.isBlank(fileName);
+ if (!isRemote()) {
+ File out = FileServerUtil.upload(base64, fileName, getUploadDir().getAbsolutePath() + "/");
+ String path = "/" + out.getParentFile().getName() + "/" + out.getName();
+ return new StoredFile(path, out.length(), FileServerUtil.getContentType(out),
+ uuidName ? out.getName() : fileName);
+ }
+
+ String suffix = "." + base64.substring(11, base64.indexOf(";"));
+ String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date());
+ String name = uuidName ? UUID.randomUUID().toString().replaceAll("-", "") + suffix : fileName;
+ String path = "/" + dateDir + "/" + name;
+
+ File tmp = new File(new File(getCacheDir(), "tmp/" + dateDir), name);
+ FileUtil.mkParentDirs(tmp);
+ byte[] bytes = Base64.getDecoder().decode(base64.substring(base64.indexOf(";") + 8));
+ FileUtil.writeBytes(bytes, tmp);
+ try {
+ String contentType = FileServerUtil.getContentType(tmp);
+ putObject(KEY_PREFIX + normalize(path), tmp, contentType);
+ return new StoredFile(path, tmp.length(), contentType, uuidName ? name : fileName);
+ } finally {
+ FileUtil.del(tmp);
+ }
+ }
+
+ // ------------------------------------------------------------------ 读取
+
+ /**
+ * 解析原文件:本地缓存命中直接返回,否则从对象存储拉取并缓存。
+ *
+ * @param path 相对路径,形如 {@code /20261021/xxx.jpg} 或 {@code 20261021/xxx.jpg}
+ * @return 本地可读文件,不存在返回 null
+ */
+ public File resolve(String path) {
+ String rel = normalize(path);
+ if (rel == null) {
+ return null;
+ }
+ if (!isRemote()) {
+ File local = new File(getUploadDir(), rel);
+ return local.isFile() ? local : null;
+ }
+ File cached = new File(getCacheDir(), rel);
+ if (cached.isFile()) {
+ return cached;
+ }
+ return materialize(KEY_PREFIX + rel, cached);
+ }
+
+ /**
+ * 解析已存在的缩略图,不做生成
+ */
+ public File resolveThumbnail(String path) {
+ String rel = normalize(path);
+ if (rel == null) {
+ return null;
+ }
+ if (!isRemote()) {
+ File local = new File(new File(getUploadDir(), "thumbnail"), rel);
+ return local.isFile() ? local : null;
+ }
+ File cached = new File(getCacheDir(), "thumbnail/" + rel);
+ if (cached.isFile()) {
+ return cached;
+ }
+ return materialize(KEY_PREFIX + "thumbnail/" + rel, cached);
+ }
+
+ /**
+ * 缩略图生成目标位置(本地缓存目录),生成后由 {@link #publishThumbnail} 回传对象存储
+ */
+ public File thumbnailTarget(String path) {
+ String rel = normalize(path);
+ if (rel == null) {
+ return null;
+ }
+ File target = new File(getCacheDir(), "thumbnail/" + rel);
+ FileUtil.mkParentDirs(target);
+ return target;
+ }
+
+ /**
+ * 缩略图在本地生成成功后回传对象存储;非图片或未生成时静默跳过
+ */
+ public void publishThumbnail(String path, File thumbnail) {
+ if (!isRemote() || thumbnail == null || !thumbnail.isFile()) {
+ return;
+ }
+ String rel = normalize(path);
+ if (rel == null) {
+ return;
+ }
+ try {
+ putObject(KEY_PREFIX + "thumbnail/" + rel, thumbnail, FileServerUtil.getContentType(thumbnail));
+ } catch (Exception e) {
+ log.warn("[storage] 缩略图回传失败 {}: {}", rel, e.toString());
+ }
+ }
+
+ // ------------------------------------------------------------------ 删除
+
+ /**
+ * 删除一个文件的全部副本:原图、缩略图、本地缓存
+ */
+ public void deleteAll(String path) {
+ String rel = normalize(path);
+ if (rel == null) {
+ return;
+ }
+ FileUtil.del(new File(getCacheDir(), rel));
+ FileUtil.del(new File(getCacheDir(), "thumbnail/" + rel));
+ if (isRemote()) {
+ objectStorage.delete(KEY_PREFIX + rel);
+ objectStorage.delete(KEY_PREFIX + "thumbnail/" + rel);
+ } else {
+ FileUtil.del(new File(getUploadDir(), rel));
+ FileUtil.del(new File(new File(getUploadDir(), "thumbnail"), rel));
+ }
+ }
+
+ // ------------------------------------------------------------------ 内部工具
+
+ private void putObject(String key, File file, String contentType) {
+ try (InputStream in = new FileInputStream(file)) {
+ objectStorage.put(key, in, file.length(), contentType);
+ } catch (IOException e) {
+ throw new StorageException("读取待上传文件失败: " + file.getAbsolutePath(), e);
+ }
+ }
+
+ /**
+ * 从对象存储拉取到本地缓存
+ */
+ private File materialize(String key, File target) {
+ if (!objectStorage.exists(key)) {
+ return null;
+ }
+ File tmp = new File(target.getParentFile(), target.getName() + ".part");
+ try (InputStream in = objectStorage.get(key)) {
+ if (in == null) {
+ return null;
+ }
+ FileUtil.mkParentDirs(target);
+ try (OutputStream out = new FileOutputStream(tmp)) {
+ IoUtil.copy(in, out);
+ }
+ if (target.exists() && !target.delete()) {
+ log.warn("[storage] 无法覆盖缓存文件 {}", target);
+ }
+ if (!tmp.renameTo(target)) {
+ log.warn("[storage] 缓存文件重命名失败 {}", target);
+ return tmp.isFile() ? tmp : null;
+ }
+ return target.isFile() ? target : null;
+ } catch (Exception e) {
+ log.warn("[storage] 物化对象失败 {}: {}", key, e.toString());
+ FileUtil.del(tmp);
+ return null;
+ }
+ }
+
+ private boolean existsInStore(String path) {
+ String rel = normalize(path);
+ return rel != null && objectStorage.exists(KEY_PREFIX + rel);
+ }
+
+ private String suffixOf(String name) {
+ return (name == null || !name.contains(".")) ? "" : name.substring(name.lastIndexOf("."));
+ }
+
+ private boolean isUuidName() {
+ return !Boolean.FALSE.equals(config.getUploadUuidName());
+ }
+
+ /**
+ * 归一化为不带前导斜杠的相对路径,并挡掉目录穿越
+ */
+ private String normalize(String path) {
+ if (StrUtil.isBlank(path)) {
+ return null;
+ }
+ String rel = path.replace("\\", "/");
+ rel = StrUtil.removePrefix(rel, "/");
+ if (rel.contains("..")) {
+ log.warn("[storage] 非法路径已拒绝: {}", path);
+ return null;
+ }
+ return rel;
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/storage/NoopObjectStorage.java b/src/main/java/com/gxwebsoft/common/core/storage/NoopObjectStorage.java
new file mode 100644
index 0000000..5005424
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/storage/NoopObjectStorage.java
@@ -0,0 +1,46 @@
+package com.gxwebsoft.common.core.storage;
+
+import java.io.InputStream;
+
+/**
+ * 未启用对象存储时的占位实现(纯本地磁盘模式)
+ *
+ * @author WebSoft
+ */
+public class NoopObjectStorage implements ObjectStorage {
+
+ public static final NoopObjectStorage INSTANCE = new NoopObjectStorage();
+
+ private NoopObjectStorage() {
+ }
+
+ @Override
+ public boolean enabled() {
+ return false;
+ }
+
+ @Override
+ public void put(String key, InputStream in, long size, String contentType) {
+ throw new StorageException("对象存储未启用, 无法写入: " + key);
+ }
+
+ @Override
+ public InputStream get(String key) {
+ return null;
+ }
+
+ @Override
+ public boolean exists(String key) {
+ return false;
+ }
+
+ @Override
+ public void delete(String key) {
+ // 本地模式无需处理
+ }
+
+ @Override
+ public String describe() {
+ return "local";
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/storage/ObjectStorage.java b/src/main/java/com/gxwebsoft/common/core/storage/ObjectStorage.java
new file mode 100644
index 0000000..5fb6c13
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/storage/ObjectStorage.java
@@ -0,0 +1,50 @@
+package com.gxwebsoft.common.core.storage;
+
+import java.io.InputStream;
+
+/**
+ * 对象存储抽象。
+ *
+ * 只有 {@code config.storage.type=s3} 时才会拿到 {@link S3ObjectStorage} 实现,
+ * 其余情况注入的是 {@link NoopObjectStorage},此时 {@link #enabled()} 返回 false,
+ * 调用方应回退到本地磁盘(仅用于没有对象存储的开发环境)。
+ *
+ * @author WebSoft
+ */
+public interface ObjectStorage {
+
+ /**
+ * 是否真正启用了对象存储
+ */
+ boolean enabled();
+
+ /**
+ * 上传对象
+ *
+ * @param key 对象键,如 {@code file/20261021/xxx.jpg}
+ * @param in 输入流
+ * @param size 字节数,未知传 -1
+ * @param contentType 内容类型,可为空
+ */
+ void put(String key, InputStream in, long size, String contentType);
+
+ /**
+ * 读取对象,不存在返回 null
+ */
+ InputStream get(String key);
+
+ /**
+ * 对象是否存在
+ */
+ boolean exists(String key);
+
+ /**
+ * 删除对象,不存在视为成功
+ */
+ void delete(String key);
+
+ /**
+ * 描述信息,用于启动日志
+ */
+ String describe();
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/storage/S3ObjectStorage.java b/src/main/java/com/gxwebsoft/common/core/storage/S3ObjectStorage.java
new file mode 100644
index 0000000..a2c13c5
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/storage/S3ObjectStorage.java
@@ -0,0 +1,135 @@
+package com.gxwebsoft.common.core.storage;
+
+import cn.hutool.core.util.StrUtil;
+import io.minio.BucketExistsArgs;
+import io.minio.GetObjectArgs;
+import io.minio.MakeBucketArgs;
+import io.minio.MinioClient;
+import io.minio.PutObjectArgs;
+import io.minio.RemoveObjectArgs;
+import io.minio.StatObjectArgs;
+import io.minio.errors.ErrorResponseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+
+/**
+ * 基于 S3 协议的对象存储实现,兼容 MinIO / RustFS。
+ *
+ * @author WebSoft
+ */
+public class S3ObjectStorage implements ObjectStorage {
+
+ private static final Logger log = LoggerFactory.getLogger(S3ObjectStorage.class);
+
+ private final MinioClient client;
+ private final String endpoint;
+ private final String bucket;
+ private final String region;
+
+ /**
+ * 桶是否已确认存在,首次写入时懒检查,避免对象存储比后端启动晚时启动失败
+ */
+ private volatile boolean bucketReady = false;
+
+ public S3ObjectStorage(String endpoint, String accessKey, String secretKey, String bucket, String region) {
+ if (StrUtil.isBlank(endpoint) || StrUtil.isBlank(accessKey) || StrUtil.isBlank(secretKey) || StrUtil.isBlank(bucket)) {
+ throw new StorageException("对象存储配置不完整: endpoint/access-key/secret-key/bucket 均不能为空");
+ }
+ MinioClient.Builder builder = MinioClient.builder()
+ .endpoint(endpoint.trim())
+ .credentials(accessKey.trim(), secretKey.trim());
+ if (StrUtil.isNotBlank(region)) {
+ builder.region(region.trim());
+ }
+ this.client = builder.build();
+ this.endpoint = endpoint.trim();
+ this.bucket = bucket.trim();
+ this.region = region;
+ }
+
+ @Override
+ public boolean enabled() {
+ return true;
+ }
+
+ @Override
+ public void put(String key, InputStream in, long size, String contentType) {
+ ensureBucket();
+ try {
+ client.putObject(PutObjectArgs.builder()
+ .bucket(bucket)
+ .object(key)
+ .stream(in, size, -1)
+ .contentType(StrUtil.isBlank(contentType) ? "application/octet-stream" : contentType)
+ .build());
+ } catch (Exception e) {
+ throw new StorageException("上传对象失败: " + key + ", " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public InputStream get(String key) {
+ try {
+ return client.getObject(GetObjectArgs.builder().bucket(bucket).object(key).build());
+ } catch (ErrorResponseException e) {
+ // 对象不存在属于正常分支,不打印堆栈
+ return null;
+ } catch (Exception e) {
+ log.warn("[storage] 读取对象失败 {}: {}", key, e.toString());
+ return null;
+ }
+ }
+
+ @Override
+ public boolean exists(String key) {
+ try {
+ client.statObject(StatObjectArgs.builder().bucket(bucket).object(key).build());
+ return true;
+ } catch (ErrorResponseException e) {
+ return false;
+ } catch (Exception e) {
+ log.warn("[storage] 探测对象失败 {}: {}", key, e.toString());
+ return false;
+ }
+ }
+
+ @Override
+ public void delete(String key) {
+ try {
+ client.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(key).build());
+ } catch (Exception e) {
+ log.warn("[storage] 删除对象失败 {}: {}", key, e.toString());
+ }
+ }
+
+ @Override
+ public String describe() {
+ return "s3://" + bucket + "@" + endpoint + (StrUtil.isBlank(region) ? "" : " (" + region + ")");
+ }
+
+ /**
+ * 确认桶存在,不存在则创建;失败时下次写入重试
+ */
+ private void ensureBucket() {
+ if (bucketReady) {
+ return;
+ }
+ synchronized (this) {
+ if (bucketReady) {
+ return;
+ }
+ try {
+ if (!client.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
+ client.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
+ log.info("[storage] 已自动创建桶 {}", bucket);
+ }
+ bucketReady = true;
+ log.info("[storage] 对象存储就绪: {}", describe());
+ } catch (Exception e) {
+ throw new StorageException("对象存储不可用(" + endpoint + "): " + e.getMessage(), e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/storage/StorageConfig.java b/src/main/java/com/gxwebsoft/common/core/storage/StorageConfig.java
new file mode 100644
index 0000000..eacaa8b
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/storage/StorageConfig.java
@@ -0,0 +1,36 @@
+package com.gxwebsoft.common.core.storage;
+
+import com.gxwebsoft.common.core.config.ConfigProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 对象存储装配。{@code config.storage.type=local} 时注入空实现(退回本地磁盘,仅开发环境用)。
+ *
+ * @author WebSoft
+ */
+@Configuration
+public class StorageConfig {
+
+ private static final Logger log = LoggerFactory.getLogger(StorageConfig.class);
+
+ @Bean
+ public ObjectStorage objectStorage(ConfigProperties configProperties) {
+ ConfigProperties.Storage storage = configProperties.getStorage();
+ if (storage == null || !"s3".equalsIgnoreCase(storage.getType())) {
+ log.info("[storage] 使用本地磁盘存储: {}", configProperties.getUploadPath());
+ return NoopObjectStorage.INSTANCE;
+ }
+ try {
+ S3ObjectStorage s3 = new S3ObjectStorage(storage.getEndpoint(), storage.getAccessKey(),
+ storage.getSecretKey(), storage.getBucket(), storage.getRegion());
+ log.info("[storage] 使用对象存储: {}", s3.describe());
+ return s3;
+ } catch (Exception e) {
+ // 配置写了 s3 就不能悄悄退回磁盘:否则文件会写进没人管理的本地目录
+ throw new IllegalStateException("[storage] 对象存储配置错误, 请检查 config.storage.*: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/storage/StorageException.java b/src/main/java/com/gxwebsoft/common/core/storage/StorageException.java
new file mode 100644
index 0000000..a73d812
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/storage/StorageException.java
@@ -0,0 +1,17 @@
+package com.gxwebsoft.common.core.storage;
+
+/**
+ * 对象存储异常
+ *
+ * @author WebSoft
+ */
+public class StorageException extends RuntimeException {
+
+ public StorageException(String message) {
+ super(message);
+ }
+
+ public StorageException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/storage/StoredFile.java b/src/main/java/com/gxwebsoft/common/core/storage/StoredFile.java
new file mode 100644
index 0000000..51c1b3a
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/storage/StoredFile.java
@@ -0,0 +1,36 @@
+package com.gxwebsoft.common.core.storage;
+
+import lombok.Data;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+/**
+ * 一次上传的结果
+ *
+ * @author WebSoft
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class StoredFile {
+
+ /**
+ * 相对路径,形如 {@code /20261021/xxx.jpg},与 sys_file_record.path 一致
+ */
+ private String path;
+
+ /**
+ * 字节数
+ */
+ private long size;
+
+ /**
+ * 内容类型(Tika 识别)
+ */
+ private String contentType;
+
+ /**
+ * 原始文件名
+ */
+ private String originalName;
+}
diff --git a/src/main/java/com/gxwebsoft/common/system/controller/FileController.java b/src/main/java/com/gxwebsoft/common/system/controller/FileController.java
index 828a822..6995edc 100644
--- a/src/main/java/com/gxwebsoft/common/system/controller/FileController.java
+++ b/src/main/java/com/gxwebsoft/common/system/controller/FileController.java
@@ -1,16 +1,15 @@
package com.gxwebsoft.common.system.controller;
import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.config.ConfigProperties;
+import com.gxwebsoft.common.core.storage.FileStorageService;
+import com.gxwebsoft.common.core.storage.StoredFile;
import com.gxwebsoft.common.core.utils.FileServerUtil;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.FileRecord;
-import com.gxwebsoft.common.system.entity.Role;
-import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.param.FileRecordParam;
import com.gxwebsoft.common.system.service.FileRecordService;
import io.swagger.annotations.Api;
@@ -26,7 +25,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -44,6 +43,8 @@ public class FileController extends BaseController {
private ConfigProperties config;
@Resource
private FileRecordService fileRecordService;
+ @Resource
+ private FileStorageService fileStorageService;
@OperationLog
@ApiOperation("上传文件")
@@ -51,25 +52,8 @@ public class FileController extends BaseController {
public ApiResult upload(@RequestParam MultipartFile file, HttpServletRequest request) {
FileRecord result = null;
try {
- String dir = getUploadDir();
- File upload = FileServerUtil.upload(file, dir, config.getUploadUuidName());
- String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length() - 1);
-// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/upload");
- String requestURL = config.getFileServer() + "/api/file/";
- String originalName = file.getOriginalFilename();
- result = new FileRecord();
- result.setCreateUserId(getLoginUserId());
- result.setName(StrUtil.isBlank(originalName) ? upload.getName() : originalName);
- result.setLength(upload.length());
- result.setPath(path);
- result.setUrl((requestURL + path).replace("file//", "file/"));
- String contentType = FileServerUtil.getContentType(upload);
- result.setContentType(contentType);
- if (FileServerUtil.isImage(contentType)) {
- result.setThumbnail(requestURL + "/thumbnail" + path);
- }
- result.setDownloadUrl(config.getFileServer() + "/download" + path);
-// result.setMerchantCode(getMerchantCode());
+ StoredFile stored = fileStorageService.store(file);
+ result = buildRecord(stored);
fileRecordService.save(result);
return success(result);
} catch (Exception e) {
@@ -89,18 +73,8 @@ public class FileController extends BaseController {
public ApiResult uploadBase64(String base64, String fileName, HttpServletRequest request) {
FileRecord result = null;
try {
- String dir = getUploadDir();
- File upload = FileServerUtil.upload(base64, fileName, getUploadDir());
- String path = upload.getAbsolutePath().substring(dir.length()).replace("\\", "/");
- String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/upload/base64");
- result = new FileRecord();
- result.setCreateUserId(getLoginUserId());
- result.setName(StrUtil.isBlank(fileName) ? upload.getName() : fileName);
- result.setLength(upload.length());
- result.setPath(path);
- result.setUrl(requestURL + path);
- result.setThumbnail(FileServerUtil.isImage(upload) ? (requestURL + "/thumbnail" + path) : null);
-// result.setMerchantCode(getMerchantCode());
+ StoredFile stored = fileStorageService.storeBase64(base64, fileName);
+ result = buildRecord(stored);
fileRecordService.save(result);
return success(result);
} catch (Exception e) {
@@ -113,42 +87,22 @@ public class FileController extends BaseController {
@ApiOperation("上传图片")
@PostMapping("/image")
public HashMap image(@RequestParam MultipartFile file, HttpServletRequest request) {
- FileRecord result = null;
- try {
- String dir = getUploadDir();
- File upload = FileServerUtil.upload(file, dir, config.getUploadUuidName());
- String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length() - 1);
-// System.out.println("request.getRequestURL() = " + request.getRequestURL());
-// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/image");
- String requestURL = config.getFileServer() + "/api/file";
-// System.out.println("requestURL = " + requestURL);
-// config.getServerUrl()
- String originalName = file.getOriginalFilename();
- result = new FileRecord();
- result.setCreateUserId(getLoginUserId());
- result.setName(StrUtil.isBlank(originalName) ? upload.getName() : originalName);
- result.setLength(upload.length());
- result.setPath(path);
- result.setUrl(config.getFileServer() + "/api/file" + path);
- String contentType = FileServerUtil.getContentType(upload);
- result.setContentType(contentType);
- if (FileServerUtil.isImage(contentType)) {
- result.setThumbnail(requestURL + "/thumbnail" + path);
+ FileRecord result = null;
+ try {
+ StoredFile stored = fileStorageService.store(file);
+ result = buildRecord(stored);
+ final HashMap map = new HashMap<>();
+ map.put("name", result.getName());
+ map.put("status", "done");
+ map.put("thumbUrl", result.getThumbnail());
+ map.put("downloadUrl", result.getDownloadUrl());
+ map.put("url", result.getUrl());
+ fileRecordService.save(result);
+ return map;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
}
- result.setDownloadUrl(requestURL + "/download" + path);
-// result.setMerchantCode(getMerchantCode());
- final HashMap map = new HashMap<>();
- map.put("name",result.getName());
- map.put("status","done");
- map.put("thumbUrl",result.getThumbnail());
- map.put("downloadUrl",result.getDownloadUrl());
- map.put("url",result.getUrl());
- fileRecordService.save(result);
- return map;
- } catch (Exception e) {
- e.printStackTrace();
- return null;
- }
}
@PreAuthorize("hasAuthority('sys:file:list')")
@@ -156,14 +110,15 @@ public class FileController extends BaseController {
@ApiOperation("根据id查询文件")
@GetMapping("/{id}")
public ApiResult get(@PathVariable("id") Integer id) {
- return success(fileRecordService.getByIdRel(id));
+ return success(fileRecordService.getByIdRel(id));
}
@ApiOperation("查看原文件")
@GetMapping("/{dir}/{name:.+}")
public void preview(@PathVariable("dir") String dir, @PathVariable("name") String name,
HttpServletResponse response, HttpServletRequest request) {
- File file = new File(getUploadDir(), dir + "/" + name);
+ // 缓存命中直接返回,否则从对象存储拉到本地缓存
+ File file = fileStorageService.resolve(dir + "/" + name);
FileServerUtil.preview(file, getPdfOutDir(), config.getOpenOfficeHome(), response, request);
}
@@ -172,9 +127,9 @@ public class FileController extends BaseController {
public void download(@PathVariable("dir") String dir, @PathVariable("name") String name,
HttpServletResponse response, HttpServletRequest request) {
String path = dir + "/" + name;
- FileRecord record = fileRecordService.getByIdPath(path);
- File file = new File(getUploadDir(), path);
- String fileName = record == null ? file.getName() : record.getName();
+ FileRecord record = fileRecordService.getByIdPath("/" + path);
+ File file = fileStorageService.resolve(path);
+ String fileName = record == null ? (file == null ? name : file.getName()) : record.getName();
FileServerUtil.preview(file, true, fileName, null, null, response, request);
}
@@ -182,9 +137,21 @@ public class FileController extends BaseController {
@GetMapping("/thumbnail/{dir}/{name:.+}")
public void thumbnail(@PathVariable("dir") String dir, @PathVariable("name") String name,
HttpServletResponse response, HttpServletRequest request) {
- File file = new File(getUploadDir(), dir + "/" + name);
- File thumbnail = new File(getUploadSmDir(), dir + "/" + name);
- FileServerUtil.previewThumbnail(file, thumbnail, config.getThumbnailSize(), response, request);
+ String path = dir + "/" + name;
+ File thumbnail = fileStorageService.resolveThumbnail(path);
+ if (thumbnail != null) {
+ FileServerUtil.preview(thumbnail, null, null, response, request);
+ return;
+ }
+ File file = fileStorageService.resolve(path);
+ if (file == null) {
+ FileServerUtil.outNotFund(response);
+ return;
+ }
+ File target = fileStorageService.thumbnailTarget(path);
+ FileServerUtil.previewThumbnail(file, target, config.getThumbnailSize(), response, request);
+ // 本地生成成功后回传对象存储,下次直接命中
+ fileStorageService.publishThumbnail(path, target);
}
@PreAuthorize("hasAuthority('sys:file:remove')")
@@ -194,11 +161,8 @@ public class FileController extends BaseController {
public ApiResult> remove(@PathVariable("id") Integer id) {
FileRecord record = fileRecordService.getById(id);
if (fileRecordService.removeById(id)) {
- if (StrUtil.isNotBlank(record.getPath())) {
- fileRecordService.deleteFileAsync(Arrays.asList(
- new File(getUploadDir(), record.getPath()),
- new File(getUploadSmDir(), record.getPath())
- ));
+ if (record != null && StrUtil.isNotBlank(record.getPath())) {
+ fileRecordService.deleteFileAsync(Collections.singletonList(record.getPath()));
}
return success("删除成功");
}
@@ -215,14 +179,13 @@ public class FileController extends BaseController {
public ApiResult> deleteBatch(@RequestBody List ids) {
List fileRecords = fileRecordService.listByIds(ids);
if (fileRecordService.removeByIds(ids)) {
- List files = new ArrayList<>();
+ List paths = new ArrayList<>();
for (FileRecord record : fileRecords) {
if (StrUtil.isNotBlank(record.getPath())) {
- files.add(new File(getUploadDir(), record.getPath()));
- files.add(new File(getUploadSmDir(), record.getPath()));
+ paths.add(record.getPath());
}
}
- fileRecordService.deleteFileAsync(files);
+ fileRecordService.deleteFileAsync(paths);
return success("删除成功");
}
return fail("删除失败");
@@ -233,13 +196,8 @@ public class FileController extends BaseController {
@ApiOperation("分页查询文件")
@GetMapping("/page")
public ApiResult> page(FileRecordParam param, HttpServletRequest request) {
- // 搜索条件
-// if (getMerchantCode() != null) {
-// param.setMerchantCode(getMerchantCode());
-// }
PageResult result = fileRecordService.pageRel(param);
-// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/page");
- String requestURL = config.getFileServer();
+ String requestURL = fileBaseUrl();
for (FileRecord record : result.getList()) {
if (StrUtil.isNotBlank(record.getPath())) {
record.setUrl(requestURL + record.getPath());
@@ -258,8 +216,7 @@ public class FileController extends BaseController {
@GetMapping("/list")
public ApiResult> list(FileRecordParam param, HttpServletRequest request) {
List records = fileRecordService.listRel(param);
-// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/list");
- String requestURL = config.getFileServer();
+ String requestURL = fileBaseUrl();
for (FileRecord record : records) {
if (StrUtil.isNotBlank(record.getPath())) {
record.setUrl(requestURL + record.getPath());
@@ -273,38 +230,36 @@ public class FileController extends BaseController {
}
/**
- * 文件上传基目录
+ * 组装文件记录,接口地址统一以 config.file-server 为准(历史实现里 downloadUrl 漏了 /api/file 前缀)
*/
- private String getUploadBaseDir() {
- return config.getUploadPath() + "file/";
+ /**
+ * 文件对外访问基址,形如 https://twai-api.gxmu.edu.cn/api/file
+ */
+ private String fileBaseUrl() {
+ return StrUtil.removeSuffix(config.getFileServer(), "/") + "/api/file";
}
- /**
- * 文件上传位置(服务器)
- */
- private String getUploadDir() {
- return config.getUploadPath() + "file/";
- }
-
- /**
- * 文件上传位置(本地)
- */
-// private String getUploadDir() {
-// return "/Users/gxwebsoft/Documents/uploads/";
-// }
-
- /**
- * 缩略图生成位置
- */
- private String getUploadSmDir() {
- return getUploadBaseDir() + "thumbnail/";
+ private FileRecord buildRecord(StoredFile stored) {
+ FileRecord record = new FileRecord();
+ record.setCreateUserId(getLoginUserId());
+ record.setName(stored.getOriginalName());
+ record.setLength(stored.getSize());
+ record.setPath(stored.getPath());
+ String base = fileBaseUrl();
+ record.setUrl(base + stored.getPath());
+ record.setContentType(stored.getContentType());
+ if (FileServerUtil.isImage(stored.getContentType())) {
+ record.setThumbnail(base + "/thumbnail" + stored.getPath());
+ }
+ record.setDownloadUrl(base + "/download" + stored.getPath());
+ return record;
}
/**
* office转pdf输出位置
*/
private String getPdfOutDir() {
- return getUploadBaseDir() + "pdf/";
+ return new File(fileStorageService.getUploadDir(), "pdf").getAbsolutePath() + "/";
}
@PreAuthorize("hasAuthority('sys:file:upload')")
@@ -312,10 +267,10 @@ public class FileController extends BaseController {
@ApiOperation("添加文件")
@PostMapping()
public ApiResult> save(@RequestBody FileRecord fileRecord) {
- if (fileRecordService.save(fileRecord)) {
- return success("上传成功");
- }
- return fail("上传失败");
+ if (fileRecordService.save(fileRecord)) {
+ return success("上传成功");
+ }
+ return fail("上传失败");
}
@PreAuthorize("hasAuthority('sys:file:update')")
@@ -323,10 +278,10 @@ public class FileController extends BaseController {
@ApiOperation("修改文件")
@PutMapping()
public ApiResult> update(@RequestBody FileRecord fileRecord) {
- if (fileRecordService.updateById(fileRecord)) {
- return success("修改成功");
- }
- return fail("修改失败");
+ if (fileRecordService.updateById(fileRecord)) {
+ return success("修改成功");
+ }
+ return fail("修改失败");
}
}
diff --git a/src/main/java/com/gxwebsoft/common/system/service/FileRecordService.java b/src/main/java/com/gxwebsoft/common/system/service/FileRecordService.java
index 3dd09ac..e4b7ca3 100644
--- a/src/main/java/com/gxwebsoft/common/system/service/FileRecordService.java
+++ b/src/main/java/com/gxwebsoft/common/system/service/FileRecordService.java
@@ -5,7 +5,6 @@ import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.FileRecord;
import com.gxwebsoft.common.system.param.FileRecordParam;
-import java.io.File;
import java.util.List;
/**
@@ -49,10 +48,10 @@ public interface FileRecordService extends IService {
FileRecord getByIdPath(String path);
/**
- * 异步删除文件
+ * 异步删除文件(对象存储 + 本地缓存,按相对路径)
*
- * @param files 文件数组
+ * @param paths 文件相对路径数组,形如 /20261021/xxx.jpg
*/
- void deleteFileAsync(List files);
+ void deleteFileAsync(List paths);
}
diff --git a/src/main/java/com/gxwebsoft/common/system/service/impl/FileRecordServiceImpl.java b/src/main/java/com/gxwebsoft/common/system/service/impl/FileRecordServiceImpl.java
index 72c9c27..a46dbe6 100644
--- a/src/main/java/com/gxwebsoft/common/system/service/impl/FileRecordServiceImpl.java
+++ b/src/main/java/com/gxwebsoft/common/system/service/impl/FileRecordServiceImpl.java
@@ -1,6 +1,7 @@
package com.gxwebsoft.common.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.gxwebsoft.common.core.storage.FileStorageService;
import com.gxwebsoft.common.core.utils.CommonUtil;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
@@ -11,7 +12,7 @@ import com.gxwebsoft.common.system.service.FileRecordService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
-import java.io.File;
+import javax.annotation.Resource;
import java.util.List;
/**
@@ -23,6 +24,9 @@ import java.util.List;
@Service
public class FileRecordServiceImpl extends ServiceImpl implements FileRecordService {
+ @Resource
+ private FileStorageService fileStorageService;
+
@Override
public PageResult pageRel(FileRecordParam param) {
PageParam page = new PageParam<>(param);
@@ -51,10 +55,11 @@ public class FileRecordServiceImpl extends ServiceImpl files) {
- for (File file : files) {
+ public void deleteFileAsync(List paths) {
+ for (String path : paths) {
try {
- file.delete();
+ // 对象存储原图/缩略图 + 本地缓存,一次清干净
+ fileStorageService.deleteAll(path);
} catch (Exception e) {
e.printStackTrace();
}
diff --git a/src/main/java/com/gxwebsoft/gxmu/service/impl/TzbcyMaterialServiceImpl.java b/src/main/java/com/gxwebsoft/gxmu/service/impl/TzbcyMaterialServiceImpl.java
index e647518..7467a14 100644
--- a/src/main/java/com/gxwebsoft/gxmu/service/impl/TzbcyMaterialServiceImpl.java
+++ b/src/main/java/com/gxwebsoft/gxmu/service/impl/TzbcyMaterialServiceImpl.java
@@ -238,7 +238,7 @@ public class TzbcyMaterialServiceImpl extends ServiceImpl get(@PathVariable("id") Integer id) {
- // 验证签名
- isCheckSign();
- return success(fileRecordService.getByIdRel(id));
+ // 验证签名
+ isCheckSign();
+ return success(fileRecordService.getByIdRel(id));
}
@ApiOperation("查看原文件")
@@ -121,7 +102,7 @@ public class OpenFileController extends BaseController {
HttpServletResponse response, HttpServletRequest request) {
// 验证签名
isCheckSign();
- File file = new File(getUploadDir(), dir + "/" + name);
+ File file = fileStorageService.resolve(dir + "/" + name);
FileServerUtil.preview(file, getPdfOutDir(), config.getOpenOfficeHome(), response, request);
}
@@ -132,9 +113,9 @@ public class OpenFileController extends BaseController {
// 验证签名
isCheckSign();
String path = dir + "/" + name;
- FileRecord record = fileRecordService.getByIdPath(path);
- File file = new File(getUploadDir(), path);
- String fileName = record == null ? file.getName() : record.getName();
+ FileRecord record = fileRecordService.getByIdPath("/" + path);
+ File file = fileStorageService.resolve(path);
+ String fileName = record == null ? (file == null ? name : file.getName()) : record.getName();
FileServerUtil.preview(file, true, fileName, null, null, response, request);
}
@@ -144,9 +125,20 @@ public class OpenFileController extends BaseController {
HttpServletResponse response, HttpServletRequest request) {
// 验证签名
isCheckSign();
- File file = new File(getUploadDir(), dir + "/" + name);
- File thumbnail = new File(getUploadSmDir(), dir + "/" + name);
- FileServerUtil.previewThumbnail(file, thumbnail, config.getThumbnailSize(), response, request);
+ String path = dir + "/" + name;
+ File thumbnail = fileStorageService.resolveThumbnail(path);
+ if (thumbnail != null) {
+ FileServerUtil.preview(thumbnail, null, null, response, request);
+ return;
+ }
+ File file = fileStorageService.resolve(path);
+ if (file == null) {
+ FileServerUtil.outNotFund(response);
+ return;
+ }
+ File target = fileStorageService.thumbnailTarget(path);
+ FileServerUtil.previewThumbnail(file, target, config.getThumbnailSize(), response, request);
+ fileStorageService.publishThumbnail(path, target);
}
@OperationLog
@@ -157,11 +149,8 @@ public class OpenFileController extends BaseController {
isCheckSign();
FileRecord record = fileRecordService.getById(id);
if (fileRecordService.removeById(id)) {
- if (StrUtil.isNotBlank(record.getPath())) {
- fileRecordService.deleteFileAsync(Arrays.asList(
- new File(getUploadDir(), record.getPath()),
- new File(getUploadSmDir(), record.getPath())
- ));
+ if (record != null && StrUtil.isNotBlank(record.getPath())) {
+ fileRecordService.deleteFileAsync(Collections.singletonList(record.getPath()));
}
return success("删除成功");
}
@@ -179,14 +168,13 @@ public class OpenFileController extends BaseController {
isCheckSign();
List fileRecords = fileRecordService.listByIds(ids);
if (fileRecordService.removeByIds(ids)) {
- List files = new ArrayList<>();
+ List paths = new ArrayList<>();
for (FileRecord record : fileRecords) {
if (StrUtil.isNotBlank(record.getPath())) {
- files.add(new File(getUploadDir(), record.getPath()));
- files.add(new File(getUploadSmDir(), record.getPath()));
+ paths.add(record.getPath());
}
}
- fileRecordService.deleteFileAsync(files);
+ fileRecordService.deleteFileAsync(paths);
return success("删除成功");
}
return fail("删除失败");
@@ -198,13 +186,8 @@ public class OpenFileController extends BaseController {
public ApiResult> page(FileRecordParam param, HttpServletRequest request) {
// 验证签名
isCheckSign();
- // 搜索条件
-// if (getMerchantCode() != null) {
-// param.setMerchantCode(getMerchantCode());
-// }
PageResult result = fileRecordService.pageRel(param);
-// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/page");
- String requestURL = config.getFileServer();
+ String requestURL = openFileBaseUrl();
for (FileRecord record : result.getList()) {
if (StrUtil.isNotBlank(record.getPath())) {
record.setUrl(requestURL + record.getPath());
@@ -224,8 +207,7 @@ public class OpenFileController extends BaseController {
// 验证签名
isCheckSign();
List records = fileRecordService.listRel(param);
-// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/list");
- String requestURL = config.getFileServer();
+ String requestURL = openFileBaseUrl();
for (FileRecord record : records) {
if (StrUtil.isNotBlank(record.getPath())) {
record.setUrl(requestURL + record.getPath());
@@ -239,63 +221,59 @@ public class OpenFileController extends BaseController {
}
/**
- * 文件上传基目录
+ * 开放接口文件外链基址,形如 https://twai-api.gxmu.edu.cn/api/open/file
*/
- private String getUploadBaseDir() {
- return File.listRoots()[config.getUploadLocation()].getAbsolutePath()
- .replace("\\", "/") + "/upload/";
+ private String openFileBaseUrl() {
+ return StrUtil.removeSuffix(config.getFileServer(), "/") + "/api/open/file";
}
/**
- * 文件上传位置(服务器)
+ * 组装文件记录
*/
- private String getUploadDir() {
- return config.getUploadPath() + "file/";
- }
-
- /**
- * 文件上传位置(本地)
- */
-// private String getUploadDir() {
-// return "/Users/gxwebsoft/Documents/uploads/";
-// }
-
- /**
- * 缩略图生成位置
- */
- private String getUploadSmDir() {
- return getUploadBaseDir() + "thumbnail/";
+ private FileRecord buildRecord(StoredFile stored, String base) {
+ FileRecord record = new FileRecord();
+ record.setCreateUserId(getLoginUserId());
+ record.setName(stored.getOriginalName());
+ record.setLength(stored.getSize());
+ record.setPath(stored.getPath());
+ record.setUrl(base + stored.getPath());
+ record.setContentType(stored.getContentType());
+ if (FileServerUtil.isImage(stored.getContentType())) {
+ record.setThumbnail(base + "/thumbnail" + stored.getPath());
+ }
+ record.setDownloadUrl(base + "/download" + stored.getPath());
+ return record;
}
/**
* office转pdf输出位置
*/
private String getPdfOutDir() {
- return getUploadBaseDir() + "pdf/";
+ return new File(fileStorageService.getUploadDir(), "pdf").getAbsolutePath() + "/";
}
@OperationLog
@ApiOperation("添加文件")
@PostMapping()
public ApiResult> save(@RequestBody FileRecord fileRecord) {
- // 验证签名
- isCheckSign();
- if (fileRecordService.save(fileRecord)) {
- return success("上传成功");
- }
- return fail("上传失败");
+ // 验证签名
+ isCheckSign();
+ if (fileRecordService.save(fileRecord)) {
+ return success("上传成功");
+ }
+ return fail("上传失败");
}
@OperationLog
@ApiOperation("修改文件")
@PutMapping()
public ApiResult> update(@RequestBody FileRecord fileRecord) {
- // 验证签名
- isCheckSign();
- if (fileRecordService.updateById(fileRecord)) {
- return success("修改成功");
- }
- return fail("修改失败");
+ // 验证签名
+ isCheckSign();
+ if (fileRecordService.updateById(fileRecord)) {
+ return success("修改成功");
+ }
+ return fail("修改失败");
}
}
diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml
index df9cb6a..5d99cc8 100644
--- a/src/main/resources/application-prod.yml
+++ b/src/main/resources/application-prod.yml
@@ -33,9 +33,27 @@ logging:
# 框架配置
config:
# 开发环境接口
- server-url: https://tuanwei-api.gxwebsoft.com/api
+ # 对外访问域名,前端(vite VITE_API_URL / uniapp API_BASE_URL)用的就是这个
+ server-url: https://twai-api.gxmu.edu.cn/api
upload-path: /www/wwwroot/tuanwei/
- file-server: https://tuanwei-api.gxwebsoft.com
+ # 文件外链前缀:数据库里回给前端的 url = 这里 + /api/file + 相对路径
+ file-server: https://twai-api.gxmu.edu.cn
+ # 文件存储:local(本地磁盘,改造前行为)| s3(对象存储)
+ # 生产指向 1Panel 里的 RustFS,端口只发布到宿主机 127.0.0.1,不对外开。
+ # 改造前的历史图片一律作废、不做兼容读取;type 改回 local 即退回本地磁盘。
+ # 后端本身是容器(twai-api),容器内的 127.0.0.1 是它自己,连不到 RustFS,
+ # 必须走 1panel-network 的容器地址。默认写容器名,Docker 内建 DNS 按名解析,
+ # RustFS 重建换 IP 也不受影响;万一容器名解析不通,用环境变量换成网络内 IP:
+ # RUSTFS_ENDPOINT=http://172.18.0.6:9000
+ storage:
+ type: s3
+ endpoint: ${RUSTFS_ENDPOINT:http://1Panel-rustfs-YrcF:9000}
+ access-key: ${RUSTFS_ACCESS_KEY:rustfsadmin}
+ secret-key: ${RUSTFS_SECRET_KEY:rustfsadmin_Qy6wjs}
+ bucket: tuanwei
+ region: us-east-1
+ # 对象存储文件首次被读取时物化到这里,可随时清空
+ cache-path: /www/wwwroot/tuanwei/filecache/
wxpay:
private-key-path: /www/wwwroot/tuanwei/cert/apiclient_key.pem