Skip to content

文件存储接入与使用指南 ​

sz-admin 用 “场景 scenes ” 管理文件。业务上传时提交 sceneCode,场景决定文件存到本地磁盘还是对象存储、如何命名,以及后续怎样访问。

第一次接入,可以按下面四步完成:

  1. 选访问方式:公开图片用 DIRECT,临时分享用 PRESIGNED,需要业务权限的附件用 PROTECTED。
  2. 配置存储:在接入案例中选择 LOCAL、RustFS、MinIO、腾讯云、阿里云或七牛云,合并到当前环境的 oss.yml。
  3. 上传一个文件:按上传与验证检查文件位置和返回地址。
  4. 接入业务页面:按业务附件接入保存资源引用、配置下载接口和前端组件。

已有项目只需调整部分配置时,可以直接查看多场景配置、寻址风格和 base-url。

选择存储和访问方式 ​

存储类型 ​

类型适合场景必要配置
LOCAL单机或共享磁盘、由 Nginx 提供静态文件、由业务接口读取的附件场景 path;根目录 sz.resource.root 默认 ./data,DIRECT 还要配置 base-url
OSSMinIO、RustFS、阿里云 OSS、腾讯云 COS、七牛云等 S3 兼容存储一组完整的 sz.oss 默认连接;场景可以复用或覆盖

项目只使用 LOCAL 时可以不配置 sz.oss。只要启用 OSS,就应保留一组可用的全局 OSS 连接;需要连接其他实例的场景再填写自己的连接字段。

访问模式 ​

serve-mode适合场景返回的 accessUrl谁能访问
DIRECT头像、Logo、公开富文本图片稳定公开地址拿到地址的人都可以访问
PRESIGNED私有 OSS 中无需业务权限判断的短时展示有效期内的签名地址拿到签名地址的人在有效期内可以访问
PROTECTED需要业务权限判断的合同、模板、附件null通过所属业务接口鉴权后访问

省略 serve-mode 时默认是 PROTECTED。公开文件必须显式配置 DIRECT。PRESIGNED 只限制链接有效期,不会检查用户是否有权查看某条业务记录;需要业务权限的附件使用 PROTECTED。

访问模式取决于实际场景配置。当前仓库的配置中,template.excel 和 teacher.attachment 在 local 环境仍是 DIRECT,在 dev、preview、prod 中是 PROTECTED;system.protected 在四个环境中都是 PROTECTED。不要仅凭场景名称判断文件是否受保护。

DIRECT 只让应用生成公开地址,不会自动开放 bucket 或创建静态路由。公开地址能否访问,还取决于存储策略和代理配置。

各类存储接入案例 ​

选择实际使用的存储,先完成一个场景的上传和读取,再接入业务页面。同一个应用也可以同时使用 LOCAL 和多个 OSS 服务,配置方法见多场景配置。

配置前先准备什么 ​

云厂商案例按官方 S3 接入要求与当前配置字段编写,仍需在自己的账号和 bucket 中完成下文验证。

  1. 找到当前环境的 config/{local,dev,preview,prod}/oss.yml。下面示例合并到这份文件中,保留已有业务场景;已有相同 code 时修改原条目,不要重复追加。
  2. 使用 OSS 时,先在存储服务中创建 bucket,准备有相应对象读写权限的访问密钥。框架不会自动创建 bucket。
  3. 示例中的 ${OSS_ACCESS_KEY} 等值是环境变量引用,需在应用的运行环境中设置。使用哪个厂商,就填写哪家的密钥。
  4. 修改 YAML 后重启应用。默认场景配置在启动时加载;启动成功后,还要检查实际上传和读取。

sz.oss 是 OSS 公共配置,sz.resource.scenes 是业务场景配置。一个场景使用公共连接时,只写自己的用途和访问方式即可,不必重复填写 endpoint、bucket、region 和寻址风格。

本文使用 access-key、secret-key、bucket-name 写法;现有配置中的 accessKey、secretKey、bucketName 同样有效。

LOCAL:文件存到本地磁盘 ​

适合单机部署,或已经准备好共享磁盘的项目。先为应用准备可读写的目录;容器部署时,将该目录挂载为持久化存储。只使用 LOCAL 时不需要配置 sz.oss。

下面把公开图片放进 article,合同附件放进 contracts:

yaml
sz:
  resource:
    root: ./data # 文件根目录;相对于应用工作目录,也可写 /data 或 E:/sz-data
    scenes:
      - code: article.image # 上传时传入的 sceneCode
        name: 文章图片
        type: LOCAL # 使用本地磁盘
        serve-mode: DIRECT # 公开访问
        path: article # 文件保存在 root/article 下
        base-url: /api/admin/resource/file/article # 开发时使用后端公开文件路由
        naming: UUID # 用随机文件名避免重名
        path-strategy: DATE # 按 yyyyMMdd 分目录
        exts: [jpg, jpeg, png, gif, webp] # 这个场景允许的扩展名
        max-size: 5 # 单文件上限,单位 MB

      - code: contract.attachment
        name: 合同附件
        type: LOCAL
        serve-mode: PROTECTED # 保存业务记录后,通过业务接口下载
        path: contracts # 与公开图片使用不同目录
        naming: UUID
        path-strategy: BIZ_DATE # 业务路径段 + 日期目录
        exts: [pdf, doc, docx]
        max-size: 10

启动后,用 article.image 上传一张图片。假设返回 objectKey=article/20261010/abc.png,应能在 ./data/article/20261010/abc.png 找到文件,返回地址为 /api/admin/resource/file/article/20261010/abc.png。这里的文件名仅用于说明路径,实际由 UUID 生成。

该相对地址由浏览器通过当前站点访问,前后端分开部署时,要让站点的 /api 代理指向后端。Java 路由中的 article 匹配场景 base-url 的最后一段;不同公开场景不要使用相同的末段,否则无法唯一匹配,会返回 404。

