dudu 后端与数据架构

从前端交互反推的 API、流式协议与时长账本计费设计 · Go 单体

v1.1 2026-06-11 依据:frontend-design.html v1.1 + design/ 设计系统 v1.1 新增:反馈接口 + 打点上报

目录

一、相对 v0.1 的变更摘要

设计系统(design/)确立的产品决策反推出以下后端变更:

v0.1v1.0(本文档)
计费模型月/年订阅 + 每日免费 10 分钟时长包(100min/¥9、500min/¥39、2000min/¥129,不过期)+ 每日免费试用 3 分钟
数据表plans / subscriptions废弃,改为 duration_packs / balance_ledger / trial_usage
WS 协议partial / final / error新增下行 usage 帧(实时余额),error code 枚举化
API未细化本文档第三章给出完整契约(与前端 UI 数据点一一对应)
新增接口GET /v1/app/latest(托盘"检查更新")、GET /v1/packs(时长包服务端可配)
反馈问题(v1.1)POST /v1/feedback:文字 + 图片(OSS 存储)+ 诊断信息,见 3.5
客户端打点(v1.1)POST /v1/metrics/batch 批量上报 + metric_events / metric_daily 表 + 每日聚合,见 3.6

总体架构、ASR Provider 抽象、技术栈(Go + gin + gorilla/websocket + PostgreSQL + Redis + wechatpay-go、阿里云与百炼同区域部署)沿用 v0.2 总体方案

二、总体架构(沿用)

桌面端 Tauri / 移动端键盘扩展+主 App
        │ HTTPS(auth/me/packs/orders) + WSS(音频流)
        ▼
┌──────────────────────────────────────────────────┐
│              Go 后端(单体,Docker 部署)           │
│  auth │ gateway(WS 中继) │ billing │ quota │ user │
└──────────┬───────────────────────┬───────────────┘
           │ WSS 同区域            │
           ▼                      ▼
  阿里云百炼 Gummy          PostgreSQL + Redis + OSS
  (流式大模型 ASR,          (账本/订单 + 实时计数
   Provider 可切火山豆包)     + 反馈图片存储)

音频走后端中继的三个理由不变:① ASR 密钥不下发;② 按秒计费必须服务端计量;③ Provider 可切换、客户端协议不变。

三、HTTP API 设计

REST + JSON,鉴权 Authorization: Bearer <JWT>(标 🔓 的无需登录)。统一错误体 {"code":"...","message":"..."}

3.1 认证

接口说明
🔓 POST /v1/auth/qr 桌面端发起扫码登录。响应 {"state":"...","qr_url":"https://open.weixin.qq.com/...&state=..."};state TTL 2 分钟(Redis)
🔓 GET /v1/auth/qr/{state} 桌面端轮询(1s 间隔)。响应 {"status":"pending"}{"status":"confirmed","token":"<JWT>","user":{...}} / {"status":"expired"}
🔓 GET /v1/auth/wechat/callback 微信网页授权回调(扫码确认后),code 换 openid/unionid → 建号或关联 → 标记 state confirmed
🔓 POST /v1/auth/wechat 移动端:body {"code":"<OpenSDK code>"}{"token":"<JWT>","user":{...}}
POST /v1/auth/logoutJWT 进 Redis 黑名单

3.2 用户与余额

GET /v1/me
→ {
  "user_id": "u_8f3k",
  "nickname_masked": "wang***",          // 前端直接展示,服务端脱敏
  "balance_seconds": 28320,              // 时长余额(秒),UI 显示 472 分钟
  "trial_daily_limit": 180,              // 每日免费试用上限(秒)
  "trial_used_today": 60,                // 今日已用(秒),UI 显示 1 / 3 分钟
  "account_state": "ok"                  // ok | trial | quota(guest 由无 token 推得)
}

account_state 服务端判定:余额 > 0 → ok;余额 = 0 且试用未用尽 → trial;两者皆尽 → quota。键盘工具条与 MicBar 四态直接消费此字段。

3.3 时长包与订单

GET /v1/packs        // 🔓 服务端可配,前端不写死价格
→ { "packs": [
  { "id": "pack_100",  "minutes": 100,  "price_cents": 900,   "unit_desc": "约 ¥0.09 / 分钟",  "tag": "" },
  { "id": "pack_500",  "minutes": 500,  "price_cents": 3900,  "unit_desc": "约 ¥0.078 / 分钟", "tag": "省 13%" },
  { "id": "pack_2000", "minutes": 2000, "price_cents": 12900, "unit_desc": "约 ¥0.065 / 分钟", "tag": "省 28%" }
]}

