账户、订单与积分
了解在 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_uuid
orders.order_no
→credits.order_no
credits.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
为模板,扩展更多按次扣费的功能。 - 为积分计算逻辑编写单元测试,验证余额、过期和提醒的正确性。
- 需要其他语言版本?我可以同步更新。