生产环境用 Nginx 提供静态文件时,可以把 base-url 改成 /resource/article,将这个 URL 前缀映射到同一个图片目录。例如,应用将 root 配为 /data,Nginx 也能读取 /data/article/ 时,在站点的 server 中加入:

nginx
location /resource/article/ {
    alias /data/article/;
    autoindex off;
}

此时 base-url 写 /resource/article;文件地址就是 /resource/article/20261010/abc.png。Nginx 和应用运行在不同容器时,两边必须挂载同一份文件。只开放 article 目录,不能把整个 /data 或 contracts 目录一起公开。

contract.attachment 上传成功时 accessUrl 为 null,接下来按业务附件接入保存引用并提供受保护下载。

LOCAL 不支持 PRESIGNED,也不能填写 bucket、endpoint、region、寻址风格等 OSS 字段。多个场景使用同一个 path 时,不会因 code 不同自动隔离;多副本部署应使用共享存储或改用 OSS。框架会检查路径穿越、跨场景目录和指向 root 外部的符号链接。

RustFS:通过 S3 接口接入 ​

先部署好 RustFS,创建私有 bucket documents,并取得 S3 API 地址和访问密钥。endpoint 填 S3 API 地址,不填管理控制台页面地址。

yaml
sz:
  oss:
    provider: RUSTFS # 使用 RustFS
    endpoint: https://storage.example.com # RustFS 的 S3 API 地址,不带 bucket
    access-key: ${RUSTFS_ACCESS_KEY} # 访问密钥 ID
    secret-key: ${RUSTFS_SECRET_KEY} # 访问密钥 Secret
    bucket-name: documents # 默认 bucket,需提前创建
    region: us-east-1 # 按 RustFS 服务端设置填写
    # PATH:对象地址如 https://storage.example.com/documents/20261010/report.pdf
    addressing-style: PATH # 可选 PATH、VIRTUAL_HOSTED;常规部署使用 PATH

  resource:
    scenes:
      - code: rustfs.document
        name: RustFS 文档
        type: OSS
        serve-mode: PRESIGNED # 私有桶,返回临时签名地址
        expire: 900 # 地址有效期,单位秒
        path-strategy: DATE # 按日期分目录,文件名默认使用 UUID

重启后,用 rustfs.document 上传一份 PDF。在 RustFS 的 documents bucket 中检查对象,再打开返回的 accessUrl。签名地址需要能从浏览器访问,因此不能填写仅后端可达的容器服务名。

如果已有虚拟主机部署,可以改用 VIRTUAL_HOSTED,并同时配置 RustFS 服务端、DNS 和证书,见寻址风格。仅修改应用中的这一行,不会完成服务端配置。

MinIO:使用现有 S3 服务 ​

先创建私有 bucket documents,准备 S3 API 地址和访问密钥。下面使用 PATH 风格,适合通过固定域名或 IP 访问的部署:

yaml
sz:
  oss:
    provider: MINIO
    endpoint: https://minio.example.com # S3 API 地址,不是控制台地址
    access-key: ${MINIO_ACCESS_KEY}
    secret-key: ${MINIO_SECRET_KEY}
    bucket-name: documents # 已创建的 bucket
    region: us-east-1 # 与服务端配置一致
    addressing-style: PATH # bucket 位于路径中,如 https://minio.example.com/documents/report.pdf

  resource:
    scenes:
      - code: minio.document
        name: MinIO 文档
        type: OSS
        serve-mode: PRESIGNED # 临时签名访问
        expire: 900 # 单位秒
        path-strategy: DATE

重启后,用 minio.document 上传 PDF,检查 documents 中的对象并打开签名地址。MinIO 与 RustFS 都通过项目现有的 S3 客户端接入;更换存储时,要分别更新连接配置并处理历史文件,不能只更改 provider。

腾讯云 COS:注意地域和完整桶名 ​

在 COS 控制台创建私有桶,记录地域和完整桶名;在访问管理中准备有目标桶访问权限的 SecretId、SecretKey。下面以广州地域为例,按腾讯云 S3 兼容配置填写:

yaml
sz:
  oss:
    provider: TENCENT
    endpoint: https://cos.ap-guangzhou.myqcloud.com # 广州地域服务地址,不带 bucket
    access-key: ${TENCENT_SECRET_ID} # 腾讯云 SecretId
    secret-key: ${TENCENT_SECRET_KEY} # 腾讯云 SecretKey
    bucket-name: documents-1250000000 # 替换为真实完整桶名,包含 APPID
    region: ap-guangzhou # 与 bucket 所在地域一致
    # 对象地址如 https://documents-1250000000.cos.ap-guangzhou.myqcloud.com/report.pdf
    addressing-style: VIRTUAL_HOSTED # bucket 位于域名前

  resource:
    scenes:
      - code: tencent.document
        name: 腾讯云文档
        type: OSS
        serve-mode: PRESIGNED # 私有桶临时签名访问
        expire: 900 # 有效期 15 分钟
        path-strategy: DATE

重启后,用 tencent.document 上传 PDF,检查控制台对象和返回的签名地址。改用其他地域时,endpoint 与 region 一起修改;只改 region 不会切换服务地址。新建 COS 桶使用虚拟主机风格,不能直接沿用自建存储的 PATH 配置。

阿里云 OSS:使用 S3 兼容 endpoint ​

在 OSS 控制台创建私有 bucket,记录桶名和地域,并准备具有目标桶权限的 RAM AccessKey。项目使用 AWS SDK,因此本例采用阿里云官方的 S3 兼容 endpoint。

