账户、订单与积分
了解在 Sushi SaaS 模板中 users、orders 与 credits 台账如何协同工作,掌握余额计算、过期处理,以及授予 / 消费 / 查询积分的 API 与服务。
为什么要了解这一部分
模板内置了认证、兼容 Stripe 的订单跟踪以及积分(Credits)余额体系。理解这些表之间的关系,才能更轻松地扩展产品、接入新的支付渠道或调整结算策略。
角色出场(核心表)
users— 档案事实表。uuid(跨表主键)、邮箱、语言、角色等。由 Better Auth 写入。accounts/sessions— Better Auth 内部表(登录态)。用于审计,不参与余额计算。orders— 每次结账的尝试/结果。order_no、status、amount、订阅字段sub_*、credits、expired_at。credits— 仅追加的台账。正数=授予/充值;负数=消费。列:trans_no、user_uuid、trans_type、credits、order_no、expired_at。tasks— 使用任务(如 文本→视频)。credits_used、credits_trans_no把任务与消费行关联起来。- 配角:
apikeys、affiliates同样挂在user_uuid上。
关系速查(无外键,但链接稳定):
users.uuid→orders.user_uuid|credits.user_uuid|tasks.user_uuid|apikeys.user_uuid|affiliates.user_uuidorders.order_no→credits.order_nocredits.trans_no→tasks.credits_trans_no
各表均带时间戳,便于回放历史、做分析。
类比:钱包 + 小票
- 把
credits想象成一个透明钱包。每次充值或消费,都会塞一张“小票”(一条记录)。 - 充值(正数行)让钱变多;消费(负数行)让钱变少。过期的礼品卡?小票还在,但不能再用来付款。
- 余额 = 未过期正数之和 − 负数之和。
这也是为什么采用“仅追加”的台账:不改历史,只不断加新小票,方便事后解释。
一次动作 → 多条记录(演练)
结账成功(Stripe)
orders:order_no对应行变为paid,保存支付元数据。credits:新增正数行{ trans_type: "order_pay", credits: <order.credits>, order_no }。affiliates:如开启,写入关联奖励。
执行任务消费(文本→视频)
credits:新增负数行{ trans_type: "task_text_to_video", credits: -N },得到其trans_no。tasks:新增行{ credits_used: N, credits_trans_no: <该 trans_no> },可追踪“谁在何处消费了多少”。
迷你 ping(1 分)
credits:负数行{ trans_type: "ping", credits: -1 }。
过期
expired_at已过去的正数行不再计入余额,但仍保留在历史中用于审计。
常用服务与 API
src/services/credit.ts#getUserCreditSummary— 返回balance、granted、consumed、expired、expiringSoon[]、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_no的tasks行。
HTTP 接口
POST /api/account/credits— 积分概览;支持{ includeLedger, ledgerLimit, includeExpiring }。POST /api/account/profile— 资料 + 概览;用{ includeCreditLedger: false }降低负载。POST /api/ping— 消费示例(默认扣 1 分)。
均使用 respJson 响应:{ code, message, data }。
本地快速体验
-
启动项目:在
.env.local填好 Postgres 的DATABASE_URL,运行pnpm dev。 -
注册账号:访问
/zh/signup,注册并登录。 -
手动增加积分(开发调试):
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 客户端执行。 -
查询余额:
curl -X POST http://localhost:3000/api/account/credits \ -H "Content-Type: application/json" \ -H "Cookie: <将浏览器中的认证 Cookie 复制过来>" -
模拟消费:向
/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.ts 的 insertCredit,为新账号增加欢迎积分。记得保持 trans_no 唯一,避免重复插入。
接入其他支付方式
- 使用
insertOrder写入你的支付回调数据。 - 同时向
credits插入对应的正数行,并通过order_no关联,方便对账。 - 如果做订阅,别忘了在界面上展示
sub_*列记录的周期信息。
存储更多上下文
需要追踪实验或渠道来源时,可以给 orders / credits 加一个 JSON 字段(如 meta)。Drizzle 的迁移工具会自动更新快照。
注意事项与小贴士
- 余额=简单求和;不会“消耗某一条正数记录”。负数记录通常通过
order_no指向来源,但历史记录不被修改。 getUserValidCredits以expired_at排序,便于在消费时“贴近地”指认来源。- 若需要严格 FIFO 或逐笔消费,请扩展模型;同时保留仅追加的台账以便审计。
- 保持
users.uuid稳定:它是 orders、credits、tasks、keys、affiliates 之间的胶水。
下一步建议
- 利用
getUserCreditSummary的统计值构建管理后台图表。 - 以
/api/ping为模板,扩展更多按次扣费的功能。 - 为积分计算逻辑编写单元测试,验证余额、过期和提醒的正确性。
- 需要其他语言版本?我可以同步更新。