POST /v1/orders      // 下单
body: { "pack_id": "pack_500", "channel": "native" | "app" }
→ native(桌面扫码): { "order_id": "o_123", "code_url": "weixin://wxpay/..." }
→ app(移动收银台):  { "order_id": "o_123", "pay_params": { appid, partnerid, prepayid, ... } }

GET /v1/orders/{id}  // 桌面端轮询"支付完成后自动开通"
→ { "status": "pending" | "paid" | "closed", "balance_seconds": 58320 }  // paid 时带最新余额

POST /v1/pay/notify  // 🔓 微信支付回调:验签 → 幂等入账(详见第五章)

3.4 应用更新

GET /v1/app/latest?platform=mac|win|android|ios
→ { "version": "1.2.0", "notes": "…", "url": "https://dl.dudu.app/...", "force": false }

3.5 反馈问题(v1.1 新增)

POST /v1/feedback        // multipart/form-data,需登录
fields:
  content      文字内容,1–1000 字(必填)
  images[]     图片 ≤3 张,jpg/png/webp,单张 ≤5MB(可选)
  diagnostics  诊断信息 JSON 字符串(可选):app_version / os_version /
               platform / device_id / 最近错误摘要(不含识别文本)
→ 200 { "feedback_id": "fb_7k2m" }
→ 429 { "code": "FEEDBACK_RATE_LIMITED", "message": "今天反馈次数已达上限" }

3.6 客户端打点(v1.1 新增)

POST /v1/metrics/batch   // 🔓 可匿名(Content-Encoding: gzip)
body: { "device_id": "d-uuid", "platform": "mac", "app_version": "1.2.0",
        "os_version": "macOS 15.5",
        "events": [
          { "event": "asr.first_partial_ms", "props": { "ms": 312 }, "client_ts": 1765430400123 },
          { "event": "asr.session", "props": { "audio_seconds": 6, "partial_count": 14,
            "cancel": false, "error_code": null }, "client_ts": 1765430406456 }
        ] }
→ 204(永不因业务原因报错,校验失败静默丢弃——打点绝不影响客户端)

四、流式识别协议 v1.1(WSS /v1/asr/stream)

长连接复用:客户端启动即连接 + ping/pong 保活,按键时零握手。一次按住-松开为一个识别会话(utterance),同连接串行多次会话。

4.1 上行

内容
文本 · start{"type":"start","session_id":"c-生成uuid","sample_rate":16000,"format":"pcm16"};服务端校验配额,不足直接回 error(QUOTA_EXCEEDED)
二进制 · 音频16kHz / 16bit / mono PCM,每 100ms 一帧(3200 字节)
文本 · stop{"type":"stop","session_id":"..."}(松开);{"type":"cancel",...}(上滑取消:仍计量但丢弃结果)

4.2 下行

内容
partial{"type":"partial","session_id":"...","text":"今天天气"} 增量结果,UI 实时刷新
final{"type":"final","session_id":"...","text":"今天天气不错。"} 句子定稿
usage(新增){"type":"usage","session_id":"...","session_seconds":4,"balance_seconds":28316,"trial_remaining":120};识别中每 2s 及 stop 后下发,驱动托盘余额 / 键盘计时旁状态实时刷新
error{"type":"error","code":"...","message":"..."}

4.3 错误码

code语义前端表现
QUOTA_EXCEEDED试用 + 余额均已用尽桌面浮层警告并引导购买;键盘转 quota 态
UNAUTHORIZEDJWT 失效 / 被踢出转未登录态,引导重新登录
ASR_UNAVAILABLE上游 ASR 故障(已重试/切备用后仍失败)"识别服务暂不可用,稍后再试"
SESSION_LIMIT单会话超长(上限 3 分钟自动截断定稿)自动结束并上屏已识别内容,提示"单次最长 3 分钟,松开后可继续"
RATE_LIMITED设备 30 分钟滑动窗口超限(会话 > 30 次或累计识别时长 > 30 分钟)提示"操作过于频繁,稍后再试"

4.4 中继实现(gateway)

五、计费模型:时长账本(核心变更)

5.1 模型

5.2 扣费规则

  1. 会话结束(stop/cancel/超时断连)按实收音频时长向上取整到秒
  2. 顺序:先扣当日试用,余下扣余额(与文案"每天另有 3 分钟免费试用"一致)
  3. start 时预检:trial_remaining + balance ≤ 0 → 拒绝并回 QUOTA_EXCEEDED
  4. 识别中每 2s 增量预扣 Redis 计数;若中途扣穿(余额归零)→ 当句允许说完(final 下发),随后回 QUOTA_EXCEEDED,避免说一半被掐断的体验
  5. cancel(上滑取消)仍计量——音频已实际消耗 ASR 资源