yaml
sz:
  oss:
    provider: ALIYUN
    endpoint: https://s3.oss-cn-hangzhou.aliyuncs.com # 杭州地域的 S3 兼容地址
    access-key: ${ALIYUN_ACCESS_KEY_ID} # RAM AccessKey ID
    secret-key: ${ALIYUN_ACCESS_KEY_SECRET} # RAM AccessKey Secret
    bucket-name: ${ALIYUN_BUCKET} # 已创建的 OSS bucket 名称
    region: aws-global # 本例按阿里官方 AWS Java SDK 2.x 的 Region.AWS_GLOBAL 填写
    # 对象地址如 https://my-documents.s3.oss-cn-hangzhou.aliyuncs.com/report.pdf
    addressing-style: VIRTUAL_HOSTED # OSS 使用虚拟主机风格

  resource:
    scenes:
      - code: aliyun.document
        name: 阿里云文档
        type: OSS
        serve-mode: PRESIGNED # 生成 S3 临时签名地址,接入时需实际验证
        expire: 900 # 有效期 15 分钟
        path-strategy: DATE

这里的 aws-global 对应阿里云 AWS Java SDK 2.x 示例;桶所在地域仍体现在 endpoint 的 cn-hangzhou 中。换地域时修改 endpoint,不能把其他语言 SDK 的 region 写法直接套用到这份示例。寻址使用 VIRTUAL_HOSTED,详见 OSS 与 S3 的兼容差异。

重启后,先用 aliyun.document 验证小文件上传和签名下载,再测试实际业务需要的文件大小。官方示例使用同步客户端,当前项目上传使用 CRT 异步客户端;此配置还需要在真实 bucket 中验证上传、分片上传和预签名。若出现 aws-chunked 相关错误,应检查 SDK 传输兼容性,不能仅靠更换 region 或密钥解决。

七牛云:使用 S3 空间名和 S3 服务域名 ​

在七牛控制台创建私有空间,在“空间概览”取得 S3 空间名,再准备 AccessKey、SecretKey。S3 空间名可能与普通空间名称不同,请以控制台显示为准。下面以华东浙江为例,地域与域名参见七牛 S3 服务域名。

yaml
sz:
  oss:
    provider: QINIU
    endpoint: https://s3.cn-east-1.qiniucs.com # S3 API 地址,不是上传域名或 CDN 域名
    access-key: ${QINIU_ACCESS_KEY}
    secret-key: ${QINIU_SECRET_KEY}
    bucket-name: ${QINIU_S3_BUCKET} # 空间概览中的 S3 空间名
    region: cn-east-1 # 对应空间地域,与上面的 endpoint 配套
    # 对象地址如 https://my-documents.s3.cn-east-1.qiniucs.com/report.pdf
    addressing-style: VIRTUAL_HOSTED # 七牛也支持 PATH,本例采用虚拟主机风格

  resource:
    scenes:
      - code: qiniu.document
        name: 七牛云文档
        type: OSS
        serve-mode: PRESIGNED # S3 服务域名需要签名访问
        expire: 900 # 有效期 15 分钟
        path-strategy: DATE

重启后,用 qiniu.document 上传 PDF,检查空间中的对象并打开签名地址。七牛 S3 服务域名不支持匿名读取,因此不要直接把上例改为 DIRECT 并依赖 endpoint 自动生成公开地址;公开图片应配置已绑定的自定义域名,见下一节。

七牛对 2026 年 4 月 8 日 10:00(UTC+8)之后新建空间调整了浏览器访问策略:通过 S3 外网域名访问时,会作为附件下载。图片需要在浏览器中直接展示时,使用自定义源站或 CDN 域名,并按其访问规则配置,详见七牛空间访问策略说明。

公开图片使用 CDN 或自定义域名 ​

上面的 OSS 文档场景使用私有 bucket 和临时签名。头像、Logo、富文本图片需要长期公开地址时,可以另外创建公开图片场景:

yaml
sz:
  resource:
    scenes:
      - code: site.image
        name: 站点公开图片
        type: OSS
        serve-mode: DIRECT # 返回稳定的公开地址
        bucket: media-public # 改为实际图片 bucket;腾讯云需填写含 APPID 的完整名称
        base-url: https://cdn.example.com # 已绑定该 bucket 的 CDN 或自定义域名
        path-strategy: DATE
        exts: [jpg, jpeg, png, gif, webp]

这段场景配置沿用所选厂商的 sz.oss。先在厂商控制台绑定域名、配置 DNS 和 HTTPS,再填写 base-url。假设桶内 key 为 20261010/logo.png,最终地址就是 https://cdn.example.com/20261010/logo.png,程序不会再次追加 bucket。

CDN 需要能够读取对应对象,可以使用公开读源站,也可以配置厂商支持的私有源站回源授权。DIRECT 生成的是不带签名的公开地址;如果 CDN 还要求每次访问附带 URL 鉴权参数,需要另行接入该厂商的 CDN 鉴权,当前 base-url 不会自动生成这些参数。

base-url 也可以写成 /media 这样的同源路径,具体拼接与代理规则见 base-url 说明。PRESIGNED 使用 S3 endpoint 生成签名,不能通过填写 CDN base-url 来替换签名域名。

上传并验证配置 ​

1. 确认场景生效 ​

重启后检查应用是否正常启动,再登录后台,在文件管理页面或接口调试工具中调用 GET /api/admin/resource/scenes,确认能找到新增的 scene code 和名称。此接口只返回场景名称,不会返回存储密钥。

2. 上传一个小文件 ​

在接口调试工具中沿用当前应用的登录凭证,以 multipart/form-data 提交。先使用一张图片或一份小于 1MB 的 PDF,避免先遇到文件大小限制。sceneCode 填所选案例中的值,例如 tencent.document;下面以 LOCAL 合同场景为例:

http
POST /api/admin/resource/upload
Content-Type: multipart/form-data

sceneCode=contract.attachment
pathSegments=1001,contract
file=<binary>

表单字段:

字段是否必填说明
sceneCode是选择资源场景。
file是上传文件。
bizKey仅 naming: BIZ_KEY 时必填用于生成文件名;Java 服务内部形参名是 namingKey,HTTP 字段仍是 bizKey。
pathSegments否逗号分隔的路径段,仅 BIZ / BIZ_DATE 使用。

成功响应中的 data:

