# 文件存储策略 **本文引用的文件** - [application.yaml](file://fms-api/src/main/resources/application.yaml) - [FileStorageProperties.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java) - [FileController.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java) - [FileService.java](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java) - [100_initialize_fms_new.sql](file://fms-api/config/migrations/100_initialize_fms_new.sql) ## 目录 1. [简介](#简介) 2. [项目结构](#项目结构) 3. [核心组件](#核心组件) 4. [架构总览](#架构总览) 5. [详细组件分析](#详细组件分析) 6. [依赖关系分析](#依赖关系分析) 7. [性能考虑](#性能考虑) 8. [故障排查指南](#故障排查指南) 9. [结论](#结论) 10. [附录](#附录) ## 简介 本文件存储策略文档面向 FMS 系统的文件上传、存储与访问能力,覆盖以下主题: - 支持的存储模式:本地文件系统与云存储(当前实现为通过 g3oss 代理的 OSS) - 存储路径规则、文件命名策略与目录结构设计 - 文件元数据管理:表结构、关联关系与持久化策略 - 文件访问 URL 生成机制与安全访问控制 - 不同环境的配置示例与调优建议 - 备份、迁移与灾难恢复策略 ## 项目结构 FMS 后端采用 Spring Boot 模块组织,文件相关能力集中在 fms-api 模块中: - 配置层:application.yaml 提供全局配置;FileStorageProperties 将 fms.file.* 映射到 Java 对象 - 控制器层:FileController 暴露 /file/* 接口,负责上传、凭证获取、记录保存、删除与静态资源访问 - 服务层:FileService 封装本地/OSS 上传、删除、DB 写入等核心逻辑 - 数据层:bf_files 表用于持久化文件元数据;迁移脚本定义系统基础表结构 ```mermaid graph TB A["前端/调用方"] --> B["FileController
/file/*"] B --> C["FileService
业务逻辑"] C --> D["FileStorageProperties
配置读取"] C --> E["数据库 bf_files
元数据持久化"] C --> F["本地文件系统
./data/files/{orgId}/{yyyy}/{MM}/{dd}/..."] C --> G["g3oss 代理服务
OSS 凭证/删除"] B --> H["静态文件访问
/file/static/{path}"] ``` 图表来源 - [FileController.java:28-113](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L28-L113) - [FileService.java:34-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L268) - [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69) - [application.yaml:10-34](file://fms-api/src/main/resources/application.yaml#L10-L34) 章节来源 - [application.yaml:1-34](file://fms-api/src/main/resources/application.yaml#L1-L34) - [FileStorageProperties.java:1-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L1-L69) - [FileController.java:1-113](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L1-L113) - [FileService.java:1-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L1-L268) ## 核心组件 - FileStorageProperties:集中管理存储类型与具体实现参数(本地存储目录、URL前缀;OSS 地址) - FileController:对外暴露文件上传、凭证获取、记录保存、删除与静态访问接口 - FileService:实现本地直传、OSS 凭证获取、删除(本地或 OSS)、以及 bf_files 元数据写入 - 数据库 bf_files:存储文件元数据,包括原始文件名、大小、扩展名、映射路径、所属模块与分类等 章节来源 - [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69) - [FileController.java:28-113](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L28-L113) - [FileService.java:34-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L268) ## 架构总览 下图展示了从请求到存储与数据库的完整流程,涵盖本地与 OSS 两种模式。 ```mermaid sequenceDiagram participant FE as "前端" participant FC as "FileController" participant FS as "FileService" participant DB as "数据库(bf_files)" participant FSYS as "本地文件系统" participant GOSS as "g3oss 代理服务" Note over FE,FC : 上传流程 FE->>FC : POST /file/upload (files, father, moduleId, cateId) FC->>FS : localUpload(...) FS->>FSYS : 写入 ./data/files/{orgId}/{yyyy}/{MM}/{dd}/{uuid.ext} FS->>DB : INSERT bf_files(元数据) FS-->>FC : 返回元数据列表 FC-->>FE : 成功响应 Note over FE,GOSS : OSS 直传流程 FE->>FC : POST /file/upload-credential FC->>FS : ossCredential() FS->>GOSS : 获取上传凭证(bucket=orgId) GOSS-->>FS : 凭证信息 FS-->>FC : 凭证 FC-->>FE : 凭证(前端直传到g3oss) FE->>FC : POST /file/save-record(回调) FC->>FS : saveRecord(meta) FS->>DB : INSERT bf_files(元数据) FC-->>FE : 成功响应 ``` 图表来源 - [FileController.java:48-76](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L48-L76) - [FileService.java:54-118](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L54-L118) - [FileService.java:162-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L162-L179) ## 详细组件分析 ### 存储模式与配置 - 存储类型:通过 fms.file.storage-type 切换(当前支持 local 与 aliyun-oss 分支逻辑) - 本地存储: - 存储目录:fms.file.local.storage-dir(默认 ./data/files) - URL 前缀:fms.file.local.url-prefix(默认 /api/file/static) - OSS: - g3oss 地址:fms.file.oss.g3oss-url(用于获取凭证与删除文件) 章节来源 - [application.yaml:27-34](file://fms-api/src/main/resources/application.yaml#L27-L34) - [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69) ### 存储路径规则与目录结构 - 机构隔离:以 orgId 作为根目录,确保多租户数据隔离 - 时间分片:按年/月/日建立子目录,便于归档与清理 - 文件命名:使用 UUID 去除连字符并保留原扩展名,避免冲突且利于缓存 - 最终路径:{storageDir}/{orgId}/{yyyy}/{MM}/{dd}/{uuid.ext} - 访问 URL:{urlPrefix}/{orgId}/{yyyy}/{MM}/{dd}/{uuid.ext} ```mermaid flowchart TD Start(["开始"]) --> Org["解析 orgId"] Org --> Date["计算 yyyy/MM/dd"] Date --> Name["生成 uuid.ext"] Name --> Path["拼接路径 {orgId}/{yyyy}/{MM}/{dd}/{uuid.ext}"] Path --> Write{"本地还是OSS?"} Write --> |本地| LocalWrite["写入本地磁盘"] Write --> |OSS| OssCred["获取g3oss凭证并直传"] LocalWrite --> Meta["写入bf_files元数据"] OssCred --> Callback["前端回调save-record"] Callback --> Meta Meta --> End(["结束"]) ``` 图表来源 - [FileService.java:63-96](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L63-L96) - [FileService.java:101-118](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L101-L118) ### 文件元数据管理与持久化 - 表名:bf_files - 关键字段(根据代码推断): - subid:雪花主键,唯一标识文件记录 - father:父级 ID(可为空) - mx_filename:原始文件名 - mx_filesize:文件大小 - mx_fileext:扩展名 - mx_mapfilename:映射路径或访问 URL - mx_moduleid:所属模块 ID(可为空) - mx_cate_id:分类 ID(可为空) - mx_xh:排序字段(默认 0) - mx_sh:审核状态(默认 0) - mx_inputuser_id:录入用户 ID - mx_inputdatetime:录入时间 - 插入时机: - 本地上传:写盘成功后立即写入 bf_files - OSS 直传:前端直传完成后回调 /file/save-record 写入 bf_files - 删除时同步: - 先删存储(本地文件或 OSS),再删除 bf_files 行 章节来源 - [FileService.java:120-148](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L120-L148) - [FileService.java:184-259](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L184-L259) - [100_initialize_fms_new.sql:1-514](file://fms-api/config/migrations/100_initialize_fms_new.sql#L1-L514) ### 文件访问 URL 生成与安全访问 - 本地访问: - 通过 /file/static/{fileName} 提供下载/预览 - 服务端校验路径穿越,仅允许在 storageDir 下访问 - 设置 Content-Disposition 为 inline,便于浏览器直接预览 - OSS 访问: - 当前未实现服务端签名 URL 生成;前端直传后由 g3oss 提供服务端访问 - 可通过后续扩展增加签名 URL 能力(如带过期时间的临时链接) 章节来源 - [FileController.java:89-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L89-L111) - [FileService.java:162-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L162-L179) ### 删除流程与一致性 - 删除入口:POST /file/delete - 删除步骤: - 查询 bf_files 获取 mx_mapfilename - 根据存储类型选择删除本地文件或调用 g3oss 删除 - 删除 bf_files 记录 - 异常处理: - 若 OSS 删除失败,抛出业务异常并回滚上层事务(如有) ```mermaid sequenceDiagram participant FE as "前端" participant FC as "FileController" participant FS as "FileService" participant DB as "数据库" participant FSYS as "本地文件系统" participant GOSS as "g3oss" FE->>FC : POST /file/delete({subid}) FC->>FS : delete(subid) FS->>DB : 查询 bf_files(mx_mapfilename) alt 本地存储 FS->>FSYS : 删除对应文件 else OSS 存储 FS->>GOSS : 调用删除接口(objectName) end FS->>DB : DELETE FROM bf_files WHERE subid=? FS-->>FC : 成功 FC-->>FE : 成功响应 ``` 图表来源 - [FileController.java:78-87](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L78-L87) - [FileService.java:184-259](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L184-L259) ## 依赖关系分析 - FileController 依赖 FileStorageProperties 与 FileService - FileService 依赖 FileStorageProperties、DbUtils、DataSource、OrgContext - FileService 与外部系统交互: - 本地文件系统:NIO Files 操作 - g3oss 代理服务:HTTP 客户端发起凭证获取与删除请求 - 数据库:bf_files 表用于元数据持久化 ```mermaid classDiagram class FileController { +config() +upload() +uploadCredential() +saveRecord() +delete() +serveFile() } class FileService { +localUpload() +saveRecord() +ossCredential() +delete() -insertFileRecord() -deleteLocalFile() -deleteOssFile() } class FileStorageProperties { +getStorageType() +getLocal() +getOss() } class DataSource class DbUtils class OrgContext FileController --> FileService : "调用" FileService --> FileStorageProperties : "读取配置" FileService --> DataSource : "连接数据库" FileService --> DbUtils : "通用查询" FileService --> OrgContext : "获取orgId/userId" ``` 图表来源 - [FileController.java:28-113](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L28-L113) - [FileService.java:34-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L34-L268) - [FileStorageProperties.java:5-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L5-L69) 章节来源 - [FileController.java:1-113](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L1-L113) - [FileService.java:1-268](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L1-L268) - [FileStorageProperties.java:1-69](file://fms-api/src/main/java/cn/g3soft/fmsapi/config/FileStorageProperties.java#L1-L69) ## 性能考虑 - 连接池与超时: - application.yaml 中定义了数据库连接池参数(初始大小、最小空闲、最大活跃、等待超时、验证查询等) - HTTP 客户端连接超时设置为 10 秒,避免阻塞 - 本地 I/O: - 使用 NIO Files.copy 进行高效写入 - 路径规范化与存在性检查减少无效 IO - OSS 直传: - 前端直传减轻服务器带宽压力 - 凭证获取与回调分离,提高并发能力 - 建议: - 在高并发场景下,适当增大连接池与线程池 - 对热点文件启用 CDN 加速(结合 OSS) - 定期归档历史日期目录,降低单目录文件数量 章节来源 - [application.yaml:14-26](file://fms-api/src/main/resources/application.yaml#L14-L26) - [FileService.java:37-40](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L37-L40) ## 故障排查指南 - 上传失败: - 检查 storageDir 是否存在且可写 - 确认 orgId 上下文是否正确注入 - 查看日志中的 IOException 与 SQL 异常 - OSS 凭证获取失败: - 核对 g3ossUrl 与网络连通性 - 检查返回码是否为 200 - 删除不一致: - 若存储删除成功但 DB 删除失败,需补偿删除 DB 记录 - 若 DB 删除成功但存储残留,需手动清理或重试删除 - 静态访问 404: - 确认请求路径是否包含正确的 orgId/日期/文件名 - 检查路径穿越防护是否误拦截 章节来源 - [FileController.java:89-111](file://fms-api/src/main/java/cn/g3soft/fmsapi/controller/FileController.java#L89-L111) - [FileService.java:162-179](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L162-L179) - [FileService.java:184-259](file://fms-api/src/main/java/cn/g3soft/fmsapi/service/FileService.java#L184-L259) ## 结论 本系统实现了灵活的文件存储策略,支持本地与 OSS 两种模式,并通过 bf_files 表统一管理文件元数据。通过时间分目录与 UUID 命名保障可扩展性与唯一性,配合安全的路径校验与可选的 OSS 直传,满足常见业务需求。后续可进一步增强签名 URL、访问审计与跨域策略。 ## 附录 ### 环境变量与配置示例 - 应用端口与上下文:server.port、server.servlet.context-path - JWT 密钥与过期:fms.auth.jwt-secret、fms.auth.jwt-expiration - 数据库连接池:fms.database.pool.* - 文件存储: - fms.file.storage-type: local 或 aliyun-oss - fms.file.local.storage-dir: 本地存储根目录 - fms.file.local.url-prefix: 静态访问前缀 - fms.file.oss.g3oss-url: g3oss 服务地址 章节来源 - [application.yaml:1-34](file://fms-api/src/main/resources/application.yaml#L1-L34) ### 备份、迁移与灾难恢复策略 - 本地文件备份: - 定期打包 storageDir 下的 orgId 目录 - 建议使用增量备份与异地容灾 - 元数据备份: - 对 bf_files 表执行常规备份(数据库层面) - 迁移: - 通过迁移脚本维护表结构与数据一致性 - 在变更存储策略时,优先保证 bf_files 与物理文件的一致性 - 灾难恢复: - 恢复 bf_files 与对应文件目录 - 校验文件完整性(大小、哈希) - 必要时重建索引与统计信息 章节来源 - [100_initialize_fms_new.sql:1-514](file://fms-api/config/migrations/100_initialize_fms_new.sql#L1-L514)