Files
moeka-project/apps/server/docs/ai-context/data-model-and-state.md
T
RainbowBird f8d1fa7a64 refactor(server): drop redis stream + worker role (#1792)
The Redis Stream `billing-events` + `worker` Railway role +
advisory-lock poller layered together didn't actually buy us reliability
— `debitFlux` swallowed XADD failures, leaving the door open to "balance
updated, ledger row never written". Collapse the whole thing back to:
`creditFlux` and `debitFlux` write `flux_transaction` ledger rows inline
within the same DB transaction that mutates `user_flux`, and `(user_id,
request_id)` remains the partial unique index that keeps retries safe.

Concrete changes:
- Inline ledger inserts in `BillingService.{debitFlux, creditFlux,
creditFluxFromStripeCheckout, creditFluxFromInvoice}`; drop `billingMq`
and `publishEvent` plumbing entirely.
- `routes/openai/v1` writes `llm_request_log` synchronously via the
existing `requestLogService`; the duplicate `llm-request-log.ts` service
module is removed.
- `bin/run-worker.ts`, `libs/mq/*`,
`services/billing/billing-events.ts`,
`services/billing/billing-consumer-handler.ts`, and matching tests are
deleted. CLI now exposes only `api`.
- `BILLING_EVENTS_*` env vars and the `DEFAULT_BILLING_EVENTS_STREAM`
helper are dropped; `docker-compose.yml` no longer ships a worker
service.
- `docs/ai-context/{workers-and-runtime, billing-architecture,
redis-boundaries-and-pubsub, data-model-and-state,
architecture-overview, README}.md`, `CLAUDE.md`, and the existing
verification docs are updated to describe the single-process synchronous
pipeline.

Tests: 29 files / 247 cases pass. Production deployments need to drop
the worker Railway service after this lands.
2026-05-08 21:14:01 +08:00

4.5 KiB

Data Model And State

真相源原则

这套服务端最关键的状态归属如下:

  • Postgres
    • 用户认证数据
    • 角色、聊天、Provider 配置
    • Flux 余额与账本
    • Stripe 业务镜像
    • LLM 请求日志
  • Redis
    • Flux 余额缓存
    • 服务配置 KV
    • 聊天跨实例广播
    • 计费事件队列

如果要判断“改哪个地方才算真的改成功”,大多数场景答案都是 Postgres。

主要表分组

认证

  • user
  • session
  • account
  • verification

来源文件:

  • src/schemas/accounts.ts

说明:

  • better-auth 直接用这组表
  • src/schemas/auth.ts 基本是重复副本,目前不是主要依赖入口

角色与用户交互

  • characters
  • character_covers
  • avatar_model
  • character_capabilities
  • character_i18n
  • character_prompts
  • user_character_likes
  • user_character_bookmarks

来源文件:

  • src/schemas/characters.ts
  • src/schemas/user-character.ts

说明:

  • 角色实体采用软删除
  • 点赞与收藏通过中间表建模
  • 计数值冗余保存在 characters 表上

聊天

  • chats
  • chat_members
  • messages
  • media
  • stickers
  • sticker_packs

来源文件:

  • src/schemas/chats.ts

说明:

  • messages.seq 是会话内顺序字段
  • 写消息时通过 SELECT ... FOR UPDATE 锁 chat 以串行生成 seq
  • senderId 是宽松字段,不强制外键

Provider 配置

  • user_provider_configs
  • system_provider_configs

来源文件:

  • src/schemas/providers.ts

说明:

  • 运行时查询时会把系统配置和用户配置拼接成一个结果集
  • configjsonb

Flux / 账本 / 审计

  • user_flux
  • flux_transaction
  • flux_transaction

来源文件:

  • src/schemas/flux.ts
  • src/schemas/flux-transaction.ts
  • src/schemas/flux-transaction.ts

职责边界:

  • user_flux
    • 当前余额快照
  • flux_transaction
    • append-only 账本流水
    • 偏系统真相源
  • flux_transaction
    • 用户可见历史
    • 偏产品展示

关键约束:

  • flux_transaction(userId, requestId) 有部分唯一索引
  • 用来做扣费 / 充值幂等

Stripe 业务镜像

  • stripe_customer
  • stripe_checkout_session
  • stripe_subscription
  • stripe_invoice

来源文件:

  • src/schemas/stripe.ts

说明:

  • 这些表是 Stripe 状态的本地镜像
  • 真正的余额变化仍由 billingService 写入 user_flux + flux_transaction
  • fluxCredited 字段用于避免重复入账

LLM 请求日志

  • llm_request_log

来源文件:

  • src/schemas/llm-request-log.ts

说明:

  • 只做追加写入
  • 明确不加 user 外键,以避免高并发写入的额外约束成本

服务与状态写入边界

createFluxService()

负责:

  • 余额读取
  • 新用户首次读取时初始化 user_flux
  • Redis cache-aside

不负责:

  • 扣费
  • 充值
  • transaction 写入

createBillingService()

负责:

  • 所有余额写操作
  • DB 事务
  • debitFlux / credit 方法:事务内 lock → check → update user_flux → insert flux_transaction ledger
  • 事务提交后 best-effort redis.set 更新 Flux 余额缓存

这是所有 Flux 写路径应收敛到的中心。

createStripeService()

负责:

  • Stripe 实体 upsert

不负责:

  • 最终 Flux 入账

真正入账通过 billingService.creditFluxFromStripeCheckout() 或相关 credit 方法完成。

Redis 中的数据类型

Flux 缓存

  • key: flux:<userId>
  • value: 字符串化整数

写入来源:

  • fluxService.getFlux() cache miss 后回填
  • billingService 余额事务提交后 best-effort redis.set 直接更新(API 进程内同步)

配置 KV

  • key: config:<CONFIG_NAME>

config-kv.ts 管理,支持:

  • 数值
  • 字符串
  • FLUX_PACKAGES JSON

聊天跨实例广播

  • channel: chat:broadcast:<userId>

幂等与并发控制

余额并发

billingService 在事务中:

  1. (可选)按 (userId, requestId) 命中 ledger → 命中即返回,跳过余下步骤
  2. SELECT user_flux FOR UPDATE
  3. 计算新余额
  4. user_flux + 写 flux_transaction ledger
  5. 事务提交后 best-effort redis.set

这保证同一用户余额更新是串行化的,并且 ledger 行与余额变更在同一原子提交里。

Stripe 幂等

主要依赖:

  • stripe_checkout_session.fluxCredited
  • stripe_invoice.fluxCredited
  • flux_transaction(userId, requestId) 唯一约束

现有代码中的结构信号

  • accounts.tsauth.ts 是重复 schema,后续如果做整理,应先统一真实使用入口再删副本。