上手实践

账户、订单与积分

了解在 Sushi SaaS 模板中 users、orders 与 credits 台账如何协同工作,掌握余额计算、过期处理,以及授予 / 消费 / 查询积分的 API 与服务。

为什么要了解这一部分

模板内置了认证、兼容 Stripe 的订单跟踪以及积分(Credits)余额体系。理解这些表之间的关系,才能更轻松地扩展产品、接入新的支付渠道或调整结算策略。


角色出场(核心表)

  • users — 档案事实表。uuid(跨表主键)、邮箱、语言、角色等。由 Better Auth 写入。
  • accounts / sessions — Better Auth 内部表(登录态)。用于审计,不参与余额计算。
  • orders — 每次结账的尝试/结果。order_nostatusamount、订阅字段 sub_*creditsexpired_at
  • credits — 仅追加的台账。正数=授予/充值;负数=消费。列:trans_nouser_uuidtrans_typecreditsorder_noexpired_at
  • tasks — 使用任务(如 文本→视频)。credits_usedcredits_trans_no 把任务与消费行关联起来。
  • 配角:apikeysaffiliates 同样挂在 user_uuid 上。

关系速查(无外键,但链接稳定):

  • users.uuidorders.user_uuid | credits.user_uuid | tasks.user_uuid | apikeys.user_uuid | affiliates.user_uuid
  • orders.order_nocredits.order_no
  • credits.trans_notasks.credits_trans_no

各表均带时间戳,便于回放历史、做分析。


类比:钱包 + 小票

  • credits 想象成一个透明钱包。每次充值或消费,都会塞一张“小票”(一条记录)。
  • 充值(正数行)让钱变多;消费(负数行)让钱变少。过期的礼品卡?小票还在,但不能再用来付款。
  • 余额 = 未过期正数之和 − 负数之和。

这也是为什么采用“仅追加”的台账:不改历史,只不断加新小票,方便事后解释。


一次动作 → 多条记录(演练)

结账成功(Stripe)

  1. ordersorder_no 对应行变为 paid,保存支付元数据。
  2. credits:新增正数行 { trans_type: "order_pay", credits: <order.credits>, order_no }
  3. affiliates:如开启,写入关联奖励。

执行任务消费(文本→视频)

  1. credits:新增负数行 { trans_type: "task_text_to_video", credits: -N },得到其 trans_no
  2. tasks:新增行 { credits_used: N, credits_trans_no: <该 trans_no> },可追踪“谁在何处消费了多少”。

迷你 ping(1 分)

  1. credits:负数行 { trans_type: "ping", credits: -1 }

过期

  • expired_at 已过去的正数行不再计入余额,但仍保留在历史中用于审计。

常用服务与 API

  • src/services/credit.ts#getUserCreditSummary — 返回 balancegrantedconsumedexpiredexpiringSoon[]ledger[](裁剪版)。
  • src/services/credit.ts#getUserCredits — 适合功能开关的轻量快照 { left_credits, is_pro, is_recharged }
  • src/services/credit.ts#decreaseCredits — 追加负数行;余额不足会抛错。
  • src/services/credit.ts#increaseCredits — 追加正数行;用于 Stripe 入账与系统赠送。
  • src/services/stripe.ts#handleCheckoutSession — 把“支付成功”转成订单状态 + 积分入账。
  • src/services/tasks.ts#createTextToVideoTask — 扣减积分,并创建引用该消费 trans_notasks 行。

HTTP 接口

  • POST /api/account/credits — 积分概览;支持 { includeLedger, ledgerLimit, includeExpiring }
  • POST /api/account/profile — 资料 + 概览;用 { includeCreditLedger: false } 降低负载。
  • POST /api/ping — 消费示例(默认扣 1 分)。

均使用 respJson 响应:{ code, message, data }


本地快速体验

  1. 启动项目:在 .env.local 填好 Postgres 的 DATABASE_URL,运行 pnpm dev

  2. 注册账号:访问 /zh/signup,注册并登录。

  3. 手动增加积分(开发调试):

    insert into credits (trans_no, user_uuid, trans_type, credits, created_at)
    values ('dev-grant-1', '<用户UUID>', 'manual_adjustment', 100, now());

    可使用 pnpm drizzle-kit studio 或任意 SQL 客户端执行。

  4. 查询余额

    curl -X POST http://localhost:3000/api/account/credits \
      -H "Content-Type: application/json" \
      -H "Cookie: <将浏览器中的认证 Cookie 复制过来>"
  5. 模拟消费:向 /api/ping 发送一条消息,会扣除 CreditsAmount.PingCost(默认 1 分)。随后再次查询余额以验证扣款。

    curl -X POST http://localhost:3000/api/ping \
      -H "Content-Type: application/json" \
      -H "Cookie: <将浏览器中的认证 Cookie 复制过来>" \
      -d '{\"message\":\"你好\"}'

如何自定义

调整“即将过期”提示窗口

修改 src/services/credit.ts 中的 EXPIRING_WINDOW_DAYS,即可改变提前提醒的天数。若想自动扣除过期积分,可编写定时任务,在 expired_at 过期后写入相应的负数行。

新用户赠送积分

insertUser 之后调用 src/models/credit.tsinsertCredit,为新账号增加欢迎积分。记得保持 trans_no 唯一,避免重复插入。

接入其他支付方式

  • 使用 insertOrder 写入你的支付回调数据。
  • 同时向 credits 插入对应的正数行,并通过 order_no 关联,方便对账。
  • 如果做订阅,别忘了在界面上展示 sub_* 列记录的周期信息。

存储更多上下文

需要追踪实验或渠道来源时,可以给 orders / credits 加一个 JSON 字段(如 meta)。Drizzle 的迁移工具会自动更新快照。


注意事项与小贴士

  • 余额=简单求和;不会“消耗某一条正数记录”。负数记录通常通过 order_no 指向来源,但历史记录不被修改。
  • getUserValidCreditsexpired_at 排序,便于在消费时“贴近地”指认来源。
  • 若需要严格 FIFO 或逐笔消费,请扩展模型;同时保留仅追加的台账以便审计。
  • 保持 users.uuid 稳定:它是 orders、credits、tasks、keys、affiliates 之间的胶水。

下一步建议

  • 利用 getUserCreditSummary 的统计值构建管理后台图表。
  • /api/ping 为模板,扩展更多按次扣费的功能。
  • 为积分计算逻辑编写单元测试,验证余额、过期和提醒的正确性。
  • 需要其他语言版本?我可以同步更新。