通用接口

本节收纳玩家端与具体业务无关的通用能力,例如客户端直传对象存储的预签名 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 是 S3 PresignPutObject 的产物,只匹配 PUT。
  • 请求体是原始二进制,前端不要做 base64 编码,也不要包 FormData。
  • hash 建议用 sha256(十六进制小写 64 字符)。同一份文件必须产生相同 hash,否则会导致对象重复占用存储;不同文件必须给不同 hash,否则会误命中他人对象(返回 exists=true 但 url 指向的不是本次要上传的内容)。
  • 签名有效期有限:拿到 upload_url 后应立即 PUT,长时间闲置后签名过期需重新调用本接口。
  • 扩展名影响 key:name 里的扩展名会拼进 S3 key,同一 hash 用不同扩展名会产生两个不同对象;建议客户端使用文件真实扩展名,避免不必要的重复上传。