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.
- package/README.md +19 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.mjs +165 -73
- package/dist/index.d.ts +255 -0
- package/dist/index.mjs +1 -0
- package/package.json +11 -2
- package/skills/beervid-app-cli/FAQ.md +214 -0
- package/skills/beervid-app-cli/QUICKSTART.md +206 -0
- package/skills/beervid-app-cli/SKILL.md +52 -9
- package/skills/beervid-app-cli/docs/database-schema.md +12 -2
- package/skills/beervid-app-cli/docs/oauth-callback.md +70 -21
- package/skills/beervid-app-cli/docs/performance-and-limits.md +153 -0
- package/skills/beervid-app-cli/docs/security-best-practices.md +132 -0
- package/skills/beervid-app-cli/docs/testing-guide.md +689 -0
- package/skills/beervid-app-cli/docs/troubleshooting.md +468 -0
- package/skills/beervid-app-cli/example/express/README.md +6 -0
- package/skills/beervid-app-cli/example/express/server.ts +23 -5
- package/skills/beervid-app-cli/example/nextjs/README.md +6 -0
- package/skills/beervid-app-cli/example/nextjs/app/api/oauth/callback/route.ts +22 -6
- package/skills/beervid-app-cli/example/nextjs/app/api/publish/tts/route.ts +2 -2
- package/skills/beervid-app-cli/example/standard/README.md +6 -0
- package/skills/beervid-app-cli/example/standard/tts-publish-flow.ts +1 -1
- package/skills/beervid-app-cli/references/api-reference.md +37 -11
- package/skills/beervid-app-cli/skill.json +36 -0
|
@@ -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
|
-
->
|
|
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
|
-
->
|
|
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
|
|
134
|
-
| `creatorUserOpenId` | TTS 账号 OpenId | OAuth
|
|
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` | 商品标题 | 同上,最多
|
|
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
|
-
- `
|
|
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 '关联商品标题(≤
|
|
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
|
|
185
|
-
const ttAbId = url.searchParams.get('ttAbId')
|
|
186
|
-
const ttsAbId = url.searchParams.get('ttsAbId')
|
|
224
|
+
const stateParam = url.searchParams.get('state')
|
|
187
225
|
|
|
188
|
-
// ①
|
|
189
|
-
if (!
|
|
226
|
+
// ① 解析 state JSON,提取回调字段
|
|
227
|
+
if (!stateParam) {
|
|
190
228
|
return new Response('缺少 state 参数', { status: 400 })
|
|
191
229
|
}
|
|
192
230
|
|
|
193
|
-
let
|
|
231
|
+
let stateObj: OAuthCallbackState
|
|
194
232
|
try {
|
|
195
|
-
|
|
196
|
-
} catch
|
|
197
|
-
return new Response('
|
|
233
|
+
stateObj = parseCallbackState(stateParam)
|
|
234
|
+
} catch {
|
|
235
|
+
return new Response('state 解析失败', { status: 400 })
|
|
198
236
|
}
|
|
199
237
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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`
|