# Data Model And State ## 真相源原则 这套服务端最关键的状态归属如下: - `Postgres` - 用户认证数据 - 角色、聊天、Provider 配置 - Flux 余额与账本 - Stripe 业务镜像 - LLM 请求日志 - `Redis` - Flux 余额缓存 - 服务配置 KV - 聊天跨实例广播 (Pub/Sub) - Sub-Flux 计量债务账本(TTS 字符等,详见 `flux-meter.md`) - TTS voices 上游响应缓存 如果要判断”改哪个地方才算真的改成功”,大多数场景答案都是 Postgres。Redis Streams 已全部移除,没有”计费事件队列”这层抽象。 ## 主要表分组 ### 认证 - `user` - `session` - `account` - `verification` 来源文件: - `src/schemas/accounts.ts` 说明: - `better-auth` 直接用这组表 - 由 `pnpm -F @proj-airi/server auth:generate` 自动产物,手改会被覆盖 ### 角色与用户交互 - `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` 说明: - 运行时查询时会把系统配置和用户配置拼接成一个结果集 - `config` 是 `jsonb` ### Flux / 账本 - `user_flux` - `flux_transaction` 来源文件: - `src/schemas/flux.ts` - `src/schemas/flux-transaction.ts` 职责边界: - `user_flux` - 当前余额快照(单行/用户) - `flux_transaction` - append-only 账本流水(type: credit / debit / initial / promo) - 同时承担系统真相源和用户可见历史,`/api/v1/flux/history` 直接读这张表 关键约束: - `flux_transaction` 对 `(userId, requestId) WHERE requestId IS NOT NULL` 有部分唯一索引 - 用来做扣费 / 充值幂等(含 admin promo grant 的 `idempotencyKey`) ### 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:` - value: 字符串化整数 写入来源: - `fluxService.getFlux()` cache miss 后回填 - `billingService` 余额事务提交后 best-effort `redis.set` 直接更新(API 进程内同步) ### 配置 KV - key: `config:` 由 `config-kv.ts` 管理,支持: - 数值 - 字符串 - `FLUX_PACKAGES` JSON ### 聊天跨实例广播 - channel: `chat:broadcast:` ## 幂等与并发控制 ### 余额并发 `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)` 唯一约束 ## 现有代码中的结构信号 - `src/schemas/flux-grant-batch.ts` 是已废弃的旧 admin batch 设计 schema,没有被 `app.ts` 装配也没有 migration 在用,是 dead code,改这块前直接删除。当前 admin 发 FLUX 走 `/api/admin/flux-grants` 同步路径,不写新表。