json
{
  "resourceId": "10086",
  "sceneCode": "contract.attachment",
  "objectKey": "contracts/1001/contract/20261010/550e8400e29b41d4a716446655440000.pdf",
  "originName": "report.pdf",
  "contentType": "application/pdf",
  "size": 98304,
  "eTag": null,
  "accessUrl": null
}

ResourceUploadResult 包含 resourceId、sceneCode、objectKey、originName、contentType、size、eTag 和 accessUrl。PROTECTED 返回 accessUrl: null 是正常结果。

富文本批量上传使用 POST /api/admin/resource/batchUpload,提交 sceneCode、可选 pathSegments 和 multipart 文件列表,返回同一结构的数组。

3. 检查文件和访问地址 ​

使用的模式上传后检查什么预期结果
LOCAL查看 root 下对应 scene.path 目录能找到返回 objectKey 对应的文件
OSS在存储控制台查看当前场景的 bucket能找到对象;桶内 key 是 objectKey 去掉 bucket 前缀后的部分
DIRECT在浏览器打开 accessUrl能读取公开文件;同源路径要通过前端站点访问
PRESIGNED打开 accessUrl,并在过期后重新生成地址有效期内可读;不能把这条临时 URL 当成长期业务数据
PROTECTED保存业务记录后,通过所属业务的下载接口读取上传返回 accessUrl 为 null;有权限且关联该资源时才能下载

最后重新进入业务页面确认历史文件仍可读取。PROTECTED 场景还应使用无业务权限或没有该资源关联的记录验证访问被拒绝。上传成功只说明写入链路正常,不能代替读取和业务权限检查。

业务附件接入 ​

以合同附件为例:前端上传得到资源引用,保存合同时由后端核对引用,查询时按访问方式补充地址,下载时检查合同权限。存储更换为 LOCAL 或任意 S3 服务后,这套业务接入方式相同。

1. 使用 ResourceRef 保存附件 ​

多文件业务字段使用前端 ResourceRef[]:

ts
export type ResourceRef = {
  objectKey: string;
  originName: string;
  contentType: string;
  sceneCode?: string;
  accessUrl?: string | null;
  resourceId?: string;
  eTag?: string;
  size?: number;
};

字段用途:

字段用途
resourceIdsys_resource 主键。前端始终按字符串处理,不要转为 number。
sceneCode、objectKey服务端定位文件;业务调用引用规范化方法后,按 resourceId 从资源记录重新取得。
originName、contentType展示文件名和生成下载响应;引用规范化时按资源记录重新取得。
accessUrlDIRECT / PRESIGNED 的展示地址;不用于授权,也不应作为长期持久字段。
eTag、size前端扩展信息。后端标准 ResourceRef 不含这两个字段,规范化后不会保留在业务附件 JSON 中;资源登记表仍保存它们。

上面的类型是前端定义。后端标准 ResourceRef 包含 resourceId、sceneCode、objectKey、originName、contentType、accessUrl,其中 accessUrl 为只读返回字段,引用规范化时不会生成它。

DIRECT / PRESIGNED 附件在重新查询后需要展示 URL 时,应在查询代码中生成,例如调用 resourceService.fillAccessUrl(vo.getAttachments())。当前生成器的业务查询模板没有自动执行这一步;PROTECTED 始终不生成公开地址。

不要只保存 URL 或 string 数组。受管业务附件至少要保留有效的 resourceId,再由后端核对和重建其余字段。

2. 保存时校验资源引用 ​

create / update 不能原样保存浏览器提交的 ResourceRef。业务 Service 使用 ResourceReferenceService:

java
entity.setAttachments(resourceReferenceService.normalizeForCreate(
        dto.getAttachments(), "contract.attachment", StpUtil.getLoginIdAsLong()));

entity.setAttachments(resourceReferenceService.normalizeForUpdate(
        existing.getAttachments(), dto.getAttachments(),
        "contract.attachment", StpUtil.getLoginIdAsLong()));

服务会:

  1. 检查 resourceId 是有效正数,且对应的 sys_resource 存在、未删除。
  2. 检查资源的 scene 与业务字段要求的 scene 一致。
  3. 新增引用必须由当前用户上传。
  4. 更新时允许保留该记录原来已经关联的资源。
  5. 从 sys_resource 重新生成 sceneCode、objectKey、文件名和 MIME,不信任前端同名字段。
  6. 不接收前端提交的 accessUrl,按 resourceId 去重并保持提交顺序。

例如原记录关联 A、B,更新后提交 A、C,最终只保存 A、C。B 只是解除业务关联,不会自动删除 sys_resource 或物理文件。

3. 提供业务下载和预览接口 ​

PROTECTED 文件通过所属业务的固定接口访问。Controller 先检查接口权限,Service 再确认当前记录确实关联该 resourceId,最后才调用公共流式服务。

java
@SaCheckPermission("contract.record.query_table")
@PostMapping("/{id}/resources/{resourceId}/download")
public ResponseEntity<StreamingResponseBody> downloadResource(
        @PathVariable Long id, @PathVariable Long resourceId) {
    contractService.validateResourceAccess(id, resourceId);
    return resourceDownloadService.download(resourceId);
}

项目内已有示例:

业务下载 / 预览路径主要检查
教师统计演示(dev/local/preview)POST /api/admin/teacher-statistics/{id}/resources/{resourceId}/download 和 /preview查询权限、业务数据范围、记录包含该资源
当前模板POST /api/admin/sys-temp-file/{id}/resources/{resourceId}/download 和 /preview查询权限、活动记录、资源 ID 与 JSON 引用同时匹配
模板历史POST /api/admin/sys-temp-file-history/{id}/resources/{resourceId}/download 和 /preview查询权限、活动父记录、历史资源 ID 与 JSON 引用同时匹配

ResourceDownloadService 查询活动资源,通过 sceneCode + objectKey 读取文件流。业务权限和记录关联需要在调用它之前检查,该服务自身不判断用户能否访问某条业务记录。下载响应使用 attachment,预览使用 inline,并返回安全文件名、X-Content-Type-Options: nosniff 和 Cache-Control: no-store。

