Skip to content
bitzorcas
中EN

Concept

Billing 模块架构概览:多租户订阅计费与资损防范

深入解析 BitzOrcas.Modern 订阅计费与资损控制体系,掌握 Subscription 订阅聚合、原子配额扣减(Quota)、发票生成与支付回调安全防重。

Last updated

在商业化 SaaS 系统的技术实现中,计费与支付(Billing & Subscriptions)是容错率最低的核心模块:

  • 并发超卖与超额(Concurrency Quota Breach):多个并发请求同时调用大模型或创建工单,若配额扣减缺乏原子锁,会导致客户透支额度造成严重的平台资损;
  • 支付回调重复与乱序(Payment Webhook Replay):微信/支付宝/Stripe 网关由于网络超时会重试多次回调,若无严格幂等会导致多次充值或状态混乱;
  • 订阅周期状态机(Subscription Lifecycle):试用期、自动续费、逾期宽限(Dunning)、降级与停服流转必须严格闭环。

BitzOrcas.Modern Billing 模块通过“原子互斥锁 + 状态机硬流转 + 幂等去重发件箱”构建了金融级计费安全防线。

订阅计费与支付全生命周期拓扑

用户完成支付

1. 租户客户端 (发起订阅/升级套餐)

2. CreateSubscriptionCommand (带 Idempotency-Key)

3. 垂直切片 Handler

4. SubscriptionAggregate (初始化待支付订阅事实)

5. IPaymentGateway (生成预支付链接/二维码)

6. PaymentWebhookController (签名验证 + 幂等去重)

7. FulfillSubscriptionCommand (激活订阅 + 刷新 Quota 配额池)

8. CAP 领域事件 (billing.subscription_activated)


第一步:订阅聚合根与状态机流转

SubscriptionAggregate.cs: 订阅聚合根
using System;
using System.ComponentModel;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Billing.Domain;
public static class SubscriptionErrors
{
public static readonly Error InvalidState =
Error.Conflict("Billing.InvalidState", "当前订阅状态无法激活。");
public static readonly Error NotFound =
Error.NotFound("Billing.SubscriptionNotFound", "订阅未找到。");
}
[BitzTable("BilSubscription", IsTenant = true, IsSoftDelete = true, Description = "租户企业订阅实例表")]
public sealed class SubscriptionAggregate : TenantAggregateRoot<string>
{
[BitzColumn(Length = 64, IsRequired = true)]
public string PlanCode { get; private set; } = string.Empty;
[BitzColumn(IsRequired = true)]
public SubscriptionStatus Status { get; private set; } = SubscriptionStatus.PendingPayment;
// 订阅有效时间窗口
[BitzColumn(IsRequired = true)]
public DateTimeOffset StartsAt { get; private set; }
[BitzColumn(IsRequired = true)]
public DateTimeOffset ExpiresAt { get; private set; }
[Obsolete("仅供 ORM 持久化物化使用。请使用 Create 工厂方法。", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public SubscriptionAggregate()
: base("0")
{
}
// 状态流转方法:完成支付并激活
public Result Activate(string transactionId, DateTimeOffset now)
{
if (Status == SubscriptionStatus.Active)
{
// 幂等保护:已激活状态直接返回成功
return Result.Success();
}
if (Status != SubscriptionStatus.PendingPayment)
{
return Result.Failure(SubscriptionErrors.InvalidState);
}
// 状态机转换
Status = SubscriptionStatus.Active;
StartsAt = now;
ExpiresAt = now.AddMonths(1);
AddDomainEvent(new SubscriptionActivatedDomainEvent(Id, TenantId, PlanCode, ExpiresAt, transactionId));
return Result.Success();
}
}

第二步:支付回调签名验证与幂等履约

ProcessPaymentWebhookCommandHandler.cs: 支付回调幂等切片
using System.Threading;
using System.Threading.Tasks;
using BitzOrcas.Application.Abstractions.Idempotency;
using BitzOrcas.Domain.Abstractions;
using BitzOrcas.Domain.Results;
public sealed class ProcessPaymentWebhookCommandHandler(
ICommandRepository<SubscriptionAggregate, string> subscriptionRepo,
IIdempotencyStore idempotencyStore,
IAppClock clock)
{
public async ValueTask<Result> Handle(ProcessPaymentWebhookCommand command, CancellationToken ct)
{
// 1. 基于三方交易号进行强幂等防重锁定
var lockKey = $"payment:webhook:{command.TransactionId}";
var acquired = await idempotencyStore.TryAcquireAsync(lockKey, ct);
if (!acquired.Acquired)
{
// 重复回调直接原样返回成功,绝不重复激活
return Result.Success();
}
// 2. 加载目标订阅聚合根
var subscriptionResult = await subscriptionRepo.FindAsync(command.SubscriptionId, ct);
if (subscriptionResult.IsFailure)
{
return Result.Failure(SubscriptionErrors.NotFound);
}
var subscription = subscriptionResult.Value;
// 3. 执行领域状态激活
var activateResult = subscription.Activate(command.TransactionId, clock.UtcNow);
if (activateResult.IsFailure) return activateResult;
// 4. 保存聚合(由 TransactionPipeline 自动提交)
await subscriptionRepo.UpdateAsync(subscription, ct);
return Result.Success();
}
}

总结

Billing 模块筑牢平台商业基座:

  • 资损零容忍:Redis 分布式锁 + 数据库乐观锁双层护航配额原子扣减;
  • 全链路幂等:支付回调与发票生成 100% 幂等防重;
  • 事件驱动履约:订阅激活自动广播,通知系统毫秒级为租户刷新可用功能与容量上限。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%