# TAPI 文件上传接入说明

本文档说明 `https://tapi.hifast.biz/v1/public/file/upload` 相关上传接口的推荐接入方式、签名规则与常见排查方式。

## 总览

上传能力包含两类接入方式：

- 推荐方式：`init -> S3 PUT -> complete`
- 兼容方式：`/upload` multipart 直传

推荐优先使用预签名三段式，因为：

- 现有签名串包含 `BODY_SHA256`
- `/upload` 是 `multipart/form-data`
- multipart 原始 body 的签名和调试成本更高
- `init` 和 `complete` 是 JSON，更适合客户端和 Apifox 调试

## 签名生效逻辑

项目保持现有旧逻辑，不做强制签名改造：

- `Signature.EnableSignature = false` 时：不校验签名
- `Signature.EnableSignature = true` 且未携带 `X-App-Id` 时：不校验签名，兼容老客户端
- `Signature.EnableSignature = true` 且携带 `X-App-Id` 时：必须同时携带并校验
  - `X-Timestamp`
  - `X-Nonce`
  - `X-Signature`

这意味着：

- 新客户端建议始终带完整签名头
- 老客户端如果没有 `X-App-Id`，仍可按旧逻辑访问

## 签名头定义

- `X-App-Id`: 客户端标识，例如 `ios-client`
- `X-Timestamp`: Unix 秒级时间戳
- `X-Nonce`: 每次请求唯一随机串
- `X-Signature`: `HMAC-SHA256` 结果的十六进制小写字符串

## StringToSign 规则

StringToSign 由下面 7 段按换行符 `\n` 拼接：

```text
METHOD
PATH
CANONICAL_QUERY
BODY_SHA256
X-App-Id
X-Timestamp
X-Nonce
```

说明：

- `METHOD`：HTTP 方法大写，例如 `POST`
- `PATH`：请求路径，例如 `/v1/public/file/upload/init`
- `CANONICAL_QUERY`：按 key 排序后的 query string，没有 query 则为空字符串
- `BODY_SHA256`：请求体原始字节的 SHA-256 十六进制小写
- 其余三项直接使用请求头值

签名计算方式：

```text
signature = hex_lower(HMAC_SHA256(app_secret, string_to_sign))
```

时间窗与防重放：

- `X-Timestamp` 默认有效时间窗是 300 秒
- `X-Nonce` 在有效时间窗内不能重复使用

## 推荐接入：预签名三段式

### 1. 初始化上传

请求：

```bash
curl -X POST 'https://tapi.hifast.biz/v1/public/file/upload/init' \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Content-Type: application/json' \
  -H 'authorization: your-token' \
  -H 'X-App-Id: ios-client' \
  -H 'X-Timestamp: 1778776400' \
  -H 'X-Nonce: nonce-001' \
  -H 'X-Signature: your-signature' \
  -d '{
    "biz_type": "app-package",
    "file_name": "demo.zip",
    "content_type": "application/zip",
    "size": 123456,
    "sha256": ""
  }'
```

典型返回：

```json
{
  "code": 200,
  "msg": "success",
  "data": {
    "file_id": "c29274ee26ab5aa211e0396e",
    "object_key": "app-upload/app-package/519/2026/05/c29274ee26ab5aa211e0396e_demo.zip",
    "upload_url": "https://bucket.s3.ap-east-1.amazonaws.com/...",
    "method": "PUT",
    "headers": {
      "Content-Type": "application/zip"
    },
    "expired_at": 1778776715
  }
}
```

### 2. 直传 S3

这一步是直接上传二进制文件到 S3，不走业务签名中间件。

```bash
curl -X PUT 'https://bucket.s3.ap-east-1.amazonaws.com/...' \
  -H 'Content-Type: application/zip' \
  --upload-file '/tmp/demo.zip'
```

说明：

- `Content-Type` 需和 `init` 返回的 `headers.Content-Type` 一致
- 允许的 `Content-Type` 由服务端 `S3.AllowedContentTypes` 配置控制，默认包含：
  - 压缩包：`application/zip`、`application/x-zip-compressed`、`application/gzip`、`application/x-gzip`
  - 通用文件：`application/octet-stream`、`text/plain`、`application/json`
  - 图片：`image/jpeg`、`image/jpg`、`image/png`、`image/webp`、`image/gif`、`image/heic`、`image/heif`、`image/bmp`
- `upload_url` 有过期时间，通常 300 秒
- 成功时 S3 常见返回 `200` 或 `204`

### 3. 完成上传

```bash
curl -X POST 'https://tapi.hifast.biz/v1/public/file/upload/complete' \
  -H 'Accept: application/json, text/plain, */*' \
  -H 'Content-Type: application/json' \
  -H 'authorization: your-token' \
  -H 'X-App-Id: ios-client' \
  -H 'X-Timestamp: 1778776405' \
  -H 'X-Nonce: nonce-002' \
  -H 'X-Signature: your-signature' \
  -d '{
    "file_id": "c29274ee26ab5aa211e0396e"
  }'
```

## 兼容接入：单接口 multipart 直传

接口：

- `POST /v1/public/file/upload`

表单字段：

- `biz_type`
- `file`

说明：

- 该接口继续保留，兼容旧客户端
- 如果请求带了 `X-App-Id`，就按现有逻辑验签
- 如果没有 `X-App-Id`，仍按旧逻辑放行
- 如果要给该接口加签，签名时必须对原始 multipart body 计算 `BODY_SHA256`
- 允许的 `Content-Type` 与预签名三段式一致；multipart 文件字段未显式携带 `Content-Type` 时，服务端会基于文件内容嗅探常见类型。

## 常见错误码

- `200`: 成功
- `400`: 参数错误
- `400 content_type is not allowed`: 文件 `Content-Type` 不在 `S3.AllowedContentTypes` 白名单内
- `40008`: 缺少签名头
- `40009`: 签名已过期
- `40010`: 签名无效
- `40011`: nonce 重放
- `10001`: 上传元数据不存在或对象不存在

## 排查建议

- `40008`：确认带了 `X-App-Id` 后，也同时带上 `X-Timestamp / X-Nonce / X-Signature`
- `40009`：检查客户端时间是否偏差过大
- `40010`：确认 `PATH`、query 排序、body 原始字节、secret 是否完全一致
- `40011`：确保每次请求都生成新的 `X-Nonce`
- `complete` 失败：确认 S3 `PUT` 已成功，且上传大小与 `init.size` 一致
