通用接口
本节收纳玩家端与具体业务无关的通用能力,例如客户端直传对象存储的预签名 URL。
获取上传预签名 URL
由服务端返回一个 S3 兼容对象存储的预签名 PUT URL,客户端拿到后自行把文件 PUT 到该 URL,实现两步式直传(不经过后端中转)。上传完成后使用返回的 url 作为公开访问地址(例如绑定为会员头像)。
采用内容寻址:服务端以客户端预先计算的内容哈希拼 S3 对象 key,同一份内容重复上传会命中已有对象、直接返回 exists=true 而不再签发新 upload_url。需登录。
URL: POST /common/get_upload_url
输入:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 是 | 文件名,长度 1-1000;服务端只取扩展名(.png / .jpg / .webp ...)用于拼 S3 对象 key,其它部分被丢弃 |
| hash | string | 是 | 由客户端预先计算的文件内容哈希(推荐 sha256 hex),长度 4-64;服务端据此做内容寻址与幂等判重,不同内容必须给不同 hash,否则会命中他人已上传的对象 |
输出:
| 字段 | 类型 | 说明 |
|---|---|---|
| exists | bool | 目标对象是否已在 S3 存在(同 hash + 同扩展名的对象)。true 表示不需要再上传,直接使用 url 即可;false 表示需要客户端把文件 PUT 到 upload_url |
| upload_url | string | 预签名 PUT URL;仅当 exists=false 时返回。签名有效期由 SDK 默认策略决定,建议客户端拿到后立即上传 |
| url | string | 上传完成后的公开访问 URL;无论 exists 是 true 还是 false 都会返回。格式为 <MEDIA_BASE_URL>/<hash 前 2 位>/<hash><ext> |
S3 对象 key 规则:
{hash[:2]}/{hash}{ext},其中ext取自请求参数name的扩展名(不含扩展名时为空串)。同一个hash使用不同扩展名视为不同对象。
业务错误:
| message | HTTP 状态码 | 触发条件 |
|---|---|---|
| 请先登录 | 401 | 未携带有效 JWT / token 过期 / 匿名会话 |
| Key: 'GetUploadURL.Name' Error:... | 400 | name 缺失或超过 1000 字符(binding tag required,max=1000,实际错误消息由框架按语言渲染) |
| Key: 'GetUploadURL.Hash' Error:... | 400 | hash 缺失或长度不在 4-64 之间(binding tag required,min=4,max=64) |
| 获取上传URL失败 | 400 | 服务端 S3 未配置(缺少 S3_URL / AWS_S3_* 环境变量)或签名生成失败 |
客户端使用流程
Step 1. 计算内容哈希 → 换取预签名 URL
前端计算文件内容的 sha256(浏览器可用 crypto.subtle.digest,或使用 crypto-js / spark-md5 等库),然后调用本接口:
curl -X POST 'https://<your-domain>/api/game/common/get_upload_url' \
-H 'Authorization: Bearer <jwt>' \
-H 'Content-Type: application/json' \
-d '{
"name": "avatar.png",
"hash": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
}'
响应示例 —— 首次上传(exists=false):
{
"data": {
"exists": false,
"upload_url": "https://s3.example.com/mybucket/ab/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Date=...&X-Amz-Expires=900&X-Amz-Signature=...&X-Amz-SignedHeaders=host",
"url": "https://cdn.example.com/ab/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789.png"
}
}
响应示例 —— 内容已存在(exists=true,无 upload_url):
{
"data": {
"exists": true,
"url": "https://cdn.example.com/ab/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789.png"
}
}
Step 2. PUT 文件二进制到 upload_url(仅当 exists=false)
对返回的 upload_url 直接发起 PUT 请求,请求体为原始文件二进制(不是 base64)。签名信息已在 URL 的 query 里,不需要再手动加签名头。Content-Type 应与文件真实 MIME 类型一致:
// 浏览器端伪代码
const file = document.querySelector('#fileInput').files[0];
const buf = await file.arrayBuffer();
// 1) 计算 sha256
const hashBuf = await crypto.subtle.digest('SHA-256', buf);
const hash = Array.from(new Uint8Array(hashBuf))
.map(b => b.toString(16).padStart(2, '0')).join('');
// 2) 换 upload_url
const { data } = await fetch('/api/game/common/get_upload_url', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: file.name, hash }),
}).then(r => r.json());
// 3) 若已存在则跳过 PUT,直接用 data.url
if (!data.exists) {
await fetch(data.upload_url, {
method: 'PUT',
headers: { 'Content-Type': file.type }, // 如 "image/png"
body: file, // 原始 File / Blob;勿包装 FormData,勿转 base64
});
}
// 4) 使用 data.url 作为最终访问地址
console.log('public url:', data.url);
Step 3. 使用返回的 url 作为公开访问地址
后续把 url 用于图片展示或提交给业务接口。例如更新会员头像:
curl -X POST 'https://<your-domain>/api/game/auth/update' \
-H 'Authorization: Bearer <jwt>' \
-H 'Content-Type: application/json' \
-d '{ "head_portrait": "https://cdn.example.com/ab/abcdef0...png" }'
参见 修改用户资料。
注意事项
- 必须使用
PUT方法上传,不要用POST/multipart/form-data;upload_url是 S3PresignPutObject的产物,只匹配PUT。 - 请求体是原始二进制,前端不要做 base64 编码,也不要包 FormData。
hash建议用 sha256(十六进制小写 64 字符)。同一份文件必须产生相同hash,否则会导致对象重复占用存储;不同文件必须给不同hash,否则会误命中他人对象(返回exists=true但url指向的不是本次要上传的内容)。- 签名有效期有限:拿到
upload_url后应立即PUT,长时间闲置后签名过期需重新调用本接口。 - 扩展名影响 key:
name里的扩展名会拼进 S3 key,同一hash用不同扩展名会产生两个不同对象;建议客户端使用文件真实扩展名,避免不必要的重复上传。