feat(infra): 批量预签名 API 及单体/微服务双模式自动配置
新增 FileApi.presignGetUrls 批量签名接口(@NotEmpty + @Size(max=500)), FileServiceImpl 实现带 null 守卫。 自动配置设计: - 单体模式:ViewshFileAutoConfiguration 直连 FileService - 微服务模式:OssPresignUrlApiAutoConfiguration 通过 Feign 代理 - 通过 @ConditionalOnMissingBean 互斥,保证同一 JVM 只有一个实现 新增 OssPresignHelper 工具类,供 Handler 层处理动态 Map 字段 (如 extInfo 中的图片 URL),提供静默降级的单个/批量/JSON数组签名方法。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,12 +7,15 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
||||
@Tag(name = "RPC 服务 - 文件")
|
||||
public interface FileApi {
|
||||
@@ -70,4 +73,16 @@ public interface FileApi {
|
||||
CommonResult<String> presignGetUrl(@NotEmpty(message = "URL 不能为空") @RequestParam("url") String url,
|
||||
@RequestParam(value = "expirationSeconds", required = false) Integer expirationSeconds);
|
||||
|
||||
/**
|
||||
* 批量生成文件预签名地址,用于读取
|
||||
*
|
||||
* @param urls 完整的文件访问地址列表
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @return 签名后的 URL 列表(与入参顺序一致)
|
||||
*/
|
||||
@PostMapping(PREFIX + "/presigned-urls")
|
||||
@Operation(summary = "批量生成文件预签名地址,用于读取")
|
||||
CommonResult<List<String>> presignGetUrls(@RequestBody @NotEmpty(message = "URL 列表不能为空") @Size(max = 500, message = "批量签名数量不能超过 500") List<String> urls,
|
||||
@RequestParam(value = "expirationSeconds", required = false) Integer expirationSeconds);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.viewsh.module.infra.api.file;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* OSS 预签名 URL 工具类
|
||||
* <p>
|
||||
* 供 Handler 层处理动态 Map 字段(如 extInfo)中的文件 URL
|
||||
*/
|
||||
@Slf4j
|
||||
public final class OssPresignHelper {
|
||||
|
||||
private OssPresignHelper() {}
|
||||
|
||||
/**
|
||||
* 单个 URL 签名(静默降级:失败返回原始 URL)
|
||||
*/
|
||||
public static String presignQuietly(FileApi fileApi, String url) {
|
||||
if (StrUtil.isEmpty(url)) {
|
||||
return url;
|
||||
}
|
||||
try {
|
||||
return fileApi.presignGetUrl(url, null).getCheckedData();
|
||||
} catch (Exception e) {
|
||||
log.warn("[presignQuietly] URL 签名失败: {}", url, e);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 数组 URL 签名:["url1","url2"] -> ["signed1","signed2"](静默降级)
|
||||
*/
|
||||
public static String presignJsonArrayQuietly(FileApi fileApi, String urlsJson) {
|
||||
if (StrUtil.isEmpty(urlsJson)) {
|
||||
return urlsJson;
|
||||
}
|
||||
try {
|
||||
List<String> urls = JSONUtil.toList(urlsJson, String.class);
|
||||
if (urls.isEmpty()) {
|
||||
return urlsJson;
|
||||
}
|
||||
Map<String, String> signedMap = presignBatch(fileApi, urls);
|
||||
List<String> signedUrls = urls.stream()
|
||||
.map(u -> signedMap.getOrDefault(u, u))
|
||||
.collect(Collectors.toList());
|
||||
return JSONUtil.toJsonStr(signedUrls);
|
||||
} catch (Exception e) {
|
||||
log.warn("[presignJsonArrayQuietly] JSON 数组签名失败: {}", urlsJson, e);
|
||||
return urlsJson;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量签名(一次 RPC),返回 原始URL -> 签名URL 映射
|
||||
*/
|
||||
public static Map<String, String> presignBatch(FileApi fileApi, Collection<String> urls) {
|
||||
if (urls == null || urls.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
// 去重
|
||||
List<String> uniqueUrls = new ArrayList<>(new LinkedHashSet<>(urls));
|
||||
try {
|
||||
List<String> signedUrls = fileApi.presignGetUrls(uniqueUrls, null).getCheckedData();
|
||||
Map<String, String> map = new HashMap<>(uniqueUrls.size());
|
||||
for (int i = 0; i < uniqueUrls.size(); i++) {
|
||||
map.put(uniqueUrls.get(i), signedUrls.get(i));
|
||||
}
|
||||
return map;
|
||||
} catch (Exception e) {
|
||||
log.warn("[presignBatch] 批量签名失败", e);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.viewsh.module.infra.api.file.config;
|
||||
|
||||
import com.viewsh.framework.common.biz.infra.file.OssPresignUrlApi;
|
||||
import com.viewsh.module.infra.api.file.FileApi;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* 基于 {@link FileApi} Feign 客户端的 {@link OssPresignUrlApi} 自动配置
|
||||
* <p>
|
||||
* 微服务模式下,各服务通过 Feign 调用 infra-server 进行批量预签名。
|
||||
* 单体模式下,infra-server 中的 {@code ViewshFileAutoConfiguration} 会直连 FileService,
|
||||
* 通过 {@link ConditionalOnMissingBean} 保证不冲突。
|
||||
*/
|
||||
// ⚠ beforeName 引用了 ViewshWebAutoConfiguration 的全限定名(字符串),
|
||||
// 因为 infra-api 不依赖 viewsh-spring-boot-starter-web,无法使用 before = ViewshWebAutoConfiguration.class。
|
||||
// 如果重命名/移动 ViewshWebAutoConfiguration,必须同步修改此处字符串。
|
||||
// grep 关键字以便重构时检索:PRESIGN_AUTO_CONFIG_ORDERING
|
||||
@AutoConfiguration(beforeName = "com.viewsh.framework.web.config.ViewshWebAutoConfiguration")
|
||||
public class OssPresignUrlApiAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(FileApi.class)
|
||||
@ConditionalOnMissingBean(OssPresignUrlApi.class)
|
||||
public OssPresignUrlApi ossPresignUrlApi(FileApi fileApi) {
|
||||
return urls -> fileApi.presignGetUrls(urls, null).getCheckedData();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.viewsh.module.infra.api.file.config.OssPresignUrlApiAutoConfiguration
|
||||
@@ -1,31 +1,37 @@
|
||||
package com.viewsh.module.infra.api.file;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.infra.api.file.dto.FileCreateReqDTO;
|
||||
import com.viewsh.module.infra.service.file.FileService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@RestController // 提供 RESTful API 接口,给 Feign 调用
|
||||
@Validated
|
||||
public class FileApiImpl implements FileApi {
|
||||
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
|
||||
@Override
|
||||
public CommonResult<String> createFile(FileCreateReqDTO createReqDTO) {
|
||||
return success(fileService.createFile(createReqDTO.getContent(), createReqDTO.getName(),
|
||||
createReqDTO.getDirectory(), createReqDTO.getType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<String> presignGetUrl(String url, Integer expirationSeconds) {
|
||||
return success(fileService.presignGetUrl(url, expirationSeconds));
|
||||
}
|
||||
|
||||
}
|
||||
package com.viewsh.module.infra.api.file;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.infra.api.file.dto.FileCreateReqDTO;
|
||||
import com.viewsh.module.infra.service.file.FileService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@RestController // 提供 RESTful API 接口,给 Feign 调用
|
||||
@Validated
|
||||
public class FileApiImpl implements FileApi {
|
||||
|
||||
@Resource
|
||||
private FileService fileService;
|
||||
|
||||
@Override
|
||||
public CommonResult<String> createFile(FileCreateReqDTO createReqDTO) {
|
||||
return success(fileService.createFile(createReqDTO.getContent(), createReqDTO.getName(),
|
||||
createReqDTO.getDirectory(), createReqDTO.getType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<String> presignGetUrl(String url, Integer expirationSeconds) {
|
||||
return success(fileService.presignGetUrl(url, expirationSeconds));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<List<String>> presignGetUrls(List<String> urls, Integer expirationSeconds) {
|
||||
return success(fileService.presignGetUrls(urls, expirationSeconds));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
package com.viewsh.module.infra.framework.file.config;
|
||||
|
||||
import com.viewsh.module.infra.framework.file.core.client.FileClientFactory;
|
||||
import com.viewsh.module.infra.framework.file.core.client.FileClientFactoryImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 文件配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ViewshFileAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FileClientFactory fileClientFactory() {
|
||||
return new FileClientFactoryImpl();
|
||||
}
|
||||
|
||||
}
|
||||
package com.viewsh.module.infra.framework.file.config;
|
||||
|
||||
import com.viewsh.framework.common.biz.infra.file.OssPresignUrlApi;
|
||||
import com.viewsh.module.infra.framework.file.core.client.FileClientFactory;
|
||||
import com.viewsh.module.infra.framework.file.core.client.FileClientFactoryImpl;
|
||||
import com.viewsh.module.infra.service.file.FileService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 文件配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ViewshFileAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FileClientFactory fileClientFactory() {
|
||||
return new FileClientFactoryImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OssPresignUrlApi ossPresignUrlApi(FileService fileService) {
|
||||
return urls -> fileService.presignGetUrls(urls, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,89 +1,98 @@
|
||||
package com.viewsh.module.infra.service.file;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
|
||||
import com.viewsh.module.infra.dal.dataobject.file.FileDO;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface FileService {
|
||||
|
||||
/**
|
||||
* 获得文件分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 文件分页
|
||||
*/
|
||||
PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 保存文件,并返回文件的访问路径
|
||||
*
|
||||
* @param content 文件内容
|
||||
* @param name 文件名称,允许空
|
||||
* @param directory 目录,允许空
|
||||
* @param type 文件的 MIME 类型,允许空
|
||||
* @return 文件路径
|
||||
*/
|
||||
String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
|
||||
String name, String directory, String type);
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址信息,用于上传
|
||||
*
|
||||
* @param name 文件名
|
||||
* @param directory 目录
|
||||
* @return 预签名地址信息
|
||||
*/
|
||||
FilePresignedUrlRespVO presignPutUrl(@NotEmpty(message = "文件名不能为空") String name,
|
||||
String directory);
|
||||
/**
|
||||
* 生成文件预签名地址信息,用于读取
|
||||
*
|
||||
* @param url 完整的文件访问地址
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
String presignGetUrl(String url, Integer expirationSeconds);
|
||||
|
||||
/**
|
||||
* 创建文件
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createFile(FileCreateReqVO createReqVO);
|
||||
FileDO getFile(Long id);
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteFile(Long id) throws Exception;
|
||||
|
||||
/**
|
||||
* 批量删除文件
|
||||
*
|
||||
* @param ids 编号列表
|
||||
*/
|
||||
void deleteFileList(List<Long> ids) throws Exception;
|
||||
|
||||
/**
|
||||
* 获得文件内容
|
||||
*
|
||||
* @param configId 配置编号
|
||||
* @param path 文件路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
byte[] getFileContent(Long configId, String path) throws Exception;
|
||||
|
||||
}
|
||||
package com.viewsh.module.infra.service.file;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
|
||||
import com.viewsh.module.infra.dal.dataobject.file.FileDO;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件 Service 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface FileService {
|
||||
|
||||
/**
|
||||
* 获得文件分页
|
||||
*
|
||||
* @param pageReqVO 分页查询
|
||||
* @return 文件分页
|
||||
*/
|
||||
PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 保存文件,并返回文件的访问路径
|
||||
*
|
||||
* @param content 文件内容
|
||||
* @param name 文件名称,允许空
|
||||
* @param directory 目录,允许空
|
||||
* @param type 文件的 MIME 类型,允许空
|
||||
* @return 文件路径
|
||||
*/
|
||||
String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
|
||||
String name, String directory, String type);
|
||||
|
||||
/**
|
||||
* 生成文件预签名地址信息,用于上传
|
||||
*
|
||||
* @param name 文件名
|
||||
* @param directory 目录
|
||||
* @return 预签名地址信息
|
||||
*/
|
||||
FilePresignedUrlRespVO presignPutUrl(@NotEmpty(message = "文件名不能为空") String name,
|
||||
String directory);
|
||||
/**
|
||||
* 生成文件预签名地址信息,用于读取
|
||||
*
|
||||
* @param url 完整的文件访问地址
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @return 文件预签名地址
|
||||
*/
|
||||
String presignGetUrl(String url, Integer expirationSeconds);
|
||||
|
||||
/**
|
||||
* 批量生成文件预签名地址,用于读取
|
||||
*
|
||||
* @param urls 完整的文件访问地址列表
|
||||
* @param expirationSeconds 访问有效期,单位秒
|
||||
* @return 签名后的 URL 列表(与入参顺序一致)
|
||||
*/
|
||||
List<String> presignGetUrls(List<String> urls, Integer expirationSeconds);
|
||||
|
||||
/**
|
||||
* 创建文件
|
||||
*
|
||||
* @param createReqVO 创建信息
|
||||
* @return 编号
|
||||
*/
|
||||
Long createFile(FileCreateReqVO createReqVO);
|
||||
FileDO getFile(Long id);
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param id 编号
|
||||
*/
|
||||
void deleteFile(Long id) throws Exception;
|
||||
|
||||
/**
|
||||
* 批量删除文件
|
||||
*
|
||||
* @param ids 编号列表
|
||||
*/
|
||||
void deleteFileList(List<Long> ids) throws Exception;
|
||||
|
||||
/**
|
||||
* 获得文件内容
|
||||
*
|
||||
* @param configId 配置编号
|
||||
* @param path 文件路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
byte[] getFileContent(Long configId, String path) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,206 +1,218 @@
|
||||
package com.viewsh.module.infra.service.file;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.http.HttpUtils;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
|
||||
import com.viewsh.module.infra.dal.dataobject.file.FileDO;
|
||||
import com.viewsh.module.infra.dal.mysql.file.FileMapper;
|
||||
import com.viewsh.module.infra.framework.file.core.client.FileClient;
|
||||
import com.viewsh.module.infra.framework.file.core.utils.FileTypeUtils;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
|
||||
|
||||
/**
|
||||
* 文件 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
public class FileServiceImpl implements FileService {
|
||||
|
||||
/**
|
||||
* 上传文件的前缀,是否包含日期(yyyyMMdd)
|
||||
*
|
||||
* 目的:按照日期,进行分目录
|
||||
*/
|
||||
static boolean PATH_PREFIX_DATE_ENABLE = true;
|
||||
/**
|
||||
* 上传文件的后缀,是否包含时间戳
|
||||
*
|
||||
* 目的:保证文件的唯一性,避免覆盖
|
||||
* 定制:可按需调整成 UUID、或者其他方式
|
||||
*/
|
||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
@Resource
|
||||
private FileConfigService fileConfigService;
|
||||
|
||||
@Resource
|
||||
private FileMapper fileMapper;
|
||||
|
||||
@Override
|
||||
public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
|
||||
return fileMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public String createFile(byte[] content, String name, String directory, String type) {
|
||||
// 1.1 处理 type 为空的情况
|
||||
if (StrUtil.isEmpty(type)) {
|
||||
type = FileTypeUtils.getMineType(content, name);
|
||||
}
|
||||
// 1.2 处理 name 为空的情况
|
||||
if (StrUtil.isEmpty(name)) {
|
||||
name = DigestUtil.sha256Hex(content);
|
||||
}
|
||||
if (StrUtil.isEmpty(FileUtil.extName(name))) {
|
||||
// 如果 name 没有后缀 type,则补充后缀
|
||||
String extension = FileTypeUtils.getExtension(type);
|
||||
if (StrUtil.isNotEmpty(extension)) {
|
||||
name = name + extension;
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 生成上传的 path,需要保证唯一
|
||||
String path = generateUploadPath(name, directory);
|
||||
// 2.2 上传到文件存储器
|
||||
FileClient client = fileConfigService.getMasterFileClient();
|
||||
Assert.notNull(client, "客户端(master) 不能为空");
|
||||
String url = client.upload(content, path, type);
|
||||
|
||||
// 3. 保存到数据库
|
||||
fileMapper.insert(new FileDO().setConfigId(client.getId())
|
||||
.setName(name).setPath(path).setUrl(url)
|
||||
.setType(type).setSize(content.length));
|
||||
return url;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
String generateUploadPath(String name, String directory) {
|
||||
// 1. 生成前缀、后缀
|
||||
String prefix = null;
|
||||
if (PATH_PREFIX_DATE_ENABLE) {
|
||||
prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
|
||||
}
|
||||
String suffix = null;
|
||||
if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
|
||||
suffix = String.valueOf(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// 2.1 先拼接 suffix 后缀
|
||||
if (StrUtil.isNotEmpty(suffix)) {
|
||||
String ext = FileUtil.extName(name);
|
||||
if (StrUtil.isNotEmpty(ext)) {
|
||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
||||
} else {
|
||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
||||
}
|
||||
}
|
||||
// 2.2 再拼接 prefix 前缀
|
||||
if (StrUtil.isNotEmpty(prefix)) {
|
||||
name = prefix + StrUtil.SLASH + name;
|
||||
}
|
||||
// 2.3 最后拼接 directory 目录
|
||||
if (StrUtil.isNotEmpty(directory)) {
|
||||
name = directory + StrUtil.SLASH + name;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public FilePresignedUrlRespVO presignPutUrl(String name, String directory) {
|
||||
// 1. 生成上传的 path,需要保证唯一
|
||||
String path = generateUploadPath(name, directory);
|
||||
|
||||
// 2. 获取文件预签名地址
|
||||
FileClient fileClient = fileConfigService.getMasterFileClient();
|
||||
String uploadUrl = fileClient.presignPutUrl(path);
|
||||
String visitUrl = fileClient.presignGetUrl(path, null);
|
||||
return new FilePresignedUrlRespVO().setConfigId(fileClient.getId())
|
||||
.setPath(path).setUploadUrl(uploadUrl).setUrl(visitUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
FileClient fileClient = fileConfigService.getMasterFileClient();
|
||||
return fileClient.presignGetUrl(url, expirationSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createFile(FileCreateReqVO createReqVO) {
|
||||
createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数
|
||||
FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
|
||||
fileMapper.insert(file);
|
||||
return file.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileDO getFile(Long id) {
|
||||
return validateFileExists(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFile(Long id) throws Exception {
|
||||
// 校验存在
|
||||
FileDO file = validateFileExists(id);
|
||||
|
||||
// 从文件存储器中删除
|
||||
FileClient client = fileConfigService.getFileClient(file.getConfigId());
|
||||
Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());
|
||||
client.delete(file.getPath());
|
||||
|
||||
// 删除记录
|
||||
fileMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void deleteFileList(List<Long> ids) {
|
||||
// 删除文件
|
||||
List<FileDO> files = fileMapper.selectByIds(ids);
|
||||
for (FileDO file : files) {
|
||||
// 获取客户端
|
||||
FileClient client = fileConfigService.getFileClient(file.getConfigId());
|
||||
Assert.notNull(client, "客户端({}) 不能为空", file.getPath());
|
||||
// 删除文件
|
||||
client.delete(file.getPath());
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
fileMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
private FileDO validateFileExists(Long id) {
|
||||
FileDO fileDO = fileMapper.selectById(id);
|
||||
if (fileDO == null) {
|
||||
throw exception(FILE_NOT_EXISTS);
|
||||
}
|
||||
return fileDO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getFileContent(Long configId, String path) throws Exception {
|
||||
FileClient client = fileConfigService.getFileClient(configId);
|
||||
Assert.notNull(client, "客户端({}) 不能为空", configId);
|
||||
return client.getContent(path);
|
||||
}
|
||||
|
||||
}
|
||||
package com.viewsh.module.infra.service.file;
|
||||
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.http.HttpUtils;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FilePageReqVO;
|
||||
import com.viewsh.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
|
||||
import com.viewsh.module.infra.dal.dataobject.file.FileDO;
|
||||
import com.viewsh.module.infra.dal.mysql.file.FileMapper;
|
||||
import com.viewsh.module.infra.framework.file.core.client.FileClient;
|
||||
import com.viewsh.module.infra.framework.file.core.utils.FileTypeUtils;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
|
||||
|
||||
/**
|
||||
* 文件 Service 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Service
|
||||
public class FileServiceImpl implements FileService {
|
||||
|
||||
/**
|
||||
* 上传文件的前缀,是否包含日期(yyyyMMdd)
|
||||
*
|
||||
* 目的:按照日期,进行分目录
|
||||
*/
|
||||
static boolean PATH_PREFIX_DATE_ENABLE = true;
|
||||
/**
|
||||
* 上传文件的后缀,是否包含时间戳
|
||||
*
|
||||
* 目的:保证文件的唯一性,避免覆盖
|
||||
* 定制:可按需调整成 UUID、或者其他方式
|
||||
*/
|
||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
|
||||
@Resource
|
||||
private FileConfigService fileConfigService;
|
||||
|
||||
@Resource
|
||||
private FileMapper fileMapper;
|
||||
|
||||
@Override
|
||||
public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
|
||||
return fileMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public String createFile(byte[] content, String name, String directory, String type) {
|
||||
// 1.1 处理 type 为空的情况
|
||||
if (StrUtil.isEmpty(type)) {
|
||||
type = FileTypeUtils.getMineType(content, name);
|
||||
}
|
||||
// 1.2 处理 name 为空的情况
|
||||
if (StrUtil.isEmpty(name)) {
|
||||
name = DigestUtil.sha256Hex(content);
|
||||
}
|
||||
if (StrUtil.isEmpty(FileUtil.extName(name))) {
|
||||
// 如果 name 没有后缀 type,则补充后缀
|
||||
String extension = FileTypeUtils.getExtension(type);
|
||||
if (StrUtil.isNotEmpty(extension)) {
|
||||
name = name + extension;
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 生成上传的 path,需要保证唯一
|
||||
String path = generateUploadPath(name, directory);
|
||||
// 2.2 上传到文件存储器
|
||||
FileClient client = fileConfigService.getMasterFileClient();
|
||||
Assert.notNull(client, "客户端(master) 不能为空");
|
||||
String url = client.upload(content, path, type);
|
||||
|
||||
// 3. 保存到数据库
|
||||
fileMapper.insert(new FileDO().setConfigId(client.getId())
|
||||
.setName(name).setPath(path).setUrl(url)
|
||||
.setType(type).setSize(content.length));
|
||||
return url;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
String generateUploadPath(String name, String directory) {
|
||||
// 1. 生成前缀、后缀
|
||||
String prefix = null;
|
||||
if (PATH_PREFIX_DATE_ENABLE) {
|
||||
prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
|
||||
}
|
||||
String suffix = null;
|
||||
if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
|
||||
suffix = String.valueOf(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// 2.1 先拼接 suffix 后缀
|
||||
if (StrUtil.isNotEmpty(suffix)) {
|
||||
String ext = FileUtil.extName(name);
|
||||
if (StrUtil.isNotEmpty(ext)) {
|
||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
||||
} else {
|
||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
||||
}
|
||||
}
|
||||
// 2.2 再拼接 prefix 前缀
|
||||
if (StrUtil.isNotEmpty(prefix)) {
|
||||
name = prefix + StrUtil.SLASH + name;
|
||||
}
|
||||
// 2.3 最后拼接 directory 目录
|
||||
if (StrUtil.isNotEmpty(directory)) {
|
||||
name = directory + StrUtil.SLASH + name;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public FilePresignedUrlRespVO presignPutUrl(String name, String directory) {
|
||||
// 1. 生成上传的 path,需要保证唯一
|
||||
String path = generateUploadPath(name, directory);
|
||||
|
||||
// 2. 获取文件预签名地址
|
||||
FileClient fileClient = fileConfigService.getMasterFileClient();
|
||||
String uploadUrl = fileClient.presignPutUrl(path);
|
||||
String visitUrl = fileClient.presignGetUrl(path, null);
|
||||
return new FilePresignedUrlRespVO().setConfigId(fileClient.getId())
|
||||
.setPath(path).setUploadUrl(uploadUrl).setUrl(visitUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
FileClient fileClient = fileConfigService.getMasterFileClient();
|
||||
return fileClient.presignGetUrl(url, expirationSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> presignGetUrls(List<String> urls, Integer expirationSeconds) {
|
||||
if (urls == null || urls.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
FileClient fileClient = fileConfigService.getMasterFileClient();
|
||||
return urls.stream()
|
||||
.map(url -> fileClient.presignGetUrl(url, expirationSeconds))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createFile(FileCreateReqVO createReqVO) {
|
||||
createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数
|
||||
FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
|
||||
fileMapper.insert(file);
|
||||
return file.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileDO getFile(Long id) {
|
||||
return validateFileExists(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFile(Long id) throws Exception {
|
||||
// 校验存在
|
||||
FileDO file = validateFileExists(id);
|
||||
|
||||
// 从文件存储器中删除
|
||||
FileClient client = fileConfigService.getFileClient(file.getConfigId());
|
||||
Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());
|
||||
client.delete(file.getPath());
|
||||
|
||||
// 删除记录
|
||||
fileMapper.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public void deleteFileList(List<Long> ids) {
|
||||
// 删除文件
|
||||
List<FileDO> files = fileMapper.selectByIds(ids);
|
||||
for (FileDO file : files) {
|
||||
// 获取客户端
|
||||
FileClient client = fileConfigService.getFileClient(file.getConfigId());
|
||||
Assert.notNull(client, "客户端({}) 不能为空", file.getPath());
|
||||
// 删除文件
|
||||
client.delete(file.getPath());
|
||||
}
|
||||
|
||||
// 删除记录
|
||||
fileMapper.deleteByIds(ids);
|
||||
}
|
||||
|
||||
private FileDO validateFileExists(Long id) {
|
||||
FileDO fileDO = fileMapper.selectById(id);
|
||||
if (fileDO == null) {
|
||||
throw exception(FILE_NOT_EXISTS);
|
||||
}
|
||||
return fileDO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getFileContent(Long configId, String path) throws Exception {
|
||||
FileClient client = fileConfigService.getFileClient(configId);
|
||||
Assert.notNull(client, "客户端({}) 不能为空", configId);
|
||||
return client.getContent(path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user