文件存储
这篇解决什么
头像、附件、导出文件要落到对象存储或磁盘,业务表只存 URL。读完能改 FileClient、文件配置、useUpload,以及业务模块里的 FileApi.createFile。
默认端口 48080,管理端前缀 /admin-api。菜单在基础设施 → 文件管理。
上传成功后返回完整访问 URL。业务字段保存这条 URL,不要再拷一份字节进业务表。
存储器
FileStorageEnum 五种实现,共用 FileClient。上传、删除、读内容都走主配置对应的客户端。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| S3(20) | 兼容 S3 协议:MinIO、阿里云 OSS、腾讯云 COS、七牛、华为 OBS、火山 TOS | .../client/s3/S3FileClient.java |
| 数据库(1) | 字节进 infra_file_content,适合少量小文件 | .../client/db/DBFileClient.java |
| 本地磁盘(10) | 写到 basePath,高可用和故障转移都弱 | .../client/local/LocalFileClient.java |
| FTP(11) / SFTP(12) | 远程目录,同样难做故障转移 | .../client/ftp/、.../client/sftp/ |
优先用 S3。没有云账号时自建 MinIO。数据库能靠主从和备份撑一小撮文件。本地和 FTP 只适合单机试跑。
不要把本地磁盘当生产主存储
多实例时文件落在不同机器上,下载会 404。也没有现成的故障转移。生产用 S3 或 MinIO,并把该配置设成主配置。
代码位置
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 后端模块 | 配置、上传、下载、客户端工厂 | ruoyi-office/yudao-module-infra/ |
| 管理端接口 | /admin-api/infra/file、/infra/file-config | FileController、FileConfigController |
| RPC | 业务模块调 createFile / presignGetUrl | yudao-module-infra-api/.../FileApi.java |
| 文件表 | 每次上传一条元数据;@TenantIgnore | infra_file → FileDO |
| 配置表 | 存储器 JSON;master 表示默认上传通道 | infra_file_config → FileConfigDO |
| 内容表 | 仅数据库存储器使用 | infra_file_content → FileContentDO |
| 前端页面 | 文件列表、文件配置 | web-antd/src/views/infra/file/、fileConfig/ |
| 上传组件 | VITE_UPLOAD_TYPE 决定走后端还是直传 S3 | web-antd/src/components/upload/use-upload.ts |
菜单路径:文件列表 /infra/file/file,文件配置 /infra/file/file-config。权限字:infra:file:query、infra:file:delete、infra:file-config:*。
单体 application.yaml 里 spring.servlet.multipart 默认单文件、整请求都是 100MB。前端 uploadFile 超时 300000 毫秒。
文件配置
打开基础设施 → 文件管理 → 文件配置。新增一条,测通后再点「主配置」。后续 FileService.createFile 只打主配置。

本系统截图:文件配置列表,主配置一行会标「是」。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 新增 / 修改 | 按存储器校验 config JSON | POST/PUT /infra/file-config |
| 测试 | 上传内置 file/erweima.jpg,返回可访问 URL | GET /infra/file-config/test |
| 主配置 | 先把其它行的 master 清掉,再把当前行设为 true | PUT /infra/file-config/update-master |
| 客户端缓存 | LoadingCache,10 秒异步刷新;主配置 key 是 0L | FileConfigServiceImpl |
主配置不能删,会抛 FILE_CONFIG_DELETE_FAIL_MASTER。演示站会锁住新增、改主配置和测试,避免密钥进表单 DOM。
S3 行要填节点、bucket、密钥、是否 Path Style、是否公开、自定义域名。七牛必须填域名。MinIO 走 Nginx 独立域名时,把 Path Style 打开。region 一般只给 AWS 填,其它云可留空由 endpoint 识别。
文件上传
两种入口:浏览器走 FileController,Java 业务走 FileApi。最终都进主配置的 FileClient.upload,并往 infra_file 插一条。
路径默认带日期目录:{directory}/yyyyMMdd/{原文件名}。FileServiceImpl.PATH_PREFIX_DATE_ENABLE 为 true。文件名不能带 /,目录不能 ..,校验在 FilePathUtils。
前端走后端
FileController 的 /upload 读 MultipartFile,再交给 createFile:
@PostMapping("/upload")
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
public CommonResult<String> uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
byte[] content = IoUtil.readBytes(file.getInputStream());
return success(fileService.createFile(content, file.getOriginalFilename(),
uploadReqVO.getDirectory(), file.getContentType()));
}useUpload 默认 VITE_UPLOAD_TYPE=server,调用 uploadFile。文件列表页、FileUpload、ImageUpload、个人中心头像都走这一套。

