# 团队对接 · TOS 资源桶 + 阿里云短信登录

> 这是给后端 / 运维同学的对接清单。把每一项的「需要回填」补齐后，原文回给前端（Claude）就能直接接入。
> 文件里所有变量名都跟 `.env.example` 对齐，回填时直接写值即可。

---

## A. 火山引擎 TOS · 静态资源

### A.1 目标

把 36 位艺人的立绘 / 氛围图 / 三视图 / 表演视频 + Hero PV 全部走 TOS + CDN 引用。
当前本地 ~1GB 资源已 `.gitignore`，**不入 git**。

### A.2 桶配置建议

| 项 | 建议值 | 备注 |
|---|---|---|
| 桶名 | `cyber-star` | 名字仅用于服务端 SDK，前端只看域名 |
| 区域 | `cn-beijing` 或就近 | 跟用户群体所在区域一致 |
| 读权限 | **公共读 (Public Read)** | 投票网站资源全部对外可访问，不需要签名 URL |
| 写权限 | 私有 | 仅服务端 / 运维通过 AK/SK 写入 |
| CDN 加速 | **强烈建议开启** | 火山 TOS 可直接挂火山 CDN，全国边缘加速；不开 CDN 时直接走桶域名也行，只是速度差一些 |
| 防盗链 | 可选 | 起步阶段不限制；后期可加 Referer 白名单 `*.airlabs.art` |
| HTTPS | **必须** | 网站走 HTTPS，资源也必须是 HTTPS，否则浏览器拦截 |

### A.3 桶内目录结构（请保持一致）

```
cyber-star/
├── portraits/
│   ├── 001.png         # 立绘（卡片用，半身）
│   ├── 001-2.png       # 氛围图 2
│   ├── 001-3.png       # 氛围图 3
│   ├── 001-view.png    # 三视图（详情页备用，可暂不上传）
│   ├── 002.png
│   ├── ...
│   └── 036-3.png
└── videos/
    ├── hero-pv.mp4         # 首页 Hero 背景 PV
    └── artists/
        ├── 001.mp4
        ├── 002.mp4
        ├── 004.mp4         # 003 / 010 / 017 / 027 暂时缺，先不传
        └── ...
```

> 路径与本地 `public/portraits/` `public/videos/` 完全对齐。前端代码里只需要把 `/portraits/...` 替换为 `${TOS_DOMAIN}/portraits/...`。

### A.4 前端要的信息（**必填**）

```yaml
# ── 公共域名（前端代码引用资源用，必须公网 HTTPS 可达）──
NEXT_PUBLIC_TOS_DOMAIN: ""    # 例：https://cyber-star.tos-cn-beijing.volces.com
                              # 或挂 CDN 后：https://cdn.cyber-star.airlabs.art

# ── 是否启用公共读 ──
PUBLIC_READ: ""               # yes / no（若 no 请说明签名策略）

# ── CDN 是否启用 + 加速域名 ──
CDN_ENABLED: ""               # yes / no
CDN_DOMAIN: ""                # 启用 CDN 时填这个，前端用这个域名而不是桶原域名
```

### A.5 服务端要的信息（**仅当后端要写桶 / 头像直传时需要**，前端暂时不需要）

> ⚠️ AK/SK 是高敏感信息，**不要发到聊天里**。请用密钥管理工具同步给运维 / CI 环境变量，或者通过加密渠道（飞书加密文件 / 1Password）给。下面只是占位提醒。

```yaml
TOS_ENDPOINT: ""              # tos-cn-beijing.volces.com
TOS_REGION: ""                # cn-beijing
TOS_BUCKET: ""                # cyber-star
TOS_ACCESS_KEY: ""            # 走密钥管理，不写明文
TOS_SECRET_KEY: ""            # 走密钥管理，不写明文
```

### A.6 上传分工

- [ ] **前端**（Claude）负责压缩。把 `public/portraits/` 和 `public/videos/` 用 sharp + ffmpeg 批量转换，输出到 `assets-compressed/`，保持 A.3 的目录结构。压缩目标：
  - 立绘 PNG → WebP，目标单张 ≤ 800KB（卡片清晰度足够）
  - 三视图 PNG → WebP，目标单张 ≤ 1.5MB
  - Hero PV mp4 → H.264 1080p，目标 ≤ 15MB
  - 单人 solo mp4 → H.264 1080p，目标单条 ≤ 3MB
- [ ] **运维 / 后端** 拿到 `assets-compressed/` 后用 `tosutil` 整目录上传到桶根：
  ```bash
  tosutil cp -r -f assets-compressed/* tos://cyber-star/
  ```
- [ ] **前端**（Claude）回填 `NEXT_PUBLIC_TOS_DOMAIN` 进 `.env`，封装 `tosUrl(path)` 工具函数，把代码里所有 `/portraits/...` `/videos/...` 改成走该函数。

### A.7 验收

- [ ] 浏览器直接打开 `${NEXT_PUBLIC_TOS_DOMAIN}/portraits/001.png` 能看到图
- [ ] 同样地址在 https 下不报 mixed-content
- [ ] 网站首页加载，Network 面板看到立绘从 TOS 域名下载，状态 200

---

## B. 阿里云短信服务 · 手机号 OTP 登录

### B.1 目标

`/api/auth/send-otp` 现在只在控制台打 log，需要接真实短信通道，给用户发 6 位验证码完成手机号登录。
全国大陆手机号，5 分钟内有效，单号码 60 秒限频（前端已做）。

### B.2 阿里云控制台需要申请的东西

