Files
docs/阿里云OSS文件中心接入设计与评审.md
2026-09-17 10:57:21 +08:00

25 KiB
Raw Permalink Blame History

阿里云 OSS 文件中心接入设计与评审

1. 文档状态

  • 适用范围:Admin、Client、国内 PDA、海外 PDA 产生或读取的图片、单证、凭证、导出文件和现场作业证据。
  • 当前结论:方案有条件通过。完成本文“P0 前置项”后可以进入代码实施。
  • 本文是 File Service 接入 OSS 的详细设计;云资源购买、生产规格与通用上线流程继续参考《阿里云生产环境选型压测与上线手册》;国内/海外部署边界继续参考《双区域部署与 Nacos 规划》和《跨区域认证与服务通信契约》。
  • 本文只冻结设计和验收门禁,不授权创建云资源、修改 Nacos、执行数据库脚本或部署环境。

2. 给非技术人员的结论

第一阶段不改 Admin、Client 和 PDA 的使用方式。用户仍在原页面选择文件或拍照,系统仍通过原业务接口上传;唯一变化是 File Service 不再把文件写到服务器磁盘,而是写入阿里云 OSS。

Admin / Client / PDA
        │ 原有上传、下载接口不变
        ▼
国内业务服务 / 海外业务服务
        │ 校验订单、仓库、客户范围和操作权限
        ▼
本区域 File Service
        │ 校验格式、大小,计算摘要,登记文件元数据
        ▼
本区域私有 OSS Bucket

这样做的直接收益:

  • File Service 可以运行多个实例,不再依赖某一台服务器的 /data/files;
  • PDA、Admin、Client 第一阶段不需要适配 OSS SDK、临时凭证或新上传协议;
  • 业务权限继续由业务服务和 File Service 控制,Bucket 不公开;
  • 国内文件留在国内区域,海外现场照片和海外业务文件留在海外区域;
  • 以后如果海外 PDA 上传速度成为瓶颈,可以在不改变文件模型的前提下增加 OSS 短时签名直传。

3. 当前实现盘点

3.1 已具备的基础

file_object 已保存以下字段,数据模型天然支持对象存储:

  • object_key:OSS Object Key;
  • storage_provider:存储提供方;
  • bucket_name:实际 Bucket;
  • content_type、size_bytes、sha256:内容校验和下载响应所需元数据;
  • tenant_id、business_type、business_id:客户范围及业务归属。

业务服务通常只保存稳定 fileId,具体文件内容由 File Service 管理,这个边界应保持不变。

3.2 当前问题

  1. BusinessFileStorage、ProductImageStorage、MeasurementImageStorage、PriceBookFileStorage、资质文件和导出文件分别直接操作本地路径,重复实现目录、读写和删除逻辑。
  2. 文件元数据仍固定写入 storage_provider=LOCAL,bucket_name 也只是逻辑目录名,不是真实 Bucket。
  3. 多个下载入口使用 Files.readAllBytes 和 ResponseEntity<byte[]>,50 MiB 文件会完整进入 JVM 内存,不适合多实例并发下载。
  4. ExportStorage.PendingFile 暴露本地 Path,接口本身与本地文件系统耦合,无法直接替换为 OSS。
  5. 海外 Nacos 已出现 file.storage.provider=s3 配置,但当前 Java 代码未读取该配置;配置命名也与国内的 baiyue.file.storage-root 不一致。
  6. 海外 Gateway 已配置 lb://file-service 路由,海外 Nacos 也有 file-service.yml,但现有双区域部署清单和海外统一 SQL 未包含海外 File Service 及 baiyue_file。生产环境若照现状部署,海外路由无法形成区域内闭环。

4. 目标与非目标

4.1 本次目标

  • 国内和海外部署单元分别拥有本区域 File Service、本区域 baiyue_file 和本区域私有 OSS Bucket;
  • 收拢为一个通用对象存储适配层,业务 Storage 只负责格式、大小、用途和业务目录规则;
  • 第一阶段保持所有外部上传、下载路径以及 PDA 操作方式不变;
  • 文件使用稳定 fileId 关联业务,不向业务表传播 OSS URL;
  • 上传、下载、删除、导出过期清理支持 OSS;
  • File Service 可以安全扩容到多实例;
  • 凭证使用 RAM 角色或临时凭证,不保存长期 AccessKey;
  • 对上传失败、元数据失败、超时和孤儿对象形成明确处理规则;
  • 开发环境继续支持本地存储,自动化测试不强依赖真实 OSS。