内置预览只允许 PNG、JPEG、WebP、GIF 和 PDF。其他类型返回 R2006,前端应提示用户改用下载。

不要新增只接收 resourceId 的通用下载 Controller。仅知道资源 ID 不能证明当前用户有权访问所属业务记录。业务状态限制也应写在 validateResourceAccess 中,例如只有已发布合同允许下载。

通用模板下载入口 ​

GET /api/admin/common/download/templates 按模板名称或别名查找文件,顺序为:

  1. classpath:/templates/ 下的静态模板。
  2. sys_temp_file 中登记的模板,通过资源服务读取。
  3. 根据 @ExcelTemplate 注册的 DTO 动态生成模板。

这个入口与上面的模板管理下载接口不同,当前没有调用 validateResourceAccess,也没有声明模板管理的业务权限检查。如果模板需要按权限隔离,必须同时处理这个入口,不能只依赖 serve-mode: PROTECTED。

4. 接入前端组件 ​

FileDownloadList ​

列表和详情使用 FileDownloadList,同时传业务 ID 和固定业务 API:

vue
<FileDownloadList
  :files="row.attachments"
  :biz-id="String(row.id)"
  :download-api="downloadContractResourceApi"
  :preview-api="previewContractResourceApi"
/>

组件访问顺序:

  1. 有 bizId + 业务 API + resourceId 时,调用业务接口并处理 Blob。
  2. 上述业务参数不齐全、但有 accessUrl 时,直接访问公开或签名地址。业务接口已经发出请求后,如果返回错误,不会再降级为公开地址。
  3. 两者都没有时提示从已保存的业务记录进入,不回退到 URL 代理。

当前 FileDownloadList 只为识别为图片的文件显示预览按钮,PDF 需要自行接入预览界面,或直接下载。BMP、SVG 等图片可能显示按钮,但通过受保护预览接口访问时会被后端拒绝;后端允许在线预览的类型仍是 PNG、JPEG、WebP、GIF 和 PDF。

UploadFiles ​

编辑表单使用 UploadFiles。默认 sceneCode 是 system.protected,正式业务建议改成自己的场景。

vue
<UploadFiles
  v-model:modelValue="form.attachments"
  scene-code="contract.attachment"
  :biz-id="form.id ? String(form.id) : undefined"
  :download-api="downloadContractResourceApi"
  @uploading-change="uploading = $event"
/>

提交时:

  • 上传过程中禁止提交。
  • 使用 normalizeResourceFiles() 去掉 uploading、localPreviewUrl 和 blob: 地址,并按 resourceId 去重;没有 resourceId 的旧数据按 sceneCode + objectKey 去重。
  • 新增记录还没有业务 ID 时,PROTECTED 文件不能调用业务下载接口;先保存业务记录,再从该记录进入下载。
  • 当前 UploadFiles 的文件列表只提供下载和删除,没有预览按钮,也没有 previewApi 参数。它为新上传图片生成的本地 Blob URL,不代表已经提供预览功能。
  • 保存后传入业务 ID、下载 API 和资源 ID,PROTECTED 文件才会通过业务接口下载。组件会在移除文件、替换对应图片或卸载时释放保存的 Blob URL。

UploadFiles 下载时有可用 accessUrl 会直接访问;没有公开地址且传入 bizId + downloadApi 时才调用业务接口。PROTECTED 正常情况下没有 accessUrl,因此会走业务接口。

使用代码生成器时 ​

生成器按字段的 Java 类型 List<ResourceRef> 识别附件字段,生成以下配套代码:

  • create / update 中的资源规范化。
  • validateResourceAccess(id, resourceId)。
  • Controller 下载和预览接口。
  • 前端下载、预览 API。

fileUpload、imageUpload 等控件配置决定前端使用的组件和绑定方式。不同组件支持的参数不同,下载与预览能力按上节说明接入。

面板会加载已注册场景供选择,未配置时默认 system.protected。未注册的场景目前只显示警告,不会阻止生成,生成前应确认场景已在目标环境配置并生效。正式模块建议使用业务专属场景,便于识别文件用途。

当前生成条件是字段类型 List<ResourceRef>,不会根据所选场景的 serve-mode 判断是否需要受保护接口。选择 PROTECTED 时可以直接沿用生成结果;如果选择 DIRECT 或 PRESIGNED,应在生成后检查下载/预览接口是否仍有必要,并删除不需要的入口。头像、Logo 这类单值 objectKey 字段不使用 List<ResourceRef>,也不会触发这一套生成逻辑。

生成的 validateResourceAccess 会检查记录存在、资源关联,并按生成器的数据权限选项应用数据范围。项目还有发布状态、流程节点等限制时,需要在生成后补充相应验证。

头像和 Logo 使用单值 objectKey ​

头像和 Logo 保存 objectKey,不使用 ResourceRef,也不走受保护下载接口:

返回对象持久字段展示字段
SysUserVOlogologoUrl
UserProfileVOavataravatarUrl
登录态 BaseUserInfologo没有单独 URL 字段

后端通过用户头像场景把 objectKey 解析为 logoUrl/avatarUrl。前端新增、编辑用户时保存 logo,提交前删除 logoUrl;个人资料保存 avatar。派生 URL 不能回写数据库。

文件管理 ​

文件管理页面用于查看资源登记和排查配置:

  • 显示文件名、用途、大小、访问方式、存储方式、创建人和登记时间。
  • DIRECT 可以打开和复制公开地址。
  • PRESIGNED 可以打开当前生成的临时地址,过期后刷新列表重新获取。
  • PROTECTED 只显示详情,并提示从所属业务记录访问,不提供下载入口。
  • system.protected 只能说明它是通用业务附件,不能从资源记录推断具体业务归属。

场景 name 会显示为文件用途。业务专属场景的 code 和 name 越清楚,排查越容易。文件管理页面不会代替业务权限校验。

