feat(file): 文件存储接入 RustFS(S3),修正文件外链域名与路径

文件上传从本地磁盘改为可切换的对象存储(默认 s3,type 改回 local 即退回原行为):

- 新增 common.core.storage 包:ObjectStorage 接口 + S3ObjectStorage(minio 8.5.17)
  + NoopObjectStorage + FileStorageService + StorageConfig + StoredFile;
  key 规则沿用 file/yyyyMMdd/uuid.ext、缩略图 file/thumbnail/...,桶内结构对齐旧磁盘目录
- 读取仍走后端代理 GET /api/file/**:s3 模式下首次读取物化到本地 cache-path 再返回,
  对外 URL 不变;历史本地文件一律作废,不做回退兼容
- ConfigProperties 增加 storage 配置块,endpoint/access-key/secret-key 支持环境变量覆盖
  (RUSTFS_ENDPOINT / RUSTFS_ACCESS_KEY / RUSTFS_SECRET_KEY),配置非法时启动即失败
- FileRecordService.deleteFileAsync 改为按 path 删除,本地与对象存储同时清理
- pom 固定 okhttp3.version=4.12.0、kotlin.version=1.9.25:Spring Boot 2.5.4 默认的
  okhttp 3.14.9 与 minio 所需的 okhttp 4.12.0 冲突,会导致启动即
  NoClassDefFoundError: okhttp3/ConnectionPool

同时修正文件外链拼接(以下均为改造前就存在的问题):

- application-prod.yml:server-url / file-server 由失效的 tuanwei-api.gxwebsoft.com
  改为生产实际域名 https://twai-api.gxmu.edu.cn(与 admin .env.production、
  uniapp request.js 一致),此前上传返回的 url 指向旧域名导致访问不到
- FileController 抽出 fileBaseUrl(),分页/列表原来漏了 /api/file 前缀
- OpenFileController 同样补 /api/open/file 前缀
- TzbcyMaterialServiceImpl 下载链接补 /api/file/download
This commit is contained in:
2026-10-21 23:11:45 +08:00
parent bf6813e71d
commit f5a89634c4
15 changed files with 867 additions and 229 deletions
+12
View File
@@ -21,6 +21,11 @@
<java.version>1.8</java.version> <java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Spring Boot 2.5.4 默认把 okhttp 钉在 3.14.9,而 minio 8.5.x 需要 4.x,
否则启动时 io.minio.S3Base 会报 RequestBody.create 方法不存在。
okhttp 4.x 依赖 kotlin-stdlib,故一并覆盖 Spring Boot 管理的 kotlin 版本。 -->
<okhttp3.version>4.12.0</okhttp3.version>
<kotlin.version>1.9.25</kotlin.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -174,6 +179,13 @@
<version>2.1.0</version> <version>2.1.0</version>
</dependency> </dependency>
<!-- minio sdk, 兼容 S3 协议对象存储(RustFS/MinIO) -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.17</version>
</dependency>
<!-- spring-boot-mail --> <!-- spring-boot-mail -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@@ -84,4 +84,39 @@ public class ConfigProperties {
// 网关地址 // 网关地址
private String serverUrl; 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;
}
} }
@@ -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;
/**
* 文件存取服务:上传、读取、删除的唯一入口。
* <p>
* 启用对象存储时文件只存在于对象存储里(本地缓存目录仅为读取时的物化副本,可随时清空);
* 未启用时退回本地磁盘,仅供没有对象存储的开发环境使用。
* 键与磁盘相对路径对齐:原图 {@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;
}
}
@@ -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";
}
}
@@ -0,0 +1,50 @@
package com.gxwebsoft.common.core.storage;
import java.io.InputStream;
/**
* 对象存储抽象。
* <p>
* 只有 {@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();
}
@@ -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);
}
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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;
}
@@ -1,16 +1,15 @@
package com.gxwebsoft.common.system.controller; package com.gxwebsoft.common.system.controller;
import cn.hutool.core.util.StrUtil; 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.annotation.OperationLog;
import com.gxwebsoft.common.core.config.ConfigProperties; 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.utils.FileServerUtil;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.PageResult; import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.FileRecord; 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.param.FileRecordParam;
import com.gxwebsoft.common.system.service.FileRecordService; import com.gxwebsoft.common.system.service.FileRecordService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
@@ -26,7 +25,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -44,6 +43,8 @@ public class FileController extends BaseController {
private ConfigProperties config; private ConfigProperties config;
@Resource @Resource
private FileRecordService fileRecordService; private FileRecordService fileRecordService;
@Resource
private FileStorageService fileStorageService;
@OperationLog @OperationLog
@ApiOperation("上传文件") @ApiOperation("上传文件")
@@ -51,25 +52,8 @@ public class FileController extends BaseController {
public ApiResult<FileRecord> upload(@RequestParam MultipartFile file, HttpServletRequest request) { public ApiResult<FileRecord> upload(@RequestParam MultipartFile file, HttpServletRequest request) {
FileRecord result = null; FileRecord result = null;
try { try {
String dir = getUploadDir(); StoredFile stored = fileStorageService.store(file);
File upload = FileServerUtil.upload(file, dir, config.getUploadUuidName()); result = buildRecord(stored);
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());
fileRecordService.save(result); fileRecordService.save(result);
return success(result); return success(result);
} catch (Exception e) { } catch (Exception e) {
@@ -89,18 +73,8 @@ public class FileController extends BaseController {
public ApiResult<FileRecord> uploadBase64(String base64, String fileName, HttpServletRequest request) { public ApiResult<FileRecord> uploadBase64(String base64, String fileName, HttpServletRequest request) {
FileRecord result = null; FileRecord result = null;
try { try {
String dir = getUploadDir(); StoredFile stored = fileStorageService.storeBase64(base64, fileName);
File upload = FileServerUtil.upload(base64, fileName, getUploadDir()); result = buildRecord(stored);
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());
fileRecordService.save(result); fileRecordService.save(result);
return success(result); return success(result);
} catch (Exception e) { } catch (Exception e) {
@@ -113,42 +87,22 @@ public class FileController extends BaseController {
@ApiOperation("上传图片") @ApiOperation("上传图片")
@PostMapping("/image") @PostMapping("/image")
public HashMap<String, Object> image(@RequestParam MultipartFile file, HttpServletRequest request) { public HashMap<String, Object> image(@RequestParam MultipartFile file, HttpServletRequest request) {
FileRecord result = null; FileRecord result = null;
try { try {
String dir = getUploadDir(); StoredFile stored = fileStorageService.store(file);
File upload = FileServerUtil.upload(file, dir, config.getUploadUuidName()); result = buildRecord(stored);
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length() - 1); final HashMap<String, Object> map = new HashMap<>();
// System.out.println("request.getRequestURL() = " + request.getRequestURL()); map.put("name", result.getName());
// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/image"); map.put("status", "done");
String requestURL = config.getFileServer() + "/api/file"; map.put("thumbUrl", result.getThumbnail());
// System.out.println("requestURL = " + requestURL); map.put("downloadUrl", result.getDownloadUrl());
// config.getServerUrl() map.put("url", result.getUrl());
String originalName = file.getOriginalFilename(); fileRecordService.save(result);
result = new FileRecord(); return map;
result.setCreateUserId(getLoginUserId()); } catch (Exception e) {
result.setName(StrUtil.isBlank(originalName) ? upload.getName() : originalName); e.printStackTrace();
result.setLength(upload.length()); return null;
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);
} }
result.setDownloadUrl(requestURL + "/download" + path);
// result.setMerchantCode(getMerchantCode());
final HashMap<String, Object> 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')") @PreAuthorize("hasAuthority('sys:file:list')")
@@ -156,14 +110,15 @@ public class FileController extends BaseController {
@ApiOperation("根据id查询文件") @ApiOperation("根据id查询文件")
@GetMapping("/{id}") @GetMapping("/{id}")
public ApiResult<FileRecord> get(@PathVariable("id") Integer id) { public ApiResult<FileRecord> get(@PathVariable("id") Integer id) {
return success(fileRecordService.getByIdRel(id)); return success(fileRecordService.getByIdRel(id));
} }
@ApiOperation("查看原文件") @ApiOperation("查看原文件")
@GetMapping("/{dir}/{name:.+}") @GetMapping("/{dir}/{name:.+}")
public void preview(@PathVariable("dir") String dir, @PathVariable("name") String name, public void preview(@PathVariable("dir") String dir, @PathVariable("name") String name,
HttpServletResponse response, HttpServletRequest request) { 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); 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, public void download(@PathVariable("dir") String dir, @PathVariable("name") String name,
HttpServletResponse response, HttpServletRequest request) { HttpServletResponse response, HttpServletRequest request) {
String path = dir + "/" + name; String path = dir + "/" + name;
FileRecord record = fileRecordService.getByIdPath(path); FileRecord record = fileRecordService.getByIdPath("/" + path);
File file = new File(getUploadDir(), path); File file = fileStorageService.resolve(path);
String fileName = record == null ? file.getName() : record.getName(); String fileName = record == null ? (file == null ? name : file.getName()) : record.getName();
FileServerUtil.preview(file, true, fileName, null, null, response, request); FileServerUtil.preview(file, true, fileName, null, null, response, request);
} }
@@ -182,9 +137,21 @@ public class FileController extends BaseController {
@GetMapping("/thumbnail/{dir}/{name:.+}") @GetMapping("/thumbnail/{dir}/{name:.+}")
public void thumbnail(@PathVariable("dir") String dir, @PathVariable("name") String name, public void thumbnail(@PathVariable("dir") String dir, @PathVariable("name") String name,
HttpServletResponse response, HttpServletRequest request) { HttpServletResponse response, HttpServletRequest request) {
File file = new File(getUploadDir(), dir + "/" + name); String path = dir + "/" + name;
File thumbnail = new File(getUploadSmDir(), dir + "/" + name); File thumbnail = fileStorageService.resolveThumbnail(path);
FileServerUtil.previewThumbnail(file, thumbnail, config.getThumbnailSize(), response, request); 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')") @PreAuthorize("hasAuthority('sys:file:remove')")
@@ -194,11 +161,8 @@ public class FileController extends BaseController {
public ApiResult<?> remove(@PathVariable("id") Integer id) { public ApiResult<?> remove(@PathVariable("id") Integer id) {
FileRecord record = fileRecordService.getById(id); FileRecord record = fileRecordService.getById(id);
if (fileRecordService.removeById(id)) { if (fileRecordService.removeById(id)) {
if (StrUtil.isNotBlank(record.getPath())) { if (record != null && StrUtil.isNotBlank(record.getPath())) {
fileRecordService.deleteFileAsync(Arrays.asList( fileRecordService.deleteFileAsync(Collections.singletonList(record.getPath()));
new File(getUploadDir(), record.getPath()),
new File(getUploadSmDir(), record.getPath())
));
} }
return success("删除成功"); return success("删除成功");
} }
@@ -215,14 +179,13 @@ public class FileController extends BaseController {
public ApiResult<?> deleteBatch(@RequestBody List<Integer> ids) { public ApiResult<?> deleteBatch(@RequestBody List<Integer> ids) {
List<FileRecord> fileRecords = fileRecordService.listByIds(ids); List<FileRecord> fileRecords = fileRecordService.listByIds(ids);
if (fileRecordService.removeByIds(ids)) { if (fileRecordService.removeByIds(ids)) {
List<File> files = new ArrayList<>(); List<String> paths = new ArrayList<>();
for (FileRecord record : fileRecords) { for (FileRecord record : fileRecords) {
if (StrUtil.isNotBlank(record.getPath())) { if (StrUtil.isNotBlank(record.getPath())) {
files.add(new File(getUploadDir(), record.getPath())); paths.add(record.getPath());
files.add(new File(getUploadSmDir(), record.getPath()));
} }
} }
fileRecordService.deleteFileAsync(files); fileRecordService.deleteFileAsync(paths);
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -233,13 +196,8 @@ public class FileController extends BaseController {
@ApiOperation("分页查询文件") @ApiOperation("分页查询文件")
@GetMapping("/page") @GetMapping("/page")
public ApiResult<PageResult<FileRecord>> page(FileRecordParam param, HttpServletRequest request) { public ApiResult<PageResult<FileRecord>> page(FileRecordParam param, HttpServletRequest request) {
// 搜索条件
// if (getMerchantCode() != null) {
// param.setMerchantCode(getMerchantCode());
// }
PageResult<FileRecord> result = fileRecordService.pageRel(param); PageResult<FileRecord> result = fileRecordService.pageRel(param);
// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/page"); String requestURL = fileBaseUrl();
String requestURL = config.getFileServer();
for (FileRecord record : result.getList()) { for (FileRecord record : result.getList()) {
if (StrUtil.isNotBlank(record.getPath())) { if (StrUtil.isNotBlank(record.getPath())) {
record.setUrl(requestURL + record.getPath()); record.setUrl(requestURL + record.getPath());
@@ -258,8 +216,7 @@ public class FileController extends BaseController {
@GetMapping("/list") @GetMapping("/list")
public ApiResult<List<FileRecord>> list(FileRecordParam param, HttpServletRequest request) { public ApiResult<List<FileRecord>> list(FileRecordParam param, HttpServletRequest request) {
List<FileRecord> records = fileRecordService.listRel(param); List<FileRecord> records = fileRecordService.listRel(param);
// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/list"); String requestURL = fileBaseUrl();
String requestURL = config.getFileServer();
for (FileRecord record : records) { for (FileRecord record : records) {
if (StrUtil.isNotBlank(record.getPath())) { if (StrUtil.isNotBlank(record.getPath())) {
record.setUrl(requestURL + 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 FileRecord buildRecord(StoredFile stored) {
* 文件上传位置(服务器) FileRecord record = new FileRecord();
*/ record.setCreateUserId(getLoginUserId());
private String getUploadDir() { record.setName(stored.getOriginalName());
return config.getUploadPath() + "file/"; 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())) {
// private String getUploadDir() { record.setThumbnail(base + "/thumbnail" + stored.getPath());
// return "/Users/gxwebsoft/Documents/uploads/"; }
// } record.setDownloadUrl(base + "/download" + stored.getPath());
return record;
/**
* 缩略图生成位置
*/
private String getUploadSmDir() {
return getUploadBaseDir() + "thumbnail/";
} }
/** /**
* office转pdf输出位置 * office转pdf输出位置
*/ */
private String getPdfOutDir() { private String getPdfOutDir() {
return getUploadBaseDir() + "pdf/"; return new File(fileStorageService.getUploadDir(), "pdf").getAbsolutePath() + "/";
} }
@PreAuthorize("hasAuthority('sys:file:upload')") @PreAuthorize("hasAuthority('sys:file:upload')")
@@ -312,10 +267,10 @@ public class FileController extends BaseController {
@ApiOperation("添加文件") @ApiOperation("添加文件")
@PostMapping() @PostMapping()
public ApiResult<?> save(@RequestBody FileRecord fileRecord) { public ApiResult<?> save(@RequestBody FileRecord fileRecord) {
if (fileRecordService.save(fileRecord)) { if (fileRecordService.save(fileRecord)) {
return success("上传成功"); return success("上传成功");
} }
return fail("上传失败"); return fail("上传失败");
} }
@PreAuthorize("hasAuthority('sys:file:update')") @PreAuthorize("hasAuthority('sys:file:update')")
@@ -323,10 +278,10 @@ public class FileController extends BaseController {
@ApiOperation("修改文件") @ApiOperation("修改文件")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody FileRecord fileRecord) { public ApiResult<?> update(@RequestBody FileRecord fileRecord) {
if (fileRecordService.updateById(fileRecord)) { if (fileRecordService.updateById(fileRecord)) {
return success("修改成功"); return success("修改成功");
} }
return fail("修改失败"); return fail("修改失败");
} }
} }
@@ -5,7 +5,6 @@ import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.FileRecord; import com.gxwebsoft.common.system.entity.FileRecord;
import com.gxwebsoft.common.system.param.FileRecordParam; import com.gxwebsoft.common.system.param.FileRecordParam;
import java.io.File;
import java.util.List; import java.util.List;
/** /**
@@ -49,10 +48,10 @@ public interface FileRecordService extends IService<FileRecord> {
FileRecord getByIdPath(String path); FileRecord getByIdPath(String path);
/** /**
* 异步删除文件 * 异步删除文件(对象存储 + 本地缓存,按相对路径)
* *
* @param files 文件数组 * @param paths 文件相对路径数组,形如 /20261021/xxx.jpg
*/ */
void deleteFileAsync(List<File> files); void deleteFileAsync(List<String> paths);
} }
@@ -1,6 +1,7 @@
package com.gxwebsoft.common.system.service.impl; package com.gxwebsoft.common.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.utils.CommonUtil;
import com.gxwebsoft.common.core.web.PageParam; import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult; 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.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.File; import javax.annotation.Resource;
import java.util.List; import java.util.List;
/** /**
@@ -23,6 +24,9 @@ import java.util.List;
@Service @Service
public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRecord> implements FileRecordService { public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRecord> implements FileRecordService {
@Resource
private FileStorageService fileStorageService;
@Override @Override
public PageResult<FileRecord> pageRel(FileRecordParam param) { public PageResult<FileRecord> pageRel(FileRecordParam param) {
PageParam<FileRecord, FileRecordParam> page = new PageParam<>(param); PageParam<FileRecord, FileRecordParam> page = new PageParam<>(param);
@@ -51,10 +55,11 @@ public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRec
@Async @Async
@Override @Override
public void deleteFileAsync(List<File> files) { public void deleteFileAsync(List<String> paths) {
for (File file : files) { for (String path : paths) {
try { try {
file.delete(); // 对象存储原图/缩略图 + 本地缓存,一次清干净
fileStorageService.deleteAll(path);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -238,7 +238,7 @@ public class TzbcyMaterialServiceImpl extends ServiceImpl<TzbcyMaterialMapper, T
if (!hasText(path)) { if (!hasText(path)) {
return null; return null;
} }
return trimEnd(config.getFileServer()) + "/download" + ensureLeadingSlash(path); return trimEnd(config.getFileServer()) + "/api/file/download" + ensureLeadingSlash(path);
} }
private File resolveSourceFile(TzbcyMaterial material, String uploadPath) { private File resolveSourceFile(TzbcyMaterial material, String uploadPath) {
@@ -3,6 +3,8 @@ package com.gxwebsoft.open.controller;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.gxwebsoft.common.core.annotation.OperationLog; import com.gxwebsoft.common.core.annotation.OperationLog;
import com.gxwebsoft.common.core.config.ConfigProperties; 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.utils.FileServerUtil;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BaseController;
@@ -22,7 +24,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@@ -39,6 +41,8 @@ public class OpenFileController extends BaseController {
private ConfigProperties config; private ConfigProperties config;
@Resource @Resource
private FileRecordService fileRecordService; private FileRecordService fileRecordService;
@Resource
private FileStorageService fileStorageService;
@OperationLog @OperationLog
@ApiOperation("上传文件") @ApiOperation("上传文件")
@@ -48,24 +52,10 @@ public class OpenFileController extends BaseController {
isCheckSign(); isCheckSign();
FileRecord result = null; FileRecord result = null;
try { try {
String dir = getUploadDir(); StoredFile stored = fileStorageService.store(file);
File upload = FileServerUtil.upload(file, dir, config.getUploadUuidName()); // 开放接口沿用请求域名,避免历史调用方依赖的 host 发生变化
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length() - 1); String base = StrUtil.removeSuffix(request.getRequestURL().toString(), "/upload");
String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/upload"); result = buildRecord(stored, base);
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(requestURL + "/download/" + path);
// result.setMerchantCode(getMerchantCode());
fileRecordService.save(result); fileRecordService.save(result);
return success(result); return success(result);
} catch (Exception e) { } catch (Exception e) {
@@ -86,18 +76,9 @@ public class OpenFileController extends BaseController {
isCheckSign(); isCheckSign();
FileRecord result = null; FileRecord result = null;
try { try {
String dir = getUploadDir(); StoredFile stored = fileStorageService.storeBase64(base64, fileName);
File upload = FileServerUtil.upload(base64, fileName, getUploadDir()); String base = StrUtil.removeSuffix(request.getRequestURL().toString(), "/upload/base64");
String path = upload.getAbsolutePath().substring(dir.length()).replace("\\", "/"); result = buildRecord(stored, base);
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());
fileRecordService.save(result); fileRecordService.save(result);
return success(result); return success(result);
} catch (Exception e) { } catch (Exception e) {
@@ -110,9 +91,9 @@ public class OpenFileController extends BaseController {
@ApiOperation("根据id查询文件") @ApiOperation("根据id查询文件")
@GetMapping("/{id}") @GetMapping("/{id}")
public ApiResult<FileRecord> get(@PathVariable("id") Integer id) { public ApiResult<FileRecord> get(@PathVariable("id") Integer id) {
// 验证签名 // 验证签名
isCheckSign(); isCheckSign();
return success(fileRecordService.getByIdRel(id)); return success(fileRecordService.getByIdRel(id));
} }
@ApiOperation("查看原文件") @ApiOperation("查看原文件")
@@ -121,7 +102,7 @@ public class OpenFileController extends BaseController {
HttpServletResponse response, HttpServletRequest request) { HttpServletResponse response, HttpServletRequest request) {
// 验证签名 // 验证签名
isCheckSign(); isCheckSign();
File file = new File(getUploadDir(), dir + "/" + name); File file = fileStorageService.resolve(dir + "/" + name);
FileServerUtil.preview(file, getPdfOutDir(), config.getOpenOfficeHome(), response, request); FileServerUtil.preview(file, getPdfOutDir(), config.getOpenOfficeHome(), response, request);
} }
@@ -132,9 +113,9 @@ public class OpenFileController extends BaseController {
// 验证签名 // 验证签名
isCheckSign(); isCheckSign();
String path = dir + "/" + name; String path = dir + "/" + name;
FileRecord record = fileRecordService.getByIdPath(path); FileRecord record = fileRecordService.getByIdPath("/" + path);
File file = new File(getUploadDir(), path); File file = fileStorageService.resolve(path);
String fileName = record == null ? file.getName() : record.getName(); String fileName = record == null ? (file == null ? name : file.getName()) : record.getName();
FileServerUtil.preview(file, true, fileName, null, null, response, request); FileServerUtil.preview(file, true, fileName, null, null, response, request);
} }
@@ -144,9 +125,20 @@ public class OpenFileController extends BaseController {
HttpServletResponse response, HttpServletRequest request) { HttpServletResponse response, HttpServletRequest request) {
// 验证签名 // 验证签名
isCheckSign(); isCheckSign();
File file = new File(getUploadDir(), dir + "/" + name); String path = dir + "/" + name;
File thumbnail = new File(getUploadSmDir(), dir + "/" + name); File thumbnail = fileStorageService.resolveThumbnail(path);
FileServerUtil.previewThumbnail(file, thumbnail, config.getThumbnailSize(), response, request); 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 @OperationLog
@@ -157,11 +149,8 @@ public class OpenFileController extends BaseController {
isCheckSign(); isCheckSign();
FileRecord record = fileRecordService.getById(id); FileRecord record = fileRecordService.getById(id);
if (fileRecordService.removeById(id)) { if (fileRecordService.removeById(id)) {
if (StrUtil.isNotBlank(record.getPath())) { if (record != null && StrUtil.isNotBlank(record.getPath())) {
fileRecordService.deleteFileAsync(Arrays.asList( fileRecordService.deleteFileAsync(Collections.singletonList(record.getPath()));
new File(getUploadDir(), record.getPath()),
new File(getUploadSmDir(), record.getPath())
));
} }
return success("删除成功"); return success("删除成功");
} }
@@ -179,14 +168,13 @@ public class OpenFileController extends BaseController {
isCheckSign(); isCheckSign();
List<FileRecord> fileRecords = fileRecordService.listByIds(ids); List<FileRecord> fileRecords = fileRecordService.listByIds(ids);
if (fileRecordService.removeByIds(ids)) { if (fileRecordService.removeByIds(ids)) {
List<File> files = new ArrayList<>(); List<String> paths = new ArrayList<>();
for (FileRecord record : fileRecords) { for (FileRecord record : fileRecords) {
if (StrUtil.isNotBlank(record.getPath())) { if (StrUtil.isNotBlank(record.getPath())) {
files.add(new File(getUploadDir(), record.getPath())); paths.add(record.getPath());
files.add(new File(getUploadSmDir(), record.getPath()));
} }
} }
fileRecordService.deleteFileAsync(files); fileRecordService.deleteFileAsync(paths);
return success("删除成功"); return success("删除成功");
} }
return fail("删除失败"); return fail("删除失败");
@@ -198,13 +186,8 @@ public class OpenFileController extends BaseController {
public ApiResult<PageResult<FileRecord>> page(FileRecordParam param, HttpServletRequest request) { public ApiResult<PageResult<FileRecord>> page(FileRecordParam param, HttpServletRequest request) {
// 验证签名 // 验证签名
isCheckSign(); isCheckSign();
// 搜索条件
// if (getMerchantCode() != null) {
// param.setMerchantCode(getMerchantCode());
// }
PageResult<FileRecord> result = fileRecordService.pageRel(param); PageResult<FileRecord> result = fileRecordService.pageRel(param);
// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/page"); String requestURL = openFileBaseUrl();
String requestURL = config.getFileServer();
for (FileRecord record : result.getList()) { for (FileRecord record : result.getList()) {
if (StrUtil.isNotBlank(record.getPath())) { if (StrUtil.isNotBlank(record.getPath())) {
record.setUrl(requestURL + record.getPath()); record.setUrl(requestURL + record.getPath());
@@ -224,8 +207,7 @@ public class OpenFileController extends BaseController {
// 验证签名 // 验证签名
isCheckSign(); isCheckSign();
List<FileRecord> records = fileRecordService.listRel(param); List<FileRecord> records = fileRecordService.listRel(param);
// String requestURL = StrUtil.removeSuffix(request.getRequestURL(), "/list"); String requestURL = openFileBaseUrl();
String requestURL = config.getFileServer();
for (FileRecord record : records) { for (FileRecord record : records) {
if (StrUtil.isNotBlank(record.getPath())) { if (StrUtil.isNotBlank(record.getPath())) {
record.setUrl(requestURL + 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() { private String openFileBaseUrl() {
return File.listRoots()[config.getUploadLocation()].getAbsolutePath() return StrUtil.removeSuffix(config.getFileServer(), "/") + "/api/open/file";
.replace("\\", "/") + "/upload/";
} }
/** /**
* 文件上传位置(服务器) * 组装文件记录
*/ */
private String getUploadDir() { private FileRecord buildRecord(StoredFile stored, String base) {
return config.getUploadPath() + "file/"; FileRecord record = new FileRecord();
} record.setCreateUserId(getLoginUserId());
record.setName(stored.getOriginalName());
/** record.setLength(stored.getSize());
* 文件上传位置(本地) record.setPath(stored.getPath());
*/ record.setUrl(base + stored.getPath());
// private String getUploadDir() { record.setContentType(stored.getContentType());
// return "/Users/gxwebsoft/Documents/uploads/"; if (FileServerUtil.isImage(stored.getContentType())) {
// } record.setThumbnail(base + "/thumbnail" + stored.getPath());
}
/** record.setDownloadUrl(base + "/download" + stored.getPath());
* 缩略图生成位置 return record;
*/
private String getUploadSmDir() {
return getUploadBaseDir() + "thumbnail/";
} }
/** /**
* office转pdf输出位置 * office转pdf输出位置
*/ */
private String getPdfOutDir() { private String getPdfOutDir() {
return getUploadBaseDir() + "pdf/"; return new File(fileStorageService.getUploadDir(), "pdf").getAbsolutePath() + "/";
} }
@OperationLog @OperationLog
@ApiOperation("添加文件") @ApiOperation("添加文件")
@PostMapping() @PostMapping()
public ApiResult<?> save(@RequestBody FileRecord fileRecord) { public ApiResult<?> save(@RequestBody FileRecord fileRecord) {
// 验证签名 // 验证签名
isCheckSign(); isCheckSign();
if (fileRecordService.save(fileRecord)) { if (fileRecordService.save(fileRecord)) {
return success("上传成功"); return success("上传成功");
} }
return fail("上传失败"); return fail("上传失败");
} }
@OperationLog @OperationLog
@ApiOperation("修改文件") @ApiOperation("修改文件")
@PutMapping() @PutMapping()
public ApiResult<?> update(@RequestBody FileRecord fileRecord) { public ApiResult<?> update(@RequestBody FileRecord fileRecord) {
// 验证签名 // 验证签名
isCheckSign(); isCheckSign();
if (fileRecordService.updateById(fileRecord)) { if (fileRecordService.updateById(fileRecord)) {
return success("修改成功"); return success("修改成功");
} }
return fail("修改失败"); return fail("修改失败");
} }
} }
+20 -2
View File
@@ -33,9 +33,27 @@ logging:
# 框架配置 # 框架配置
config: 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/ 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: wxpay:
private-key-path: /www/wwwroot/tuanwei/cert/apiclient_key.pem private-key-path: /www/wwwroot/tuanwei/cert/apiclient_key.pem