4.2 本次不做

  • 第一阶段不让浏览器或 PDA 直接访问 OSS;
  • 不把 Bucket 设置为公共读,不生成永久 URL;
  • 不引入 CDN、图片处理、媒体转码或病毒扫描平台;
  • 不自动迁移当前开发/测试磁盘文件,现有环境数据按项目约定重新上传;
  • 不做本地磁盘与 OSS 双写、双读或运行时降级;
  • 不默认启用国内、海外 Bucket 的跨区域复制;确有容灾或合规要求时另行评审;
  • 不因为 OSS 接入改变业务文件的权限、保留期或业务状态规则。

5. 已冻结的架构决策

5.1 国内、海外各自闭环

部署单元 文件入口 元数据数据库 对象存储 访问方式
国内 国内 Gateway、Firstleg、Order、Finance 等 国内 RDS 中的 baiyue_file 与国内 File Service 同地域的私有 Bucket File Service 优先使用 OSS 内网 Endpoint
海外 gateway-overseas、Lastleg 等 海外 RDS 中独立的 baiyue_file 与海外 File Service 同地域的私有 Bucket File Service 优先使用 OSS 内网 Endpoint

同名 Schema 可以存在于不同区域的独立 RDS 中,它们不是共享数据库。任何服务不得跨区域直连另一地区的 baiyue_file、Nacos 或 OSS 内网 Endpoint。

海外 Admin/Client 页面访问海外文件时,仍走海外 API Base 和海外 Gateway;不把文件请求绕回国内 Gateway。跨区域业务事件只传递稳定业务事实或明确的文件引用,不携带 OSS 长期凭证。

5.2 第一阶段统一采用“服务端中转”

选择服务端中转的原因:

  • 当前主要文件上限为 5、10、50 MiB,没有必须立即采用分片直传的大文件;
  • 现有 PDA 已具备上传进度、Token 刷新和重试链路;
  • 业务服务必须先校验任务、仓库、客户范围和业务状态;
  • File Service 需要校验图片/PDF/XLSX 实际内容并计算 SHA-256;
  • 可以不配置浏览器 OSS CORS,减少上线变量;
  • 所有外部 API 第一阶段保持不变,改造范围集中在 File Service 和部署配置。

第二阶段只有在真实监控证明海外上传存在明显耗时或失败率问题时,才启用短时签名直传,见第 12 节。

5.3 Bucket 不是权限系统

所有 Bucket 固定为私有并开启“阻止公共访问”。业务可见性继续由业务服务和 File Service 按登录用户、权限、客户范围、业务类型和业务 ID 判断。知道 Object Key 或历史 URL 不代表拥有读取权限。

5.4 文件对象不可变

  • 每次上传生成新的 UUID Object Key;
  • 替换文件时创建新对象和新元数据,再按业务规则标记旧记录失效;
  • 上传请求启用禁止同名覆盖;
  • 原始文件名只保存为元数据和下载文件名,不进入 Object Key;
  • 现场证据、财务凭证和业务单证默认不执行物理覆盖。

6. OSS 资源设计

6.1 Bucket 数量与命名

每个环境、每个部署区域至少一个独立文件 Bucket。示例仅表示规则,实际名称需满足 OSS 全局唯一:

baiyue-<environment>-cn-files-<suffix>
baiyue-<environment>-overseas-files-<suffix>

开发、测试、生产不得共用 Bucket。一个 File Service 实例只配置本部署单元的一个业务 Bucket,不在代码中根据业务类型跨区域选择 Bucket。

6.2 基础设置

项目 决策
地域 与本区域 File Service/ECS 同地域
存储类型 初始使用标准存储
数据冗余 生产优先 ZRS,测试可用 LRS;最终按目标地域产品可用性确认
ACL Private
阻止公共访问 开启
传输协议 HTTPS;区域内服务优先内网 Endpoint
服务端加密 默认 SSE-OSS/AES256;明确合规要求时使用 SSE-KMS
版本控制 生产开启;配合历史版本生命周期控制成本
访问日志 开启并发送到独立日志 Bucket 或日志服务,日志对象不能写回同一业务前缀造成递归
CORS 第一阶段不配置;启用 Web 直传时只允许正式域名和必要方法/Header

