订阅与循环计费 Rails app 的 models 关联设计
结论
建议把 Subscription 作为 User 与 Plan 之间的关联模型:
- 一个
User可以有多个Subscription - 一个
Plan可以被多个Subscription引用 - 每个
Subscription只属于一个User和一个Plan - 一个
Subscription可以对应一笔或多笔付款记录Transaction
所以,Subscription 不应设置 has_many :plans,User 也不能使用 belongs_to :plan, through: :subscription。如果需要从用户直接访问其订阅过的套餐,应使用 has_many :plans, through: :subscriptions。
class User < ApplicationRecord
has_many :subscriptions
has_many :plans, through: :subscriptions
end
class Plan < ApplicationRecord
has_many :subscriptions
has_many :users, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :user
belongs_to :plan
has_many :transactions
end
class Transaction < ApplicationRecord
belongs_to :subscription
end
为什么这样设计
Plan 表示套餐模板,其中可以包含套餐名称、价格、服务额度和计费周期。用户购买套餐时,通常不需要为该用户新建一条 Plan 记录。
Subscription 表示某个用户在某段时间内订阅了某个套餐。它保存一次具体订阅关系的信息,例如:
user_idplan_idstarts_atends_atstatus- 当时实际采用的价格
因此,一个 Subscription 应当指向一个 Plan:
class Subscription < ApplicationRecord
belongs_to :plan
end
不应写成:
has_many :plans
只有一次订阅可以同时包含多个套餐时,Subscription 才需要通过额外的明细表关联多个 Plan。普通的套餐订阅系统通常不需要这样设计。
用户与套餐的访问方式
用户和套餐之间是多对多关系,这层关系由 Subscription 承载:
class User < ApplicationRecord
has_many :subscriptions
has_many :plans, through: :subscriptions
end
定义后可以执行:
user.subscriptions
user.plans
plan.subscriptions
plan.users
下面这种写法不正确:
belongs_to :plan, through: :subscription
belongs_to 表示当前表保存了目标记录的外键,例如 subscriptions.plan_id。它不能用来表达这种通过中间集合访问目标记录的关系。
如果业务需要取得用户当前使用的套餐,可以先定义“当前订阅”,再通过它取得套餐。例如:
class User < ApplicationRecord
has_many :subscriptions
has_one :current_subscription,
-> { where(status: "active").order(starts_at: :desc) },
class_name: "Subscription"
has_one :current_plan,
through: :current_subscription,
source: :plan
end
不过,业务必须明确保证“当前订阅”的判定规则。只依赖排序,无法阻止同一个用户同时存在多条 active 记录。
Transaction 应该如何关联
一次订阅续费可能涉及多次支付尝试、退款或补扣,这种情况下适合使用一对多关系:
class Subscription < ApplicationRecord
belongs_to :user
belongs_to :plan
has_many :transactions
end
class Transaction < ApplicationRecord
belongs_to :subscription
end
这样,一条订阅记录可以关联:
- 支付成功记录
- 支付失败记录
- 重试记录
- 退款记录
查询方式如下:
subscription.transactions
transaction.subscription
transaction.subscription.user
transaction.subscription.plan
如果业务能严格保证每个订阅周期只有一笔付款,也可以使用一对一关系:
class Subscription < ApplicationRecord
has_one :transaction
end
class Transaction < ApplicationRecord
belongs_to :subscription
end
实际支付流程通常会出现失败重试或退款,所以 has_many :transactions 一般更合适。
推荐的表结构
如果业务采用“每个月生成一条订阅或续费记录”的方式,可以使用类似的结构:
create_table :subscriptions do |t|
t.references :user, null: false, foreign_key: true
t.references :plan, null: false, foreign_key: true
t.string :status, null: false
t.datetime :starts_at, null: false
t.datetime :ends_at
t.integer :price_cents, null: false
t.string :currency, null: false
t.timestamps
end
create_table :transactions do |t|
t.references :subscription, null: false, foreign_key: true
t.string :status, null: false
t.integer :amount_cents, null: false
t.string :currency, null: false
t.string :provider_reference
t.timestamps
end
即使 Plan 已经保存了价格,也建议在 Subscription 或 Transaction 中记录成交时的金额。套餐价格以后可能调整,历史付款记录不应随之变化。
更常见的另一种建模方式
如果用户的订阅关系长期有效,只是每个月扣一次款,更常见的设计是:
Subscription表示持续的订阅合同- 每次月度扣费生成一条
Transaction - 不要每个月新建一条
Subscription
关联仍然是:
class User < ApplicationRecord
has_many :subscriptions
end
class Plan < ApplicationRecord
has_many :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :user
belongs_to :plan
has_many :transactions
end
class Transaction < ApplicationRecord
belongs_to :subscription
end
例如,用户订阅一年时只保留一条 Subscription,每个月新增一条 Transaction。取消、暂停、恢复和更换套餐都围绕这条订阅记录处理。
如果业务确实将每个月视为一次独立续订,也可以每月创建一条 Subscription。不过,最好将它命名为 SubscriptionPeriod、Renewal 或 BillingCycle,避免同一个模型同时表示“长期订阅”和“单月账期”。
数据完整性注意事项
Rails 关联不能代替数据库约束。至少应为外键添加索引和外键约束:
add_index :subscriptions, :user_id
add_index :subscriptions, :plan_id
add_index :transactions, :subscription_id
如果每个用户在任意时刻只能有一个有效订阅,还需要同时在业务逻辑和数据库层进行约束。直接添加:
validates :user_id, uniqueness: true
通常过于严格,因为这也会禁止用户保留历史订阅记录。具体方案取决于数据库类型,以及 status 和起止时间的定义。
此外,Transaction 虽然可以作为 Rails model 名称,但容易与 Active Record 中的数据库事务概念混淆。如果它表示支付记录,使用 Payment、Charge 或 PaymentTransaction 通常更容易理解。
整体关系可以概括为:
User 1 ─── * Subscription * ─── 1 Plan
|
1
|
*
Transaction
Plan 定义卖什么,Subscription 记录谁订了什么,Transaction 记录实际发生的付款。
备注:内容仅供参考。
