beervid-app-cli 0.2.2 → 0.2.4

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.
Files changed (35) hide show
  1. package/README.md +45 -0
  2. package/SKILL.md +204 -376
  3. package/dist/cli.mjs +117 -151
  4. package/docs/database-schema.md +231 -0
  5. package/docs/oauth-callback.md +282 -0
  6. package/docs/retry-and-idempotency.md +295 -0
  7. package/docs/tt-poll-task.md +239 -0
  8. package/docs/tts-product-cache.md +256 -0
  9. package/example/express/README.md +58 -0
  10. package/example/express/package.json +20 -0
  11. package/example/express/server.ts +431 -0
  12. package/example/express/tsconfig.json +12 -0
  13. package/example/nextjs/.env.example +3 -0
  14. package/example/nextjs/README.md +54 -0
  15. package/example/nextjs/app/api/oauth/callback/route.ts +34 -0
  16. package/example/nextjs/app/api/oauth/url/route.ts +30 -0
  17. package/example/nextjs/app/api/products/route.ts +43 -0
  18. package/example/nextjs/app/api/publish/tt/route.ts +116 -0
  19. package/example/nextjs/app/api/publish/tts/route.ts +58 -0
  20. package/example/nextjs/app/api/status/[shareId]/route.ts +41 -0
  21. package/example/nextjs/app/layout.tsx +9 -0
  22. package/example/nextjs/app/page.tsx +80 -0
  23. package/example/nextjs/lib/beervid-client.ts +107 -0
  24. package/example/nextjs/next.config.ts +4 -0
  25. package/example/nextjs/package.json +19 -0
  26. package/example/nextjs/tsconfig.json +23 -0
  27. package/example/standard/README.md +51 -0
  28. package/example/standard/api-client.ts +181 -0
  29. package/example/standard/get-oauth-url.ts +44 -0
  30. package/example/standard/package.json +18 -0
  31. package/example/standard/query-products.ts +141 -0
  32. package/example/standard/tsconfig.json +12 -0
  33. package/example/standard/tt-publish-flow.ts +194 -0
  34. package/example/standard/tts-publish-flow.ts +246 -0
  35. package/package.json +3 -1