6.3 Object Key

Object Key 统一由 File Service 生成,不接受终端传入完整 Key:

evidence/<businessType>/<ownerScope>/<businessId>/<uuid>.<ext>
documents/<businessType>/<ownerScope>/<businessId>/<uuid>.<ext>
product-images/<customerId>/<uuid>.<ext>
qualifications/<customerId>/<uuid>.<ext>
exports/<taskId>/<uuid>.xlsx
temp/<uploadId>/<uuid>.part

约束:

  • ownerScope 使用平台范围 0 或真实客户范围键;
  • Object Key 不包含手机号、身份证号、客户名称、原始文件名等个人或业务敏感信息;
  • businessType 必须来自 File Service 白名单,不能由调用方任意创建目录;
  • Bucket 已区分环境和区域,Object Key 不重复拼接环境或地域;
  • 数据库保存完整 Object Key、真实 Bucket 名和 storage_provider=OSS。

6.4 生命周期

前缀 初始规则
exports/ 与产品当前导出保留期一致,默认 7 天后删除
temp/ 7 天后删除,只用于未确认的分片或暂存对象
未完成分片上传 7 天后自动终止并清理
evidence/、documents/、qualifications/ 未确认法务、财务和审计保留期前不自动删除当前版本
历史版本 保留期需单独确认,不能用当前对象生命周期规则直接覆盖

生命周期配置必须先在测试 Bucket 验证。更新规则具有覆盖语义,发布前导出旧规则并进行差异复核。

7. File Service 代码设计

7.1 通用存储端口

新增不包含业务规则的通用接口,示意如下:

public interface ObjectStorage {
    StoredObject put(PutObjectCommand command);
    StoredObject head(String objectKey);
    Resource get(String objectKey);
    boolean delete(String objectKey);
    Page<String> list(String prefix, String continuationToken);
}

实现:

  • LocalObjectStorage:local profile 和自动化测试使用;
  • OssObjectStorage:非本地环境使用 OSS Java SDK V2;
  • 通过 baiyue.file.storage.provider=local|oss 选择,非法值或 OSS 必填配置缺失时启动失败;
  • 不保留 s3 假实现或“配置为 OSS、失败后写本地”的降级分支。

put 输入包含 Object Key、内容流、长度、Content-Type、是否禁止覆盖和可选服务端加密头;输出包含 ETag、长度、OSS Request ID 等诊断信息。业务层计算并保存 SHA-256,ETag 不作为内容摘要使用。

7.2 业务 Storage 保留的职责

现有各类业务 Storage 不再直接调用 Files.*,但继续负责:

  • 文件大小上限;
  • 声明 Content-Type 与实际文件签名一致性;
  • 图片可解码性;
  • PDF、XLSX 等格式检查;
  • 原始文件名清理;
  • businessType、客户范围和业务 ID 对应的 Object Key;
  • 上传完成后的 file_object 元数据登记。

第一阶段不强制修改资质文件对外 URL 形态;File Service 可以继续使用现有受鉴权下载路径,在内部把文件名映射为 OSS Object Key。后续若把资质统一成 fileId,必须连同 IAM DTO、调用端和审核流程作为独立契约变更处理。

7.3 上传一致性

正式顺序固定为:

  1. 业务服务完成订单、任务、仓库、操作人和客户范围校验;
  2. File Service 校验类型、大小和内容签名;
  3. File Service 生成唯一 Object Key;
  4. 流式上传 OSS,同时计算 SHA-256;
  5. OSS 明确返回成功后插入 file_object;
  6. 返回稳定 fileId,业务服务再绑定业务记录。

