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:
@@ -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("修改失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user