chore(docs): remove deprecated API documentation files
This commit is contained in:
@@ -1,267 +0,0 @@
|
|||||||
# Cozsweet 全站行为埋点前端接入
|
|
||||||
|
|
||||||
> 发布状态(2026-07-14):自动页面/按钮采集 SDK 已部署 `pro` 预发,生产仍是旧版支付专项 SDK。前端应先接 `pro` 完成联调;生产自动采集要等 unified 下一次生产镜像发布后再启用。
|
|
||||||
|
|
||||||
## 1. 目标
|
|
||||||
|
|
||||||
前端接入一次后,自动统计:
|
|
||||||
|
|
||||||
- 用户访问了哪个页面。
|
|
||||||
- Next.js 站内路由切换。
|
|
||||||
- 所有原生按钮、链接和带 `role="button"` 的控件点击。
|
|
||||||
- 带 `data-analytics-key` 的自定义可点击组件。
|
|
||||||
|
|
||||||
支付漏斗另外上报明确业务事件,统计付费墙原因、套餐曝光、套餐点击、创建订单、打开支付页和最终支付结果。
|
|
||||||
|
|
||||||
## 2. 环境地址
|
|
||||||
|
|
||||||
| 环境 | SDK | 事件接口 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| pro 预发 | `https://proapi.banlv-ai.com/js/cozsweet-payment-analytics.js` | `https://proapi.banlv-ai.com/api/behavior/events` |
|
|
||||||
| production 生产 | `https://api.banlv-ai.com/js/cozsweet-payment-analytics.js` | `https://api.banlv-ai.com/api/behavior/events` |
|
|
||||||
|
|
||||||
## 3. Next.js 根布局接入
|
|
||||||
|
|
||||||
只在根布局加载一次。预发必须先指定预发事件地址,否则 SDK 默认发送到生产。
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import Script from 'next/script'
|
|
||||||
|
|
||||||
export function BehaviorAnalyticsScripts() {
|
|
||||||
const isPro = process.env.NEXT_PUBLIC_APP_ENV === 'pro'
|
|
||||||
const apiBase = isPro
|
|
||||||
? 'https://proapi.banlv-ai.com'
|
|
||||||
: 'https://api.banlv-ai.com'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Script id="cozsweet-analytics-config" strategy="beforeInteractive">
|
|
||||||
{`window.COZSWEET_ANALYTICS_ENDPOINT=${JSON.stringify(
|
|
||||||
`${apiBase}/api/behavior/events`,
|
|
||||||
)}`}
|
|
||||||
</Script>
|
|
||||||
<Script
|
|
||||||
src={`${apiBase}/js/cozsweet-payment-analytics.js`}
|
|
||||||
strategy="afterInteractive"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
SDK 会自动生成并持久化:
|
|
||||||
|
|
||||||
- `anonymousId`:未登录用户标识。
|
|
||||||
- `sessionId`:30 分钟会话标识。
|
|
||||||
- `pagePath/pageTitle/referrer`。
|
|
||||||
- `siteHost/pageUrl/UTM`。
|
|
||||||
|
|
||||||
登录成功或恢复会话后设置 Token,让事件同时绑定 `userId`:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
window.CozsweetPaymentAnalytics?.setToken(accessToken)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 所有按钮点击
|
|
||||||
|
|
||||||
以下元素会自动上报 `eventType=element_click`,前端不需要在每个 `onClick` 里手工请求接口:
|
|
||||||
|
|
||||||
- `button`
|
|
||||||
- `a[href]`
|
|
||||||
- `[role="button"]`
|
|
||||||
- `[role="link"]`
|
|
||||||
- `input[type="button|submit|reset"]`
|
|
||||||
- `[data-analytics-key]`
|
|
||||||
|
|
||||||
重要按钮必须提供稳定且不随语言变化的 `data-analytics-key`:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<button data-analytics-key="chat.send_message">Send</button>
|
|
||||||
|
|
||||||
<button data-analytics-key="recharge.open" data-analytics-label="Open recharge">
|
|
||||||
Recharge
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
data-analytics-key="private_album.unlock"
|
|
||||||
data-analytics-label="Unlock private album"
|
|
||||||
onClick={unlockAlbum}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
纯 `div onClick` 如果没有 `role="button"`,必须添加 `data-analytics-key`,否则浏览器无法可靠识别它是按钮。
|
|
||||||
|
|
||||||
不需要统计的区域:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
<div data-analytics-ignore>
|
|
||||||
{/* 此节点及其子节点不自动记录点击 */}
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
程序触发、没有真实 DOM 点击时可以手工记录:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
window.CozsweetPaymentAnalytics?.elementClick(
|
|
||||||
'chat.retry_generation',
|
|
||||||
'Retry generation',
|
|
||||||
{ messageId },
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
不要把聊天正文、邮箱、手机号、Token 或支付参数放进 `data-analytics-key`、标签或 metadata。
|
|
||||||
|
|
||||||
## 5. 支付漏斗必须手工上报
|
|
||||||
|
|
||||||
通用按钮点击只能知道“点了某个按钮”,不能知道套餐价格、订单 ID 和进入充值页的原因。以下事件必须在支付业务代码对应步骤调用。
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const analytics = window.CozsweetPaymentAnalytics
|
|
||||||
|
|
||||||
analytics?.paywallShown('daily_chat_limit', {
|
|
||||||
entryPoint: 'chat_input',
|
|
||||||
isVip: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
analytics?.rechargeModalOpen('chat_input', 'daily_chat_limit')
|
|
||||||
|
|
||||||
plans.forEach((plan, index) => {
|
|
||||||
analytics?.planImpression(plan, index + 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
analytics?.planClick(plan, {
|
|
||||||
entryPoint: 'paywall_modal',
|
|
||||||
triggerReason: 'daily_chat_limit',
|
|
||||||
})
|
|
||||||
|
|
||||||
analytics?.createOrderStart(plan, payChannel)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await createOrder({
|
|
||||||
planId: plan.planId,
|
|
||||||
payChannel,
|
|
||||||
autoRenew,
|
|
||||||
})
|
|
||||||
const data = response.data
|
|
||||||
const orderId = data.orderId
|
|
||||||
const checkoutUrl = analytics?.extractCheckoutUrl(data)
|
|
||||||
|
|
||||||
analytics?.createOrderSuccess(plan, orderId, payChannel)
|
|
||||||
if (checkoutUrl) {
|
|
||||||
analytics?.checkoutOpened(orderId, plan, payChannel, checkoutUrl)
|
|
||||||
window.location.href = checkoutUrl
|
|
||||||
} else {
|
|
||||||
analytics?.checkoutFailed(
|
|
||||||
orderId,
|
|
||||||
plan,
|
|
||||||
'missing_checkout_url',
|
|
||||||
payChannel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
analytics?.createOrderFailed(
|
|
||||||
plan,
|
|
||||||
error instanceof Error ? error.message : 'create_order_error',
|
|
||||||
payChannel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
轮询订单状态:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
analytics?.statusPollStart(orderId, plan)
|
|
||||||
|
|
||||||
analytics?.statusPollUpdate(orderId, data.status, plan, {
|
|
||||||
expiresAt: data.expiresAt,
|
|
||||||
expiresInSeconds: data.expiresInSeconds,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 前端主动放弃轮询且订单仍未进入终态时调用
|
|
||||||
analytics?.statusPollTimeout(orderId, plan)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. 固定事件名
|
|
||||||
|
|
||||||
| 步骤 | 事件名 | 必要 metadata |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 触发付费墙 | `paywall_shown` | `triggerReason`, `entryPoint` |
|
|
||||||
| 打开充值页 | `recharge_modal_open` | `triggerReason`, `entryPoint` |
|
|
||||||
| 套餐曝光 | `payment_plan_impression` | `planId`, `amountCents`, `currency`, `position` |
|
|
||||||
| 点击套餐 | `payment_plan_click` | `planId`, `amountCents`, `currency` |
|
|
||||||
| 开始建单 | `payment_create_order_start` | `planId`, `payChannel` |
|
|
||||||
| 建单成功 | `payment_create_order_success` | `planId`, `orderId`, `payChannel` |
|
|
||||||
| 建单失败 | `payment_create_order_failed` | `planId`, `reason`, `payChannel` |
|
|
||||||
| 打开支付页 | `payment_checkout_opened` | `planId`, `orderId`, `payChannel` |
|
|
||||||
| 支付页缺失/打开失败 | `payment_checkout_failed` | `orderId`, `reason` |
|
|
||||||
| 支付状态 | `payment_status_paid/failed/expired` | `orderId`, `status` |
|
|
||||||
| 轮询超时 | `payment_status_poll_timeout` | `orderId` |
|
|
||||||
|
|
||||||
`triggerReason` 使用固定值:
|
|
||||||
|
|
||||||
- `daily_chat_limit`
|
|
||||||
- `private_topic_limit`
|
|
||||||
- `insufficient_credits`
|
|
||||||
- `sidebar_recharge`
|
|
||||||
- `vip_cta`
|
|
||||||
- `ad_landing`
|
|
||||||
- `manual_recharge`
|
|
||||||
- `unknown`
|
|
||||||
|
|
||||||
不要继续使用已经废弃的 `weekly_chat_limit` 作为新事件原因;当前免费聊天口径是每日额度。
|
|
||||||
|
|
||||||
## 7. 请求格式
|
|
||||||
|
|
||||||
SDK 最终批量发送:
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST /api/behavior/events
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer <TOKEN> // 有登录 Token 时携带
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"events": [
|
|
||||||
{
|
|
||||||
"eventType": "element_click",
|
|
||||||
"eventName": "element_click",
|
|
||||||
"anonymousId": "browser-uuid",
|
|
||||||
"sessionId": "session-uuid",
|
|
||||||
"pagePath": "/chat",
|
|
||||||
"pageTitle": "Chat",
|
|
||||||
"elementKey": "recharge.open",
|
|
||||||
"elementText": "Recharge",
|
|
||||||
"elementTag": "button",
|
|
||||||
"metadata": {
|
|
||||||
"siteHost": "cozsweet.com",
|
|
||||||
"pageUrl": "https://cozsweet.com/chat"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{"ok": true, "inserted": 1}
|
|
||||||
```
|
|
||||||
|
|
||||||
埋点失败不能阻塞聊天、解锁或支付流程。SDK 已吞掉网络异常,业务代码不要 `await` 埋点后再继续支付。
|
|
||||||
|
|
||||||
## 8. 联调验收
|
|
||||||
|
|
||||||
1. 打开浏览器 Network,筛选 `behavior/events`。
|
|
||||||
2. 首次打开页面应看到 `page_view`。
|
|
||||||
3. Next.js 路由切换后应再次看到新 `pagePath` 的 `page_view`。
|
|
||||||
4. 点击任意按钮应看到 `element_click` 和稳定 `elementKey`。
|
|
||||||
5. 从付费墙点击一个套餐,依次检查专用支付事件及 `planId`。
|
|
||||||
6. 建单成功后必须看到同一 `orderId` 出现在建单、支付页和状态事件中。
|
|
||||||
7. 接口响应必须为 `ok=true` 且 `inserted>0`。
|
|
||||||
8. Manager 选择同一日期范围,确认数据出现;统计不是实时推送时,刷新页面即可。
|
|
||||||
|
|
||||||
## 9. 当前线上为 0 的原因
|
|
||||||
|
|
||||||
截至 2026-07-14,线上 `cozsweet.com` 页面及加载的前端 JS 中没有找到本 SDK,也没有找到支付漏斗事件代码。数据库近期大部分 `element_click/page_view` 来自 Manager 后台自己的追踪,而不是 Cozsweet 用户端。后端只能自动记录建单开始/成功/失败,无法替前端知道付费墙是否展示、套餐是否曝光、用户点击了哪个套餐、支付页是否真正打开,因此这些步骤显示 0 是符合当前数据事实的。
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
# Facebook ASID / PSID 绑定与登录接口说明
|
|
||||||
|
|
||||||
## 总原则
|
|
||||||
|
|
||||||
后端不再依赖 Meta 做 ASID / PSID 实时互转。前端拿到哪个 ID 就传哪个 ID,后端只用本地数据库的绑定关系判断是不是同一个用户。
|
|
||||||
|
|
||||||
- ASID: Facebook Login 返回的 app-scoped id。
|
|
||||||
- PSID: Messenger / ManyChat / Page 链接带来的 page-scoped id。
|
|
||||||
- 只有当同一个后端用户已经同时绑定 ASID 和 PSID 时,`psid` 才能直接当作 Facebook 登录凭据换 token。
|
|
||||||
|
|
||||||
生产 API Base URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
https://api.banlv-ai.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## 场景 2: 已登录用户绑定 PSID / ASID
|
|
||||||
|
|
||||||
### Purpose
|
|
||||||
|
|
||||||
用户已经登录 Cozsweet,前端从 URL 或 Messenger 环境拿到 `psid` 后,调用该接口把 PSID 绑定到当前登录用户。
|
|
||||||
|
|
||||||
### Request URL
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST https://api.banlv-ai.com/api/user/facebook/identity
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
需要登录 token:
|
|
||||||
|
|
||||||
```http
|
|
||||||
Authorization: Bearer <TOKEN>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request Format
|
|
||||||
|
|
||||||
```http
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
| Field | Type | Required | Example | Meaning |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| psid | string | no | `37370387172559600` | Messenger PSID。 |
|
|
||||||
| asid | string | no | `1582386133449953` | Facebook ASID。 |
|
|
||||||
| fbPsid | string | no | `37370387172559600` | `psid` 的别名。 |
|
|
||||||
| fbAsid | string | no | `1582386133449953` | `asid` 的别名。 |
|
|
||||||
|
|
||||||
至少传一个字段。推荐前端只拿到 PSID 时传:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "psid": "37370387172559600" }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST 'https://api.banlv-ai.com/api/user/facebook/identity' \
|
|
||||||
-H 'Authorization: Bearer <TOKEN>' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
--data-raw '{
|
|
||||||
"psid": "37370387172559600"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response Format
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "Facebook 身份绑定成功",
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"fbAsid": "1582386133449953",
|
|
||||||
"fbPsid": "37370387172559600",
|
|
||||||
"facebookBinding": {
|
|
||||||
"conflicts": [],
|
|
||||||
"bound": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Failures
|
|
||||||
|
|
||||||
| Status | Meaning |
|
|
||||||
| --- | --- |
|
|
||||||
| 400 | 没有传 `asid/fbAsid` 或 `psid/fbPsid`。 |
|
|
||||||
| 401 | 未登录或 token 失效。 |
|
|
||||||
| 409 | 该 ASID 或 PSID 已绑定到其他正式账号。 |
|
|
||||||
|
|
||||||
## 场景 3: PSID 直接登录
|
|
||||||
|
|
||||||
### Purpose
|
|
||||||
|
|
||||||
前端只有 `psid`,希望后端用本地绑定关系直接换登录 token,不再触发 Facebook Login。
|
|
||||||
|
|
||||||
### Request URL
|
|
||||||
|
|
||||||
```http
|
|
||||||
POST https://api.banlv-ai.com/api/auth/login/facebook/psid
|
|
||||||
```
|
|
||||||
|
|
||||||
### Authentication
|
|
||||||
|
|
||||||
不需要 Bearer token。
|
|
||||||
|
|
||||||
### Request Format
|
|
||||||
|
|
||||||
```http
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
| Field | Type | Required | Example | Meaning |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| psid | string | yes | `37370387172559600` | Messenger PSID。 |
|
|
||||||
| deviceId | string | no | `web-abc` | 未匹配到账号时创建/返回游客 token 用;不传则后端按 PSID 生成稳定设备 ID。 |
|
|
||||||
| bindToGuest | boolean | no | `true` | 未匹配到任何用户时,是否把 PSID 先绑定到游客账号,默认 true。 |
|
|
||||||
|
|
||||||
### Request Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST 'https://api.banlv-ai.com/api/auth/login/facebook/psid' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
--data-raw '{
|
|
||||||
"psid": "37370387172559600"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Success Response: 已有完整绑定,直接登录
|
|
||||||
|
|
||||||
同一个用户已有 ASID 和 PSID 时返回正式登录 token:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "Facebook PSID 登录成功",
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"token": "<JWT>",
|
|
||||||
"refreshToken": "<REFRESH_TOKEN>",
|
|
||||||
"matchedBy": "psid",
|
|
||||||
"fbAsid": "1582386133449953",
|
|
||||||
"fbPsid": "37370387172559600",
|
|
||||||
"hasCompleteFacebookIdentity": true,
|
|
||||||
"isGuest": false,
|
|
||||||
"user": {
|
|
||||||
"id": "<USER_ID>",
|
|
||||||
"fbAsid": "1582386133449953",
|
|
||||||
"fbPsid": "37370387172559600"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Success Response: 未知或绑定不完整,返回游客
|
|
||||||
|
|
||||||
查不到 PSID,或 PSID 对应用户还没有 ASID 时,不当作 Facebook 登录,只返回游客 token:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "Facebook PSID 未匹配,已返回游客登录",
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"token": "<GUEST_JWT>",
|
|
||||||
"refreshToken": "",
|
|
||||||
"matchedBy": "guest",
|
|
||||||
"fbPsid": "37370387172559600",
|
|
||||||
"fbAsid": null,
|
|
||||||
"hasCompleteFacebookIdentity": false,
|
|
||||||
"isGuest": true,
|
|
||||||
"userId": "<GUEST_USER_ID>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果 PSID 查到了用户,但该用户缺 ASID,`matchedBy` 会是:
|
|
||||||
|
|
||||||
```text
|
|
||||||
psid_incomplete
|
|
||||||
```
|
|
||||||
|
|
||||||
前端应继续按游客态处理,或引导用户进行 Facebook Login。
|
|
||||||
|
|
||||||
### Failures
|
|
||||||
|
|
||||||
| Status | Meaning |
|
|
||||||
| --- | --- |
|
|
||||||
| 422 | PSID 格式无效。 |
|
|
||||||
|
|
||||||
## Profile 字段
|
|
||||||
|
|
||||||
`GET /api/user/profile` 和登录响应里的 `user` 会包含:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"fbAsid": "1582386133449953",
|
|
||||||
"fbPsid": "37370387172559600"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
没有绑定时字段为 `null` 或不存在。
|
|
||||||
|
|
||||||
## 推荐前端流程
|
|
||||||
|
|
||||||
1. 页面打开时从 URL / Messenger 环境读取 `psid`。
|
|
||||||
2. 如果本地已有登录 token:
|
|
||||||
- 调 `POST /api/user/facebook/identity` 绑定 `psid` 到当前用户。
|
|
||||||
3. 如果本地没有登录 token:
|
|
||||||
- 先调 `POST /api/auth/login/facebook/psid`。
|
|
||||||
- 如果返回 `hasCompleteFacebookIdentity=true`,保存 token,当作登录成功。
|
|
||||||
- 如果返回 `isGuest=true`,保存游客 token。
|
|
||||||
|
|
||||||
## 重要注意
|
|
||||||
|
|
||||||
- 后端不会再调用 Meta 做 ASID / PSID 互转。
|
|
||||||
- PSID 直登只相信我们数据库里已有的绑定关系。
|
|
||||||
- `409 FACEBOOK_ID_CONFLICT` 必须提示用户换账号或联系客服,不要在前端覆盖绑定。
|
|
||||||
- 前端保存 token 的逻辑和原登录流程一致。
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
# Cozsweet 独立付费图片包前端接口
|
|
||||||
|
|
||||||
## 1. 用途
|
|
||||||
|
|
||||||
Cozsweet 前端通过本接口展示独立的 8/15 张私密图片包,并使用用户积分解锁。FB 只导入原始图片素材,后端素材池负责跨批次自动凑成 8 张或 15 张图片包。该功能不走现金支付,不使用旧 `/moments` 接口。
|
|
||||||
|
|
||||||
## 2. 接口地址
|
|
||||||
|
|
||||||
| 功能 | 方法 | 路径 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 图片包列表 | GET | `/api/private-room/albums` |
|
|
||||||
| 积分解锁 | POST | `/api/private-room/albums/{albumId}/unlock` |
|
|
||||||
|
|
||||||
## 4. 固定价格
|
|
||||||
|
|
||||||
| 图片数量 | `unlockCost` | 单张价格 |
|
|
||||||
| ---: | ---: | ---: |
|
|
||||||
| 8 | 320 credits | 40 credits |
|
|
||||||
| 15 | 600 credits | 40 credits |
|
|
||||||
|
|
||||||
接口只会返回 `currency: "credits"`。前端不得显示现金价格,也不得调用支付套餐或创建订单接口。
|
|
||||||
|
|
||||||
## 5. GET /api/private-room/albums
|
|
||||||
|
|
||||||
### 请求参数
|
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| `character` | string | 否 | `elio` | 角色 ID。 |
|
|
||||||
| `collectionKey` | string | 否 | 全部 | 只查看一个素材分组。 |
|
|
||||||
| `limit` | integer | 否 | 20 | 1-50。 |
|
|
||||||
| `cursor` | ISO datetime | 否 | - | 上一页返回的 `nextCursor`。 |
|
|
||||||
|
|
||||||
### 请求示例
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl 'https://api.banlv-ai.com/api/private-room/albums?character=elio&limit=20' \
|
|
||||||
-H 'Authorization: Bearer <TOKEN>'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 锁定图片包响应
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"albumId": "a1b2c3d4-0000-0000-0000-000000000000",
|
|
||||||
"momentId": "album:a1b2c3d4-0000-0000-0000-000000000000",
|
|
||||||
"characterId": "elio",
|
|
||||||
"collectionKey": "manila_202607",
|
|
||||||
"title": "Private Manila set",
|
|
||||||
"content": null,
|
|
||||||
"previewText": "Only for you.",
|
|
||||||
"imageCount": 8,
|
|
||||||
"mediaCount": 8,
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"url": "https://dbapi.banlv-ai.com/storage/v1/object/public/elio-schedules/01.jpg",
|
|
||||||
"type": "image",
|
|
||||||
"locked": true,
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"locked": true,
|
|
||||||
"unlocked": false,
|
|
||||||
"unlockCost": 320,
|
|
||||||
"requiredCredits": 320,
|
|
||||||
"creditCostPerImage": 40,
|
|
||||||
"currency": "credits",
|
|
||||||
"canUnlockWithCredits": false,
|
|
||||||
"publishedAt": "2026-07-13T00:00:00+00:00",
|
|
||||||
"lockDetail": {
|
|
||||||
"locked": true,
|
|
||||||
"showContent": false,
|
|
||||||
"showUpgrade": true,
|
|
||||||
"reason": "private_album",
|
|
||||||
"requiredCredits": 320,
|
|
||||||
"currentCredits": 100,
|
|
||||||
"shortfallCredits": 220,
|
|
||||||
"mediaCount": 8,
|
|
||||||
"unlockCostPerImage": 40
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nextCursor": null,
|
|
||||||
"hasMore": false,
|
|
||||||
"creditBalance": 100,
|
|
||||||
"currency": "credits",
|
|
||||||
"creditCostPerImage": 40,
|
|
||||||
"packageOptions": [
|
|
||||||
{"imageCount": 8, "creditCost": 320},
|
|
||||||
{"imageCount": 15, "creditCost": 600}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
锁定和解锁状态都会返回完整 `images[].url`。前端必须根据 `locked` / `unlocked` / `lockDetail.locked` 决定是否显示锁层和是否允许查看原图,不能用 URL 是否为空判断解锁状态。
|
|
||||||
|
|
||||||
### 已解锁图片包
|
|
||||||
|
|
||||||
同一个用户解锁后,再次请求列表会返回:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"albumId": "a1b2c3d4-0000-0000-0000-000000000000",
|
|
||||||
"locked": false,
|
|
||||||
"unlocked": true,
|
|
||||||
"content": "Only for you.",
|
|
||||||
"imageCount": 8,
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"url": "https://dbapi.banlv-ai.com/storage/v1/object/public/elio-schedules/01.jpg",
|
|
||||||
"type": "image",
|
|
||||||
"locked": false,
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 分页
|
|
||||||
|
|
||||||
`hasMore=true` 时,把 `nextCursor` 原样传回:
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /api/private-room/albums?cursor=<nextCursor>&limit=20
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. POST /api/private-room/albums/{albumId}/unlock
|
|
||||||
|
|
||||||
### 请求格式
|
|
||||||
|
|
||||||
```http
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer <TOKEN>
|
|
||||||
```
|
|
||||||
|
|
||||||
Body:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"expectedCost": 320
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 示例 | 说明 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| `expectedCost` | integer | 否但建议 | 320 | 用户确认时看到的价格;价格变化则拒绝扣分。 |
|
|
||||||
|
|
||||||
### 请求示例
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST 'https://api.banlv-ai.com/api/private-room/albums/a1b2c3d4-0000-0000-0000-000000000000/unlock' \
|
|
||||||
-H 'Authorization: Bearer <TOKEN>' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{"expectedCost":320}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 解锁成功
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"albumId": "a1b2c3d4-0000-0000-0000-000000000000",
|
|
||||||
"locked": false,
|
|
||||||
"unlocked": true,
|
|
||||||
"reason": "ok",
|
|
||||||
"unlockCost": 320,
|
|
||||||
"creditsCharged": 320,
|
|
||||||
"previousCreditBalance": 500,
|
|
||||||
"creditBalance": 180,
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"url": "https://...",
|
|
||||||
"type": "image",
|
|
||||||
"locked": false,
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
前端直接使用响应里的 `images` 替换锁卡,并用 `creditBalance` 刷新余额。
|
|
||||||
|
|
||||||
### 积分不足
|
|
||||||
|
|
||||||
业务失败仍返回 HTTP 200:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"albumId": "a1b2c3d4-0000-0000-0000-000000000000",
|
|
||||||
"locked": true,
|
|
||||||
"unlocked": false,
|
|
||||||
"reason": "insufficient_credits",
|
|
||||||
"unlockCost": 320,
|
|
||||||
"requiredCredits": 320,
|
|
||||||
"creditBalance": 100,
|
|
||||||
"shortfallCredits": 220,
|
|
||||||
"creditsCharged": 0,
|
|
||||||
"images": [
|
|
||||||
{"url": "https://dbapi.banlv-ai.com/storage/v1/object/public/elio-schedules/01.jpg", "type": "image", "locked": true, "index": 0}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
前端可以打开积分充值页,但后端不会创建图片包现金订单。用户充值积分后,再次点击同一个解锁接口。
|
|
||||||
|
|
||||||
### 重复解锁
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"unlocked": true,
|
|
||||||
"locked": false,
|
|
||||||
"reason": "already_unlocked",
|
|
||||||
"creditsCharged": 0,
|
|
||||||
"creditBalance": 180,
|
|
||||||
"images": [{"url": "https://...", "locked": false, "index": 0}]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7. `reason` 处理
|
|
||||||
|
|
||||||
| `data.reason` | 含义 | 前端动作 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `ok` | 已扣积分并解锁 | 显示真实图片并更新余额。 |
|
|
||||||
| `already_unlocked` | 以前已解锁,本次不扣 | 直接显示返回图片。 |
|
|
||||||
| `insufficient_credits` | 积分不足,未扣分 | 保持锁定,可引导购买积分。 |
|
|
||||||
| `cost_changed` | 价格与 `expectedCost` 不一致 | 刷新列表并重新确认。 |
|
|
||||||
| `unlock_in_progress` | 同一图片包正在并发解锁 | 禁用按钮并稍后刷新。 |
|
|
||||||
| `deduct_failed` | 扣积分失败 | 保持锁定并允许重试。 |
|
|
||||||
| `persist_failed_refunded` | 解锁记录失败,积分已尽力退回 | 保持锁定、刷新余额并告警。 |
|
|
||||||
| `not_found` | 图片包不存在或已停用 | 移除卡片。 |
|
|
||||||
|
|
||||||
## 8. TypeScript 建议
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export interface PrivateAlbumImage {
|
|
||||||
url: string;
|
|
||||||
type: "image";
|
|
||||||
locked: boolean;
|
|
||||||
index: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PrivateAlbum {
|
|
||||||
albumId: string;
|
|
||||||
momentId: string;
|
|
||||||
characterId: string;
|
|
||||||
collectionKey: string;
|
|
||||||
title: string;
|
|
||||||
content: string | null;
|
|
||||||
previewText: string;
|
|
||||||
imageCount: 8 | 15;
|
|
||||||
mediaCount: 8 | 15;
|
|
||||||
images: PrivateAlbumImage[];
|
|
||||||
locked: boolean;
|
|
||||||
unlocked: boolean;
|
|
||||||
unlockCost: 320 | 600;
|
|
||||||
requiredCredits: 320 | 600;
|
|
||||||
creditCostPerImage: 40;
|
|
||||||
currency: "credits";
|
|
||||||
canUnlockWithCredits: boolean;
|
|
||||||
publishedAt: string;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 9. 前端流程
|
|
||||||
|
|
||||||
1. 进入图片包页面,调用 `GET /api/private-room/albums`。
|
|
||||||
2. `locked=true` 时可使用返回 URL 渲染封面/模糊图,但必须覆盖锁层并禁止打开原图。
|
|
||||||
3. 用户确认后调用解锁接口并传当前 `expectedCost`。
|
|
||||||
4. `reason=ok/already_unlocked` 时使用返回的真实图片。
|
|
||||||
5. `reason=insufficient_credits` 时保持锁定并引导购买积分。
|
|
||||||
6. 充值完成后重新调用解锁接口,不需要恢复现金订单。
|
|
||||||
7. 解锁请求进行中禁用按钮,避免重复点击。
|
|
||||||
|
|
||||||
## 10. HTTP 错误
|
|
||||||
|
|
||||||
| HTTP 状态 | 原因 | 处理 |
|
|
||||||
| ---: | --- | --- |
|
|
||||||
| 401 | Token 无效或缺失 | 刷新 Token/重新登录。 |
|
|
||||||
| 422 | `albumId`、query 或 body 格式错误 | 修正请求。 |
|
|
||||||
| 500 | 数据库或服务异常 | 保持当前锁定状态并允许重试。 |
|
|
||||||
|
|
||||||
## 11. 测试方法
|
|
||||||
|
|
||||||
1. 先在 pro 执行 `database/private-albums-migration.sql`。
|
|
||||||
2. 使用 FB 测试导入生成一个 8 张图片包。
|
|
||||||
3. 用少于 320 积分的测试账号请求解锁,确认不扣积分、不出现现金订单。
|
|
||||||
4. 给测试账号补足积分,再请求解锁,确认扣 320 且返回 8 个真实 URL。
|
|
||||||
5. 重复解锁,确认 `creditsCharged=0`。
|
|
||||||
6. 刷新列表,确认仍为 `unlocked=true`。
|
|
||||||
7. 15 张图片包按同样方式验证扣 600。
|
|
||||||
8. 测试结束后删除测试素材、图片包、图片和解锁记录。
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# Cozsweet 咖啡打赏接口
|
|
||||||
|
|
||||||
## 1. 用途
|
|
||||||
|
|
||||||
前端展示三个一次性咖啡打赏档位,并复用现有支付建单、支付跳转和订单轮询流程。
|
|
||||||
|
|
||||||
## 2. 接口地址
|
|
||||||
|
|
||||||
| 功能 | 方法 | 路径 | 鉴权 |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| 查询打赏品类 | GET | `/api/payment/tip-plans` | 不需要 |
|
|
||||||
| 创建打赏订单 | POST | `/api/payment/create-order` | Bearer Token |
|
|
||||||
| 查询订单状态 | GET | `/api/payment/order-status?order_id=...` | Bearer Token |
|
|
||||||
|
|
||||||
## 3. 查询打赏品类
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl 'https://api.banlv-ai.com/api/payment/tip-plans'
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"plans": [
|
|
||||||
{
|
|
||||||
"planId": "tip_coffee_usd_4_99",
|
|
||||||
"planName": "Small Coffee",
|
|
||||||
"orderType": "tip",
|
|
||||||
"tipType": "coffee_small",
|
|
||||||
"description": "Buy Elio a small coffee",
|
|
||||||
"amountCents": 499,
|
|
||||||
"currency": "USD",
|
|
||||||
"autoRenew": false,
|
|
||||||
"isFirstRechargeOffer": false,
|
|
||||||
"firstRechargeDiscountPercent": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"planId": "tip_coffee_usd_9_99",
|
|
||||||
"planName": "Medium Coffee",
|
|
||||||
"orderType": "tip",
|
|
||||||
"tipType": "coffee_medium",
|
|
||||||
"description": "Buy Elio a medium coffee",
|
|
||||||
"amountCents": 999,
|
|
||||||
"currency": "USD",
|
|
||||||
"autoRenew": false,
|
|
||||||
"isFirstRechargeOffer": false,
|
|
||||||
"firstRechargeDiscountPercent": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"planId": "tip_coffee_usd_19_99",
|
|
||||||
"planName": "Large Coffee",
|
|
||||||
"orderType": "tip",
|
|
||||||
"tipType": "coffee_large",
|
|
||||||
"description": "Buy Elio a large coffee",
|
|
||||||
"amountCents": 1999,
|
|
||||||
"currency": "USD",
|
|
||||||
"autoRenew": false,
|
|
||||||
"isFirstRechargeOffer": false,
|
|
||||||
"firstRechargeDiscountPercent": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
当前三个档位如下,顺序就是接口返回顺序:
|
|
||||||
|
|
||||||
| `planId` | `tipType` | 价格 |
|
|
||||||
| --- | --- | ---: |
|
|
||||||
| `tip_coffee_usd_4_99` | `coffee_small` | USD 4.99 |
|
|
||||||
| `tip_coffee_usd_9_99` | `coffee_medium` | USD 9.99 |
|
|
||||||
| `tip_coffee_usd_19_99` | `coffee_large` | USD 19.99 |
|
|
||||||
|
|
||||||
三个档位都不按国家换币、不参与首充半价,也不发会员或积分。前端必须使用接口返回的 `planId`,不要根据价格自行拼接。
|
|
||||||
|
|
||||||
## 4. 创建打赏订单
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST 'https://api.banlv-ai.com/api/payment/create-order' \
|
|
||||||
-H 'Authorization: Bearer <TOKEN>' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"planId": "tip_coffee_usd_9_99",
|
|
||||||
"payChannel": "stripe",
|
|
||||||
"autoRenew": false
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
| 字段 | 类型 | 必填 | 固定/可选值 | 说明 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| `planId` | string | 是 | 上表三个 ID 之一 | 咖啡打赏产品 ID。 |
|
|
||||||
| `payChannel` | string | 是 | `stripe` / `ezpay` | 支付渠道。 |
|
|
||||||
| `autoRenew` | boolean | 是 | `false` | 打赏是一次性支付,必须传 false。 |
|
|
||||||
|
|
||||||
成功响应与现有充值相同:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"message": "success",
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"orderId": "pay_xxx",
|
|
||||||
"payParams": {
|
|
||||||
"url": "https://checkout.example/..."
|
|
||||||
},
|
|
||||||
"expiresAt": "2026-07-14T10:30:00+00:00",
|
|
||||||
"expiresInSeconds": 1800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
前端必须打开 `payParams` 中的支付 URL,并每 3-5 秒轮询订单状态。
|
|
||||||
|
|
||||||
## 5. 订单状态与 WebSocket
|
|
||||||
|
|
||||||
支付完成后:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"orderId": "pay_xxx",
|
|
||||||
"status": "paid",
|
|
||||||
"orderType": "tip",
|
|
||||||
"planId": "tip_coffee_usd_9_99",
|
|
||||||
"creditsAdded": 0
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
curl -X POST "https://flow.banlv-ai.com/webhooks/action-notify?chat_id=oc_xxx" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"message":"Gitea 内容操作完成,已同步。"}'
|
|
||||||
Reference in New Issue
Block a user