按这个顺序在阿里云控制台 → **短信服务** 申请：

#### B.2.1 短信签名

- **签名内容**：`Cyber Star` 或 `虚拟偶像出道企划` 或公司主体相关
- **签名类型**：通用
- **使用场景**：验证码（推荐）/ 网站会员注册
- **审核材料**：网站备案截图 / 公司营业执照（看运营那边怎么对接）
- **审核时长**：1-2 工作日

#### B.2.2 短信模板

- **模板类型**：验证码
- **模板内容**（必须严格使用 `${code}` 占位符）：
  ```
  您的验证码是 ${code}，5 分钟内有效，请勿泄露。
  ```
- **审核时长**：1-2 工作日
- 申请通过后会拿到一个 `SMS_xxxxxxxx` 的模板 Code

#### B.2.3 RAM 子账号 + AccessKey

- 在阿里云 RAM 创建子账号 `cyber-star-sms`，授予 `AliyunDysmsFullAccess` 权限
- 生成 AccessKey ID + Secret（一次性显示，存好）

### B.3 前端要的信息（**必填**）

```yaml
# ── 短信签名与模板（公开信息，可以聊天里给）──
SMS_SIGN_NAME: ""             # 例：Cyber Star
SMS_TEMPLATE_CODE: ""         # 例：SMS_298765432

# ── 接入信息（敏感，走密钥管理）──
SMS_ACCESS_KEY: ""            # 阿里云 RAM 子账号 AK
SMS_SECRET_KEY: ""            # 阿里云 RAM 子账号 SK
SMS_REGION: ""                # 默认 cn-hangzhou，国内一般不变
```

### B.4 上下游依赖（**必填，缺一个都跑不起来**）

#### B.4.1 Redis

```yaml
REDIS_URL: ""                 # redis://default:PASSWORD@host:port
                              # 用途：① OTP 验证码 5min TTL 存储 ② 限流计数（手机号 60s/IP 5min 等）
```

如果还没有 Redis 实例，可以先用火山 Redis（同地域低延迟）或自建。**最低规格够用**，QPS 不大。

#### B.4.2 MySQL

```yaml
DATABASE_URL: ""              # mysql://user:pwd@host:port/db?charset=utf8mb4
                              # 用途：用户表 / 投票记录 / 应援关系 / 每日额度
```

- 火山 RDS for MySQL 8 或自建均可
- 第一次部署需要跑：
  ```bash
  npx prisma migrate deploy
  npx prisma db seed   # 灌 36 位艺人 + ActivityConfig（活动开关 / 起止时间 / 每日额度）
  ```

#### B.4.3 NextAuth

```yaml
AUTH_SECRET: ""               # openssl rand -base64 32 生成
AUTH_URL: ""                  # 例：https://cyber-star.airlabs.art（生产域名，不含末尾斜杠）
```

### B.5 后端要做的代码改动（仅 1 处，~10 行）

`src/app/api/auth/send-otp/route.ts` 第 41-53 行现在长这样：

```ts
const redis = getRedis();
if (redis) {
  await redis.set(`sms:otp:${phone}`, code, "EX", 300);
}

// TODO[团队]: 接入真实短信服务（阿里云 / 火山引擎 SMS）
if (process.env.NODE_ENV !== "production") {
  console.log(`[dev-otp] 发送给 ${phone}: ${code}`);
}
```

要改成调阿里云 SDK 真实发短信。**这一步前端（Claude）可以代写**，需要后端确认接 SDK 用 `@alicloud/dysmsapi20170525` 还是 `@alicloud/pop-core`，或者后端自己写也行。

### B.6 验收

- [ ] 真实手机号点「获取验证码」，30 秒内收到短信，内容包含 6 位数字
- [ ] 验证码填回去能登录成功，导航栏显示头像
- [ ] 60 秒内对同一手机号再次请求，接口返回 429（限流生效）
- [ ] OTP 5 分钟后失效

---

## C. 回填模板（**直接复制这一块改值，整段返回给前端**）

```yaml
# ───── A. TOS 资源桶 ─────
NEXT_PUBLIC_TOS_DOMAIN: ""
PUBLIC_READ: ""
CDN_ENABLED: ""
CDN_DOMAIN: ""

# ───── B. 阿里云短信 ─────
SMS_SIGN_NAME: ""
SMS_TEMPLATE_CODE: ""
SMS_REGION: ""

# ───── B.4 基础依赖 ─────
REDIS_URL: ""
DATABASE_URL: ""
AUTH_SECRET: ""
AUTH_URL: ""

# ───── 备注（其他需要前端知道的事情）─────
# 例如：访问令牌怎么续期、桶有没有 Referer 白名单、SMS 模板审核状态等
NOTES: |

```

**敏感信息（AK/SK）请单独走密钥管理或加密渠道，不要写在这个文件里。**

---

## D. 时间线建议

```
Day 0 (今天)         ─ 后端 / 运维拿到这份文档，开始走流程
Day 1                ─ 阿里云提交签名 / 模板审核（等 1-2 天）
                     ─ 创建 TOS 桶 + CDN 域名
                     ─ 部署 MySQL / Redis 实例
Day 1-3              ─ 前端这边：本地压缩 1GB 资源 → 输出到 assets-compressed/
                     ─ 前端代码适配 NEXT_PUBLIC_TOS_DOMAIN 占位
Day 2-3              ─ 短信审核通过 → 回填模板 + AK
                     ─ 资源上传到 TOS
Day 3                ─ 联调：真实手机号收验证码，登录，投票，资源走 CDN
Day 4                ─ 上线
```