失败规则:

  • OSS 明确失败:不插入元数据,返回存储不可用;
  • OSS 成功、数据库明确失败:尝试删除刚上传对象;删除失败则记录对象键和 OSS Request ID,交由孤儿对象核对;
  • 网络超时导致上传结果未知:不得立即盲删或用相同 Key 覆盖;通过 HEAD 核对唯一 Key,确认对象存在后可重试元数据登记;
  • 业务绑定失败:文件元数据保留为未绑定或按现有业务规则处理,不伪造成功绑定;
  • 所有重试使用同一幂等意图,但对象写入禁止覆盖。

孤儿清理只处理超过安全窗口、数据库无引用且状态可以确定的对象。状态未知的对象保留并告警,不因清理任务制造数据丢失。

7.4 下载

第一阶段保持原 API 和权限判断,File Service 从 OSS 流式读取并流式写入 HTTP 响应:

  • 不使用 readAllBytes;
  • 保留正确的 Content-Type、UTF-8 文件名和 Content-Length;
  • 业务文件响应继续使用 Cache-Control: no-store;
  • OSS 404 映射为文件内容不存在,5xx/超时映射为存储暂不可用;
  • 日志记录 fileId、Object Key 摘要、OSS Request ID、业务 requestId/traceId,不记录完整预签名 URL或敏感原始文件名;
  • 需要缩略图的入口仍由 File Service 读取原图并生成,不直接公开原图 Object。

7.5 删除和替换

  • 一般业务证据采用逻辑失效,不因页面“移除”立即物理删除;
  • 明确允许替换的资质文件创建新对象,业务元数据切换成功后再清理旧对象;
  • 临时文件、失败上传和到期导出允许物理删除;
  • 删除对象失败不伪造成功,导出过期任务下一轮继续重试;
  • Bucket 开启版本控制后,删除标记和历史版本的实际占用必须进入成本监控。

7.6 导出文件

现有 ExportStorage.PendingFile 暴露本地 Path,需调整为与存储实现无关的“生成后提交”语义。第一阶段最简单可靠的做法:

  1. 在容器临时目录生成最大 50 MiB 的 XLSX;
  2. 关闭工作簿和输出流;
  3. 上传 exports/<taskId>/<uuid>.xlsx 到 OSS;
  4. OSS 成功后把导出任务标记为 SUCCEEDED;
  5. 无论成功失败都清理本地临时文件。

临时目录不是业务存储,不使用共享卷,也不能作为下载回退来源。后续如果导出上限显著增加,再评估管道流或 OSS 分片上传。

8. 配置与凭证

8.1 统一配置

国内、海外 Nacos 使用同一个配置模型,由不同 Namespace 注入不同值:

baiyue:
  file:
    storage:
      provider: ${FILE_STORAGE_PROVIDER:local}
      region: ${FILE_STORAGE_REGION:}
      endpoint: ${FILE_STORAGE_ENDPOINT:}
      internal-endpoint: ${FILE_STORAGE_INTERNAL_ENDPOINT:true}
      bucket: ${FILE_STORAGE_BUCKET:}
      prefix: ${FILE_STORAGE_PREFIX:}
      connect-timeout-ms: ${FILE_STORAGE_CONNECT_TIMEOUT_MS:3000}
      read-timeout-ms: ${FILE_STORAGE_READ_TIMEOUT_MS:30000}
      max-attempts: ${FILE_STORAGE_MAX_ATTEMPTS:3}

约束:

  • local provider 只允许 local/test profile;
  • 非本地环境必须显式配置 provider=oss、Region、Endpoint 和 Bucket;
  • 删除当前未被代码消费的 file.storage.provider=s3 配置,避免产生“已经接入”的错觉;
  • Bucket、Endpoint、Region 属于非敏感配置;长期 AccessKey/Secret 不得写入源码、Nacos、镜像或 Jenkins 日志。

8.2 凭证

生产优先给 ECS、ECI 或 ACK 工作负载绑定仅供 File Service 使用的 RAM 角色,由凭证提供链自动获取和刷新临时凭证。本地真实 OSS 联调才允许通过进程环境变量注入测试账号的短期凭证。

最小权限限定到当前 Bucket 及业务前缀,按实现实际需要授予:

  • 上传对象;
  • 读取对象及元数据;
  • 删除允许物理删除的对象;
  • 分页列举指定前缀,用于到期导出和孤儿核对;
  • 若使用 SSE-KMS,额外授予指定密钥所需的最小 KMS 权限。

