beervid-app-cli 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -83,6 +83,14 @@ interface OpenApiResponse<T> {
83
83
  - 发布挂车视频时依赖商品信息
84
84
  - 发布后通常立即完成,不走 TT 的轮询链路
85
85
 
86
+ ### TT / TTS 关联场景
87
+
88
+ - TTS 账号本身不支持视频数据查询。
89
+ - 如果同一达人既要挂车发布,又要查询视频播放/点赞/评论等数据,必须同时完成 TTS 授权和 TT 授权。
90
+ - 官方当前没有提供 `uno_id` 之类可直接打通 TT/TTS 的关联字段。
91
+ - 当前推荐在两边授权完成后,调用 `account/info` 拉取账号详情,并以 `username` 作为你方系统内的关联键。
92
+ - 关联完成后,发布挂车继续使用 `creatorUserOpenId`,查询视频数据使用对应 TT 账号的 `businessId`。
93
+
86
94
  ## 六类核心能力
87
95
 
88
96
  只保留能力定位,详细接口说明去看引用文档:
@@ -102,7 +110,7 @@ interface OpenApiResponse<T> {
102
110
 
103
111
  ```text
104
112
  获取 TT OAuth URL
105
- -> 用户授权回调得到 ttAbId
113
+ -> 用户授权回调,从 `state` JSON 中解析得到 ttAbId
106
114
  -> ttAbId 作为 businessId 持久化
107
115
  -> 获取上传凭证
108
116
  -> 上传普通视频,拿到 fileUrl
@@ -116,7 +124,7 @@ interface OpenApiResponse<T> {
116
124
 
117
125
  ```text
118
126
  获取 TTS OAuth URL
119
- -> 用户授权回调得到 ttsAbId
127
+ -> 用户授权回调,从 `state` JSON 中解析得到 ttsAbId
120
128
  -> ttsAbId 作为 creatorUserOpenId 持久化
121
129
  -> 查询商品,得到 productId + productTitle
122
130
  -> 获取上传凭证
@@ -124,22 +132,39 @@ interface OpenApiResponse<T> {
124
132
  -> 发布挂车视频
125
133
  ```
126
134
 
135
+ ### 同一达人既要挂车发布又要查视频数据
136
+
137
+ ```text
138
+ 先授权 TTS
139
+ -> 得到 ttsAbId -> 持久化为 creatorUserOpenId
140
+ -> 调用 account/info -> 记录 TTS username
141
+
142
+ 再授权 TT
143
+ -> 得到 ttAbId -> 持久化为 businessId
144
+ -> 调用 account/info -> 记录 TT username
145
+
146
+ 用 username 在你方系统中建立 TT <-> TTS 关联
147
+ -> 挂车发布时使用 creatorUserOpenId
148
+ -> 视频查数时使用 businessId
149
+ ```
150
+
127
151
  ## 关键参数链路
128
152
 
129
153
  这是主文件里最值得保留的部分,因为它决定了接口如何串起来:
130
154
 
131
155
  | 参数 | 含义 | 产出来源 |
132
156
  | ------------------------ | -------------------- | -------------------------------------------------- |
133
- | `businessId` | TT 账号业务 ID | OAuth 回调参数 `ttAbId` |
134
- | `creatorUserOpenId` | TTS 账号 OpenId | OAuth 回调参数 `ttsAbId` |
157
+ | `businessId` | TT 账号业务 ID | OAuth 回调 `state` JSON 中的 `ttAbId` |
158
+ | `creatorUserOpenId` | TTS 账号 OpenId | OAuth 回调 `state` JSON 中的 `ttsAbId` |
135
159
  | `accountId` | 平台账号 ID | 即 `ttAbId` 或 `ttsAbId`,用于查询账号详情 |
160
+ | `username` | 账号用户名 | `account/info` 返回;当前推荐作为 TT/TTS 关联键 |
136
161
  | `uploadToken` | 上传凭证 | `upload-token/generate` 返回 |
137
162
  | `fileUrl` | 普通上传后的视频 URL | `file-upload` 返回 |
138
163
  | `videoFileId` / `fileId` | TTS 视频文件 ID | `file-upload/tts-video` 返回 |
139
164
  | `shareId` | TT 普通发布追踪 ID | `tiktok/video/publish` 返回 |
140
165
  | `videoId` | TikTok 视频 ID | TTS 发布直接返回;TT 从状态查询 `post_ids[0]` 获取 |
141
166
  | `productId` | 商品 ID | `tts/products/query` 返回的商品列表 |
142
- | `productTitle` | 商品标题 | 同上,最多 29 字符,超出应先截断 |
167
+ | `productTitle` | 商品标题 | 同上,最多 30 字符,超出应先截断 |
143
168
  | `itemIds` | 视频 ID 数组 | 来源于 `videoId`,用于查询视频数据 |
144
169
 
145
170
  ```text
@@ -173,8 +198,9 @@ creatorUserOpenId -> upload-tts-video -> fileId -> shoppable-publish -> videoId
173
198
  ### 常见业务约束
174
199
 
175
200
  - TT 普通视频发布后需要轮询;只有 `PUBLISH_COMPLETE` 且 `post_ids` 非空才算成功完成;TTS 挂车视频通常不需要
176
- - 仅 TT 授权账号可查询视频数据
177
- - `productTitle` 最多 29 字符,超出时应提前截断
201
+ - 仅 TT 授权账号可查询视频数据;TTS-only 账号不能查数
202
+ - 如果同一达人既要 TTS 挂车发布又要查视频数据,必须额外授权 TT;当前推荐以 `username` 关联 TT/TTS,不能依赖官方 `uno_id`
203
+ - `productTitle` 最多 30 字符,超出时应提前截断
178
204
  - 商品图片字段可能是特殊字符串格式,解析细节见 [`docs/tts-product-cache.md`](./docs/tts-product-cache.md)
179
205
  - 视频查询接口可能同时返回 camelCase 与 snake_case 字段,兼容细节见 [`references/api-reference.md`](./references/api-reference.md)
180
206
 
@@ -209,7 +235,7 @@ export BEERVID_APP_BASE_URL="https://open.beervid.ai"
209
235
  | `beervid upload` | 上传视频 | `--file`, `--type tts`, `--creator-id`, `--token` |
210
236
  | `beervid publish` | 发布普通或挂车视频 | `--type normal | shoppable` 加对应业务参数 |
211
237
  | `beervid poll-status` | 轮询 TT 发布状态 | `--business-id`, `--share-id`, `--interval`, `--max-polls` |
212
- | `beervid query-video` | 查询视频数据 | `--business-id`, `--item-ids` |
238
+ | `beervid query-video` | 查询视频数据 | `--business-id`, `--item-ids`, `--cursor`, `--max-count` |
213
239
  | `beervid query-products` | 查询 TTS 商品 | `--creator-id`, `--product-type`, `--cursor` |
214
240
  | `beervid publish-tt-flow` | 执行 TT 完整发布流程 | `--business-id`, `--file`, `--caption` |
215
241
  | `beervid publish-tts-flow` | 执行 TTS 完整发布流程 | `--creator-id`, `--file`, `--interactive`, `--product-id`, `--product-title` |
@@ -247,6 +273,7 @@ beervid poll-status --business-id=biz_12345 --share-id share_abc123
247
273
 
248
274
  # 查询视频数据
249
275
  beervid query-video --business-id=biz_12345 --item-ids 7123456789012345678
276
+ beervid query-video --business-id=biz_12345 --cursor 0 --max-count 20
250
277
 
251
278
  # 查询商品
252
279
  beervid query-products --creator-id=open_user_abc
@@ -266,6 +293,13 @@ beervid publish-tts-flow --creator-id=open_user_abc --file https://example.com/v
266
293
 
267
294
  ## 读哪份文档
268
295
 
296
+ ### 快速开始
297
+
298
+ 如果你是第一次接触 BEERVID Open API,从这里开始:
299
+
300
+ - **5 分钟快速上手**:[`QUICKSTART.md`](./QUICKSTART.md) - 从零到发布第一个视频
301
+ - **常见问题**:[`FAQ.md`](./FAQ.md) - 快速查找常见问题的答案
302
+
269
303
  ### 接口细节
270
304
 
271
305
  当你需要以下内容时,直接读取 [`references/api-reference.md`](./references/api-reference.md):
@@ -286,9 +320,18 @@ beervid publish-tts-flow --creator-id=open_user_abc --file https://example.com/v
286
320
  - TTS 商品缓存:[`docs/tts-product-cache.md`](./docs/tts-product-cache.md)
287
321
  - 重试与幂等:[`docs/retry-and-idempotency.md`](./docs/retry-and-idempotency.md)
288
322
 
323
+ ### 运维与优化
324
+
325
+ 生产环境部署和优化指南:
326
+
327
+ - **性能与限流**:[`docs/performance-and-limits.md`](./docs/performance-and-limits.md) - API 限流策略、并发控制、性能优化
328
+ - **安全最佳实践**:[`docs/security-best-practices.md`](./docs/security-best-practices.md) - API Key 安全、OAuth 安全、生产环境检查清单
329
+ - **故障排查**:[`docs/troubleshooting.md`](./docs/troubleshooting.md) - 常见错误及解决方案、调试技巧
330
+ - **测试指南**:[`docs/testing-guide.md`](./docs/testing-guide.md) - 单元测试、集成测试、Mock 数据
331
+
289
332
  ### 示例工程
290
333
 
291
- 当用户需要“可运行参考实现”时,按技术栈读取:
334
+ 当用户需要”可运行参考实现”时,按技术栈读取:
292
335
 
293
336
  - 纯脚本示例:[`example/standard/README.md`](./example/standard/README.md)
294
337
  - Express 示例:[`example/express/README.md`](./example/express/README.md)
@@ -35,6 +35,7 @@ CREATE TABLE beervid_accounts (
35
35
 
36
36
  -- 账号详情(来自 POST /api/v1/open/account/info)
37
37
  username VARCHAR(256) DEFAULT NULL,
38
+ link_username VARCHAR(256) DEFAULT NULL COMMENT '推荐的 TT/TTS 关联键(建议存归一化后的 username)',
38
39
  display_name VARCHAR(256) DEFAULT NULL,
39
40
  seller_name VARCHAR(256) DEFAULT NULL COMMENT 'TTS 账号的卖家名称',
40
41
  profile_url TEXT DEFAULT NULL COMMENT '头像 URL',
@@ -57,7 +58,8 @@ CREATE TABLE beervid_accounts (
57
58
  UNIQUE KEY uk_account (account_type, account_id),
58
59
  KEY idx_app_user (app_user_id),
59
60
  KEY idx_business_id (business_id),
60
- KEY idx_creator_user_open_id (creator_user_open_id)
61
+ KEY idx_creator_user_open_id (creator_user_open_id),
62
+ KEY idx_link_username (link_username)
61
63
  );
62
64
  ```
63
65
 
@@ -68,9 +70,17 @@ CREATE TABLE beervid_accounts (
68
70
  | `account_id` | OAuth 回调参数 `ttAbId` 或 `ttsAbId` | 唯一标识,与 `account_type` 组成唯一键 |
69
71
  | `business_id` | 等同于 `ttAbId` | TT 账号的所有操作(发布、轮询、查数据)都以此为入参 |
70
72
  | `creator_user_open_id` | 等同于 `ttsAbId` | TTS 账号的所有操作(上传、发布、查商品)都以此为入参 |
73
+ | `link_username` | `account/info` 返回的 `username` 归一化后保存 | 当前推荐用作 TT/TTS 的关联键;官方暂无 `uno_id` |
71
74
  | `access_token` | `account/info` 返回 | 按需存储,用于特殊场景 |
72
75
  | `app_user_id` | 你方系统 | 一个用户可绑定多个 TT/TTS 账号 |
73
76
 
77
+ ### TT / TTS 关联建议
78
+
79
+ - 同一达人如果既授权了 TTS,又授权了 TT,建议保存为两条账号记录。
80
+ - 官方当前没有提供 `uno_id` 这类 TT/TTS 强关联字段。
81
+ - 推荐额外维护 `link_username`,例如保存 `LOWER(TRIM(username))` 后的值,作为当前最稳妥的软关联键。
82
+ - 真正调用接口时不要使用 `link_username` 替代业务 ID;TT 继续使用 `business_id`,TTS 继续使用 `creator_user_open_id`。
83
+
74
84
  ---
75
85
 
76
86
  ## 2. 视频表 `beervid_videos`
@@ -98,7 +108,7 @@ CREATE TABLE beervid_videos (
98
108
 
99
109
  -- TTS 挂车专用
100
110
  product_id VARCHAR(128) DEFAULT NULL COMMENT '关联商品 ID',
101
- product_title VARCHAR(64) DEFAULT NULL COMMENT '关联商品标题(≤29字符)',
111
+ product_title VARCHAR(64) DEFAULT NULL COMMENT '关联商品标题(≤30字符)',
102
112
 
103
113
  -- 发布状态
104
114
  publish_status VARCHAR(32) DEFAULT 'PENDING'
@@ -24,9 +24,16 @@
24
24
 
25
25
  ## 回调参数
26
26
 
27
- OAuth 授权完成后,回调 URL 会携带以下关键参数:
27
+ OAuth 授权完成后,回调 URL 会携带 `state` 查询参数,其值是一个 **JSON 字符串**。业务字段包含在这个 JSON 内部:
28
28
 
29
- | 账号类型 | 回调参数 | 含义 | 后续用途 |
29
+ ```
30
+ 回调 URL 示例:
31
+ https://your-app.com/callback?state={"ttAbId":"xxx","code":"yyy",...}
32
+ ```
33
+
34
+ 从 `state` JSON 中提取的关键字段:
35
+
36
+ | 账号类型 | 字段 | 含义 | 后续用途 |
30
37
  |----------|----------|------|----------|
31
38
  | TT | `ttAbId` | TT 账号的 businessId | 所有 TT 操作的入参 |
32
39
  | TTS | `ttsAbId` | TTS 账号的 creatorUserOpenId | 所有 TTS 操作的入参 |
@@ -34,6 +41,39 @@ OAuth 授权完成后,回调 URL 会携带以下关键参数:
34
41
  > **重要**:`ttAbId` 就是后续所有 TT API 的 `businessId`,`ttsAbId` 就是所有 TTS API 的 `creatorUserOpenId`。
35
42
  > 这两个值必须可靠持久化,丢失意味着需要用户重新授权。
36
43
 
44
+ ## TT / TTS 账号关联建议
45
+
46
+ 如果同一达人既要做 TTS 挂车发布,又要查询视频数据,建议按下面方式建模:
47
+
48
+ - TTS 授权和 TT 授权分别落成两条独立账号记录,不要混存成一条。
49
+ - 回调阶段先持久化 `ttAbId` / `ttsAbId`,再异步调用 `account/info` 补全账号详情。
50
+ - 官方当前没有提供 `uno_id` 这类可直接打通 TT/TTS 的稳定字段。
51
+ - 当前推荐使用 `account/info` 返回的 `username` 作为关联键,在你方系统里建立 TT 和 TTS 的软关联。
52
+ - 业务调用时不要把 `username` 当接口入参;挂车发布仍使用 `creatorUserOpenId`,视频查数仍使用 `businessId`。
53
+
54
+ ### 解析 state 中的回调字段
55
+
56
+ ```typescript
57
+ interface OAuthCallbackState {
58
+ ttAbId?: string
59
+ ttsAbId?: string
60
+ code?: string
61
+ [key: string]: unknown // 可能包含你之前追加的自定义字段
62
+ }
63
+
64
+ function parseCallbackState(stateParam: string): OAuthCallbackState {
65
+ try {
66
+ const parsed = JSON.parse(stateParam) as unknown
67
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
68
+ return parsed as OAuthCallbackState
69
+ }
70
+ throw new Error('state 不是合法 JSON 对象')
71
+ } catch {
72
+ throw new Error('state 解析失败')
73
+ }
74
+ }
75
+ ```
76
+
37
77
  ---
38
78
 
39
79
  ## 获取授权链接后如何设置或追加 State
@@ -181,35 +221,44 @@ async function consumeStateNonce(nonce: string): Promise<boolean> {
181
221
  ```typescript
182
222
  async function handleOAuthCallback(req: Request): Promise<Response> {
183
223
  const url = new URL(req.url)
184
- const state = url.searchParams.get('state')
185
- const ttAbId = url.searchParams.get('ttAbId')
186
- const ttsAbId = url.searchParams.get('ttsAbId')
224
+ const stateParam = url.searchParams.get('state')
187
225
 
188
- // ① 验证 State Token
189
- if (!state) {
226
+ // ① 解析 state JSON,提取回调字段
227
+ if (!stateParam) {
190
228
  return new Response('缺少 state 参数', { status: 400 })
191
229
  }
192
230
 
193
- let statePayload: { userId: string; nonce: string }
231
+ let stateObj: OAuthCallbackState
194
232
  try {
195
- statePayload = verifyStateToken(state)
196
- } catch (err) {
197
- return new Response('授权链接已过期,请重新发起授权', { status: 403 })
233
+ stateObj = parseCallbackState(stateParam)
234
+ } catch {
235
+ return new Response('state 解析失败', { status: 400 })
198
236
  }
199
237
 
200
- // 一次性消费检查
201
- const isFirstUse = await consumeStateNonce(statePayload.nonce)
202
- if (!isFirstUse) {
203
- return new Response('该授权回调已处理过,请勿重复提交', { status: 409 })
238
+ const { ttAbId, ttsAbId } = stateObj
239
+
240
+ // 验证你方追加的自定义安全字段(如果有)
241
+ if (stateObj.customStateToken) {
242
+ let statePayload: { userId: string; nonce: string }
243
+ try {
244
+ statePayload = verifyStateToken(stateObj.customStateToken)
245
+ } catch (err) {
246
+ return new Response('授权链接已过期,请重新发起授权', { status: 403 })
247
+ }
248
+
249
+ // ③ 一次性消费检查
250
+ const isFirstUse = await consumeStateNonce(statePayload.nonce)
251
+ if (!isFirstUse) {
252
+ return new Response('该授权回调已处理过,请勿重复提交', { status: 409 })
253
+ }
204
254
  }
205
255
 
206
- // 判断账号类型并持久化
256
+ // 判断账号类型并持久化
207
257
  if (ttAbId) {
208
258
  await saveAccount({
209
259
  accountType: 'TT',
210
260
  accountId: ttAbId,
211
261
  businessId: ttAbId,
212
- appUserId: statePayload.userId,
213
262
  status: 'ACTIVE',
214
263
  })
215
264
  }
@@ -219,21 +268,19 @@ async function handleOAuthCallback(req: Request): Promise<Response> {
219
268
  accountType: 'TTS',
220
269
  accountId: ttsAbId,
221
270
  creatorUserOpenId: ttsAbId,
222
- appUserId: statePayload.userId,
223
271
  status: 'ACTIVE',
224
272
  })
225
273
  }
226
274
 
227
- // 异步拉取账号详情(不阻塞回调响应)
275
+ // 异步拉取账号详情(不阻塞回调响应)
228
276
  const accountId = ttAbId || ttsAbId
229
277
  const accountType = ttAbId ? 'TT' : 'TTS'
230
278
 
231
- // 使用 fire-and-forget 或投递到消息队列
232
279
  syncAccountInfoAsync(accountType, accountId!).catch((err) => {
233
280
  console.error('异步同步账号信息失败(不影响授权结果):', err.message)
234
281
  })
235
282
 
236
- // 返回成功页面或重定向
283
+ // 返回成功页面或重定向
237
284
  return Response.redirect('/dashboard?oauth=success', 302)
238
285
  }
239
286
  ```
@@ -248,6 +295,7 @@ async function handleOAuthCallback(req: Request): Promise<Response> {
248
295
  - 回调请求应快速响应,避免用户长时间等待
249
296
  - 头像同步等操作允许延迟几秒完成
250
297
  - 即使同步失败,授权本身已成功
298
+ - 如果要关联同一达人的 TT / TTS 账号,建议在这里同步并持久化 `username`
251
299
 
252
300
  ```typescript
253
301
  async function syncAccountInfoAsync(
@@ -261,6 +309,7 @@ async function syncAccountInfoAsync(
261
309
 
262
310
  await updateAccount(accountId, {
263
311
  username: data.username,
312
+ linkUsername: data.username?.trim().toLowerCase(),
264
313
  displayName: data.displayName,
265
314
  sellerName: data.sellerName,
266
315
  profileUrl: data.profileUrl,
@@ -0,0 +1,153 @@
1
+ # 性能优化与限流说明
2
+
3
+ 本文档只保留与 BEERVID Open API 接入直接相关的性能建议,重点关注限流、分页、轮询和批量调用。
4
+
5
+ ## 目录
6
+
7
+ 1. [限流处理](#限流处理)
8
+ 2. [分页与批量查询](#分页与批量查询)
9
+ 3. [TT 发布轮询](#tt-发布轮询)
10
+ 4. [上传与文件处理](#上传与文件处理)
11
+ 5. [实践建议](#实践建议)
12
+
13
+ ---
14
+
15
+ ## 限流处理
16
+
17
+ ### 1. 遇到 `429` 时做退避重试
18
+
19
+ 当前仓库的 `openApiPost` / `openApiGet` 在失败时会抛出 `Error.message`,所以建议按错误消息判断是否命中了限流:
20
+
21
+ ```typescript
22
+ function isRateLimitError(error: unknown): boolean {
23
+ const message = error instanceof Error ? error.message : String(error)
24
+ return message.includes('(code: 429)') || message.includes('Too Many Requests')
25
+ }
26
+
27
+ async function retryWithBackoff<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
28
+ for (let i = 0; i < maxRetries; i++) {
29
+ try {
30
+ return await fn()
31
+ } catch (error) {
32
+ if (isRateLimitError(error) && i < maxRetries - 1) {
33
+ const delayMs = Math.pow(2, i) * 1000
34
+ await sleep(delayMs)
35
+ continue
36
+ }
37
+ throw error
38
+ }
39
+ }
40
+
41
+ throw new Error('Max retries exceeded')
42
+ }
43
+ ```
44
+
45
+ ### 2. 非幂等发布不要无脑重试
46
+
47
+ - `query-products`、`query-video`、`poll-status` 这类读操作适合退避重试
48
+ - 上传和发布动作要区分对待
49
+ - 对于 `publish` / `publish-tt-flow` / `publish-tts-flow`,优先保证幂等控制,再决定是否补偿重试
50
+
51
+ ---
52
+
53
+ ## 分页与批量查询
54
+
55
+ ### 1. `query-products` 的 `pageSize` 最大为 `20`
56
+
57
+ OpenAPI 已明确商品查询每页最多 `20` 条,当前 CLI 也已按这个范围校验:
58
+
59
+ ```bash
60
+ beervid query-products --creator-id open_user_abc --page-size 20
61
+ ```
62
+
63
+ 如果需要拉更多商品,使用分页游标继续查:
64
+
65
+ ```bash
66
+ beervid query-products --creator-id open_user_abc --cursor <next-cursor>
67
+ ```
68
+
69
+ ### 2. `query-video` 支持分页
70
+
71
+ 视频查询现在支持:
72
+
73
+ - 不传 `itemIds`,按账号查询视频列表
74
+ - 传 `--cursor`
75
+ - 传 `--max-count`(10-20)
76
+
77
+ ```bash
78
+ beervid query-video --business-id biz_123 --cursor 0 --max-count 20
79
+ beervid query-video --business-id biz_123 --item-ids 7123,7124
80
+ ```
81
+
82
+ ### 3. 批量查询时按 20 条一批拆分
83
+
84
+ ```typescript
85
+ function chunk<T>(array: T[], size: number): T[][] {
86
+ return Array.from(
87
+ { length: Math.ceil(array.length / size) },
88
+ (_, i) => array.slice(i * size, (i + 1) * size)
89
+ )
90
+ }
91
+
92
+ for (const batch of chunk(videoIds, 20)) {
93
+ await openApiPost('/api/v1/open/tiktok/video/query', {
94
+ businessId,
95
+ itemIds: batch,
96
+ })
97
+ }
98
+ ```
99
+
100
+ ---
101
+
102
+ ## TT 发布轮询
103
+
104
+ ### 1. `poll-status` 只在 TT 普通发布链路里使用
105
+
106
+ 推荐链路:
107
+
108
+ 1. `publish`
109
+ 2. 拿到 `shareId`
110
+ 3. `poll-status`
111
+ 4. 当 `status === PUBLISH_COMPLETE` 且 `post_ids` 非空时,再去 `query-video`
112
+
113
+ ### 2. 轮询间隔不要压太短
114
+
115
+ 当前 CLI 默认:
116
+
117
+ - `--interval 5`
118
+ - `--max-polls 60`
119
+
120
+ 这套默认值已经覆盖大多数场景;如果你在服务端自己实现轮询任务,也建议保持秒级而不是毫秒级高频轮询。
121
+
122
+ ---
123
+
124
+ ## 上传与文件处理
125
+
126
+ ### 1. 优先上传本地文件,远程 URL 会多一次下载
127
+
128
+ 当前 CLI 的 `--file` 同时支持本地路径和 URL,但传 URL 时会先下载再上传:
129
+
130
+ ```bash
131
+ beervid upload --file ./video.mp4
132
+ beervid upload --file https://example.com/video.mp4
133
+ ```
134
+
135
+ 如果你已经在服务端拿到了文件内容,优先走本地文件/内存文件上传,减少链路耗时。
136
+
137
+ ### 2. 大文件场景加超时与进度监控
138
+
139
+ 如果你不是直接用 CLI,而是自己封装上传,至少补这两项:
140
+
141
+ - 请求超时
142
+ - 上传进度日志
143
+
144
+ 当前项目里如果需要上传进度,仍然推荐优先用 XHR 而不是裸 `fetch`。
145
+
146
+ ---
147
+
148
+ ## 实践建议
149
+
150
+ - 把所有 Open API 调用都收口到统一 client 层,方便集中做超时、重试和日志
151
+ - 读接口优先重试,写接口优先幂等
152
+ - 商品查询和视频查询一律按分页参数设计,不要默认一次拉完全部数据
153
+ - TT 轮询结果只有在 `post_ids` 真正返回后,才进入后续查数链路
@@ -0,0 +1,132 @@
1
+ # 安全最佳实践
2
+
3
+ 本文档只保留与 BEERVID Open API 接入直接相关的安全要求,避免掺入与当前 API 无关的通用后端模板内容。
4
+
5
+ ## 目录
6
+
7
+ 1. [API Key 使用](#api-key-使用)
8
+ 2. [OAuth 回调处理](#oauth-回调处理)
9
+ 3. [请求与日志](#请求与日志)
10
+ 4. [账号与 Token 存储](#账号与-token-存储)
11
+ 5. [上线前检查](#上线前检查)
12
+
13
+ ---
14
+
15
+ ## API Key 使用
16
+
17
+ ### 1. 不要硬编码 `BEERVID_APP_KEY`
18
+
19
+ ```typescript
20
+ const apiKey = process.env.BEERVID_APP_KEY
21
+ if (!apiKey) {
22
+ throw new Error('BEERVID_APP_KEY is required')
23
+ }
24
+ ```
25
+
26
+ ### 2. 只在服务端保存和使用 API Key
27
+
28
+ - 不要把 `BEERVID_APP_KEY` 下发到浏览器或移动端
29
+ - 前端如果需要调用能力,应先请求你自己的后端,再由后端调用 Open API
30
+ - CLI 场景下优先使用 `beervid config --app-key` 或环境变量,不要把 Key 写进代码仓库
31
+
32
+ ### 3. 区分不同环境的 Key
33
+
34
+ - 测试、预发、生产环境分别使用独立 Key
35
+ - 怀疑泄露时立即轮换 Key
36
+ - 不要在日志、截图、报错信息里输出完整 Key
37
+
38
+ ---
39
+
40
+ ## OAuth 回调处理
41
+
42
+ ### 1. 校验并持久化 `state`
43
+
44
+ BEERVID 的 TT / TTS OAuth 流程依赖回调 URL 中的 `state` 参数;你至少应做到:
45
+
46
+ - 生成授权链接时为当前用户生成一次性 `state`
47
+ - 回调时校验 `state` 是否存在、是否过期、是否已使用
48
+ - 从 `state` 中提取 `ttAbId` / `ttsAbId` 后再落库
49
+
50
+ 推荐直接参考 [oauth-callback.md](./oauth-callback.md) 中的完整落地方式。
51
+
52
+ ### 2. 回调地址使用 HTTPS
53
+
54
+ - 生产环境回调 URL 使用 HTTPS
55
+ - 只在你自己控制的域名下接收回调
56
+ - 不要把完整回调 URL 原样打进日志,避免把 `state` 暴露出去
57
+
58
+ ---
59
+
60
+ ## 请求与日志
61
+
62
+ ### 1. 统一通过服务端封装请求
63
+
64
+ 建议统一封装 `openApiGet`、`openApiPost`、`openApiUpload` 这类函数,集中处理:
65
+
66
+ - `X-API-KEY` 注入
67
+ - 基础地址拼接
68
+ - 超时控制
69
+ - 响应解包
70
+ - 错误格式化
71
+
72
+ ### 2. 日志里只打印脱敏后的关键信息
73
+
74
+ 推荐记录:
75
+
76
+ - 接口路径
77
+ - 业务账号 ID(`businessId` / `creatorUserOpenId`)
78
+ - `shareId` / `videoId`
79
+ - Open API `code`、`message`
80
+
81
+ 不要记录:
82
+
83
+ - 完整 API Key
84
+ - 完整 access token
85
+ - 完整 OAuth `state`
86
+ - 含敏感 query 参数的原始 URL
87
+
88
+ ### 3. 对外错误信息保持收敛
89
+
90
+ - 对用户返回“授权失效”“参数错误”“上传失败”这类业务级信息
91
+ - 详细堆栈和 Open API 原始报错只保留在服务端日志中
92
+
93
+ ---
94
+
95
+ ## 账号与 Token 存储
96
+
97
+ ### 1. 只存必须的账号字段
98
+
99
+ 对当前项目来说,真正高频使用的字段通常是:
100
+
101
+ - `accountType`
102
+ - `accountId`
103
+ - `businessId`
104
+ - `creatorUserOpenId`
105
+ - `username`
106
+ - `displayName`
107
+ - `sellerName`
108
+ - `profileUrl`
109
+ - `followersCount`
110
+
111
+ ### 2. `accessToken` 按需存储
112
+
113
+ `account/info` 会返回 `accessToken`。如果你的业务不会直接使用它:
114
+
115
+ - 可以不入库
116
+ - 或仅短期缓存,不作为常规展示字段返回给前端
117
+
118
+ 如果必须持久化,至少做到:
119
+
120
+ - 数据库字段与普通展示字段分开
121
+ - 日志脱敏
122
+ - 读取权限最小化
123
+
124
+ ---
125
+
126
+ ## 上线前检查
127
+
128
+ - `BEERVID_APP_KEY` 仅存在于服务端环境变量或本地 CLI 配置中
129
+ - OAuth 回调已校验 `state`,且 `ttAbId` / `ttsAbId` 已正确落库
130
+ - 对外接口不会返回 API Key、access token、原始 `state`
131
+ - Open API 请求封装已统一超时和错误处理
132
+ - 业务日志里只保留脱敏后的账号 ID、`shareId`、`videoId`