多场景配置 ​

按场景连接不同存储 ​

同一个应用可以让不同场景连接不同的 S3 兼容存储。例如公开图片使用默认 MinIO,归档文件使用另一套 RustFS。业务仍使用统一的 ResourceService 和存储驱动,不需要按厂商编写不同的接入代码。

每个场景最终只使用一套连接。场景需要独立连接时,同时填写 endpoint、access-key、secret-key;同一实例只是 bucket 不同时,只填写不同的 bucket。

公共配置写一次,场景只写差异 ​

sz.oss 保存默认连接、bucket、region、寻址风格和公开域名。普通 OSS 场景直接复用;只有 bucket、签名区域、寻址风格、公开地址或连接不同的场景才单独配置。

对 RustFS 的支持 ​

RustFS 通过现有 S3 客户端接入,配置步骤见 RustFS 案例。provider: RUSTFS 提供默认寻址设置,不会自动更换 endpoint、创建 bucket 或迁移历史文件。

配置继承规则 ​

配置生效规则
code场景唯一标识。业务上传和读取都依赖它,确定后不要随意更改。
name场景展示名称,用于文件管理页面等位置。
endpoint、access-key、secret-key场景三项都不写时使用全局连接。任一项出现时,三项必须全部填写,不能把场景 endpoint 与全局账号混用。
bucket场景值优先;未填写时使用 sz.oss.bucket-name。独立连接场景也可以继承全局 bucket。
region场景值优先;未填写时使用全局 region,全局缺省值为 us-east-1。单独修改 region 不会切换 endpoint。
provider场景值优先;用于厂商默认设置,单独修改不会切换 endpoint 或账号。
addressing-style场景值优先;未填写时使用全局设置或 provider 默认值。
domain只供使用默认连接的 DIRECT 场景自动生成公开地址。使用独立连接时不会继承。
base-url只对当前 DIRECT 场景生效;显式非空值优先于 domain 和 endpoint。LOCAL DIRECT 必须填写。

启用 OSS 时,全局 endpoint、access-key、secret-key 应作为一组配置和更换。即使业务场景都填写了独立连接,当前装配仍需要可用的全局 endpoint,因此不要给仅 OSS 项目删除整个 sz.oss 段。

旧 sz.oss.instances 和场景 oss-instance 已取消。独立连接直接写在场景顶层。

同一实例使用多个 bucket ​

保留已配置的 sz.oss,把需要的场景合并为下例。两个场景使用不同 bucket;已有同名场景时修改对应条目。桶名要替换为目标服务中的实际名称:

yaml
sz:
  resource:
    scenes:
      - code: site.image
        type: OSS
        serve-mode: DIRECT
        bucket: media-public

      - code: report.private
        type: OSS
        serve-mode: PRESIGNED
        bucket: reports-private
        expire: 900

公开与私有文件建议分 bucket 管理。serve-mode 不会修改存储桶策略;PRESIGNED 或 PROTECTED 文件如果放进允许匿名读取的 bucket,仍可以绕过应用直接访问。

场景使用独立连接 ​

场景可以连接另一套服务,也可以在同一个 endpoint 上使用另一组账号:

yaml
sz:
  oss:
    provider: MINIO
    endpoint: https://storage.example.com
    access-key: ${OSS_ACCESS_KEY}
    secret-key: ${OSS_SECRET_KEY}
    bucket-name: media-public
    region: us-east-1
    addressing-style: PATH

  resource:
    scenes:
      - code: partner.private
        type: OSS
        serve-mode: PRESIGNED
        bucket: partner-documents
        endpoint: https://storage.example.com
        access-key: ${PARTNER_ACCESS_KEY}
        secret-key: ${PARTNER_SECRET_KEY}
        expire: 900

独立连接的 bucket、region 和寻址风格仍可从全局继承。DIRECT 独立连接没有 base-url 时,只会按自己的 endpoint 生成地址,不使用全局 domain。

多个存储服务同时使用 ​

默认连接使用 MinIO,指定场景改用腾讯云 COS 时,可以这样配置。场景需要同时填写 endpoint 和两项密钥,再写上腾讯云要求的 region、bucket 和寻址风格;其他场景继续复用全局连接。

yaml
sz:
  oss:
    provider: MINIO
    endpoint: https://storage.example.com
    access-key: ${OSS_ACCESS_KEY}
    secret-key: ${OSS_SECRET_KEY}
    bucket-name: media-public
    region: us-east-1
    addressing-style: PATH

  resource:
    scenes:
      - code: tencent.document
        type: OSS
        serve-mode: PRESIGNED
        provider: TENCENT
        endpoint: https://cos.ap-guangzhou.myqcloud.com
        access-key: ${TENCENT_SECRET_ID}
        secret-key: ${TENCENT_SECRET_KEY}
        bucket: documents-1250000000
        region: ap-guangzhou
        addressing-style: VIRTUAL_HOSTED
        expire: 900

腾讯云 endpoint 必须对应 bucket 所在区域,bucket 名称包含 APPID。腾讯云当前建议使用虚拟主机风格,详见腾讯云 COS 的 S3 兼容配置。

provider 默认值速查 ​

provider常见服务默认寻址风格
MINIOMinIOPATH
RUSTFSRustFSPATH
ALIYUN阿里云 OSSVIRTUAL_HOSTED
TENCENT腾讯云 COSVIRTUAL_HOSTED
QINIU七牛云 S3VIRTUAL_HOSTED

provider 只是默认设置。目标服务要求的 endpoint、region、bucket 和寻址方式仍要按实际环境填写。

寻址风格 ​

addressing-style 决定 bucket 放在请求路径中,还是放在域名前面;region 是签名区域,两者分别配置。以 endpoint=https://storage.example.com 为例:

值bucket=images、key=2026/10/logo.png 时的地址
PATHhttps://storage.example.com/images/2026/10/logo.png
VIRTUAL_HOSTEDhttps://images.storage.example.com/2026/10/logo.png