不得给应用绑定账号级 AliyunOSSFullAccess。

9. 可观测性与健康检查

新增指标至少包括:

  • 上传/下载/删除请求量、成功率和耗时;
  • 按文件类别统计上传字节和失败数;
  • OSS 4xx、5xx、超时、限流和重试次数;
  • 元数据写入失败及孤儿对象待核对数量;
  • 导出临时文件、到期删除失败和历史版本容量;
  • 国内/海外分别统计,不混为一个区域指标。

健康检查分层:

  • Liveness 只证明进程存活,不依赖 OSS;
  • Readiness 检查配置完整、凭证可用以及目标 Bucket 可访问;
  • 发布验收执行一次测试前缀的上传、HEAD、下载校验和删除闭环;
  • 不在每次健康检查中持续创建新对象。

告警应至少覆盖连续上传失败、下载错误率、凭证过期/无权限、Bucket 不可达和孤儿对象持续增长。

10. 实施拆分

工作包 A:存储适配层

  • 引入 OSS Java SDK V2 和凭证提供链;
  • 新增 ObjectStorage、本地实现、OSS 实现及配置校验;
  • 补充 Put/Get/Head/Delete/List 的单元测试和错误映射测试。

工作包 B:业务文件收敛

  • 改造 Business、Product Image、Measurement、Loading Evidence、Price Book、Qualification 等 Storage;
  • 所有新元数据写入真实 storage_provider 和 bucket_name;
  • 下载改为流式响应;
  • 保持外部 API、权限及 PDA 调用路径不变。

工作包 C:导出文件

  • 重构 ExportStorage,去除业务层对永久本地 Path 的依赖;
  • 使用容器临时文件生成并提交 OSS;
  • 验证任务成功、失败、过期和清理重试。

工作包 D:双区域部署

  • 国内、海外分别创建 File Service 部署项、baiyue_file 初始化基线和 OSS 配置;
  • 海外统一 SQL 纳入 File Schema;
  • 国内/海外 Gateway 分别只路由本区域 File Service;
  • 更新 Nacos 模板、部署清单、健康检查和最小权限 RAM 角色;
  • 运行网关路由合同、Mapper 合同和受影响服务测试。

工作包 E:真实 OSS 验收

  • 使用隔离测试 Bucket;
  • 完成上传、下载、格式拒绝、大小拒绝、客户隔离、平台范围、替换、导出过期和权限拒绝场景;
  • 验证 File Service 双实例任一实例均能读取另一实例上传的对象;
  • 验证 OSS 暂时不可用、数据库失败和结果未知时不会返回伪成功;
  • 验证国内文件没有写入海外 Bucket,海外文件没有写入国内 Bucket。

11. 发布与回滚

项目当前未上线,不迁移开发/测试历史文件,也不建设新旧存储兼容层。

发布步骤:

  1. 在隔离环境创建私有测试 Bucket 和 RAM 角色;
  2. 部署使用 OSS 的 File Service,但暂不切换正式 Gateway 流量;
  3. 执行真实 OSS 验收和双实例读取验证;
  4. 更新最新数据库初始化基线,清空或重新初始化测试文件数据;
  5. 切换本环境 File Service;
  6. 重新上传测试资质、商品图片、PDA 证据和业务附件;
  7. 通过后再移除正式部署对持久化 /data/files 卷的依赖。

回滚原则:

  • 验收未通过时停止晋级,不通过运行时双写维持两套事实;
  • 在尚未产生需保留数据的测试环境,可回滚镜像并重新初始化测试文件数据;
  • 上线后若已经产生真实业务文件,不允许简单回滚到本地磁盘模式,应先恢复 OSS 访问或发布修复版本;
  • 不删除故障期间状态未知的 OSS 对象。

12. 第二阶段:短时签名直传的启用条件

只有满足以下任一条件才进入第二阶段:

  • 海外 PDA 上传 P95/P99 明显超出业务可接受值;
  • 真实网络导致中转上传失败率持续偏高;
  • 单文件上限提高到需要分片或断点续传;
  • File Service 公网带宽或中转流量成为明确瓶颈。

届时流程必须是:

