lh 11 jam lalu
induk
melakukan
d5fb30574c
1 mengubah file dengan 0 tambahan dan 378 penghapusan
  1. 0 378
      docs/frontend-integration.md

+ 0 - 378
docs/frontend-integration.md

@@ -1,378 +0,0 @@
-# 前端对接文档
-
-> 适用平台:有声合成平台 / 掌维 AI 短剧
-> 后端:Laravel,接口统一前缀 `/api`
-> 本文档覆盖前端需要改造对接的两块内容:**接口签名**与**登录图片验证码**,以及通用约定与错误码。
-
----
-
-## 一、通用约定
-
-### 1.1 接口地址与鉴权
-
-- 所有业务接口以 `/api` 开头,例如 `POST /api/anime/detail`。
-- 登录后,除少量免登录接口外,均需在请求头携带登录令牌:
-
-  ```
-  d-token: <登录接口返回的 token>
-  ```
-- 少数导出类接口服务端也兼容从参数 `d_token` 读取令牌,**新代码请统一使用请求头**。
-- 免登录接口(不需要 `d-token`):`GET /api/login`、`GET /api/captcha`、`ANY /api/video/seedanceCallback`。
-
-### 1.2 响应格式
-
-成功:
-
-```json
-{ "msg": "", "code": 0, "data": { } }
-```
-
-失败(业务异常):
-
-```json
-{ "code": 1002, "msg": "签名异常", "data": { } }
-```
-
-> 约定:`code === 0` 表示成功,其余为业务错误码,前端按 `code` 判断并提示 `msg`。
-> 业务异常统一返回 HTTP 200;个别接口由框架做参数校验时可能返回 HTTP 422,建议在响应拦截器里一并处理。
-
-### 1.3 常用错误码
-
-| code  | msg                  | 说明                                      |
-| ----- | -------------------- | ----------------------------------------- |
-| 0     | -                    | 成功                                      |
-| 1002  | 签名异常             | 签名不正确 / 过期 / 缺少签名头 / 请求重复 |
-| 1003  | 请确认填写的数据无误 | 参数错误                                  |
-| 1005  | 没有权限             | 无权限访问该资源(含跨公司访问)          |
-| 1008  | 请先登录             | 未登录或 token 失效                       |
-| 10021 | 验证码错误           | 图片验证码错误或已过期                    |
-
----
-
-## 二、接口签名
-
-### 2.1 适用范围
-
-服务端在以下接口组启用了验签:
-
-- 剧本管理:`/api/deepseek/*`(`scriptList`、`scriptInfo`、`saveEpisodeContent` 等)
-- AI 生成:`/api/AIGeneration/*`
-- 动漫管理:`/api/anime/*`
-- 画布模式:`/api/canvas/*`
-- 音频素材上传:`/api/book/uploadAudioEffect`、`/api/book/uploadBgm`
-
-**推荐做法:前端统一给所有请求加签名头,不必区分接口。** 服务端对未开启验签的接口不会校验,多传请求头没有任何副作用,这样可以避免后续接口调整归属时前端漏改。
-
-> 注意:服务端有开关,未开启验签时签名头会被忽略,接口照常返回。因此前端上线后如果发现一切正常,不代表签名已经生效,需要和后端确认开关状态。
-
-### 2.2 签名规则
-
-三个请求头:
-
-| 请求头      | 内容                                         |
-| ----------- | -------------------------------------------- |
-| `X-Time`  | 当前时间戳(**秒**,10 位)            |
-| `X-Nonce` | 前端生成的随机串,同一账号在时间窗内不可重复 |
-| `X-Sign`  | 签名结果,**小写十六进制**(64 位)    |
-
-签名串按固定顺序拼接,字段间用 `|` 分隔:
-
-```
-{请求方法大写}|{接口路径}|{X-Time}|{X-Nonce}|d-token={d-token}
-```
-
-签名算法:
-
-```
-X-Sign = HMAC-SHA256(签名串, SIGN_SALT)    // 结果转小写 hex
-```
-
-其中 `SIGN_SALT` 由后端提供,需在打包时注入前端配置。
-
-**接口路径(PATH)的规则**,这是最容易算错的地方:
-
-- 以 `/` 开头
-- 不含协议和域名
-- **不含 query string**(`?a=1&b=2` 要丢掉)
-- 与后端路由注册的路径一致,例如 `/api/anime/detail`
-
-示例:
-
-```
-请求:POST /api/anime/detail?anime_id=123
-X-Time  = 1737280000
-X-Nonce = abc123
-d-token = 0123456789abcdef0123456789abcdef
-
-签名串 = POST|/api/anime/detail|1737280000|abc123|d-token=0123456789abcdef0123456789abcdef
-X-Sign = HMAC-SHA256(签名串, SIGN_SALT)
-```
-
-**有效期与重放保护**:
-
-- `X-Time` 与本机时间偏差超过 **120 秒**(可由后端配置)判定为过期;
-- 同一 `X-Nonce` 在时间窗内只能使用一次,重复请求会返回 1002。
-
-### 2.3 自检用例(用于验证前端实现是否正确)
-
-用下面的固定输入自测,输出的 `X-Sign` 必须与表中一致,否则说明拼接规则或编码有误。
-
-> 注意:用例中的时间戳是固定值(已过期),**仅用于校验算法实现,不能用于真实请求**。真实请求必须使用当前时间。
-
-| 项               | 用例一                                                               | 用例二                                                               |
-| ---------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- |
-| 请求方法         | `POST`                                                             | `GET`                                                              |
-| 接口路径         | `/api/anime/detail`                                                | `/api/canvas/list`                                                 |
-| X-Time           | `1737280000`                                                       | `1737280000`                                                       |
-| X-Nonce          | `abc123`                                                           | `nonce9876`                                                        |
-| d-token          | `0123456789abcdef0123456789abcdef`                                 | `fedcba9876543210fedcba9876543210`                                 |
-| SIGN_SALT        | `example_salt`                                                     | `example_salt`                                                     |
-| **X-Sign** | `f9a6c3cc7cf1b7f4b390fc08d91c5070445d60aadeba4b823509ea3092d7ae4b` | `3bc9b788a618dc44f06dba1aa2b0e5ebb703166b7f2b496eef49233a6461c4eb` |
-
-可以直接用 Node 跑一遍验证:
-
-```js
-// node sign-selfcheck.js
-const crypto = require('crypto')
-
-function sign(method, path, time, nonce, token, salt) {
-  const str = `${method}|${path}|${time}|${nonce}|d-token=${token}`
-  return crypto.createHmac('sha256', salt).update(str).digest('hex')
-}
-
-console.log(sign('POST', '/api/anime/detail', '1737280000', 'abc123',
-  '0123456789abcdef0123456789abcdef', 'example_salt'))
-// 期望:f9a6c3cc7cf1b7f4b390fc08d91c5070445d60aadeba4b823509ea3092d7ae4b
-
-console.log(sign('GET', '/api/canvas/list', '1737280000', 'nonce9876',
-  'fedcba9876543210fedcba9876543210', 'example_salt'))
-// 期望:3bc9b788a618dc44f06dba1aa2b0e5ebb703166b7f2b496eef49233a6461c4eb
-```
-
-两条都输出一致,说明签名算法实现正确;剩下只需要保证 `path`、`token` 与真实请求对齐。
-
-### 2.4 前端实现
-
-#### 方案 A:使用 crypto-js(推荐,改动最小)
-
-```bash
-npm install crypto-js
-```
-
-```js
-// src/utils/sign.js
-import HmacSHA256 from 'crypto-js/hmac-sha256'
-import Hex from 'crypto-js/enc-hex'
-
-const SIGN_SALT = process.env.VUE_APP_SIGN_SALT
-
-/**
- * 生成接口签名头
- * @param {string} method  请求方法
- * @param {string} path    接口路径(以 / 开头,不含域名与 query)
- * @param {string} token   登录令牌
- */
-export function buildSignHeaders(method, path, token) {
-  const time  = Math.floor(Date.now() / 1000).toString()
-  const nonce = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`
-  const str   = `${method.toUpperCase()}|${path}|${time}|${nonce}|d-token=${token}`
-
-  return {
-    'X-Time':  time,
-    'X-Nonce': nonce,
-    'X-Sign':  HmacSHA256(str, SIGN_SALT).toString(Hex)
-  }
-}
-```
-
-#### 方案 B:使用浏览器原生 Web Crypto(不新增依赖)
-
-```js
-// src/utils/sign.js
-const SIGN_SALT = process.env.VUE_APP_SIGN_SALT
-
-const enc = new TextEncoder()
-let cachedKey = null
-
-async function getKey() {
-  if (!cachedKey) {
-    cachedKey = crypto.subtle.importKey(
-      'raw', enc.encode(SIGN_SALT), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
-    )
-  }
-  return cachedKey
-}
-
-export async function buildSignHeaders(method, path, token) {
-  const time  = Math.floor(Date.now() / 1000).toString()
-  const nonce = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`
-  const str   = `${method.toUpperCase()}|${path}|${time}|${nonce}|d-token=${token}`
-
-  const sig = await crypto.subtle.sign('HMAC', await getKey(), enc.encode(str))
-  const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('')
-
-  return { 'X-Time': time, 'X-Nonce': nonce, 'X-Sign': hex }
-}
-```
-
-> Web Crypto 的 `crypto.subtle` 只在 HTTPS 或 localhost 下可用,若前端部署在 http 环境请使用方案 A。
-
-#### 挂到 axios 拦截器
-
-```js
-import { buildSignHeaders } from '@/utils/sign'
-import store from '@/store'
-
-service.interceptors.request.use(async (config) => {
-  if (config.skipSign) return config          // 个别请求可跳过
-
-  const token = store.getters.token || ''
-  // 关键:path 必须是含 /api 前缀的真实路径,且不含 query
-  const rawUrl = String(config.url || '')
-  const path   = '/' + rawUrl.split('?')[0].replace(/^\/+/, '')
-  const method = (config.method || 'get').toUpperCase()
-
-  const headers = await buildSignHeaders(method, path, token)
-  Object.assign(config.headers, headers)
-
-  return config
-}, (error) => Promise.reject(error))
-```
-
-### 2.5 签名相关常见错误
-
-| 现象                   | 排查方向                                                                                                                          |
-| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
-| 一直 1002              | 先跑第 2.3 节自检用例;再打印前端实际拼出的`path`,与后端 `$request->path()` 对比                                             |
-| 偶发 1002              | 服务器与客户端时钟偏差超过 120 秒,检查服务器 NTP                                                                                 |
-| 1002 且提示 nonce 重复 | 同一个`X-Nonce` 被复用(例如请求重试时复用了旧头),重试需重新生成                                                              |
-| `path` 对不上        | 注意`axios` 的 `baseURL`:若 `/api` 写在 `baseURL` 里、`url` 只写了 `/anime/detail`,需手动补成 `/api/anime/detail` |
-| token 对不上           | 签名里的`d-token` 必须与请求头 `d-token` 完全一致(含大小写),若 token 做过 trim 或转换需保持一致                            |
-
----
-
-## 三、登录图片验证码
-
-### 3.1 获取验证码
-
-```
-GET /api/captcha
-```
-
-免登录,无需签名。返回:
-
-```json
-{
-  "msg": "",
-  "code": 0,
-  "data": {
-    "captcha_key": "8f14e45fceea167a5a36dedd4bea2543",
-    "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
-  }
-}
-```
-
-- captcha_key:本次验证码的唯一标识,登录时回传;
-- `image`:完整的 data URI,可直接作为 `<img>` 的 `src`,无需额外处理。
-
-服务端对该接口有 **20 次/分钟/IP** 的限流。
-
-### 3.2 登录
-
-```
-GET /api/login?account=<账号>&passwd=<密码>&captcha_key=<key>&captcha_code=<用户输入>
-```
-
-| 参数             | 必填                 | 说明                           |
-| ---------------- | -------------------- | ------------------------------ |
-| `account`      | 是                   | 账号                           |
-| `passwd`       | 是                   | 密码                           |
-| `captcha_key`  | 后端开启验证码时必填 | 获取验证码接口返回的`key`    |
-| `captcha_code` | 后端开启验证码时必填 | 用户输入的验证码,大小写不敏感 |
-
-成功返回:
-
-```json
-{
-  "msg": "",
-  "code": 0,
-  "data": {
-    "uid": 12,
-    "nickname": "张三",
-    "token": "f3a8c1e0b7d94a2f8e6c5b1d0a9f3e72",
-    "role": "admin"
-  }
-}
-```
-
-`role` 取值:`superadmin`(平台)、`admin`(公司管理员)、`user`(组员)。
-
-### 3.3 验证码的关键行为
-
-- **有效期 5 分钟**,过期后需重新获取;
-- **一次性消费**:无论校验成功还是失败,`key` 立即失效,所以用户输错后必须自动刷新一张新验证码;
-- 大小写不敏感;
-- 错误返回 `code = 10021`。
-
-> 服务端有总开关。开关未开启时,不传 `captcha_key` / `captcha_code` 也能正常登录,因此前端可以先上线,等后端开启后再生效。
-
-### 3.4 前端实现要点
-
-登录页:
-
-1. 进入页面时调用 `GET /api/captcha`,把 `data.image` 绑到 `<img :src="captcha.image">`;
-2. 点击图片重新获取(同时清空输入框);
-3. 提交登录时带上 `captcha_key` 与 `captcha_code`;
-4. **登录返回 `code === 10021` 时,自动重新拉取验证码并提示用户重新输入**;
-5. 其它失败(如密码错误)也建议刷新验证码,避免用户拿到已消费的 `key` 反复提交。
-
----
-
-## 四、其它常用接口
-
-### 4.1 获取当前用户信息
-
-```
-GET /api/userInfo
-```
-
-需登录。返回 `uid`、`nickname`、`token`、`role`、`points` 等字段。
-
-### 4.2 退出登录
-
-```
-GET /api/logout
-```
-
----
-
-## 五、灰度与上线说明
-
-本文档涉及的两项能力都由后端开关控制,前端可以先行上线,行为与现在完全一致:
-
-| 能力       | 后端开关            | 未开启时的表现             |
-| ---------- | ------------------- | -------------------------- |
-| 接口签名   | `CHECK_SIGN`      | 签名头被忽略,按原逻辑放行 |
-| 登录验证码 | `CAPTCHA_ENABLED` | 登录不校验验证码           |
-
-**建议的上线顺序**
-
-1. 后端部署(两个开关均为关闭状态,线上零影响);
-2. 前端发布带签名与验证码的版本;此时功能表现不变,可确认页面无异常;
-3. 后端依次开启开关并清理配置缓存(`php artisan config:clear`):
-   - 先开 `CHECK_SIGN`,观察日志是否出现验签失败;
-   - 再开 `CAPTCHA_ENABLED`,确认登录流程正常。
-
-**需要向后端确认的信息**
-
-- `SIGN_SALT` 的正式值(需与前端打包配置一致,改动需两端同步);
-- `CHECK_SIGN` 与 `CAPTCHA_ENABLED` 的开启时间点;
-- 是否存在验签白名单账号(白名单账号不校验签名,表现为"签名错误也能调用成功",属正常现象,不要据此判断前端实现有误)。
-
----
-
-## 六、变更记录
-
-| 日期       | 内容                                                         |
-| ---------- | ------------------------------------------------------------ |
-| 2026-09-21 | 初版:接口签名(HMAC-SHA256 + nonce 防重放)、登录图片验证码 |