13. 支付基建
会员专享 · 非会员仅可阅读 30% 的正文。
- 发布时间
- August 10, 2025
- 阅读时间
- 9 min read
- 作者
- Felix
- 访问
- 会员专享
非会员仅可阅读 30% 的正文。
支付基建分为三章,第一章章是有关核心业务的支付基建,第二章是关于三方的支付接入,第三章是后台+用户支付融合。
这也是分成两层,当底层的表结构和核心API完成后,上层就仅仅只是扩展第三方的支付方式与兼容当前的支付设计与业务部分。
支付相关的表结构
支付相关有核心的四张表:产品表、订单表、订阅表、交易历史表。
1. 产品表 (products)
产品表存储所有可购买的产品或订阅的信息。
export type ProductType = 'one_time' | 'subscription'
export type Currency = 'USD' | 'CNY' | 'EUR' | 'JPY' | 'GBP'
export type SubscriptionInterval = 'day' | 'week' | 'month' | 'year'
export const products = sqliteTable('products', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text('name').notNull(),
description: text('description'),
type: text('type').$type<ProductType>().notNull(),
price: real('price').notNull(),
currency: text('currency').$type<Currency>().notNull().default('USD'),
interval: text('interval').$type<SubscriptionInterval>(), // 仅用于订阅
tokenAmount: integer('tokenAmount'), // 如果产品提供tokens
active: integer('active', { mode: 'boolean' }).notNull().default(true),
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
})
字段解释:
- name: 产品名称,必填字段
- description: 产品描述
- type: 产品类型,使用
ProductType枚举类型,可以是一次性购买(one_time)或订阅(subscription) - price: 产品价格,使用浮点数存储
- currency: 货币类型,使用
Currency枚举类型,默认为USD - interval: 订阅周期,仅对订阅类型产品有效,可以是天(
day)、周(week)、月(month)或年(year) - tokenAmount: 产品提供的token数量,对于提供token的产品使用
- active: 产品是否激活,布尔值,默认为true
- createdAt: 产品创建时间,自动设置为当前时间戳
- updatedAt: 产品更新时间,自动设置为当前时间戳
2. 订单表 (orders)
订单表记录所有交易信息,包括一次性购买和订阅的初始交易。
export type OrderStatus = 'pending' | 'completed' | 'failed' | 'refunded'
export type PaymentMethod = 'credit_card' | 'paypal' | 'upgrade.chat' | 'other'
export const orders = sqliteTable('orders', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
userId: text('userId')
.notNull()
.references(() => users.id),
productId: text('productId')
.notNull()
.references(() => products.id),
amount: real('amount').notNull(),
currency: text('currency').$type<Currency>().notNull(),
status: text('status').$type<OrderStatus>().notNull().default('pending'),
paymentMethod: text('paymentMethod').$type<PaymentMethod>(),
paymentIntentId: text('paymentIntentId'), // 支付网关的交易ID
metadata: text('metadata'), // 存储JSON格式的额外信息
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
})
字段解释:
- userId: 下单用户的ID,外键关联到users
- productId: 购买的产品ID,外键关联到products表
- amount: 订单金额,使用浮点数存储
- currency: 货币类型,使用
Currency枚举类型 - status: 这是用来跟踪订单的完整生命周期,订单状态,使用
OrderStatus枚举类型,默认为pending,可以是待处理(pending)、已完成(completed)、失败(failed)或已退款(refunded) - paymentMethod: 支付方式,使用
PaymentMethod枚举类型,可以是信用卡(credit_card)、PayPal(paypal)等 - paymentIntentId: 外部支付网关的交易ID,用于与支付服务提供商集成
- metadata: 额外的元数据,以JSON格式存储,可用于存储特定于业务的信息
- createdAt: 订单创建时间,自动设置为当前时间戳
- updatedAt: 订单更新时间,自动设置为当前时间戳
3. 订阅表 (subscriptions)
订阅表记录用户的活跃订阅信息,跟踪订阅的状态和周期。
export type SubscriptionStatus = 'active' | 'canceled' | 'expired' | 'past_due'
export const subscriptions = sqliteTable('subscriptions', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
userId: text('userId')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
productId: text('productId')
.notNull()
.references(() => products.id),
orderId: text('orderId').references(() => orders.id),
status: text('status').$type<SubscriptionStatus>().notNull().default('active'),
currentPeriodStart: integer('current_period_start', { mode: 'timestamp_ms' }).notNull(),
currentPeriodEnd: integer('current_period_end', { mode: 'timestamp_ms' }).notNull(),
cancelAtPeriodEnd: integer('cancel_at_period_end', { mode: 'boolean' }).default(false),
subscriptionId: text('subscriptionId'), // 支付网关的订阅ID
metadata: text('metadata'), // 存储JSON格式的额外信息
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
})
字段解释:
- userId: 订阅用户的ID,外键关联到users表
- productId: 订阅的产品ID,外键关联到products表
- orderId: 创建此订阅的初始订单ID,外键关联到orders表
- status: 订阅状态,使用
SubscriptionStatus枚举类型,默认为active,可以是活跃(active)、已取消(canceled)、已过期(expired)或逾期未付(past_due) - currentPeriodStart: 当前订阅周期的开始时间,时间戳格式
- currentPeriodEnd: 当前订阅周期的结束时间,时间戳格式
- cancelAtPeriodEnd: 是否在当前周期结束时取消订阅,布尔值,默认为false
- subscriptionId: 外部支付网关的订阅ID,用于与支付服务提供商集成
- metadata: 额外的元数据,以JSON格式存储
- createdAt: 订阅创建时间,自动设置为当前时间戳
- updatedAt: 订阅更新时间,自动设置为当前时间戳
4. 交易历史表 (transactions)
交易历史表记录所有token余额的变动,包括购买、使用、退款等操作。
export const transactions = sqliteTable('transactions', {
id: text('id')
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
userId: text('userId')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
orderId: text('orderId').references(() => orders.id),
type: text('type').$type<TransactionType>().notNull(),
amount: integer('amount').notNull(), // token数量,可以是正数(增加)或负数(消费)
createdAt: integer('created_at', { mode: 'timestamp_ms' })
.default(sql`CURRENT_TIMESTAMP`)
.notNull()
})
字段解释:
- userId: 交易关联的用户ID,外键关联到users表,设置了级联删除
- orderId: 关联的订单ID(如果适用),外键关联到orders表
- type: 交易类型,使用
TransactionType枚举类型,可以是购买(purchase)、使用(usage)、退款(refund)、订阅续费(subscription_renewal)、赠送(gift)或促销(promotion) - amount: token数量变化,整数类型,正数表示增加,负数表示消费
- createdAt: 交易创建时间,自动设置为当前时间戳
表之间的关系与数据流
我们来梳理一下所有表之间的数据流向和业务逻辑
1. 用户购买产品:
- 创建
orders表记录,状态为pending - 支付成功后,更新
orders表记录状态为completed - 创建
transactions表记录,类型为purchase,金额为正数 - 更新
userUsage表,增加totalTokens
数据流向:
products → orders → transactions → userUsage
业务逻辑: 用户选择一次性购买产品后,系统首先创建订单记录,然后处理支付。支付成功后,系统更新订单状态,并创建一条交易记录来记录代币的增加,同时更新用户的代币总量。这样用户就可以使用这些代币来访问系统的功能。
2. 用户订阅产品:
- 创建
orders表记录,状态为pending - 支付成功后,更新
orders表记录状态为completed - 创建
subscriptions表记录,设置周期起止时间 - 创建
transactions表记录,类型为purchase,金额为正数 - 更新
userUsage表,增加totalTokens
数据流向:
products → orders → subscriptions → transactions → userUsage
订单表记录的是财务关系,订阅表记录的是持续服务关系,一个订阅可能关联多个订单,这也是需要拉2个表的原因。在系统设计上要明确的区别
支付关系和服务关系。
业务逻辑: 用户选择订阅产品后,系统首先创建订单记录,然后处理首次支付。支付成功后,系统更新订单状态,创建订阅记录(包含当前周期的开始和结束时间),并创建一条交易记录来记录代币的增加,同时更新用户的代币总量。订阅记录会跟踪服务的有效期,而订单记录则跟踪财务交易。
3. 订阅续费:
这里很烦的是有两种情况,一种情况你的订阅是完全依赖上游支付厂商的通知来订阅的,另外一种情况是要需要你手动请求后续周期的订阅。
- 检查
subscriptions表中即将到期的记录 - 处理续费支付(手动发起/或者被通知),创建新的
orders表记录 - 更新
subscriptions表中的周期信息 - 创建新的
transactions表记录,类型为subscription_renewal,金额为正数 - 更新
userUsage表,增加totalTokens
数据流向:
subscriptions → orders → transactions → userUsage
业务逻辑: 当订阅周期即将结束时,系统会自动处理续费。续费成功后,系统会创建新的订单记录(记录这次财务交易),更新订阅的周期信息(延长服务期限),创建一条交易记录来记录代币的增加,并更新用户的代币总量。这样用户可以继续享受订阅服务并获得新的代币。
4. 用户使用token:
- 检查
userUsage表确认用户有足够的可用代币 - 创建
transactions表记录,类型为usage,金额为负数 - 更新
userUsage表,增加usedTokens
数据流向:
userUsage → transactions → userUsage
订阅后解锁完整文章
支持创作、解锁全文,未来更新也会第一时间送达。
评论
加入讨论
还没有评论,来占个沙发吧。