哪种开源电商解决方案适合订阅业务?
结论
如果网站已经基于 Drupal,建议优先考虑 Drupal Commerce + Commerce Recurring。这套方案可以直接沿用现有的用户、权限和内容体系,也方便通过 Drupal 事件系统、REST 接口或 JSON
对接 provisioning system。具体可以采用以下组合:
- Drupal Commerce:管理商品、订单、支付、税费和多币种
- Commerce Recurring:管理订阅周期、续费和周期性订单
- Commerce Invoice:管理发票实体和发票生成
- 支付网关模块:完成支付方式 tokenization 和后续自动扣款
- Drupal Tax / VAT 相关扩展:根据客户所在地、VAT 号码和税区计算税费
- 自定义事件订阅器或队列任务:将开通、续订、暂停和取消同步到 provisioning system
如果计费规则很复杂,包括按用量计费、分段价格、余额账户、账单调整或大量补扣款,可以将 Kill Bill 作为独立的订阅计费核心,让 Drupal Commerce 负责商店和客户入口。这样通常比把所有计费规则都放进 Drupal 更稳妥。
为什么优先选择 Drupal Commerce
网站已经在使用 Drupal,继续采用 Drupal Commerce,就不必额外维护一套用户、权限和内容系统。客户登录、产品页面、购买流程以及售后入口都可以留在同一个平台。
它也更适合通过应用层完成集成:
- 通过事件订阅器处理订单、付款和状态变更。
- 使用 Drupal REST、JSON 或自定义 API 提供外部接口。
- 将耗时的开通操作放入 Queue API,避免支付回调超时。
- 使用自定义字段保存 provisioning ID、订阅计划编号和同步状态。
不过,Drupal Commerce 本身不是完整的 SaaS billing platform。自动续费、发票和 VAT 通常需要多个模块配合,并加入少量定制开发。只安装核心模块并不能完成全部功能。
推荐的业务模型
不要把“订阅”“付款”和“服务实例”合并成同一个对象。更合适的关系是:
Customer
└── Subscription
├── Recurring Order
│ └── Payment
├── Invoice
└── Provisioned Service
每个对象承担不同职责:
Subscription:记录套餐、周期、开始时间、下次续费时间和状态Order:记录某个账期的商品、税费、折扣和应付总额Payment:记录支付授权、扣款结果、退款和网关交易编号Invoice:保存具有固定编号的账单凭证和税务信息Provisioned Service:表示 SaaS tenant、license 或支持服务权益
按照这种方式设计,即使付款失败,系统仍能保留发票和订阅记录。以后更换支付网关时,provisioning 数据也不会受到影响。
实施步骤
1. 先验证支付网关的续费能力
自动续费要求支付网关能够保存支付方式,并允许商户在后续账期主动发起扣款。选型时至少要确认:
- 是否支持 tokenized payment method
- 是否允许 off-session 或 merchant-initiated payment
- 是否支持所需的国家、币种和支付方式
- 欧洲交易是否支持相应的强客户认证流程
- webhook 能否报告支付成功、失败、退款和争议
- Drupal 对应的网关模块是否真正支持 recurring payment
能够完成一次付款,不代表能够自动续费。不同网关模块对 Commerce Recurring 的支持也有差异,需要针对计划使用的 Drupal 版本和支付网关实际验证。
2. 建立订阅产品
为支持服务和 SaaS 套餐分别建立商品类型,并明确以下规则:
- 计费周期,例如月付或年付
- 首次收费时间
- 是否提供试用期
- 升级和降级的生效时间
- 周期中变更是否按比例计费
- 取消后立即停用,还是到期后停用
- 续费失败后的宽限期
建议把 provisioning 使用的 plan code 保存为商品变体字段,例如:
support_standard_yearly
saas_team_monthly
saas_enterprise_yearly
不要使用商品标题做系统映射。标题可能被翻译,也可能由运营人员修改。
3. 配置税务判断
“欧洲含 VAT、其他地区不含 VAT”只能作为业务概括。实际配置时,至少要考虑:
- 客户是个人还是企业
- 客户所在的国家或地区
- 商品属于 SaaS、电子服务还是人工支持服务
- 企业是否提供有效的 VAT ID
- 是否适用 reverse charge
- 应根据账单地址、服务地还是其他证据判断所在地
- 发票上需要显示哪些税务字段
因此,不适合简单设置一条“欧盟国家统一加 VAT”的固定规则。税率、申报方式和证据要求可能变化,应先由税务人员确认规则,再映射到 Commerce 的 tax type 和 tax zone。
系统还应保存 VAT ID 的验证结果、验证时间和验证时使用的客户地址,避免以后只能看到客户修改后的资料。
4. 生成不可变的发票快照
不能在用户打开发票页面时,根据当前订单重新计算发票。生成发票时,应固定保存以下信息:
- 发票号码
- 开票日期和币种
- 买卖双方的名称和地址
- VAT ID
- 商品描述、数量和单价
- 税率、税额和总额
- reverse charge 等必要说明
- 对应的订单和支付记录
- 作废、退款或贷项凭证关系
正式开票后,不应直接覆盖原发票。如果金额发生变化,应按照适用的会计规则生成更正记录或 credit note。
5. 通过事件和 API 对接 provisioning system
可以在订单付款成功、订阅取消或续费失败时触发同步。不建议让 provisioning system 直接查询 Commerce 数据库。
下面是一个简化的 Drupal 事件订阅器示例。订单付款完成后,它会向内部 provisioning API 发送消息:
<?php
namespace Drupal\saas_provisioning\EventSubscriber;
use Drupal\commerce_order\Event\OrderEvent;
use Drupal\commerce_order\OrderEvents;
use GuzzleHttp\ClientInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class PaidOrderSubscriber implements EventSubscriberInterface {
public function __construct(
private readonly ClientInterface $httpClient,
) {}
public static function getSubscribedEvents(): array {
return [
OrderEvents::ORDER_PAID => 'onOrderPaid',
];
}
public function onOrderPaid(OrderEvent $event): void {
$order = $event->getOrder();
$items = [];
foreach ($order->getItems() as $orderItem) {
$purchasedEntity = $orderItem->getPurchasedEntity();
$items[] = [
'sku' => $purchasedEntity?->getSku(),
'quantity' => (string) $orderItem->getQuantity(),
];
}
$this->httpClient->request('POST', 'https://provisioning.internal/v1/orders', [
'headers' => [
'Idempotency-Key' => 'commerce-order-' . $order->id(),
'Accept' => 'application/json',
],
'json' => [
'order_id' => (string) $order->id(),
'order_uuid' => $order->uuid(),
'customer_id' => (string) $order->getCustomerId(),
'items' => $items,
],
'timeout' => 10,
]);
}
}
对应的服务定义可以写成:
services:
saas_provisioning.paid_order_subscriber:
class: Drupal\saas_provisioning\EventSubscriber\PaidOrderSubscriber
arguments:
- '@http_client'
tags:
- { name: event_subscriber }
在生产环境中,不建议在支付事件里同步等待 provisioning 完成。更可靠的处理流程是:
付款成功
→ 写入 Drupal 队列或 outbox
→ 后台 worker 调用 provisioning API
→ provisioning 返回实例 ID
→ 保存同步结果
→ 失败时按策略重试
接口需要支持幂等性。支付网关、消息队列和定时任务都可能重复投递消息,同一个订单即使被处理两次,也不应创建两个 SaaS tenant。
例如:
POST /v1/subscriptions/provision
Idempotency-Key: commerce-order-18472
Content-Type: application/json
{
"customer_id": "9281",
"subscription_id": "4135",
"plan_code": "saas_team_monthly",
"effective_at": "2026-09-01T00:00:00Z"
}
其他值得评估的方案
Kill Bill
Kill Bill 更侧重订阅计费和支付编排,适合复杂账期、插件化支付、账户余额以及用量计费等场景。Drupal 可以继续负责商品展示和客户门户,订阅账单则交给 Kill Bill 管理。
这种方案的系统结构、部署和运维会复杂很多,而且 Drupal 与 Kill Bill 之间通常需要自行开发集成层。
Medusa
Medusa 适合采用 headless commerce 和 JavaScript/TypeScript 技术栈的团队。它有比较清晰的服务端扩展方式和 API,但订阅、税务发票以及完整的 SaaS billing 流程通常仍需要额外模块或定制,不能视为默认具备的现成功能。
如果继续保留 Drupal 作为内容平台,就需要维护 Drupal 与独立 commerce backend 之间的身份、购物车和订单集成。
Solidus
Solidus 有成熟的订单、支付和扩展机制,数据模型也比较清晰,适合拥有 Ruby 团队并愿意维护独立商店的情况。对于订阅计费、欧洲 VAT 和发票功能,仍需逐项验证相关扩展的维护状态。
Solidus 与 Drupal 不共用运行时,因此集成成本高于 Drupal Commerce。
不建议直接集成数据库
即使某个平台的数据表很容易理解,也不应让 provisioning system 直接写入订单、付款或订阅表。电商系统通常还依赖以下机制:
- 状态机转换
- 实体保存钩子
- 税额重新计算
- 付款状态聚合
- 缓存失效
- 审计日志
- 后台任务
绕过应用层直接写数据库,容易出现“表中已有记录,但业务事件没有发生”的不一致状态。数据库可以用于只读报表或故障排查,正式集成则应优先使用 API、事件、消息队列或 outbox。
最终建议
如果订阅主要采用固定月费或年费,建议选择 Drupal Commerce + Commerce Recurring + Commerce Invoice + 合适的支付网关模块,再通过事件队列连接 provisioning system。这通常是成本较低,也最符合现有架构的方案。
正式开发前,可以先完成一个最小验证项目,完整测试以下流程:
- 欧洲个人客户购买并计算 VAT。
- 有效 VAT ID 的企业客户购买。
- 非欧洲客户购买。
- 首次付款并自动开通服务。
- 自动续费并生成新账期发票。
- 续费失败、重试和宽限期处理。
- 套餐升级、降级和取消。
- 退款、发票更正和服务回收。
- 重复 webhook 或重复队列消息的幂等处理。
如果这些场景需要大幅修改 Commerce Recurring 的核心行为,或者还要加入复杂的用量计费,应尽早评估 Kill Bill 等专用 billing engine,避免继续扩大 Drupal 内部的定制范围。
备注:内容仅供参考。
