From 3032f28d642ff8c03ab978df0812a844e3f2caa5 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 2 Jul 2026 14:40:13 +0800 Subject: [PATCH] Remove outdated PAYWALL_API documentation and add example plans JSON for payment plans and first recharge offers. --- docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.md | 206 ----- .../FRONTEND_PROACTIVE_WAKEUP_API.zh-CN.md | 206 ----- docs/backend/FRONTEND_VIP_CREDIT_API.md | 309 ------- docs/backend/PAYWALL_API.md | 786 ------------------ docs/backend/plans-examples.json | 135 +++ 5 files changed, 135 insertions(+), 1507 deletions(-) delete mode 100644 docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.md delete mode 100644 docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.zh-CN.md delete mode 100644 docs/backend/FRONTEND_VIP_CREDIT_API.md delete mode 100644 docs/backend/PAYWALL_API.md create mode 100644 docs/backend/plans-examples.json diff --git a/docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.md b/docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.md deleted file mode 100644 index 7c3358b2..00000000 --- a/docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.md +++ /dev/null @@ -1,206 +0,0 @@ -# Elio Proactive Wakeup API - -Target environment: `pro` pre-release. - -Base URL: - -```text -https://proapi.banlv-ai.com -``` - -All endpoints require: - -```http -X-Admin-Token: -Content-Type: application/json -``` - -`ADMIN_VIEW_TOKEN` can only read rules. Updating rules or running scans requires `ADMIN_TOKEN`. - -## Status - -The proactive wakeup scheduler is deployed on `pro`, but real sending is guarded by environment flags: - -```text -ENABLE_PROACTIVE_WAKEUP=false -PROACTIVE_WAKEUP_DRY_RUN=true -``` - -With the current default, the hourly scheduler skips sending. The admin UI can still manually call dry-run for monitoring. - -## Rule Model - -```ts -type ProactiveRule = { - id: string; - rule_key: - | "global_anti_harassment" - | "festival" - | "social_post_share" - | "dormant_wakeup"; - category: "guardrail" | "festival" | "social_post" | "dormant"; - enabled: boolean; - priority: number; - trigger_conditions: Record; - behavior_strategy: Record; - frequency_limit: Record; - created_at: string; - updated_at: string; -}; -``` - -Default rules: - -| Rule | Purpose | Main Limits | -| --- | --- | --- | -| `festival` | Valentine, Christmas, birthday personalized wakeup | Max 1 per festival cycle | -| `social_post_share` | Share Elio latest Facebook post | Max 1 per FB post event | -| `dormant_wakeup` | 48h inactivity wakeup | Max 1 per dormant cycle | -| `global_anti_harassment` | Stop waking users after 7 idle days | Global suppression | - -## List Rules - -```http -GET /api/admin/proactive/rules -``` - -Example: - -```bash -curl -H "X-Admin-Token: $ADMIN_TOKEN" \ - https://proapi.banlv-ai.com/api/admin/proactive/rules -``` - -Response: - -```json -{ - "success": true, - "data": [ - { - "id": "8b14a3bf-6c01-4d0c-85b5-613399cb8f14", - "rule_key": "dormant_wakeup", - "category": "dormant", - "enabled": true, - "priority": 30, - "trigger_conditions": { - "min_idle_hours": 48, - "min_user_messages": 5 - }, - "behavior_strategy": { - "mode": "ai_dormant_wakeup", - "tone": "soft_missing", - "send_channel": "cozsweet" - }, - "frequency_limit": { - "cycle_scope": "dormant_period", - "max_per_cycle": 1 - }, - "created_at": "2026-06-29T07:38:46.155433+00:00", - "updated_at": "2026-06-29T10:52:11.041169+00:00" - } - ] -} -``` - -## Update Rule - -```http -PATCH /api/admin/proactive/rules/{rule_key} -``` - -Allowed request fields: - -```ts -type UpdateProactiveRuleRequest = { - enabled?: boolean; - priority?: number; - trigger_conditions?: Record; - behavior_strategy?: Record; - frequency_limit?: Record; -}; -``` - -Example: - -```bash -curl -X PATCH \ - -H "X-Admin-Token: $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"enabled":true}' \ - https://proapi.banlv-ai.com/api/admin/proactive/rules/dormant_wakeup -``` - -Response: - -```json -{ - "success": true, - "data": { - "rule_key": "dormant_wakeup", - "enabled": true, - "priority": 30, - "trigger_conditions": { - "min_idle_hours": 48, - "min_user_messages": 5 - } - } -} -``` - -## Run Scan - -```http -POST /api/admin/proactive/run?dry_run=true -``` - -Query parameters: - -| Name | Type | Default | Meaning | -| --- | --- | --- | --- | -| `dry_run` | boolean | `true` | `true` previews eligible candidates; `false` sends and writes delivery records only when `ENABLE_PROACTIVE_WAKEUP=true`. | - -Example: - -```bash -curl -X POST \ - -H "X-Admin-Token: $ADMIN_TOKEN" \ - "https://proapi.banlv-ai.com/api/admin/proactive/run?dry_run=true" -``` - -Current pro response with no eligible users: - -```json -{ - "success": true, - "data": { - "enabled": false, - "dryRun": true, - "candidates": 0, - "sent": 0, - "items": [] - } -} -``` - -When candidates exist, `items` contains rule/cycle context and, in dry-run mode, generated preview messages. - -## Frontend States - -Handle these states in the admin UI: - -| State | How to detect | UI suggestion | -| --- | --- | --- | -| Unauthorized | HTTP `403`, `message=admin token required` | Ask operator to enter an admin token. | -| No candidates | `success=true`, `data.candidates=0` | Show an empty state. | -| Dry-run preview | `data.dryRun=true`, `items.length>0` | Show candidate list and preview text. | -| Sending disabled | `data.enabled=false` | Display that real sending is off in environment config. | -| Send executed | `data.dryRun=false`, `data.sent>0` | Show sent count and delivery rows. | -| Partial failure | Any item with `status="failed"` | Show per-user failure reason and keep successful rows. | - -## Notes - -- User eligibility requires cozsweet/web user messages `>= 5` and last interaction older than the rule threshold. -- The global anti-harassment rule suppresses wakeups once the user has been idle for 7 days. -- Birthday wakeups require birthday data. The migration includes `users.birthday`; if the column is not present yet, birthday matching is skipped without breaking other rules. -- Facebook post sharing requires inserting an `elio_fb_post` row into `proactive_message_events`. diff --git a/docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.zh-CN.md b/docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.zh-CN.md deleted file mode 100644 index cb4a7b7c..00000000 --- a/docs/backend/FRONTEND_PROACTIVE_WAKEUP_API.zh-CN.md +++ /dev/null @@ -1,206 +0,0 @@ -# Elio 主动唤醒 API - -目标环境:`pro` 预发布环境。 - -基础地址: - -```text -https://proapi.banlv-ai.com -``` - -所有接口都需要: - -```http -X-Admin-Token: -Content-Type: application/json -``` - -`ADMIN_VIEW_TOKEN` 只能读取规则。更新规则或运行扫描需要使用 `ADMIN_TOKEN`。 - -## 状态 - -主动唤醒调度器已经部署在 `pro` 环境,但真实发送受环境变量控制: - -```text -ENABLE_PROACTIVE_WAKEUP=false -PROACTIVE_WAKEUP_DRY_RUN=true -``` - -在当前默认配置下,每小时调度器会跳过真实发送。管理后台仍然可以手动调用 dry-run,用于监控和预览。 - -## 规则模型 - -```ts -type ProactiveRule = { - id: string; - rule_key: - | "global_anti_harassment" - | "festival" - | "social_post_share" - | "dormant_wakeup"; - category: "guardrail" | "festival" | "social_post" | "dormant"; - enabled: boolean; - priority: number; - trigger_conditions: Record; - behavior_strategy: Record; - frequency_limit: Record; - created_at: string; - updated_at: string; -}; -``` - -默认规则: - -| 规则 | 用途 | 主要限制 | -| --- | --- | --- | -| `festival` | 情人节、圣诞节、生日等个性化唤醒 | 每个节日周期最多 1 次 | -| `social_post_share` | 分享 Elio 最新 Facebook 帖子 | 每个 Facebook 帖子事件最多 1 次 | -| `dormant_wakeup` | 48 小时未活跃用户唤醒 | 每个沉睡周期最多 1 次 | -| `global_anti_harassment` | 用户闲置 7 天后停止唤醒 | 全局抑制规则 | - -## 获取规则列表 - -```http -GET /api/admin/proactive/rules -``` - -示例: - -```bash -curl -H "X-Admin-Token: $ADMIN_TOKEN" \ - https://proapi.banlv-ai.com/api/admin/proactive/rules -``` - -响应: - -```json -{ - "success": true, - "data": [ - { - "id": "8b14a3bf-6c01-4d0c-85b5-613399cb8f14", - "rule_key": "dormant_wakeup", - "category": "dormant", - "enabled": true, - "priority": 30, - "trigger_conditions": { - "min_idle_hours": 48, - "min_user_messages": 5 - }, - "behavior_strategy": { - "mode": "ai_dormant_wakeup", - "tone": "soft_missing", - "send_channel": "cozsweet" - }, - "frequency_limit": { - "cycle_scope": "dormant_period", - "max_per_cycle": 1 - }, - "created_at": "2026-06-29T07:38:46.155433+00:00", - "updated_at": "2026-06-29T10:52:11.041169+00:00" - } - ] -} -``` - -## 更新规则 - -```http -PATCH /api/admin/proactive/rules/{rule_key} -``` - -允许的请求字段: - -```ts -type UpdateProactiveRuleRequest = { - enabled?: boolean; - priority?: number; - trigger_conditions?: Record; - behavior_strategy?: Record; - frequency_limit?: Record; -}; -``` - -示例: - -```bash -curl -X PATCH \ - -H "X-Admin-Token: $ADMIN_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"enabled":true}' \ - https://proapi.banlv-ai.com/api/admin/proactive/rules/dormant_wakeup -``` - -响应: - -```json -{ - "success": true, - "data": { - "rule_key": "dormant_wakeup", - "enabled": true, - "priority": 30, - "trigger_conditions": { - "min_idle_hours": 48, - "min_user_messages": 5 - } - } -} -``` - -## 运行扫描 - -```http -POST /api/admin/proactive/run?dry_run=true -``` - -查询参数: - -| 名称 | 类型 | 默认值 | 含义 | -| --- | --- | --- | --- | -| `dry_run` | boolean | `true` | `true` 表示预览符合条件的候选用户;`false` 仅在 `ENABLE_PROACTIVE_WAKEUP=true` 时执行真实发送并写入投递记录。 | - -示例: - -```bash -curl -X POST \ - -H "X-Admin-Token: $ADMIN_TOKEN" \ - "https://proapi.banlv-ai.com/api/admin/proactive/run?dry_run=true" -``` - -当前 `pro` 环境在没有符合条件用户时的响应: - -```json -{ - "success": true, - "data": { - "enabled": false, - "dryRun": true, - "candidates": 0, - "sent": 0, - "items": [] - } -} -``` - -当存在候选用户时,`items` 会包含规则、周期上下文;在 dry-run 模式下,还会包含生成的预览消息。 - -## 前端状态 - -管理后台需要处理以下状态: - -| 状态 | 检测方式 | UI 建议 | -| --- | --- | --- | -| 未授权 | HTTP `403`,`message=admin token required` | 提示操作人员输入 admin token。 | -| 无候选用户 | `success=true`,`data.candidates=0` | 展示空状态。 | -| Dry-run 预览 | `data.dryRun=true`,`items.length>0` | 展示候选用户列表和预览文本。 | -| 真实发送已禁用 | `data.enabled=false` | 提示当前环境配置已关闭真实发送。 | -| 已执行发送 | `data.dryRun=false`,`data.sent>0` | 展示发送数量和投递记录行。 | -| 部分失败 | 任意 item 的 `status="failed"` | 展示每个用户的失败原因,同时保留成功记录。 | - -## 备注 - -- 用户符合条件的前提是 cozsweet/web 用户消息数 `>= 5`,并且最后一次互动时间早于规则阈值。 -- 全局反骚扰规则会在用户闲置 7 天后抑制唤醒。 -- 生日唤醒需要生日数据。迁移中包含 `users.birthday`;如果该列尚不存在,生日匹配会被跳过,但不会影响其他规则。 -- Facebook 帖子分享需要向 `proactive_message_events` 插入一条 `elio_fb_post` 记录。 diff --git a/docs/backend/FRONTEND_VIP_CREDIT_API.md b/docs/backend/FRONTEND_VIP_CREDIT_API.md deleted file mode 100644 index 58431195..00000000 --- a/docs/backend/FRONTEND_VIP_CREDIT_API.md +++ /dev/null @@ -1,309 +0,0 @@ -# VIP / Credits Frontend API - -本文档是前端接入会员、积分、权益和历史解锁的推荐接口说明。新版接入原则: - -- 前端权益状态只认 `GET /api/user/entitlements`。 -- 游客和登录非会员权益一致,只按 `isVip` 区分会员/非会员。 -- 支付完成后只刷新一次 `GET /api/user/entitlements`。 -- `dolBalance` 是旧字段名,等价于 `creditBalance`,新代码优先使用 `creditBalance`。 - -## 推荐调用链 - -```text -打开充值/VIP 弹窗 - -> GET /api/payment/plans - -> POST /api/payment/create-order - -> 打开 data.payParams.url / checkout_url / payment_url - -> GET /api/payment/order-status 轮询,或等待 WebSocket payment_success - -> GET /api/user/entitlements 刷新权益 -``` - -不要再按订单类型分别调用 `/api/payment/vip-status` 和 `/api/user/credits` 刷新状态。 - -## GET /api/user/entitlements - -用途:当前用户 VIP、积分和权益快照。支持游客 token 和登录 token。 - -认证:需要 `Authorization: Bearer `,游客 token 也可以。 - -响应示例: - -```json -{ - "success": true, - "data": { - "userId": "user-1", - "isGuest": false, - "isVip": false, - "vipExpiresAt": null, - "creditBalance": 120, - "dolBalance": 120, - "policy": { - "membershipState": "non_vip", - "nonVipEntitlementsShared": true, - "guestSameAsLoggedInNonVip": true, - "refreshAfterPayment": "GET /api/user/entitlements" - }, - "costs": { - "normal_message": 2, - "private_message": 10, - "voice_message": 20, - "photo": 40, - "voice_call_minute": 50, - "private_album_10": 300, - "private_album_20": 600 - }, - "quotas": { - "normalChatFreeDaily": 30, - "privateUnlockFreeDaily": 1, - "voiceMessageFreeDaily": 0, - "photoFreeDaily": 0 - }, - "historyUnlock": { - "enabled": true, - "order": "oldest_first", - "chargeMode": "highest_cost_per_locked_message", - "insufficientBalanceBehavior": "no_deduction", - "costs": { - "private_message": 10, - "voice_message": 20, - "photo": 40 - } - } - } -} -``` - -前端使用: - -- App 启动、登录成功、游客 token 创建成功后调用。 -- 支付成功、充值成功、历史解锁成功后调用。 -- `isGuest` 只用于 UI 身份展示,不用于权益判断。 - -## GET /api/payment/plans - -用途:获取当前国家对应的会员/积分套餐。 - -认证:不需要登录。后端通过 `CF-IPCountry` 判断国家;登录后创建订单会再次按用户国家校验价格。 - -响应重点字段: - -```json -{ - "plan_id": "vip_monthly", - "plan_name": "月度会员", - "order_type": "vip_monthly", - "amount_cents": 1999, - "original_amount_cents": 1999, - "daily_price_cents": 66, - "currency": "USD", - "pricing_tier": "T1", - "vip_days": 30, - "dol_amount": null -} -``` - -字段说明: - -- `amount_cents`: 实付金额,传给支付服务。 -- `original_amount_cents`: 划线价;无划线价时为 `null`,字段一定存在。 -- `currency`: 支付币种。 -- `plan_id`: 创建订单时传回后端。 - -## POST /api/payment/create-order - -用途:创建支付订单。 - -认证:需要登录 token。 - -请求: - -```json -{ - "planId": "vip_monthly", - "payChannel": "stripe", - "autoRenew": true -} -``` - -响应: - -```json -{ - "success": true, - "data": { - "orderId": "ord-1", - "payParams": { - "url": "https://pay.example/ord-1" - } - } -} -``` - -前端使用: - -- `planId` 必须来自 `/api/payment/plans`。 -- `payParams` 原样交给支付 SDK 或打开其中的支付 URL。 -- 后端创建订单时使用 plans 里的 `amount_cents/currency`,不会再固定覆盖成 CNY。 - -## GET /api/payment/order-status - -用途:轮询支付订单状态。 - -认证:需要登录 token,只能查自己的订单。 - -请求: - -```text -GET /api/payment/order-status?order_id=ord-1 -``` - -响应: - -```json -{ - "success": true, - "data": { - "orderId": "ord-1", - "status": "paid", - "orderType": "vip", - "planId": "vip_monthly" - } -} -``` - -状态: - -- `pending`: 支付处理中,继续轮询。 -- `paid`: 支付成功,停止轮询并调用 `/api/user/entitlements`。 -- `failed`: 支付失败,停止轮询并提示用户。 - -## WebSocket payment_success / payment_failed - -用途:支付服务回调后,后端主动推送支付结果。 - -成功事件示例: - -```json -{ - "type": "payment_success", - "orderId": "ord-1", - "payType": "vip", - "planName": "月度会员", - "vipExpiresAt": "2026-07-26T00:00:00+00:00" -} -``` - -积分充值成功示例: - -```json -{ - "type": "payment_success", - "orderId": "ord-2", - "payType": "dol", - "planName": "1000 Credits", - "dolAmount": 1000, - "dolBalance": 1120 -} -``` - -前端收到 `payment_success` 后: - -```text -停止 order-status 轮询 --> GET /api/user/entitlements --> 用 creditBalance/isVip 更新页面 -``` - -## POST /api/chat/unlock-history - -用途:一键解锁历史锁定消息,按创建时间从旧到新处理。 - -认证:需要登录 token。 - -请求:无 body。 - -余额不足响应: - -```json -{ - "success": true, - "data": { - "unlocked": false, - "reason": "insufficient_balance", - "totalLocked": 2, - "unlockedCount": 0, - "privateCount": 1, - "imageCount": 1, - "voiceCount": 0, - "requiredCredits": 50, - "currentCredits": 20, - "remainingCredits": 20, - "shortfallCredits": 30, - "costsByMessage": { - "private-1": 10, - "photo-1": 40 - } - } -} -``` - -余额充足响应: - -```json -{ - "success": true, - "data": { - "unlocked": true, - "reason": "ok", - "requiredCredits": 60, - "currentCredits": 100, - "remainingCredits": 40, - "shortfallCredits": 0, - "messageIds": ["voice-1", "photo-1"] - } -} -``` - -计费规则: - -- 私密消息:10 credits。 -- 语音消息:20 credits。 -- 照片:40 credits。 -- 同一条消息命中多个类型时,按最高成本计一次。 -- 余额不足时不扣费、不解锁,只返回差额。 -- 已解锁过的消息会跳过。 - -## 兼容接口 - -以下接口保留给旧前端,但新前端不推荐作为权益状态来源: - -- `GET /api/payment/vip-status`: 只返回 `isVip/vipExpiresAt`。 -- `GET /api/user/credits`: 只返回 `dolBalance`。 -- `GET /api/user/stats`: 用户统计、亲密度、记忆等,不再用于支付后刷新权益。 - -## 测试证明 - -本地测试命令: - -```powershell -.\.venv\Scripts\python.exe -m pytest -q tests/test_vip_credit_entitlements.py -.\.venv\Scripts\python.exe -m pytest -q tests/test_private_unlock.py tests/test_elio_image_paywall.py -``` - -已通过结果: - -```text -tests/test_vip_credit_entitlements.py: 13 passed -tests/test_private_unlock.py tests/test_elio_image_paywall.py: 11 passed -``` - -覆盖证明: - -- `/api/user/entitlements`: 已测游客、登录非会员、VIP;游客和登录非会员的 `policy/costs/quotas` 一致。 -- `/api/payment/plans`: 已测 T1/T2 国家;所有计划都有 `original_amount_cents`。 -- `/api/payment/create-order`: 已测国家价格计划会传入创建订单;支付服务收到的金额和币种来自 plan。 -- `/api/payment/order-status`: 已测 `pending/paid/failed` 和禁止查询他人订单。 -- WebSocket `payment_success`: 已测 VIP 和积分发放事件 payload。 -- `/api/chat/unlock-history`: 已测无锁定消息、余额不足不扣费、余额充足扣费、已解锁跳过。 -- 兼容接口:已测 `/api/payment/vip-status` 和 `/api/user/credits` 仍可用。 diff --git a/docs/backend/PAYWALL_API.md b/docs/backend/PAYWALL_API.md deleted file mode 100644 index dc1a3dd1..00000000 --- a/docs/backend/PAYWALL_API.md +++ /dev/null @@ -1,786 +0,0 @@ -# 付费墙接口说明(PAYWALL_API) - -> 适用服务:`ai-boyfriend-unified` -> 聊天接口路径:`POST /api/chat/send` -> 历史接口路径:`GET /api/chat/history` -> 当前无单条私密消息解锁接口;VIP 自动查看全部私密消息 -> WebSocket 路径:`/ws` -> 字段命名:**camelCase**(与现有接口完全一致) -> 文档语言:中文 - ---- - -## 总览:三个付费墙功能 - -本文档覆盖三个会员限制功能,它们共用同一套「Elio 人设提示 + 是否展示开会员引导」的交互模式: - -| 功能 | 触发场景 | 限制对象 | 核心接口 | 核心字段 | -|------|---------|---------|---------|---------| -| ① 每日免费消息次数 | 用户发消息 | 注册非VIP(游客豁免) | `POST /api/chat/send` | `blocked` / `blockReason` / `blockDetail` | -| ② AI 男友照片查看 | 用户请求发图 | 非VIP(含游客) | `POST /api/chat/send` | `paywallTriggered` / `showUpgrade` / `imageType` / `imageUrl` | -| ③ 私密消息锁定 | 查看被锁定的私密消息 | 非VIP(含游客) | `GET /api/chat/history` | `isPrivate` / `privateLocked` / `privateHint` | - -> **当前状态**:付费墙、VIP 判定、订单接口、RabbitMQ 支付结果消费、VIP/DOL 发放逻辑已经写入后端。充值能否真正闭环取决于 `PAYMENT_SERVICE_URL`、`RABBITMQ_URL`、`orders` 表迁移以及支付服务 B 是否部署完成。若 B 未配置或不可用,聊天付费墙仍会返回 `showUpgrade=true`,但前端创建订单会失败,用户无法完成购买。 - ---- - -## 目录 - -1. [设计原则与兼容性](#1-设计原则与兼容性) -2. [功能①:免费消息次数限制](#2-功能免费消息次数限制) -3. [功能②:AI 男友照片查看](#3-功能ai-男友照片查看) -4. [功能③:私密消息查看](#4-功能私密消息查看) -5. [POST /api/chat/send 完整响应结构](#5-post-apichatsend-完整响应结构) -6. [WebSocket 事件](#6-websocket-事件) -7. [支付服务与前端充值对接](#7-支付服务与前端充值对接) -8. [前端接入清单](#8-前端接入清单) -9. [当前未生效或依赖外部配置的内容](#9-当前未生效或依赖外部配置的内容) -10. [Elio 回复语音字段](#10-elio-回复语音字段) -11. [Manager 图片上传错误码](#11-manager-图片上传错误码) -12. [附录:图片意图关键词](#12-附录图片意图关键词) - ---- - -## 1. 设计原则与兼容性 - -1. **不新增聊天接口**:功能①②都复用现有 `POST /api/chat/send`,功能③只复用 `GET /api/chat/history`。 -2. **响应结构固定**:付费墙相关字段在对应接口的**每一条**响应中都会出现(默认值 `false`/`null`),前端可稳定读取,不会"有时有有时没有"。 -3. **完全向后兼容**:所有现有字段原样保留,仅**新增**字段,不删改任何旧字段。 -4. **升级引导判断**:图片付费墙用 `showUpgrade === true` 展示"开通会员"引导;消息次数限制沿用 `blocked === true` + `blockReason` 判断;私密消息只看 `privateLocked`。 -5. **Fail-safe**:所有付费墙逻辑失败时降级放行,不影响正常聊天。 - ---- - -## 2. 功能①:免费消息次数限制 - -### 规则 - -- 生产环境:游客每天最多免费发送 **30** 条消息,游客累计最多 **50** 条消息;注册的非 VIP 用户每天最多免费发送 **30** 条消息。 -- 预发环境:上述数量为生产的 1/10,即游客每天 **3** 条、累计 **5** 条;注册非 VIP 每天 **3** 条。 -- **VIP 不受限**。 -- 超限后,`POST /api/chat/send` 返回 `blocked=true`,`reply` 为空,前端据此弹开通会员提示。 - -### 触发时的响应(data 字段) - -```json -{ - "mode": "http", - "reply": "", - "voiceUrl": "", - "audioUrl": "", - "intimacyChange": 0, - "newIntimacy": 0, - "relationshipStage": "密友", - "currentMood": "happy", - "messageId": "", - "isGuest": false, - "timestamp": 1780975180614, - "blocked": true, - "blockReason": "daily_limit", - "blockDetail": { - "type": "daily_msg_limit", - "usedToday": 30, - "limit": 30 - }, - "paywallTriggered": false, - "showUpgrade": false, - "imageType": null, - "imageUrl": null -} -``` - -### 前端处理 - -``` -if (data.blocked === true && data.blockReason === "daily_limit") { - 展示"今日免费消息已用完,开通会员畅聊"引导 UI; - data.blockDetail 提供 usedToday / limit 供文案展示。 -} - -if (data.blocked === true && data.blockReason === "total_limit") { - 展示"游客免费消息已用完,注册或开通会员继续聊"引导 UI; - data.blockDetail 提供 usedTotal / limit 供文案展示。 -} -``` - -> 注:消息次数限制当前用 `blocked/blockReason/blockDetail` 表达(沿用既有字段)。这与 `showUpgrade` 不冲突——限流场景 `showUpgrade=false`,前端通过 `blocked` 判断。 - ---- - -## 3. 功能②:AI 男友照片查看 - -### 规则 - -照片有两种触发方式: - -**(a) 用户主动索要**(如"发张照片"、"让我看看你"、"send me a photo"): -- **VIP** → 返回当天最接近当前时间的 Elio 日程图(`imageUrl` 有值,`imageType="elio_schedule"`)。 -- **非VIP**(含游客)→ 返回 Elio 升级提示,`showUpgrade=true`、`imageUrl=null`。 - -**(b) Elio 主动发图**(无需用户索要): -- 普通聊天中,主生成模型判断"此刻适合发照片"(如夜晚、亲密氛围、用户在想念时),且用户为 **VIP**、当天主动照片配额未用完、当天确有日程图时,后端会在该条普通回复里**附带** `imageUrl`(HTTP)/ 推送 `image` 事件(WS)。 -- 非 VIP **静默不发、不打扰**(不弹升级提示)。 -- VIP 每天主动收图上限 **3 张**(`PHOTO_DAILY_LIMIT=3`,UTC 日切)。 -- 主动发图时 `paywallTriggered=false`、`showUpgrade=false`,前端只需照常渲染 `imageUrl` 即可。 - -> 对前端而言协议不变:无论 (a) 还是 (b),都通过同一个 `imageUrl` 字段(HTTP)或 `image` 事件(WS)拿到图片,直接渲染即可。 - -### 响应字段 - -| 字段 | 类型 | 说明 | -|------|------|------| -| `paywallTriggered` | bool | 本条是否触发图片付费墙 | -| `showUpgrade` | bool | **前端是否展示开会员引导(核心判断字段)** | -| `imageType` | string \| null | `null`=无图 / `"elio_schedule"`=Elio 日程图 | -| `imageUrl` | string \| null | 图片公开 URL(VIP 有图时有值) | - -### 三种场景(data 字段示例) - -**A. 非VIP 请求图片(当前所有用户):** -```json -{ - "reply": "宝贝,我的照片只有会员才能看到哦……开通会员,我就把最近的照片发给你。😉", - "paywallTriggered": true, - "showUpgrade": true, - "imageType": null, - "imageUrl": null -} -``` - -**B. VIP 请求图片 + 当天有图:** -```json -{ - "reply": "嗯,你想看我……那就给你看一张。这是我今天下午的照片,你喜欢吗?", - "paywallTriggered": false, - "showUpgrade": false, - "imageType": "elio_schedule", - "imageUrl": "https://lehwkihwnlqkavhcspel.supabase.co/storage/v1/object/public/elio-schedules/schedules/42/a1b2c3d4.jpg" -} -``` - -**C. VIP 请求图片 + 当天无图:** -```json -{ - "reply": "今天还没拍什么好看的,等我有了好照片再发给你好不好?", - "paywallTriggered": false, - "showUpgrade": false, - "imageType": null, - "imageUrl": null -} -``` - -> 上面仅列出付费墙字段,完整 data 结构见第 5 节。 - -### 前端处理 - -``` -1. 始终渲染 data.reply 为 Elio 文字气泡。 -2. if (data.showUpgrade) 展示开会员引导。 -3. if (data.imageUrl) 渲染图片(URL 可直接用于 ,无需鉴权)。 -``` - ---- - -## 4. 功能③:私密消息查看 - -### 规则 - -- AI 回复中若包含私密/亲密/大尺度内容,后端**异步**判定并给该消息打标 `is_private=true`,生成模糊预告 `private_hint`。 -- 用户拉取历史时: - - **VIP** → 所有历史私密消息直接返回完整内容;之前锁住的内容也会一起解开,后续也不再锁。 - - **非VIP** → 私密消息内容被屏蔽,返回 `privateLocked=true` + `privateHint`,前端展示**锁定卡片**。 -- 当前产品**没有单条私密消息解锁流程**。是否展示完整内容,只取决于当前用户是不是 VIP。 - -### 4.1 GET /api/chat/history —— 历史含锁定标记 - -每条消息(`data.messages[]`)字段: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `role` | string | `user` / `assistant` | -| `type` | string | `text` / `voice`;有 Elio 语音地址时为 `voice` | -| `content` | string | 消息内容;**非VIP查看私密消息时为空字符串** | -| `id` | string | 消息 ID | -| `created_at` | string | ISO8601 时间 | -| `audioUrl` | string \| null | Elio 语音播放 URL;用户文本消息为 `null` | -| `voiceUrl` | string \| null | 同 `audioUrl`,兼容旧字段 | -| `isPrivate` | bool \| null | 是否私密消息 | -| `privateLocked` | bool \| null | **true = 当前用户无权查看,内容已锁定** | -| `privateHint` | string \| null | 非会员看到的模糊预告文案 | - -`data` 顶层建议前端只关心: - -| 字段 | 类型 | 说明 | -|------|------|------| -| `isVip` | bool | 当前用户是否 VIP | - -> 兼容旧版本客户端,后端可能仍返回 `privateFreeLimit`、`privateUsedToday`、`privateCanViewFree` 等旧字段;当前前端可以忽略。 - -**历史响应示例(非VIP,含一条锁定的私密消息):** -```json -{ - "messages": [ - { - "role": "user", - "type": "text", - "content": "今天好想你", - "id": "msg-1", - "created_at": "2026-06-09T03:00:00Z", - "audioUrl": null, - "voiceUrl": null, - "isPrivate": false, - "privateLocked": false, - "privateHint": null - }, - { - "role": "assistant", - "type": "voice", - "content": "", - "id": "msg-2", - "created_at": "2026-06-09T03:00:05Z", - "audioUrl": "https://.../audio/msg-2.mp3", - "voiceUrl": "https://.../audio/msg-2.mp3", - "isPrivate": true, - "privateLocked": true, - "privateHint": "他好像想说什么悄悄话,解锁会员才能看到…" - } - ], - "total": 2, - "limit": 50, - "offset": 0, - "isVip": false -} -``` - -### 4.2 前端处理 - -``` -拉取 history: - 对每条 privateLocked===true 的消息,渲染为锁定卡片(展示 privateHint)。 - -用户开通 VIP 后: - 重新拉取 history; - 之前被锁住的私密消息会直接返回完整 content,不需要再调额外解锁接口。 -``` - ---- - -## 5. POST /api/chat/send 完整响应结构 - -`data` 字段的**全部字段**(现有 + 新增),所有 `/api/chat/send` 响应都包含: - -> 注意:`privateLocked` / `privateHint` / `isPrivate` **不属于** `/api/chat/send` 的响应字段;它们只出现在 `GET /api/chat/history` 的 `data.messages[]` 单条历史消息里。私密消息由后端异步打标,因此前端要在刷新/拉取历史时根据 `privateLocked=true` 渲染模糊锁定卡片。 - -| 字段 | 类型 | 现有/新增 | 说明 | -|------|------|----------|------| -| `mode` | string | 现有 | `"http"` / `"websocket"` | -| `reply` | string | 现有 | Elio 回复文本 | -| `voiceUrl` | string | 现有 | 语音 URL(后台异步生成) | -| `audioUrl` | string | 现有 | 兼容旧客户端 | -| `intimacyChange` | int | 现有 | 亲密度变化(恒 0) | -| `newIntimacy` | int | 现有 | 当前亲密度 | -| `relationshipStage` | string | 现有 | 关系阶段 | -| `currentMood` | string | 现有 | Elio 情绪 | -| `messageId` | string | 现有 | 消息 ID | -| `isGuest` | bool \| null | 现有 | 是否游客 | -| `timestamp` | int | 现有 | Unix 毫秒时间戳 | -| `blocked` | bool \| null | 现有 | 是否消息次数限流拦截(功能①) | -| `blockReason` | string \| null | 现有 | `"daily_limit"` / `"total_limit"` | -| `blockDetail` | object \| null | 现有 | 每日限制:`{ type, usedToday, limit }`;游客总量限制:`{ type, usedTotal, limit }` | -| `paywallTriggered` | bool | 新增 | 是否触发图片付费墙(功能②) | -| `showUpgrade` | bool | 新增 | **前端是否展示开会员引导** | -| `imageType` | string \| null | 新增 | `null` / `"elio_schedule"` | -| `imageUrl` | string \| null | 新增 | 图片公开 URL | - ---- - -## 6. WebSocket 事件 - -通过 `/ws` 连接时: - -### image 事件(VIP 有图时额外推送) -```json -{ "type": "image", "data": { "imageUrl": "https://.../schedules/42/xxx.jpg" } } -``` - -### paywall_status 事件(每次图片请求都推送) -```json -{ - "type": "paywall_status", - "data": { - "paywallTriggered": true, - "showUpgrade": true, - "imageType": null, - "imageUrl": null - } -} -``` - -文字回复(含升级提示)沿用现有整句推送机制,前端无需改动。 - ---- - -## 7. 支付服务与前端充值对接 - -支付链路分三段: - -```text -前端 -> 后端 A(ai-boyfriend-unified) -> 支付服务 B -> RabbitMQ -> 后端 A 发放权益 -> 前端刷新状态 -``` - -### 7.1 后端 A 提供给前端的接口 - -#### GET /api/payment/plans - -获取套餐列表,无需登录。前端在开通会员/充值弹窗中展示。 - -响应 `data.plans[]` 字段: - -| 字段 | 类型 | 说明 | -|---|---|---| -| `plan_id` | string | 前端创建订单时传入的套餐 ID | -| `plan_name` | string | 展示名称 | -| `order_type` | string | 传给支付服务 B 的订单类型 | -| `amount_cents` | int | 金额,单位为分 | -| `original_amount_cents` | int \| null | 原价/划线价,单位为分;仅 VIP 套餐返回 | -| `daily_price_cents` | int \| null | 日均展示价,单位为分;仅 VIP 套餐返回 | -| `currency` | string | VIP 套餐当前为 `USD`;DOL 充值项当前为 `CNY` | -| `vip_days` | int \| null | VIP 增加天数,`null` 表示永久或非 VIP 商品 | -| `dol_amount` | int \| null | DOL 积分数量,VIP 商品为 `null` | - -当前套餐: - -| plan_id | plan_name | order_type | amount_cents | original_amount_cents | daily_price_cents | currency | 发放内容 | -|---|---|---:|---:|---:|---:|---|---| -| `vip_monthly` | Monthly | `vip_monthly` | 1990 | 2490 | 66 | USD | VIP +30 天 | -| `vip_quarterly` | Quarterly | `vip_quarterly` | 4990 | 5990 | 55 | USD | VIP +90 天 | -| `vip_annual` | Annual | `vip_annual` | 16990 | 19990 | 47 | USD | VIP +365 天 | -| `dol_100` | 100 DOL | `dol` | 600 | null | null | CNY | DOL +100 | -| `dol_500` | 500 DOL | `dol` | 2800 | null | null | CNY | DOL +500 | - -#### POST /api/payment/create-order - -创建订单,需登录。 - -```http -POST /api/payment/create-order -Authorization: Bearer -Content-Type: application/json -``` - -请求体: - -```json -{ - "planId": "vip_monthly", - "payChannel": "stripe", - "autoRenew": true -} -``` - -字段说明: - -| 字段 | 必填 | 说明 | -|---|---|---| -| `planId` | 是 | 来自 `GET /api/payment/plans` 的 `plan_id` | -| `payChannel` | 是 | 支付通道取值约定:`stripe` / `ezpay` / `paycools`。当前生产代码若仍只放开 `stripe` / `ezpay`,需要同步把 A/B 两侧白名单加上 `paycools` | -| `autoRenew` | 是 | 是否自动续费;DOL 和永久会员建议传 `false` | - -成功响应: - -```json -{ - "success": true, - "data": { - "orderId": "pay_abc123", - "payParams": { - "checkout_url": "https://pay.example.com/checkout/pay_abc123" - } - } -} -``` - -`payParams` 是支付服务 B 返回给前端的拉起支付信息。对于 Stripe Checkout / ezPay / PayCools 这类浏览器跳转支付,B 必须在 `payParams` 内提供可直接打开的支付链接。PayCools 返回的支付链接应放在 `checkout_url`、`payment_url` 或 `url` 中。 - -当前 Stripe 返回为 Checkout 链接模式。前端创建订单成功后应优先打开: - -1. `data.paymentUrl` -2. `data.checkoutUrl` -3. `data.url` -4. `data.payParams.paymentUrl` -5. `data.payParams.checkoutUrl` -6. `data.payParams.url` - -打开支付链接后继续轮询 `GET /api/payment/order-status?order_id=...`;支付服务 B 收到 Stripe webhook 后会发布 MQ,后端 A 消费后把订单从 `pending` 更新为 `paid`。 - -前端读取支付链接的优先级: - -1. `payParams.checkout_url` -2. `payParams.payment_url` -3. `payParams.approval_url` -4. `payParams.url` - -如果 B 使用 SDK 参数而不是跳转链接,也可以在 `payParams` 中返回 SDK 所需字段,但必须提前和前端约定字段名。后端 A 不解析 `payParams`,只做透传。 - -错误: - -| 状态码 | 场景 | -|---|---| -| `400` | `planId` 无效,或 `payChannel` 不是后端当前放开的支付通道 | -| `401` | 未登录或 token 无效 | -| `502` | `PAYMENT_SERVICE_URL` 未配置,或支付服务 B 创建订单失败 | -| `500` | 后端 A 创建订单过程中发生未知错误 | - -#### GET /api/payment/order-status - -轮询订单状态,需登录。只能查询当前登录用户自己的订单。 - -```http -GET /api/payment/order-status?order_id=pay_abc123 -Authorization: Bearer -``` - -响应: - -```json -{ - "success": true, - "data": { - "orderId": "pay_abc123", - "status": "pending", - "orderType": "vip_monthly", - "planId": "vip_monthly" - } -} -``` - -`status` 取值: - -| status | 前端动作 | -|---|---| -| `pending` | 继续等待;建议每 3~5 秒轮询一次 | -| `paid` | 停止轮询,刷新 VIP 状态或 DOL 余额 | -| `failed` | 停止轮询,提示支付失败/取消 | - -#### GET /api/payment/vip-status - -查询当前用户 VIP 状态,需登录。 - -```http -GET /api/payment/vip-status -Authorization: Bearer -``` - -响应: - -```json -{ - "success": true, - "data": { - "isVip": true, - "vipExpiresAt": "2026-07-17T10:00:00+00:00" - } -} -``` - -`vipExpiresAt=null` 表示永久会员。 - -### 7.2 后端 A 调支付服务 B:创建订单 - -后端 A 在收到前端 `create-order` 后,会调用支付服务 B: - -```http -POST {PAYMENT_SERVICE_URL}/orders -Authorization: Bearer {PAYMENT_SERVICE_API_KEY} -Content-Type: application/json -``` - -请求体: - -```json -{ - "user_id": "用户 UUID", - "order_type": "vip_monthly", - "pay_channel": "paycools", - "automatic_renewal": true, - "amount_cents": 1990, - "currency": "USD" -} -``` - -B 必须返回: - -```json -{ - "order_id": "pay_abc123", - "pay_params": { - "checkout_url": "https://pay.example.com/checkout/pay_abc123" - } -} -``` - -约定: - -- `pay_params` 结构由支付服务 B 决定,后端 A 不解析,前端原样使用。 -- 浏览器跳转支付必须返回可打开的 URL,推荐统一使用 `pay_params.checkout_url`。 -- PayCools 返回支付链接时,建议放在 `pay_params.checkout_url`;前端也兼容 `payment_url` / `approval_url` / `url`。 -- 当前支付对接文档约定的 `pay_channel` 是 `stripe` / `ezpay` / `paycools`;生产代码也需要同步支持这些取值。如果要接其他通道,需要先由支付服务 B 的 `/orders` 支持该通道,再把后端 A 的渠道校验和前端选项一起放开。 -- 后端 A 会把 `order_id`、`user_id`、`order_type`、`plan_id`、`status=pending` 写入本地 `orders` 表。 -- 发放什么权益由 A 侧 `plan_id` 决定,B 不需要知道 VIP 天数或 DOL 数量。 - -### 7.3 支付服务 B 回传支付结果:RabbitMQ - -B 在支付成功或失败后,向 RabbitMQ 队列发布消息。 - -| 项目 | 值 | -|---|---| -| 队列名 | 默认 `payment.events`,可通过 `RABBITMQ_QUEUE` 修改 | -| 消息格式 | JSON UTF-8 | -| 推荐属性 | durable queue + persistent message | - -消息体: - -```json -{ - "user_id": "用户 UUID", - "order_id": "pay_abc123", - "status": "success", - "info": "" -} -``` - -字段: - -| 字段 | 必填 | 说明 | -|---|---|---| -| `user_id` | 是 | 当前订单所属用户 | -| `order_id` | 是 | B 创建订单时返回给 A 的订单号 | -| `status` | 是 | `success` / `fail` | -| `info` | 否 | 失败原因或备注 | -| `sign` | 视配置 | 如果 A 配置了 `PAYMENT_WEBHOOK_SECRET`,B 需要按双方约定签名 | - -A 收到消息后的行为: - -| status | A 侧处理 | -|---|---| -| `success` | 查本地订单 -> 按 `plan_id` 发放 VIP 或 DOL -> 订单置为 `paid` -> 推送 `payment_success` | -| `fail` | 订单置为 `failed` -> 推送 `payment_failed` | - -幂等规则:同一订单已经是 `paid` 时,再收到 `success` 不会重复发放权益。 - -### 7.4 支付 WebSocket 事件 - -权益发放后,A 会通过 `/ws` 推送给在线用户。 - -VIP 支付成功: - -```json -{ - "type": "payment_success", - "orderId": "pay_abc123", - "payType": "vip", - "planName": "月度会员", - "vipExpiresAt": "2026-07-17T10:00:00+00:00" -} -``` - -DOL 支付成功: - -```json -{ - "type": "payment_success", - "orderId": "pay_def456", - "payType": "dol", - "planName": "100 DOL", - "dolAmount": 100, - "dolBalance": 350 -} -``` - -支付失败: - -```json -{ - "type": "payment_failed", - "orderId": "pay_abc123", - "info": "用户取消支付" -} -``` - -### 7.5 环境变量 - -```env -PAYMENT_SERVICE_URL=https://pay.example.com -PAYMENT_SERVICE_API_KEY= -PAYMENT_SERVICE_TIMEOUT=10.0 - -RABBITMQ_URL=amqp://user:pass@host:5672/vhost -RABBITMQ_QUEUE=payment.events -PAYMENT_WEBHOOK_SECRET= -``` - ---- - -## 8. 前端接入清单 - -### 8.1 触发开通会员入口 - -前端需要在这些场景展示开通会员入口: - -- `POST /api/chat/send` 返回 `blocked=true` 且 `blockReason="daily_limit"`:每日免费消息用完。 -- `POST /api/chat/send` 返回 `showUpgrade=true`:照片付费墙或其他升级提示。 -- `GET /api/chat/history` 返回 `privateLocked=true`:这条私密消息当前只有 VIP 可见。 -- 用户主动点击会员中心/充值按钮。 - -### 8.2 购买流程 - -推荐流程: - -```text -1. 打开升级弹窗 -2. GET /api/payment/plans 加载套餐 -3. 用户选择 planId、payChannel、autoRenew -4. POST /api/payment/create-order -5. 从 data.payParams 读取 checkout_url/payment_url/approval_url/url 并打开支付页,PayCools 链接也走这个逻辑,或按约定调用支付 SDK -6. 每 3~5 秒 GET /api/payment/order-status?order_id=... -7. 收到 paid 或 WebSocket payment_success 后停止轮询 -8. payType=vip 时调用 /api/payment/vip-status 并刷新本地会员状态 -9. payType=dol 时刷新用户资料或积分余额,使用最新 dolBalance -``` - -前端不应该自己推断权益是否发放成功,应以 `order-status=paid`、`payment_success` 或 `vip-status` 的后端结果为准。 -前端也不应该调用任何“确认支付成功”接口来改订单状态;订单状态只能由支付服务 B 接到支付通道回调后,通过 RabbitMQ 通知后端 A 更新。 - -### 8.3 支付中状态 - -创建订单后到支付结果返回前,按钮/弹窗应显示支付处理中,避免重复创建订单。用户关闭支付窗口时不要立即判定失败,继续轮询一小段时间或允许用户手动刷新状态。 - ---- - -## 9. 当前未生效或依赖外部配置的内容 - -这些内容已经有后端代码或接口定义,但上线生效需要对应外部条件: - -| 功能 | 代码状态 | 生效条件 | 未满足时表现 | -|---|---|---|---| -| 付费墙展示 | 已接入聊天、图片、私密消息 | 无额外条件 | 非 VIP 返回 `blocked` 或 `showUpgrade` | -| 创建订单 | 已有 `/api/payment/create-order` | 配置 `PAYMENT_SERVICE_URL`,支付服务 B 可访问,且 B 返回 `order_id` + 可拉起支付的 `pay_params` | 返回 502,或前端拿不到支付链接 | -| 支付结果发放权益 | 已有 RabbitMQ 消费者 | 配置 `RABBITMQ_URL`,B 发布 `payment.events` 消息 | 订单停留 `pending`,VIP/DOL 不会到账 | -| 订单轮询 | 已有 `/api/payment/order-status` | 执行 `database/payment-migration.sql` 创建 `orders` 表 | 查询不到订单或写入失败 | -| VIP 解锁付费功能 | 已有 VIP 判定 | `users.is_vip=true` 且未过期,或永久会员 | 非 VIP 仍按限制处理 | -| DOL 充值到账 | 已有 DOL 发放 | `users.dol_balance` 字段存在,B 回传成功 MQ | 前端不能只靠创建订单展示到账 | -| Elio 回复语音 | `POST /api/chat/send` 已返回 `voiceUrl` / `audioUrl` 字段;`GET /api/chat/history` 的消息项也返回 `type` / `audioUrl`;WS 模式会推 `voice_ready` | TTS 服务可用,且前端按字段播放 | HTTP 首包里通常为空,后台生成后写入 DB;稍后拉 history 可拿到 `audioUrl` | - -VIP 当前会解除/放宽的功能: - -- 每日免费消息次数限制。 -- 私密消息查看限制。 -- AI 男友照片查看和主动发图。 -- 语音/TTS 相关门控中需要 VIP 的普通语音回复。 - ---- - -## 10. Elio 回复语音字段 - -当前产品口径是 **Elio 可以发语音给用户,用户不需要发语音给 Elio**。因此聊天发送接口只需要前端传文字 `message`,不需要用户音频上传字段。 - -### 10.1 POST /api/chat/send 中的语音字段 - -`POST /api/chat/send` 的 `data` 中包含: - -| 字段 | 说明 | -|---|---| -| `voiceUrl` | Elio 回复语音 URL,新字段 | -| `audioUrl` | 兼容旧客户端,语义同 `voiceUrl` | - -当前 HTTP 模式下,AI 回复文本会立即返回,TTS 语音在后台异步生成,所以首包里通常是: - -```json -{ - "reply": "AI 的回复内容", - "voiceUrl": "", - "audioUrl": "" -} -``` - -后台生成完成后会把语音地址写入 `chat_messages.voice_url`。`GET /api/chat/history` 会把这条消息返回为 `type="voice"`,并带 `audioUrl` / `voiceUrl`。因此前端若走 HTTP 首包,不能假设立刻拿到语音 URL;可以稍后刷新 history 获取。 - -语音消息对应的文字版不需要单独转写接口: - -| 场景 | 前端直接读取的字段 | -|---|---| -| `POST /api/chat/send` 首包 | `reply` | -| `GET /api/chat/history` 单条消息 | `content` | - -### 10.2 WebSocket 模式的语音事件 - -如果前端使用 WebSocket 模式,后端会在分句 TTS 生成完成后推送: - -```json -{ - "type": "voice_ready", - "index": 0, - "voiceUrl": "https://.../audio/xxx.mp3", - "audioUrl": "https://.../audio/xxx.mp3" -} -``` - -前端应优先使用 `voiceUrl` 播放,`audioUrl` 仅作为旧字段兼容。 - -> 注意:当前 `/api/chat/send` 请求体没有用户音频字段;用户侧语音输入不是当前需求,不需要额外转写字段或用户 `audioUrl`。 - ---- - -## 11. Manager 图片上传错误码 - -用于 ai-boyfriend-Manager 后台 `POST/PUT /api/schedule` 的图片上传(运营管理员侧)。 - -### 11.1 HTTP 400 — 格式不支持 -```json -{ "detail": "不支持的文件格式 image/gif,仅允许 JPEG、PNG、WEBP" } -``` - -### 11.2 HTTP 400 — 文件过大(>10MB) -```json -{ "detail": "文件大小超过 10 MB 限制(当前 12582912 字节)" } -``` - -### 11.3 image_warning — 图片处理失败(非阻断,HTTP 200) - -图片通过校验但压缩/上传失败时不返回 500,而是 200 + `image_warning`,日程文字正常保存: -```json -{ "id": 42, "title": "苏州行", "image_url": null, "image_warning": "图片压缩失败:PIL 解码错误" } -``` - -| image_warning 示例 | 含义 | -|---|---| -| `"图片压缩失败:PIL 解码错误"` | 图片损坏或格式异常 | -| `"图片上传失败:Storage 连接超时"` | Storage 网络异常 | -| `"图片上传失败:权限不足"` | Bucket 权限配置错误 | -| `"数据库写入 image_url 失败"` | 图片已传但 URL 未关联日程 | - ---- - -## 12. 附录:图片意图关键词 - -正则匹配,仅检测消息**前 600 字符**,大小写不敏感: - -| 语言 | 模式 | -|------|------| -| 中文 | `发.*照片`、`发.*图`、`让我看看你`、`给我看看你`、`你今天的照片`、`你最近的照片`、`你的照片`、`你的图片` | -| 英文 | `send.*photo`、`send.*picture`、`show.*picture`、`let me see you`、`what do you look like` | - ---- - -## 附:数据库迁移 - -付费墙、支付、语音、图片功能依赖的库表迁移脚本: - -- `database/paywall-migration.sql` — users 会员字段(`is_vip`/`vip_expires_at`)、chat_messages 私密字段(`is_private`/`private_hint`)、`daily_private_unlocks` 表 -- `database/payment-migration.sql` — `orders` 表,用于本地订单状态、轮询和 MQ 回调发放权益 -- `database/migrate_elio_schedules_image_url.sql` — elio_schedules 表 `image_url` 列(功能②) -- `database/voice-quota-migration.sql` — users 语音配额字段(`daily_voice_count`/`daily_voice_date`) -- `database/photo-quota-migration.sql` — users 主动照片配额字段(`daily_photo_count`/`daily_photo_date`,Elio 主动发图日限) - -> 上线前需在 Supabase SQL Editor 执行以上脚本,并创建 `elio-schedules` 公开 Storage Bucket。 -> 若支付功能要闭环,必须确认 `orders` 表、`users.is_vip`、`users.vip_expires_at`、`users.dol_balance` 均已存在。 diff --git a/docs/backend/plans-examples.json b/docs/backend/plans-examples.json new file mode 100644 index 00000000..c3493919 --- /dev/null +++ b/docs/backend/plans-examples.json @@ -0,0 +1,135 @@ +{ + "code": 200, + "message": "success", + "success": true, + "data": { + "isFirstRecharge": true, + "firstRechargeOffer": { + "enabled": true, + "type": "first_recharge_half_price", + "discountPercent": 50 + }, + "plans": [ + { + "planId": "vip_monthly", + "planName": "月度会员", + "orderType": "vip_monthly", + "vipDays": 30, + "dolAmount": null, + "creditBalance": 3000, + "amountCents": 995, + "originalAmountCents": 1990, + "dailyPriceCents": 33, + "currency": "USD", + "isFirstRechargeOffer": true, + "firstRechargeDiscountPercent": 50, + "promotionType": "first_recharge_half_price" + }, + { + "planId": "vip_quarterly", + "planName": "季度会员", + "orderType": "vip_quarterly", + "vipDays": 90, + "dolAmount": null, + "creditBalance": 9000, + "amountCents": 2495, + "originalAmountCents": 4990, + "dailyPriceCents": 27, + "currency": "USD", + "isFirstRechargeOffer": true, + "firstRechargeDiscountPercent": 50, + "promotionType": "first_recharge_half_price" + }, + { + "planId": "vip_annual", + "planName": "年度会员", + "orderType": "vip_annual", + "vipDays": 365, + "dolAmount": null, + "creditBalance": 36000, + "amountCents": 8995, + "originalAmountCents": 17990, + "dailyPriceCents": 24, + "currency": "USD", + "isFirstRechargeOffer": true, + "firstRechargeDiscountPercent": 50, + "promotionType": "first_recharge_half_price" + }, + { + "planId": "dol_1000", + "planName": "1,000 Credits", + "orderType": "dol", + "vipDays": null, + "dolAmount": 1000, + "creditBalance": 1000, + "amountCents": 495, + "originalAmountCents": 990, + "dailyPriceCents": null, + "currency": "USD", + "isFirstRechargeOffer": true, + "firstRechargeDiscountPercent": 50, + "promotionType": "first_recharge_half_price" + }, + { + "planId": "dol_2000", + "planName": "2,000 Credits", + "orderType": "dol", + "vipDays": null, + "dolAmount": 2000, + "creditBalance": 2000, + "amountCents": 845, + "originalAmountCents": 1690, + "dailyPriceCents": null, + "currency": "USD", + "isFirstRechargeOffer": true, + "firstRechargeDiscountPercent": 50, + "promotionType": "first_recharge_half_price" + }, + { + "planId": "dol_3000", + "planName": "3,000 Credits", + "orderType": "dol", + "vipDays": null, + "dolAmount": 3000, + "creditBalance": 3000, + "amountCents": 1145, + "originalAmountCents": 2290, + "dailyPriceCents": null, + "currency": "USD", + "isFirstRechargeOffer": true, + "firstRechargeDiscountPercent": 50, + "promotionType": "first_recharge_half_price" + }, + { + "planId": "dol_5000", + "planName": "5,000 Credits", + "orderType": "dol", + "vipDays": null, + "dolAmount": 5000, + "creditBalance": 5000, + "amountCents": 1745, + "originalAmountCents": 3490, + "dailyPriceCents": null, + "currency": "USD", + "isFirstRechargeOffer": true, + "firstRechargeDiscountPercent": 50, + "promotionType": "first_recharge_half_price" + }, + { + "planId": "dol_10000", + "planName": "10,000 Credits", + "orderType": "dol", + "vipDays": null, + "dolAmount": 10000, + "creditBalance": 10000, + "amountCents": 3245, + "originalAmountCents": 6490, + "dailyPriceCents": null, + "currency": "USD", + "isFirstRechargeOffer": true, + "firstRechargeDiscountPercent": 50, + "promotionType": "first_recharge_half_price" + } + ] + } +} \ No newline at end of file