优先级为:场景 addressing-style → 全局 addressing-style → 旧全局 force-path-style → provider 默认值。场景可以单独覆盖。

旧全局 sz.oss.force-path-style 仍兼容。如果它与全局 sz.oss.addressing-style 同时存在且含义冲突,应用会启动失败;场景单独设置不同的 addressing-style 是允许的。新配置使用 addressing-style。

RustFS 的 PATH 模式不需要配置虚拟主机域名。使用 VIRTUAL_HOSTED 时,服务端要设置 RUSTFS_SERVER_DOMAINS,例如基础域名 storage.example.com,并让 images.storage.example.com 等 bucket 子域名解析到存储服务。使用 HTTPS 时,证书也要覆盖实际访问的子域名;经过其他端口的代理时,还需按服务端要求配置端口。详见 RustFS Virtual Host Style。

base-url 的两种写法与优先级 ​

scene.base-url 只用于 DIRECT,支持两种同级写法:

  • 同源路径:/media。浏览器使用当前页面的协议、域名和端口访问,部署时必须配置对应代理。
  • 完整地址:https://files.example.com/media。适合独立文件域名、CDN 或反向代理。

两种写法都是最终公开前缀。程序不会再追加 endpoint、bucket 或 LOCAL path,只会追加从 objectKey 去掉当前场景存储前缀后的相对路径。

地址如何拼接 ​

假设场景配置如下:

yaml
sz:
  resource:
    scenes:
      - code: site.image
        type: OSS
        serve-mode: DIRECT
        bucket: images
        base-url: /media

以已经存储的 objectKey=images/2026/10/logo.png 为例:

text
objectKey                  = images/2026/10/logo.png
去掉 bucket 后的相对 key   = 2026/10/logo.png
页面 origin                = https://admin.example.com
最终 URL                   = https://admin.example.com/media/2026/10/logo.png

代理需要完成以下映射:

text
外部 /media/2026/10/logo.png
  → 存储 /images/2026/10/logo.png

只写 base-url 不会自动创建代理规则。改成 base-url: https://files.example.com/media 后,最终 URL 为 https://files.example.com/media/2026/10/logo.png,相对 key 不变。

未填写 base-url 时 ​

OSS DIRECT 会自动生成公开地址:

  1. 使用默认连接时,先使用 sz.oss.domain,没有 domain 再使用 endpoint。
  2. 使用独立连接时,只使用场景自己的 endpoint,不继承全局 domain。
  3. LOCAL DIRECT 不使用任何 OSS 地址,必须显式填写 base-url。

自动生成地址不会改变存储服务的访问规则。例如七牛的 S3 服务域名需要签名,DIRECT 场景应使用已绑定的自定义域名,不能直接使用自动拼出的 S3 地址。

domain 与 base-url 的含义不同:

yaml
sz:
  oss:
    # PATH:domain 是服务根地址,程序会追加 bucket
    addressing-style: PATH
    domain: https://files.example.com

bucket 为 images 时,PATH 最终前缀是 https://files.example.com/images。

VIRTUAL_HOSTED 可以为单个 bucket 填写完整桶域名:

yaml
sz:
  oss:
    addressing-style: VIRTUAL_HOSTED
    domain: https://images.files.example.com

多个 bucket 可以使用模板:

yaml
sz:
  oss:
    addressing-style: VIRTUAL_HOSTED
    domain: https://{bucket}.files.example.com

scheme 只会给未带协议的 endpoint 或 domain 补协议,默认是 https;它不会处理场景 base-url。新配置建议直接写完整的 http:// 或 https:// endpoint。

PRESIGNED 使用实际 endpoint、region 和寻址风格参与签名,不使用 domain 或 base-url。endpoint 必须能被浏览器访问,生成签名后不能替换主机名。PROTECTED 不生成公开地址。

文件名、目录和上传限制 ​

场景字段速查 ​

字段可选值和默认值说明
code无默认值场景唯一标识,业务上传、读取和配置都使用它。
name可省略场景展示名称,未填写时显示 code。
typeLOCAL(默认)、OSSLOCAL 使用 path,OSS 使用 bucket。当前省略时固定为 LOCAL,sz.resource.default-storage-type 不会改变此默认值,建议逐场景明确填写。
serve-modePROTECTED(默认)、DIRECT、PRESIGNEDPRESIGNED 仅支持 OSS。
namingUUID(默认)、BIZ_KEY、ORIGINAL、TIMESTAMPBIZ_KEY 使用上传表单中的 bizKey;ORIGINAL 遇到同一 objectKey 已存在时会追加时间戳。
path-strategyFLAT(默认)、DATE、BIZ、BIZ_DATE同时作用于 LOCAL 和 OSS;BIZ 使用 pathSegments,与 bizKey 无关。
expire默认 3600 秒只对 PRESIGNED 生效,必须大于 0。
exts小写扩展名集合场景白名单与全局 allowed-exts 同时生效,结果相当于取交集。
mimesMIME 集合非空时使用场景 MIME 白名单;未配置或为空时使用全局 allowed-mime-types。校验依据是上传请求的 Content-Type,未提供时跳过此项。
max-size数字,单位 MB;0 表示不额外限制与全局 security.max-size 取较小值。

路径策略示例,假设基础目录为 files、文件名为 report.pdf、pathSegments=1001,contract:

策略objectKey 目录结构
FLATfiles/report.pdf
DATEfiles/yyyyMMdd/report.pdf
BIZfiles/1001/contract/report.pdf
BIZ_DATEfiles/1001/contract/yyyyMMdd/report.pdf

BIZ_KEY 使用处理后的 bizKey 加文件扩展名命名,相同目录、bizKey 和扩展名会得到同一 objectKey,可以覆盖旧文件。UUID 使用不含连字符的随机 UUID;ORIGINAL 尽量保留原文件名,不支持的字符会替换为下划线,同名冲突时追加时间戳;TIMESTAMP 使用 yyyyMMddHHmmssSSS 格式的日期时间命名。