本系统截图:文件列表,可上传、复制链接、预览或删除。
业务模块调用 FileApi
其它模块不要直接碰 FileService。引入 yudao-module-infra-api,注入 FileApi:
default String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content,
String name, String directory, String type) {
return createFile(new FileCreateReqDTO()
.setName(name).setDirectory(directory).setType(type).setContent(content))
.getCheckedData();
}资产二维码、OA / 合同 OnlyOffice 回写、培训证书都是这个方法。directory 建议按业务写,例如 asset/qr、oa/cloud、contract。
文件下载
返回的 URL 规则随存储器变。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| S3 公开桶 | 直接拼自定义域名 + 对象路径,浏览器自己拉 | S3FileClient.presignGetUrl |
| 本地 / DB / FTP | 没有公网对象地址,走后端读字节 | AbstractFileClient.formatFileUrl |
| 同源预览 | 前端用已存 URL 预览时走后端转发,躲开 OSS 跨域 | GET /infra/file/preview-by-url |
本地、数据库拼出来的地址是:
{domain}/admin-api/infra/file/{configId}/get/{path}对应接口匿名可访问,方便 <img> 和下载链接:
@GetMapping("/{configId}/get/**")
@PermitAll
@TenantIgnore
public void getFileContent(HttpServletRequest request,
HttpServletResponse response,
@PathVariable("configId") Long configId) throws Exception {
String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false);
path = HttpUtils.decodeUrlPath(path);
byte[] content = fileService.getFileContent(configId, path);
// ...
}安全放行写在 yudao-module-infra 的 SecurityConfiguration:/infra/file/*/get/**。列表页预览图片用 getFilePreviewUrlByUrl,不要直接把外链塞进 <img>。
文件客户端
framework/file 包把存储器差抽象掉。工厂按 storage 反射出实现,配置变更会 refresh。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
FileClient | upload / delete / getContent;S3 另有预签名 | .../client/FileClient.java |
AbstractFileClient | 初始化、刷新、拼 /get/ 下载地址 | 同包 |
FileClientFactoryImpl | ConcurrentHashMap<configId, client> | 同包 |
FileConfigService | 读库、校验、缓存、测试上传 | service/file/FileConfigServiceImpl.java |
public interface FileClient {
Long getId();
String upload(byte[] content, String path, String type) throws Exception;
void delete(String path) throws Exception;
byte[] getContent(String path) throws Exception;
default String presignPutUrl(String path) {
throw new UnsupportedOperationException("不支持的操作");
}
default String presignGetUrl(String url, Integer expirationSeconds) {
throw new UnsupportedOperationException("不支持的操作");
}
}只有 S3FileClient 实现预签名。其它存储器调用会抛「不支持的操作」。
按本仓库生成的 ER:infra_file 只存元数据;字节只在数据库存储器进 infra_file_content。
S3 对象存储
云厂商只要兼容 S3,就填同一套字段。实现用 AWS SDK v2 的 S3Client / S3Presigner。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
endpoint | 节点。MinIO 写 http://127.0.0.1:9000 | S3FileClientConfig |
bucket / accessKey / accessSecret | 桶和密钥 | 同上 |
domain | 自定义域名;七牛必填 | 同上 |
enablePathStyleAccess | MinIO + Nginx 独立域名时打开 | 同上 |
enablePublicAccess | true 公开读;false 走 GET 预签名 | 同上 |
region | AWS 必填;其它云可空 | 同上 |
公开桶才能用裸 URL 访问。自定义域名方便以后换云,业务表里的 URL 不用批量改 host。
官方跨域说明:阿里云 OSS、腾讯云 COS、七牛 Kodo。
前端直传 S3
默认仍是「浏览器 → 后端 → S3」。大文件会占满应用机带宽。web-antd 已支持前端直传。
把环境变量改成 client:
# apps/web-antd/.env.development
VITE_UPLOAD_TYPE=client改完要重启 Vite。useUpload 会先要预签名,再 PUT 到对象存储,最后异步记一条 infra_file:
if (isClientUpload) {
const fileName = await generateFileName(file);
const presignedInfo = await getFilePresignedUrl(fileName, directory);
return baseRequestClient
.put(presignedInfo.uploadUrl, file, {
headers: { 'Content-Type': file.type },
})
.then(() => {
createFile0(presignedInfo, file);
return { url: removeUrlQuery(presignedInfo.url) };
});
}直传只认 S3 主配置
VITE_UPLOAD_TYPE=client 时,主配置必须是存储器 20。本地、FTP、数据库没有 presignPutUrl。桶上要放行浏览器 Origin 的 CORS(允许 PUT、暴露 ETag)。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 预签名上传 | 返回 uploadUrl、访问 url、path、configId | GET /infra/file/presigned-url |
| 记元数据 | 直传成功后再插 infra_file | POST /infra/file/create |
| 前端 | 直传或走后端二选一 | use-upload.ts、api/infra/file/index.ts |
预签名默认 24 小时(S3FileClient.EXPIRATION_DEFAULT)。写入 infra_file.url 前会 HttpUtils.removeUrlQuery,去掉签名参数。
私有桶
enablePublicAccess=false 时,裸 URL 打不开。读文件用 GET 预签名。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 刷新临时地址 | 管理端按已存 URL 换签名 | GET /infra/file/presigned-get-url |
| 业务 RPC | 返回给前端的也必须是签名后的 | FileApi.presignGetUrl |
| 落库 | 去掉 Query,避免过期串写进字段、撑爆长度 | HttpUtils.removeUrlQuery |
public String presignGetUrl(String url, Integer expirationSeconds) {
FileClient fileClient = fileConfigService.getMasterFileClient();
return fileClient.presignGetUrl(url, expirationSeconds);
}合同预览、发票 OCR 已经按这个刷新临时地址。新增或修改业务单据时,提交的如果是带签名的 URL,后端先剥 Query 再入库。
私有桶 URL 不要原样入库
签名 Query 有过期时间,过期即 403;整串也往往超过 url 列长。库里只留去参后的地址,展示时再 presignGetUrl。
配置与操作
管理员在 基础设施 → 文件配置 选存储器并设默认,上传走文件列表。库里只存去参后的地址。FileClient 在 infra,不在 framework。
开启见 框架层。截图见上文配置和列表。
