外观
上传与直传签名
经应用上传时,应用接收文件流,调用存储适配器,再保存文件记录。客户端直传时,应用只签发上传参数,文件直接发往存储服务;签名成功不代表文件已上传或已入库。
依赖、建表、Store 和 Mapper 扫描前提见快速开始。不同供应商的能力见存储配置与访问。
经应用上传
| 方法与路径 | 参数 | 成功时 data |
|---|---|---|
POST /api/app/resource/upload | 查询参数 method 默认 default;multipart 字段 file | FileRecordVo |
POST /api/app/resource/batch/upload | 查询参数 method 默认 default;多个同名 multipart 字段 files | List<FileRecordVo> |
批量请求示例:
bash
curl --request POST 'http://localhost:8080/api/app/resource/batch/upload?method=default' \
--form 'files=@first.txt' \
--form 'files=@second.txt'App Controller 整体标记 @LoginIgnore,方法不读取会话用户,也不根据当前用户限定上传源。method 只是配置中的存储源名称,不是权限范围。文件记录字段和响应包装见文件记录管理。
默认会计算 MD5、使用 yyyy/MM/dd 日期目录和随机名称,保留原始扩展名。记录的 type 根据文件名推断 MIME 类型,不能将其当成内容校验结果。当前 HTTP 接口不接收自定义目录或关闭随机命名的配置。
批量上传按输入顺序逐个执行,遇到异常即中断;之前上传的对象和已写入的记录不自动回滚。单次上传也不具备跨对象存储和数据库的原子性,落库失败后的对象清理由应用补偿。
在 Java 业务中调用
只调用 Java 服务时引入 springboot-business-resource-business 及所需适配器。当前供应商适配器会传递引入 App Controller,模块选择见概览。把示例类放到应用扫描范围,调用 uploadGeneratedFile 时传入已生成的本地文件:
java
package com.example.resource;
import com.own.business.resource.business.service.ResourceService;
import com.own.business.resource.common.entity.vo.FileRecordVo;
import com.own.business.resource.common.model.UploadModel;
import com.own.business.resource.common.model.UploadModelConfig;
import org.springframework.stereotype.Service;
import java.io.File;
@Service
public class GeneratedFilePublisher {
private final ResourceService resourceService;
public GeneratedFilePublisher(ResourceService resourceService) {
this.resourceService = resourceService;
}
public FileRecordVo uploadGeneratedFile(File file) {
var upload = new UploadModel(file);
try {
return resourceService.upload("default", upload, UploadModelConfig.DEFAULT);
} finally {
upload.close();
}
}
}ResourceService 也提供 MultipartFile、本地 File、本地路径字符串及列表/数组重载。UploadModelConfig.NOT_RANDOM_NAME 可保留原文件名,调用方需处理同日同名覆盖。服务会调用 setDateFolder(),覆盖事先在上传模型上设置的目录;自定义目录支持属于下文的签名流程。
低层 ResolveObjectStoreUtil.get(method).uploadFile(...) 仅操作对象,不自动写记录、执行完整服务的去重或目录策略。优先使用 ResourceService 完成上传与落库。
MD5 复用与当前限制
经应用上传的策略由配置决定:
全局 own.resource.unique | 行为 |
|---|---|
REPEAT | 跳过复用查询,每次上传并登记新记录 |
UNIQUE | 按 MD5 跨存储位置查询,命中后直接返回旧记录 |
NEXT(默认) | 读取所选源的 unique;源为 UNIQUE(默认)时按 MD5 与该源位置查询,源为 REPEAT 时跳过 |
签名接口使用另一条判断路径:只要传入非空 MD5 且源 position 非空,就查询该位置的历史记录,不受全局或源 unique 开关控制。首次直传可省略 md5,避免进入该分支。
md5Expire 单位为天,不是签名有效期。非空时增加 createTime >= LocalDate.now().minusDays(md5Expire) 条件,即从相应日期零点开始;为空时不限制创建时间。模型没有校验其非负性。
当前复用查询不能当成可靠的秒传流程
getPoByMd5 调用了会在空结果时抛异常的 getPoByWrapper,因此未命中会抛出 business_not_found,并不会按注释所述返回 null 后继续上传。查询也没有 LIMIT 1,多个匹配记录可能触发单条查询异常,不能保证“取最新一条”。快速开始使用 REPEAT;需要秒传时应先在实现层修正并验证空结果和重复记录行为。
MD5 命中直接返回旧记录,不验证实际对象仍存在,不创建新的业务归属关系,也不按用户或工作空间过滤。它不是并发防重机制,数据库索引不是 MD5 唯一约束。listByMd5 等 Java 列表方法采用列表查询,不具有上述单条查询的空结果语义。
获取直传签名
| 方法与路径 | 请求参数 | 配置 |
|---|---|---|
GET /api/app/resource/signature | fileName 必填;method 默认 default;md5、md5Expire 可选 | 普通签名 |
GET /api/app/resource/signature/common | fileName 必填;method 默认 default | GET 请求体为 UploadFileForSignatureCommonConfig |
POST /api/app/resource/signature | fileName 必填 | JSON UploadFileForSignatureConfig,通过业务路由选择源 |
/signature/common 确实使用 GET 携带 JSON 请求体,并非查询参数绑定;客户端和网关需要支持该调用方式。常规查询参数签名示例(源名 oss 需已配置):
bash
curl --get 'http://localhost:8080/api/app/resource/signature' \
--data-urlencode 'method=oss' \
--data-urlencode 'fileName=report.csv'通用配置字段为 randomName、folder、md5、md5Expire。默认随机命名;folder 作为前缀并追加日期目录。通用 check() 没有实际字段约束,目录与命名可接受范围应由应用限定。
业务签名请求为 POST /api/app/resource/signature?fileName=avatar.png,例如:
json
{
"module": "profile",
"sign": "avatar",
"randomName": true,
"folder": "avatars"
}module、sign 必须非空白,分别对应“模块名不能为空”“标号不能为空”校验。它们用于路由,不自动保存成业务关联或鉴权凭证。默认 SignatureBusinessMethod 忽略两者并返回 own.resource.primary。
需要分流时,在应用配置中提供自己的 Bean。示例源名必须在多源配置中存在:
java
package com.example.config;
import com.own.business.resource.core.method.SignatureBusinessMethod;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ResourceSignatureConfiguration {
@Bean
public SignatureBusinessMethod signatureBusinessMethod() {
return (config, module, sign) ->
"profile".equals(module) && "avatar".equals(sign) ? "images" : "documents";
}
}识别响应分支与供应商参数
data 为 SignatureModelShowVo。isExist=1 时返回 fileId 和 record,无需再次上传;isExist=0 时使用签名参数直传,新签名没有预先创建数据库记录,内置实现的 fileId 通常为空。isPrivate 为 0/1,仅反映源配置。
| 适配器 | signature 及其他关键字段 | 当前有效期 |
|---|---|---|
| 阿里云 OSS | signature 为表单策略签名;accessId、Base64 policy、dir、host;可返回 uploadCallbackUrl | 30 秒,expire 为 Unix 秒时间戳;策略文件上限固定为 1,048,576,000 字节 |
| 华为 OBS | signature 是完整 PUT 预签名 URL,dir 是对象键;签名绑定 Content-Type: text/plain | 固定 3600 秒,未填充 expire |
| 七牛 Kodo | signature 是 bucket 上传 token,policy 保存 bucket,host 实际取 position,不能直接认为它是上传节点 | 本模块未显式指定,使用 SDK 默认 token 行为;未填充 expire |
| 火山 TOS | signature 是完整 PUT 预签名 URL,dir 为对象键,policy 保存 bucket,type=volc | 固定 3600 秒,未填充 expire |
| MinIO、本地 | 未实现上传签名,调用默认方法抛出 SIGNATURE_UNDEFINED 对应业务异常 | 不适用 |
这些返回值不能使用一套 OSS 表单参数通用上传。华为与火山按 PUT 地址发送文件内容,七牛按 token 接入其上传流程;浏览器直传的跨域规则、供应商上传参数与结果处理需在对应存储环境配好。配置 private-read-expire 不会改变上传签名的上述有效期。
直传完成后的回调与落库
回调入口为 POST /api/app/resource/upload/callback/{serviceName},参数是原始文本请求体。serviceName 是 aliyun、huawei、qiniu、minio、volc、local 等供应商标识,不是 method 多源名称。
Controller 根据服务名找工具列表,使用其中第一个工具调用 checkUploadCallBack;服务不存在时报 not found service,校验返回 false 时报 check fail。同供应商多个源不会根据 bucket 或 method 精确路由回调。
回调校验能力不统一
当前只有阿里云适配器重写了回调验签,按 OSS 公钥和 Authorization 校验请求;其他内置实现继承默认返回 true 的方法,不能将它们视为已验证来源的回调入口。App 控制器又整体免登录,接入应用需限制实际开放的回调路由,或在实现层补齐验证与业务归属校验。
解析器接受 KEY=value&KEY=value 形式的 URL 编码文本,将大写下划线字段名转换为 DTO 小驼峰名称,并对值做 URL 解码。对应字段有 NAME、OLD_NAME、PATH、POSITION、TYPE、SIZE、MD5。它不是 JSON 文件元数据接口;值中的 &、= 等字符应编码,否则会被当前拆分逻辑截断或忽略。
通过校验后执行 FileRecordDto.check() 和 addByDto,成功返回 ResultModel<FileRecordVo>。当前 DTO 没有专用必填约束,回调也没有请求幂等标识,重复回调可重复入库。
阿里云签名只返回配置的回调地址,不自动构造客户端所需的完整回调参数及元数据映射;其他供应商签名实现没有填入回调地址。直传集成必须显式安排可信的完成确认和落库过程,不能把“对象上传成功”当成“文件记录已存在”。已有对象也可由可信后台通过记录登记接口保存。
阿里云经应用上传的实现捕获并记录上传异常后继续返回,服务层仍可能写入记录;其签名失败也可能返回空模型。该适配器接入时应核对对象存在性与失败路径,不能仅用数据库记录判断存储操作成功。