全局扩展名或 MIME 白名单未配置、为空时,会使用框架内置白名单;全局文件大小上限默认 50MB。场景 exts 只能收窄全局范围,非空 mimes 则替代全局 MIME 列表,两者规则不同。

无论扩展名白名单如何设置,上传都会拒绝内置危险扩展名和含危险双扩展名的文件。

设置全局上传限制 ​

例如,项目只接收 JPEG、PNG 和 PDF,可以这样设置。它会影响所有场景,已有业务需要其他格式时应一并保留:

yaml
sz:
  resource:
    security:
      allowed-exts: [jpg, jpeg, png, pdf] # 全局扩展名白名单,无需写点号
      allowed-mime-types: [image/jpeg, image/png, application/pdf] # 请求 Content-Type 白名单
      max-size: 10MB # 全局大小上限,带单位;场景 max-size 则是以 MB 为单位的数字

WARNING

资源模块限制不是 HTTP 上传的唯一限制。当前 Spring 配置的 spring.servlet.multipart.max-file-size 为 10MB,max-request-size 为 15MB;Nginx、网关也可能有自己的上限。即使 sz.resource.security.max-size 配成 50MB,请求仍可能先被这些入口拒绝。提高上传上限时要一起检查 Spring、Nginx 和网关配置。

部署、切换存储与升级 ​

部署检查 ​

  • bucket 需要提前创建。框架不会自动创建、删除或迁移 bucket。
  • 私有附件 bucket 保持私有。DIRECT 直连源站时需允许公开读;使用 CDN 时也可以配置私有源站回源授权。不要开放匿名写入。
  • PRESIGNED 的 endpoint 必须是浏览器可达地址,且不能在签名后替换域名。
  • LOCAL 生产环境只对 DIRECT 目录配置 Nginx 白名单,PROTECTED 目录不能静态开放。
  • RustFS 使用 VIRTUAL_HOSTED 时,同时检查 RUSTFS_SERVER_DOMAINS、DNS、证书和实际请求端口。

切换 RustFS 或其他存储 ​

仅调整 provider 不会自动迁移文件。实际更换存储时,先备份配置、对象和数据库引用,再选择处理方式:

更换方式历史文件如何处理
新 scene 接收新文件,旧 scene 保留旧引用继续使用旧 scene 和旧存储,不必迁移历史文件。
同一 scene 只更换 endpoint把对象复制到新存储,保持 bucket 名称和桶内 key 不变,原引用才能继续使用。
同一 scene 更换 bucket除复制对象外,还要核对并同步 sys_resource.object_key 与业务 ResourceRef 中的桶名前缀。

例如,原 objectKey 为 old-bucket/20261010/report.pdf,实际桶内 key 是 20261010/report.pdf。把场景 bucket 改成 new-bucket 后,程序不会自动改写旧前缀:未更新的引用会尝试读取新桶中的 old-bucket/20261010/report.pdf。如果按原桶内 key 复制文件,引用也应同步为 new-bucket/20261010/report.pdf。

迁移对象使用 S3 API 或迁移工具,不要直接把 MinIO 数据目录挂载给 RustFS。先在验证环境检查对象数量、关键前缀、抽样内容,以及历史读取和新上传,再安排正式切换。若同时升级到 v2.1.0,先按 v2.1.0 升级指南 完成资源访问契约迁移;仅切换存储时,按本节完成对象、引用和回退验证。

常见错误与排查 ​

受保护资源访问使用以下业务码:

HTTP业务码含义
403R2001当前用户无权访问该业务资源
404R2002业务记录或活动资源不存在
400R2003资源引用无效
400R2004资源场景不匹配
403R2005新增资源不是当前用户上传
415R2006文件类型不允许在线预览
500R2007存储读取失败

当前组件会显示后端返回的错误信息。415 / R2006 表示不允许预览,可以改用下载;403 不应降级为公开访问;404 应刷新业务数据并检查关联是否仍存在。

常见问题 ​

现象优先检查
上传返回 413 或还没进入业务接口就失败Spring multipart 的 10MB / 15MB 限制、Nginx client_max_body_size、网关限制
PROTECTED 上传成功但 accessUrl 为空正常行为;保存业务记录后通过固定业务接口访问
DIRECT 地址 404base-url 对应的 Nginx / 网关路由、bucket 策略、LOCAL path 映射
PRESIGNED 打开后签名错误endpoint、region、寻址风格、浏览器实际访问主机是否与签名一致
云存储报 NoSuchBucketendpoint 对应地域、完整 bucket 名称;腾讯云包含 APPID,七牛使用 S3 空间名
阿里云上传报 aws-chunked 错误AWS SDK 传输兼容性;按阿里云接入章节检查,不要只修改密钥
七牛文件能下载但不能直接预览新建空间的 S3 外网域名访问策略;需要公开展示时使用已配置的自定义域名
启动时报独立连接缺字段场景 endpoint、access-key、secret-key 必须三项齐全
启动时报旧实例字段或寻址冲突删除 sz.oss.instances / oss-instance;统一 addressing-style 与旧 force-path-style
同 bucket 的两个场景文件混在一起scene.code 不会自动创建目录;使用不同 bucket、LOCAL path 或 BIZ pathSegments 隔离

不兼容变更 ​

  • serve-mode: TOKEN 已改为 PROTECTED。
  • POST /api/admin/common/files/download 任意 URL 代理已删除。
  • ProxyDownloadDTO、CommonService.urlDownload()、前端 useUrlDownload 和 fileDownload({ url }) 已删除。
  • 业务下载使用 bizId + downloadApi;FileDownloadList 的业务预览另传 previewApi,不使用旧 accessContext。
  • ResourceRef.accessUrl 不是受保护文件的定位或授权依据。
  • 通用模板下载入口仍保留,按 classpath、模板表、动态模板的顺序查找;它不复用按资源 ID 下载接口的业务校验。

相关文档 ​