@@ -0,0 +1,256 @@
1
+ # TTS 商品缓存建议
2
+
3
+ > 本文档描述如何设计 TTS 商品的本地缓存策略,减少对 BEERVID API 的重复请求,同时保证商品数据的时效性。
4
+
5
+ ## 为什么需要缓存
6
+
7
+ 挂车发布流程需要先查商品列表再选择商品。如果每次发布都实时查询:
8
+ - **延迟高**:商品分页查询可能需要多次 API 调用(shop + showcase,多页)
9
+ - **重复浪费**:同一创作者的商品列表短时间内不会频繁变化
10
+ - **配额风险**:高频调用可能触发 API 限流
11
+
12
+ ---
13
+
14
+ ## 缓存策略总览
15
+
16
+ ```
17
+ ┌──────────────┐ ┌────────────────┐ ┌────────────────┐
18
+ │ 发布请求 │────→│ 本地商品缓存 │────→│ 选择商品 │
19
+ │ │ │ (beervid_products)│ │ 进入发布流程 │
20
+ └──────────────┘ └────────────────┘ └────────────────┘
21
+
22
+ 缓存过期或为空?
23
+
24
+
25
+ ┌────────────────┐
26
+ │ BEERVID API │
27
+ │ products/query │
28
+ │ 全量拉取刷新 │
29
+ └────────────────┘
30
+ ```
31
+
32
+ ---
33
+
34
+ ## 缓存刷新策略
35
+
36
+ ### 推荐方案:惰性刷新 + 定期预热
37
+
38
+ | 策略 | 触发时机 | 说明 |
39
+ |------|----------|------|
40
+ | **惰性刷新** | 用户发起发布且缓存过期 | 实时拉取商品,写入缓存后继续发布流程 |
41
+ | **定期预热** | Cron 定时任务 | 后台定期刷新活跃账号的商品缓存 |
42
+ | **手动刷新** | 用户主动触发 | 提供"刷新商品列表"按钮 |
43
+
44
+ ### 缓存过期时间建议
45
+
46
+ | 场景 | 过期时间 | 理由 |
47
+ |------|----------|------|
48
+ | 常规使用 | 24 小时 | 商品列表变化频率低 |
49
+ | 高频发布 | 6 小时 | 需要更新商品状态(库存、审核) |
50
+ | 交互式选择 | 1 小时 | 用户正在浏览商品,需要较新数据 |
51
+
52
+ ---
53
+
54
+ ## 全量拉取实现
55
+
56
+ 商品查询需要同时查询 `shop` 和 `showcase` 两种来源,按 `id` 去重合并。
57
+
58
+ 以下示例默认采用 [`docs/database-schema.md`](./database-schema.md) 中包含 `deleted_at` 的软删除表结构。
59
+
60
+ ```typescript
61
+ async function refreshProductCache(
62
+ creatorId: string,
63
+ pageSize: number = 20,
64
+ maxPages: number = 5
65
+ ): Promise<void> {
66
+ const allProducts = new Map<string, Product>()
67
+
68
+ for (const productType of ['shop', 'showcase'] as const) {
69
+ let pageToken = ''
70
+ let page = 0
71
+
72
+ while (page < maxPages) {
73
+ page++
74
+ const data = await openApiPost('/api/v1/open/tts/products/query', {
75
+ creatorUserOpenId: creatorId,
76
+ productType,
77
+ pageSize,
78
+ pageToken,
79
+ })
80
+
81
+ const groups = Array.isArray(data) ? data : [data]
82
+ for (const group of groups) {
83
+ for (const product of group.products ?? []) {
84
+ if (!allProducts.has(product.id)) {
85
+ allProducts.set(product.id, {
86
+ ...product,
87
+ images: (product.images ?? []).map(extractImageUrl),
88
+ source: product.source ?? productType,
89
+ })
90
+ }
91
+ }
92
+
93
+ // 更新分页游标
94
+ if (group.nextPageToken === null || group.nextPageToken === undefined) {
95
+ pageToken = '' // 已是最后一页
96
+ break
97
+ }
98
+ pageToken = group.nextPageToken
99
+ }
100
+
101
+ if (!pageToken) break
102
+ }
103
+ }
104
+
105
+ // 写入数据库:全量替换该创作者的商品缓存
106
+ await db.transaction(async (tx) => {
107
+ // 软删除旧数据
108
+ await tx.execute(
109
+ 'UPDATE beervid_products SET deleted_at = NOW() WHERE creator_user_open_id = ? AND deleted_at IS NULL',
110
+ [creatorId]
111
+ )
112
+
113
+ // 插入新数据
114
+ for (const product of allProducts.values()) {
115
+ await tx.execute(`
116
+ INSERT INTO beervid_products
117
+ (product_id, creator_user_open_id, title, price_amount, price_currency,
118
+ images, sales_count, brand_name, shop_name, source,
119
+ review_status, inventory_status, cached_at, refreshed_at)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
121
+ ON DUPLICATE KEY UPDATE
122
+ title = VALUES(title),
123
+ price_amount = VALUES(price_amount),
124
+ images = VALUES(images),
125
+ sales_count = VALUES(sales_count),
126
+ review_status = VALUES(review_status),
127
+ inventory_status = VALUES(inventory_status),
128
+ refreshed_at = NOW(),
129
+ deleted_at = NULL
130
+ `, [
131
+ product.id, creatorId, product.title,
132
+ product.price?.amount, product.price?.currency,
133
+ JSON.stringify(product.images), product.salesCount,
134
+ product.brandName, product.shopName, product.source,
135
+ product.reviewStatus, product.inventoryStatus,
136
+ ])
137
+ }
138
+ })
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 商品图片 URL 解析
145
+
146
+ BEERVID API 返回的商品图片格式为非标准字符串:
147
+
148
+ ```
149
+ {height=200, url=https://img.tiktokcdn.com/xxx.jpg, width=200}
150
+ ```
151
+
152
+ **必须在入库前解析为标准 URL**,否则前端无法直接使用:
153
+
154
+ ```typescript
155
+ function extractImageUrl(imageStr: string): string {
156
+ const match = imageStr.match(/url=([^,}]+)/)
157
+ return match?.[1]?.trim() ?? ''
158
+ }
159
+
160
+ // 使用
161
+ const images = (product.images ?? []).map(extractImageUrl).filter(Boolean)
162
+ // 结果: ["https://img.tiktokcdn.com/xxx.jpg"]
163
+ ```
164
+
165
+ > **建议**:将解析后的 URL 数组以 JSON 格式存入数据库的 `images` 字段。
166
+
167
+ ---
168
+
169
+ ## 分页游标管理
170
+
171
+ 如果单次拉取(maxPages 限制)未能获取所有商品,需要持久化游标以支持增量拉取:
172
+
173
+ ```typescript
174
+ interface CreatorCacheState {
175
+ creatorId: string
176
+ lastRefreshedAt: Date
177
+ shopPageToken: string | null // null = 已拉完
178
+ showcasePageToken: string | null
179
+ totalProductsCached: number
180
+ }
181
+
182
+ // 判断是否需要刷新
183
+ function needsRefresh(state: CreatorCacheState, ttlMs: number): boolean {
184
+ if (!state.lastRefreshedAt) return true
185
+ return Date.now() - state.lastRefreshedAt.getTime() > ttlMs
186
+ }
187
+
188
+ // 判断是否有未拉取的页
189
+ function hasMorePages(state: CreatorCacheState): boolean {
190
+ return state.shopPageToken !== null || state.showcasePageToken !== null
191
+ }
192
+ ```
193
+
194
+ ---
195
+
196
+ ## 查询缓存时的筛选
197
+
198
+ 发布挂车视频时,不是所有商品都可用。需要筛选:
199
+
200
+ ```sql
201
+ -- 查询可发布商品(按销量降序)
202
+ SELECT * FROM beervid_products
203
+ WHERE creator_user_open_id = ?
204
+ AND deleted_at IS NULL
205
+ AND (review_status = 'APPROVED' OR review_status IS NULL)
206
+ AND (inventory_status = 'IN_STOCK' OR inventory_status IS NULL)
207
+ ORDER BY sales_count DESC
208
+ LIMIT 20;
209
+ ```
210
+
211
+ 如果你不采用软删除,可以去掉 `deleted_at IS NULL` 条件,并在刷新缓存时改成物理删除或全量覆盖。
212
+
213
+ ---
214
+
215
+ ## 定期预热设计
216
+
217
+ ```typescript
218
+ // 每 6 小时执行一次
219
+ cron.schedule('0 */6 * * *', async () => {
220
+ // 查找活跃账号(最近 7 天有发布行为的 TTS 账号)
221
+ const activeCreators = await db.query(`
222
+ SELECT DISTINCT a.creator_user_open_id
223
+ FROM beervid_accounts a
224
+ JOIN beervid_videos v ON v.account_id = a.id
225
+ WHERE a.account_type = 'TTS'
226
+ AND a.status = 'ACTIVE'
227
+ AND v.created_at > NOW() - INTERVAL 7 DAY
228
+ `)
229
+
230
+ for (const creator of activeCreators) {
231
+ try {
232
+ await refreshProductCache(creator.creator_user_open_id)
233
+ console.log(`商品缓存已刷新: ${creator.creator_user_open_id}`)
234
+ } catch (err) {
235
+ console.error(`商品缓存刷新失败: ${creator.creator_user_open_id}`, err.message)
236
+ }
237
+
238
+ // 防止 API 限流:每个账号间隔 2 秒
239
+ await new Promise(r => setTimeout(r, 2000))
240
+ }
241
+ })
242
+ ```
243
+
244
+ ---
245
+
246
+ ## 缓存要点速查
247
+
248
+ | 要点 | 建议 |
249
+ |------|------|
250
+ | 刷新触发 | 惰性(发布时检查过期)+ 定期预热(Cron 6h) |
251
+ | 过期时间 | 常规 24h,高频发布 6h |
252
+ | 数据源 | 同时查 shop + showcase,按 id 去重 |
253
+ | 图片存储 | 入库前解析为标准 URL,JSON 数组格式 |
254
+ | 可用性筛选 | review_status=APPROVED + inventory_status=IN_STOCK |
255
+ | 分页 | 首次全量拉取,持久化游标支持增量 |
256
+ | 并发安全 | 刷新操作加分布式锁,防止多实例重复拉取 |
@@ -0,0 +1,58 @@
1
+ # Express 后端集成示例
2
+
3
+ 使用 Express 框架集成 BEERVID Open API 的后端服务示例,包含 OAuth 回调处理、TT/TTS 完整发布流程。
4
+
5
+ ## 前置条件
6
+
7
+ - Node.js ≥ 20
8
+ - BEERVID APP_KEY
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ cd example/express
14
+ npm install
15
+ ```
16
+
17
+ ## 配置
18
+
19
+ ```bash
20
+ export BEERVID_APP_KEY="your-api-key"
21
+ # 可选
22
+ export BEERVID_APP_BASE_URL="https://open.beervid.ai"
23
+ export PORT=3000
24
+ ```
25
+
26
+ ## 运行
27
+
28
+ ```bash
29
+ npx tsx server.ts
30
+ ```
31
+
32
+ 服务启动后访问 `http://localhost:3000`。
33
+
34
+ ## API 路由
35
+
36
+ | 方法 | 路径 | 说明 |
37
+ |------|------|------|
38
+ | GET | `/oauth/tt` | 获取 TT OAuth URL 并重定向 |
39
+ | GET | `/oauth/tts` | 获取 TTS OAuth URL 并重定向 |
40
+ | GET | `/oauth/callback` | OAuth 回调处理 |
41
+ | POST | `/api/publish/tt` | TT 完整发布流程(含后台轮询) |
42
+ | POST | `/api/publish/tts` | TTS 完整发布流程 |
43
+ | GET | `/api/status/:shareId` | 查询发布状态 |
44
+ | GET | `/api/products/:creatorId` | 查询商品列表 |
45
+
46
+ ## 请求示例
47
+
48
+ ```bash
49
+ # TT 完整发布
50
+ curl -X POST http://localhost:3000/api/publish/tt \
51
+ -H "Content-Type: application/json" \
52
+ -d '{"businessId": "biz_123", "videoUrl": "https://cdn.beervid.ai/uploads/xxx.mp4", "caption": "My video"}'
53
+
54
+ # TTS 完整发布
55
+ curl -X POST http://localhost:3000/api/publish/tts \
56
+ -H "Content-Type: application/json" \
57
+ -d '{"creatorId": "open_user_abc", "videoFileId": "vf_abc123", "productId": "prod_789", "productTitle": "Widget"}'
58
+ ```
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "beervid-example-express",
3
+ "private": true,
4
+ "type": "module",
5
+ "engines": {
6
+ "node": ">=20.0.0"
7
+ },
8
+ "scripts": {
9
+ "start": "tsx server.ts",
10
+ "dev": "tsx watch server.ts"
11
+ },
12
+ "dependencies": {
13
+ "express": "^5.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/express": "^5.0.2",
17
+ "tsx": "^4.21.0",
18
+ "typescript": "^5.9.3"
19
+ }
20
+ }