5.3 支付入账时序

客户端                 后端                          微信支付
  │ POST /v1/orders     │                              │
  │────────────────────▶│ 创建订单(pending) + 统一下单  │
  │                     │─────────────────────────────▶│
  │ ◀ code_url/pay_params│◀──────────────  prepay  ─────│
  │  (展示二维码/拉起收银台)                            │
  │                     │◀──── POST /v1/pay/notify ────│ 用户支付完成
  │                     │ 验签 → 事务{ 订单 pending→paid │
  │                     │   + ledger 入账 + 余额冗余列 } │
  │ 轮询 GET /orders/:id │ → 应答微信 SUCCESS            │
  │ ◀ paid + 新余额      │                              │

5.4 并发与一致性

六、数据表设计(PostgreSQL)

users             id PK · nickname · avatar_url · balance_seconds int (冗余) · created_at
wechat_identities id PK · user_id FK · openid UNIQUE · unionid INDEX · app_type(web|mobile)
duration_packs    id PK · minutes · price_cents · unit_desc · tag · sort · active   -- 服务端可配
orders            id PK · user_id FK · pack_id FK · price_cents · channel(native|app)
                  · status(pending|paid|closed) · transaction_id UNIQUE NULL · paid_at · created_at
balance_ledger    id PK · user_id FK · delta_seconds (+购买/赠送 −用量) · reason(purchase|usage|gift)
                  · order_id FK NULL · session_id NULL · created_at        -- 只追加
trial_usage       user_id+date PK · used_seconds                           -- 每日试用,自然日即键
asr_sessions      id PK · user_id FK · device_id · audio_seconds · trial_part · balance_part
                  · provider · canceled bool · created_at                  -- 用量明细/排障
devices           id PK · user_id FK · platform(mac|win|ios|android) · last_seen · app_version

-- v1.1 新增
feedbacks         id PK · user_id FK · content text · images jsonb(OSS keys) · diagnostics jsonb
                  · platform · app_version · status(new|triaged|resolved) · created_at
metric_events     id bigserial · user_id FK NULL · device_id · platform · app_version · os_version
                  · event · props jsonb · client_ts · received_at (BRIN) -- 保留 90 天定期清理
metric_daily      date+event+platform PK · count · p50_ms · p95_ms       -- 每日聚合任务写入

废弃 v0.1 的 plans / subscriptions / usage_records(后者并入 asr_sessions)。

完整性校验:任意时刻 users.balance_seconds == SUM(balance_ledger.delta_seconds),对账任务每日核对并告警。

七、Redis Key 设计

Key类型 / TTL用途
quota:{uid}:balancestring(无 TTL,懒加载自 DB)余额秒数实时计数,Lua 原子扣减
quota:{uid}:trial:{yyyymmdd}string / TTL 48h当日试用已用秒数,自然过期即重置
authqr:{state}string(JSON) / TTL 120s扫码登录状态机 pending→confirmed(+JWT)
jwt:block:{jti}string / TTL=token 剩余有效期登出 / 踢出黑名单
rate:{did}:asr:cntZSET 滑动窗口 / 30min设备 30 分钟内会话次数(≤30 次)
rate:{did}:asr:secsZSET 滑动窗口 / 30min设备 30 分钟内累计识别秒数(≤1800s)

八、Go 模块结构

server/
  cmd/server/main.go
  internal/
    auth/        微信 OAuth(扫码 state 机 + OpenSDK code 换取)· JWT 签发/校验/黑名单
    asr/         Provider 接口 + gummy / volcano 实现(StartSession/SendAudio/Results/Close)
    gateway/     WS /v1/asr/stream:会话状态机 · 双泵中继 · 计量 · usage 帧下发
    billing/     packs 查询 · 统一下单(native/app) · 回调验签幂等入账 · 查单对账任务
    quota/       Redis Lua 原子扣减(试用桶→余额桶)· 异步落库 ledger · 一致性对账
    user/        /v1/me 聚合(含 account_state 判定)· devices · app/latest
    feedback/    反馈接收 · 图片校验与 OSS 转存 · 限频 · webhook 通知(v1.1)
    telemetry/   打点批量接收(白名单/截断/异步入库)· 每日聚合任务 · 超标告警(v1.1)
    store/       gorm models + migrations
  pkg/protocol/  WS 帧结构 / 错误码常量 —— 5 端对齐的唯一真相源

九、安全与风控

十、验证方式