终端申请上传意图
  → 业务服务校验任务/客户/仓库
  → File Service 分配唯一 Object Key 和短时受限签名
  → 终端直传 OSS
  → 终端提交完成确认
  → File Service HEAD/内容规则校验并登记 fileId
  → 业务服务绑定 fileId

直传凭证必须限制 Bucket、Object Key、方法、大小、Content-Type 和有效期。Web 直传再配置精确 CORS;PDA 不持有长期 AccessKey。传输加速只能在真实跨境测速证明收益且预算允许时启用,不作为第一阶段前置条件。

13. 验收标准

  • Admin、Client、国内 PDA、海外 PDA 原上传和下载接口无需改地址;
  • 国内与海外 File Service、File DB、Bucket 在本区域形成闭环;
  • 非本地环境启动时若仍为 local 或 OSS 配置缺失,服务明确失败;
  • 所有正式文件对象写入 OSS,数据库记录真实 provider、Bucket 和 Object Key;
  • 文件格式、大小、业务类型、客户范围和权限校验不弱于改造前;
  • 下载为流式传输,不再整体读入 JVM 内存;
  • OSS 或数据库失败时不返回上传成功;
  • 结果未知时能够用唯一 Key 和 Request ID 核对,不盲删对象;
  • File Service 双实例可交叉读写;
  • 长期 AccessKey 不存在于源码、Nacos、镜像和日志;
  • Bucket 私有、阻止公共访问、HTTPS、加密、生命周期和日志配置通过检查;
  • 国内/海外文件路由隔离通过真实验收;
  • 本地测试、模块测试、网关路由合同和 Mapper 合同通过;
  • 真实 OSS 验收通过后才移除 /data/files 持久化卷。

14. 方案评审

14.1 评审维度

本次从现有代码适配性、业务权限、数据一致性、双区域部署、PDA 网络、安全、扩容能力、可测试性和运维成本九个维度评审。

14.2 评审发现

级别 发现 结论与处理
P0 海外 Gateway 和 Nacos 已假定存在 File Service,但海外部署清单及统一 SQL 没有 File Service/baiyue_file 实施前必须补齐海外 File Service、独立 File DB 和部署基线,否则海外文件链路不能闭环
P0 现有海外 file.storage.provider=s3 配置没有实现消费方 必须删除假配置并统一为 baiyue.file.storage.*,不能把填写环境变量当作已接入 OSS
P0 当前多个 Storage 直接操作本地文件,无法通过挂载或单点替换安全扩容 必须先建立统一对象存储端口,再逐类收敛;不得只修改其中一个文件类型
P1 多个下载入口整体读取 byte[] OSS 改造同时改为流式下载,避免 50 MiB 文件并发放大内存
P1 ExportStorage 暴露 Path 采用“临时生成、关闭、上传提交、清理”的第一阶段实现
P1 长期 AK 配置容易被误放入 Nacos 生产只使用 RAM 角色/临时凭证,配置模型不定义 AK/Secret 字段
P1 上传成功、数据库失败会形成孤儿对象 使用唯一 Key、明确结果删除、未知结果 HEAD 核对及安全窗口清理
P1 国内/海外单 Bucket 会导致跨境依赖、延迟和故障耦合 坚持每区域独立 Bucket 和 File Service,不做业务服务跨区读写
P2 立即做 PDA/Web 直传会同时引入 STS、CORS、确认接口和内容校验变化 第一阶段服务端中转;以真实指标决定第二阶段
P2 OSS ETag 可能被误当作文件哈希 继续由 File Service 计算 SHA-256,ETag 只用于传输诊断

14.3 评审结论

方案在完成以下 P0 条件后可以实施:

  1. 明确海外区域正式部署 File Service,并在海外独立 RDS 建立 baiyue_file;
  2. 国内、海外统一使用 baiyue.file.storage.* 配置模型,删除未实现的 S3 假配置;
  3. 所有文件类别一次性收敛到统一对象存储端口,不保留生产本地磁盘回退;
  4. 第一阶段保持终端接口不变,先完成服务端 OSS 中转和真实双区域验收。

满足以上条件后,本方案在当前文件规模和系统阶段下,比直接让所有终端接 OSS 更简单,改动面更小,也更容易保持权限和业务一致性。最终评审状态:有条件通过。

15. 官方参考