项目

支付模块 (Pro)

您必须拥有 ABP团队版或更高级别的许可证 才能使用此模块。

支付模块实现了应用程序的支付网关集成。它提供一次性付款和定期付款选项。

有关模块功能的概述,请参见 模块描述页面

如何安装

支付模块未预装在 启动模板 中。因此,需要手动安装。有两种方式可以将模块安装到您的应用程序中。

使用ABP CLI

ABP CLI允许使用 add-module 命令将模块添加到解决方案中。您可以查阅其 文档 了解更多信息。因此,可以使用以下命令添加支付模块:

abp add-module Volo.Payment

手动安装

如果您修改了解决方案结构,使用ABP CLI添加模块可能不适用。在这种情况下,可以手动将支付模块添加到解决方案中。

为此,将下面列出的包添加到解决方案中相应的项目。例如,将 Volo.Payment.Application 包添加到您的 .Application.csproj 中,如下所示:

<PackageReference Include="Volo.Payment.Application" Version="x.x.x" />

添加包引用后,打开项目的模块类(例如:{ProjectName}ApplicationModule),并将以下代码添加到 DependsOn 特性中。

[DependsOn(
  //...
  typeof(AbpPaymentApplicationModule)
)]

如果您使用的是 Blazor Web App,则需要将 Volo.Payment.Admin.Blazor.WebAssembly 包添加到 .Blazor.Client.csproj 项目,并将 Volo.Payment.Admin.Blazor.Server 包添加到 .Blazor.csproj 项目。

支持的网关包

要使用某个支付网关,您需要按照上面手动安装部分的说明,将相关的NuGet包添加到您的相关项目中,并在您的相关模块中添加 DependsOn。例如,如果您不想使用 PayU,则不必使用其NuGet包。

将支付网关的包添加到应用程序后,您还需要配置全局支付模块选项以及已添加的支付网关模块的选项。请参阅下面的选项部分。

创建自定义支付网关

如果您需要与现有支付网关不同的网关,您可以自行创建自定义支付网关。创建自定义支付网关需要两个步骤。第一步是创建实现 IPaymentGateway 的支付网关对象。此接口公开了核心支付操作,不包含任何UI。第二步是为支付网关创建UI。此UI用于将用户重定向到支付网关并验证支付。

按照 此处的说明 创建自定义支付网关。

本模块遵循 模块开发最佳实践指南,由多个NuGet和NPM包组成。如果您想了解这些包及其之间的关系,请参阅该指南。

您可以访问 支付模块包列表页面 查看与此模块相关的包列表。

用户界面

公共页面

支付网关选择

此页面允许选择支付网关。如果最终应用程序配置了一个支付网关,则将跳过此页面。

payment-gateway-selection

PayU 预付款页面

此页面用于向PayU发送用户的姓名、姓氏和电子邮件地址。

payment-payu-prepayment-page

管理页面

Angular UI

安装

要将应用程序配置为使用支付模块,首先需要从 @volo/abp.ng.payment/admin/config 导入 PaymentAdminConfigModule 到根配置中。PaymentAdminConfigModule 有一个静态的 forRoot 方法,您应该调用它以进行适当的配置:

// app.config.ts
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { PaymentAdminConfigModule } from '@volo/abp.ng.payment/admin/config';

export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    importProvidersFrom([
      PaymentAdminConfigModule.forRoot()
    ]),
  ],
};

支付管理模块应按如下所示导入并在您的路由数组中懒加载:

// app.routes.ts
const APP_ROUTES: Routes = [
  // ...
  {
    path: 'payment',
    loadChildren: () =>
      import('@volo/abp.ng.payment/admin').then(c => c.createRoutes()),
  },
];

支付计划页面

可以在此页面管理订阅的支付计划。您可以将每个网关的外部订阅连接到某个计划。

payment plans

payment plans gateway plans

支付请求列表

此页面列出应用程序中的所有支付请求操作。

payment request list

选项

PaymentOptions

PaymentOptions 用于存储支付网关列表。对于现有的支付网关,您无需手动配置此选项。但是,您可以像下面这样添加一个新的网关:

Configure<PaymentOptions>(options =>
{
    options.Gateways.Add(
        new PaymentGatewayConfiguration(
            "MyPaymentGatewayName",
            new FixedLocalizableString("MyPaymentGatewayName"),
            typeof(MyPaymentGateway)
        )
    );
});

PaymentOptions 属性:

  • GatewayConfigurations:网关配置列表。
    • Name:支付网关名称。
    • DisplayName:支付网关的显示名称。
    • PaymentGatewayType:支付网关类型。
    • Order:支付网关的顺序。

PaymentWebOptions

PaymentWebOptions 用于配置与Web应用程序相关的设置。

  • CallbackUrl:内部支付网关模块返回的最终回调URL。用户将在您的网站上被重定向到此URL。
  • RootUrl:您网站的根URL。
  • GatewaySelectionCheckoutButtonStyle:在网关选择页面上为“结账”按钮添加的CSS样式。此类可用于通过Google标签管理器等第三方工具跟踪用户活动。
  • PaymentGatewayWebConfigurations:用于存储与Web相关的支付网关配置。
    • Name:支付网关名称。
    • PrePaymentUrl:将用户重定向到支付网关进行付款之前的页面URL。
    • PostPaymentUrl:用户从支付网关重定向回您的网站时的页面URL。此页面主要用于验证支付。
    • Order:网关选择页面上支付网关的顺序。
    • Recommended:支付网关是否被推荐。此信息显示在支付网关选择页面上。
    • ExtraInfos:支付网关的附加信息字符串列表。这些文本显示在支付网关选择页面上。

PayuOptions

PayuOptions 用于配置PayU支付网关选项。

  • Merchant:PayU账户的商户代码。
  • Signature:商户的签名。
  • LanguageCode:订单的语言。如果可用,将用于发送给客户的确认邮件。
  • CurrencyCode:订单的货币代码(USD,EUR等)。
  • VatRate:订单的增值税率。
  • PriceType:订单的价格类型(GROSS 或 NET)。
  • Shipping:表示运费的正数。
  • Installment:分期付款期数。可以是1到12之间的整数。
  • TestOrder:订单是否为测试订单(true或false)。
  • Debug:在PAYU端写入详细日志。

PayuWebOptions

PayuWebOptions 用于配置PayU支付网关的Web选项。

  • Recommended:支付网关是否被推荐。此信息显示在支付网关选择页面上。
  • ExtraInfos:支付网关的附加信息字符串列表。这些文本显示在支付网关选择页面上。
  • PrePaymentCheckoutButtonStyle:在PayU预付款页面上为“结账”按钮添加的CSS样式。此类可用于通过Google标签管理器等第三方工具跟踪用户活动。

TwoCheckoutOptions

TwoCheckoutOptions 用于配置TwoCheckout支付网关选项。

  • Signature:商户2Checkout账户的签名。
  • CheckoutUrl:2Checkout结账URL(必须设置为 <https://secure.2checkout.com/order/checkout.php)。>
  • LanguageCode:订单的语言。如果可用,将用于发送给客户的确认邮件。
  • CurrencyCode:订单的货币代码(USD,EUR等)。

TwoCheckoutWebOptions

TwoCheckoutWebOptions 用于配置TwoCheckout支付网关的Web选项。

  • Recommended:支付网关是否被推荐。此信息显示在支付网关选择页面上。
  • ExtraInfos:支付网关的附加信息字符串列表。这些文本显示在支付网关选择页面上。

StripeOptions

StripeOptions 用于配置Stripe支付网关选项。

  • PublishableKey:Stripe账户的可发布密钥。
  • SecretKey:Stripe账户的密钥。
  • WebhookSecret:用于处理Webhook。您可以从 Stripe仪表板 获取。如果您不使用订阅和定期付款,则不是必需的。
  • Currency:订单的货币代码(USD,EUR等,完整列表见 Stripe文档)。默认值为USD。
  • Locale:订单的语言。默认值为'auto'。
  • PaymentMethodTypes:此Checkout会话可以接受的支付方式类型列表(例如,card)。请参阅 <https://stripe.com/docs/payments/checkout/payment-methods。默认值为'card'。>

StripeWebOptions

StripeWebOptions 用于配置Stripe支付网关的Web选项。

  • Recommended:支付网关是否被推荐。此信息显示在支付网关选择页面上。
  • ExtraInfos:支付网关的附加信息字符串列表。这些文本显示在支付网关选择页面上。

PayPalOptions

PayPalOptions 用于配置PayPal支付网关选项。

  • ClientId:PayPal账户的客户端ID。
  • Secret:PayPal账户的密钥。
  • CurrencyCode:订单的货币代码(USD,EUR等)。
  • Environment:支付环境。("Sandbox" 或 "Live",默认值为 "Sandbox")
  • Locale:PayPal支持的语言和地区,用于本地化PayPal结账页面。请参阅 <https://developer.paypal.com/docs/api/reference/locale-codes/。>

PayPalWebOptions

PayPalWebOptions 用于配置PayPal支付网关的Web选项。

  • Recommended:支付网关是否被推荐。此信息显示在支付网关选择页面上。
  • ExtraInfos:支付网关的附加信息字符串列表。这些文本显示在支付网关选择页面上。

IyzicoOptions

IyzicoOptions 用于配置Iyzico支付网关选项。

  • BaseUrl:Iyzico的基础API URL(例如:<https://sandbox-api.iyzipay.com)。>
  • ApiKey:Iyzico账户的API密钥。
  • SecretKey:Iyzico账户的密钥。
  • Currency:订单的货币代码(可以使用USD,EUR,GBP和TRY)。
  • Locale:订单的语言。
  • InstallmentCount:分期付款期数值。对于单期付款应为1(有效值:1, 2, 3, 6, 9, 12)。

IyzicoWebOptions

IyzicoWebOptions 用于配置Iyzico支付网关的Web选项。

  • Recommended:支付网关是否被推荐。此信息显示在支付网关选择页面上。
  • ExtraInfos:支付网关的附加信息字符串列表。这些文本显示在支付网关选择页面上。
  • PrePaymentCheckoutButtonStyle:在Iyzico预付款页面上为“结账”按钮添加的CSS样式。此类可用于通过Google标签管理器等第三方工具跟踪用户活动。

AlipayOptions

AlipayOptions 用于配置Alipay支付网关选项。支付宝网关仅支持CNY货币

  • Protocol:支付宝协议(例如:https)。
  • GatewayHost:支付宝的网关主机。
  • SignType:支付宝的签名类型。
  • AppId:支付宝账户的应用ID。
  • MerchantPrivateKey:支付宝账户的商户私钥。
  • MerchantCertPath:支付宝账户的商户证书路径。
  • AlipayCertPath:支付宝账户的支付宝证书路径。
  • AlipayRootCertPath:支付宝账户的支付宝根证书路径。
  • AlipayPublicKey:支付宝账户的支付宝公钥。
  • NotifyUrl:支付宝的通知URL。
  • EncryptKey:支付宝的加密密钥。

AlipayWebOptions

  • Recommended:支付网关是否被推荐。此信息显示在支付网关选择页面上。
  • ExtraInfos:支付网关的附加信息字符串列表。这些文本显示在支付网关选择页面上。
  • PrePaymentCheckoutButtonStyle:在Iyzico预付款页面上为“结账”按钮添加的CSS样式。此类可用于通过Google标签管理器等第三方工具跟踪用户活动。

您可以查看 支付宝文档 了解更多细节。

除了在模块类中配置选项,您也可以在 appsettings.json 文件中进行配置,如下所示:

"Payment": {
    "Payu": {
      "Merchant": "TEST",
      "Signature": "SECRET_KEY",
      "LanguageCode": "en",
      "CurrencyCode": "USD",
      "VatRate": "0",
      "PriceType": "GROSS",
      "Shipping": "0",
      "Installment": "1",
      "TestOrder": "1",
      "Debug": "1"
    },
    "TwoCheckout": {
      "Signature": "SECRET_KEY",
      "CheckoutUrl": "https://secure.2checkout.com/order/checkout.php",
      "LanguageCode": "en",
      "CurrencyCode": "USD",
      "TestOrder": "1"
    },
    "PayPal": {
      "ClientId": "CLIENT_ID",
      "Secret": "SECRET",
      "CurrencyCode": "USD",
      "Environment": "Sandbox",
      "Locale": "en_US"
    },
    "Stripe": {
      "PublishableKey": "PUBLISHABLE_KEY",
      "SecretKey": "SECRET_KEY",
      "PaymentMethodTypes": ["alipay"]
    },
    "Iyzico": {
      "ApiKey": "API_KEY",
      "SecretKey": "SECRET_KEY",
      "BaseUrl": "https://sandbox-api.iyzipay.com",
      "Locale": "en",
      "Currency": "USD"
    },
    "Alipay": {
      "AppId": "APP_ID",
      "GatewayHost": "openapi.alipaydev.com",
      "AlipayPublicKey": "ALIPAY_PUBLIC_KEY",
      "MerchantPrivateKey": "MERCHANT_PRIVATE_KEY"
    }
  }

内部结构

领域层

聚合

本模块遵循 实体最佳实践与约定 指南。

PaymentRequest

支付请求代表应用程序中的一个支付请求。

  • PaymentRequest(聚合根):代表系统中的支付请求。
    • Products(集合):支付请求的产品列表。
    • State:支付请求的状态(可以是等待、完成、失败或已退款)。
    • Currency:支付请求的货币代码(USD,EUR等)。
    • Gateway:此支付请求使用的支付网关名称。
    • FailReason:支付请求失败的原因。
Plan

计划用于订阅付款。包含 PlanGateway 列表以配置每个网关。

  • Plan(聚合根):代表定期付款的计划。
    • PlanGateways(集合):网关计划列表。
    • Name:计划的名称(可选)。
  • GatewayPlan(实体):代表计划的网关配置。
    • PlanId:所属计划的ID。
    • Gateway:所属网关。必须是唯一的。
    • ExternalId:存储网关订阅的唯一配置,例如priceId、planId、subscriptionId或productId等。

仓储

本模块遵循 仓储最佳实践与约定 指南。

为此模块定义了以下自定义仓储:

  • IPaymentRequestRepository
  • IPlanRepository

应用层

应用服务

  • PaymentRequestAppService(实现 IPaymentRequestAppService):用于创建支付请求和访问支付请求详细信息。
  • GatewayAppService(实现 IGatewayAppService):用于向UI提供支付网关列表。
  • PlanAppService(实现 IPlanAppService):用于管理订阅计划。

数据库提供程序

通用

表/集合前缀和模式

所有表/集合默认使用 Pay 前缀。如果需要更改表前缀或设置模式名称(如果您的数据库提供程序支持),请在 PaymentDbProperties 类上设置静态属性。

连接字符串

此模块使用 AbpPayment 作为连接字符串名称。如果您未定义此名称的连接字符串,它将回退到 Default 连接字符串。

有关详细信息,请参阅 连接字符串 文档。

Entity Framework Core

  • PayPaymentRequests
    • AbpRoleClaims
      • PayPaymentRequestProducts
  • PayPlans
  • PayGatewayPlans

MongoDB

集合
  • PayPaymentRequests
  • PayPlans

实体扩展

模块实体扩展 系统是一个 高级 扩展系统,允许您为依赖模块的现有实体 定义新属性。它可以在一个点上自动 将属性添加到实体数据库HTTP API和用户界面

要扩展支付模块的实体,请在您的 DomainShared 项目中打开 YourProjectNameModuleExtensionConfigurator 类,并修改 ConfigureExtraProperties 方法,如下所示。

public static void ConfigureExtraProperties()
{
    OneTimeRunner.Run(() =>
    {
        ObjectExtensionManager.Instance.Modules()
            .ConfigurePayment(payment =>
            {
                payment.ConfigurePlan(plan => // 扩展 Plan 实体
                {
                    plan.AddOrUpdateProperty<string>( //属性类型:string
                      "PlanDescription", //属性名称
                      property => {
                        //验证规则
                        property.Attributes.Add(new RequiredAttribute()); //向定义的属性添加必需属性

                        //...此属性的其他配置
                      }
                    );
                });
              
              payment.ConfigureGatewayPlan(gatewayPlan => // 扩展 GatewayPlan 实体
                {
                    gatewayPlan.AddOrUpdateProperty<string>( //属性类型:string
                      "GatewayPlanDescription", //属性名称
                      property => {
                        //验证规则
                        property.Attributes.Add(new RequiredAttribute()); //向定义的属性添加必需属性
                        property.Attributes.Add(
                          new StringLengthAttribute(MyConsts.MaximumDescriptionLength) {
                            MinimumLength = MyConsts.MinimumDescriptionLength
                          }
                        );

                        //...此属性的其他配置
                      }
                    );
                });     
            });
    });
}
  • ConfigurePayment(...) 方法用于配置支付模块的实体。
  • payment.ConfigurePlan(...) 用于配置支付模块的 Plan 实体。您可以添加或更新 Plan 实体的额外属性。
  • payment.ConfigureGatewayPlan(...) 用于配置支付模块的 GatewayPlan 实体。您可以添加或更新 GatewayPlan 实体的额外属性。
  • 您还可以为您定义的属性设置一些验证规则。在上面的示例中,为名为 "GatewayPlanDescription" 的属性添加了 RequiredAttributeStringLengthAttribute
  • 当您定义新属性时,它会自动为您添加到 实体HTTP APIUI 中。
    • 一旦定义属性,它将出现在相关实体的创建和更新表单中。
    • 新属性也会出现在相关页面的数据表中。

分布式事件

  • Volo.Payment.PaymentRequestCompleted (PaymentRequestCompletedEto):在支付完成时发布。

    • Id:代表 PaymentRequest 实体 ID。
    • Gateway:代表完成支付所使用的网关。
    • Currency:代表支付的货币。
    • Products(集合):代表 PaymentRequest 中包含哪些产品。
  • Volo.Payment.SubscriptionCanceled (SubscriptionCanceledEto):在订阅停止或取消时发布。

    • PaymentRequestId:代表 PaymentRequest 实体 ID。
    • State:代表 PaymentRequest 的状态,例如 WaitingCompletedFailedRefunded
    • Currency:代表支付的货币。
    • Gateway:代表完成支付所使用的网关。
    • FailReason:代表网关提供的失败原因。
    • ExternalSubscriptionId:代表网关的订阅 ID。
    • PeriodEndDate:代表订阅的结束日期。订阅可能已取消,但持续到最后一个周期结束。
  • Volo.Payment.SubscriptionCreated (SubscriptionCreatedEto):在订阅创建时发布。

    • PaymentRequestId:代表 PaymentRequest 实体 ID。
    • State:代表 PaymentRequest 的状态,例如 WaitingCompletedFailedRefunded
    • Currency:代表支付的货币。
    • Gateway:代表完成支付所使用的网关。
    • ExternalSubscriptionId:代表网关的订阅 ID。
    • PeriodEndDate:代表订阅的结束日期。订阅可能已取消,但持续到最后一个周期结束。
  • Volo.Payment.RecurringPaymentUpdated (SubscriptionUpdatedEto):在应用程序中或支付网关仪表板中更新订阅时发布。如果订阅是从网关仪表板更新的,此事件将在Webhook交付后立即发布。

    • PaymentRequestId:代表 PaymentRequest 实体 ID。
    • State:代表 PaymentRequest 的状态,例如 WaitingCompletedFailedRefunded
    • Currency:代表支付的货币。
    • Gateway:代表完成支付所使用的网关。
    • ExternalSubscriptionId:代表网关的订阅 ID。
    • PeriodEndDate:代表订阅的结束日期。订阅可能已取消,但持续到最后一个周期结束。

找不到您需要的内容?请查看 标准分布式事件

一次性付款

此模块实现了一次性付款;

您可以使用支付模块支持的一个或多个支付网关从客户那里收取一次性付款。支付模块对于一次性付款的工作方式非常简单。它创建一个本地支付请求记录,并将客户重定向到支付网关(PayPal、Stripe等)进行处理。当客户在支付网关付款后,支付模块处理外部支付网关的响应并验证支付是否确实已付。如果支付验证成功,支付模块将客户重定向回最初发起支付过程的主应用程序。

下图演示了支付过程的流程:

payment-module-flow

每个支付网关实现都包含 PrePayment 和 PostPayment 页面。

如果需要外部支付网关请求额外信息,则 PrePayment 页面会向用户询问这些信息。例如,2Checkout不需要任何额外信息,因此2Checkout的PrePayment页面会在不询问任何额外信息的情况下将用户重定向到2Checkout。

PostPayment 页面负责验证外部支付网关的响应。当用户完成支付时,用户将被重定向到该支付网关的PostPayment页面,PostPayment页面验证支付状态。如果支付成功,则更新支付请求的状态,并将用户重定向回主应用程序。

注意:主应用程序有责任处理支付请求被多次使用的情况。例如,如果PostPayment页面生成一个像 https://mywebsite.com/PaymentSucceed?PaymentRequestId={PaymentRequestId} 这样的URL,最终用户可能会多次手动访问此URL。如果您已经为给定的PaymentRequestId交付了产品,则不应在此URL第二次访问时再次交付。

创建一次性付款

为了启动支付流程,注入 IPaymentRequestAppService,使用其 CreateAsync 方法创建支付请求,并将用户重定向到网关选择页面,附带创建的支付请求ID。以下是一个示例Razor页面代码,在其OnPost方法中启动支付流程。

网关选择页面的重定向必须是 POST 请求。如果将其实现为 GET 请求,将会出错。您可以使用 LocalRedirectPreserveMethod 来保持重定向请求的方法为POST。

public class IndexModel: PageModel
{
    private readonly IPaymentRequestAppService _paymentRequestAppService;

    public IndexModel(IPaymentRequestAppService paymentRequestAppService)
    {
        _paymentRequestAppService = paymentRequestAppService;
    }

    public virtual async Task<IActionResult> OnPostAsync()
    {
        var paymentRequest = await _paymentRequestAppService.CreateAsync(new PaymentRequestCreateDto()
        {
            Currency= "USD",
            Products = new List<PaymentRequestProductCreateDto>()
            {
                new PaymentRequestProductCreateDto
                {
                    Code = "Product_01",
                    Name = "LEGO Super Mario",
                    Count = 2,
                    UnitPrice = 60,
                    TotalPrice = 200
                }
            }
        });
        
        return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + paymentRequest.Id);
    }
}

如果支付成功,支付模块将返回到配置的 PaymentWebOptions.CallbackUrl。主应用程序可以针对成功的支付采取必要的操作(激活用户账户、触发发货流程等)。

订阅

此模块还实现了定期付款;

您可以使用本模块支持的支付网关开始订阅并从客户那里收取定期付款。它的工作方式与一次性付款不同。支付模块通过选定网关的Webhook事件工作。它会像一次性付款一样创建一个本地支付请求记录,但会在客户付款的每个周期跟踪该支付请求,并发布有关取消、更新和继续的事件。

payment-module-flow

启用WebHooks

配置Web Hooks对于订阅非常重要,否则您的应用程序将无法获取订阅更改,例如已取消或已更新的状态。每个网关都有自己的配置:

Stripe

  1. 前往 Stripe仪表板上的WebHooks
  2. 使用 添加端点 按钮创建一个新的Webhook。
    • 端点URLyourdomain.com/api/payment/stripe/webhook
    • 要发送的事件
      • customer.subscription.created
      • customer.subscription.deleted
      • customer.subscription.updated
      • checkout.session.completed(可选)如果您不设置此项,支付将通过回调处理。
  3. Stripe将创建一个Webhook密钥。复制该密钥,并将其配置为 StripeOptions 下的 WebhookSecret

配置计划

在开始定期付款之前,必须正确配置 PlanGatewayPlan

  1. 前往您的支付网关(Stripe)仪表板并创建产品和定价。
  2. 在您的应用程序中创建一个 Plan 实体。
  3. 转到“管理网关计划”部分,为网关创建一个新的 GatewayPlan,并将价格或产品ID粘贴为 ExternalId

创建租户版本订阅

请遵循 saas 文档。

创建定期付款

创建定期付款与创建支付几乎相同。将 PaymentType 属性设置为 Recurring 并传递 PlanId 就足以启动定期支付请求。如果给定的计划有多个GatewayPlan,用户将能够选择支付网关。

网关选择页面的重定向必须是 POST 请求。如果将其实现为 GET 请求,将会出错。您可以使用 LocalRedirectPreserveMethod 来保持重定向请求的方法为POST。

public class SubscriptionModel : PageModel
{
    private IPaymentRequestAppService PaymentRequestAppService { get; }

    public SubscriptionModel(IPaymentRequestAppService paymentRequestAppService)
    {
        PaymentRequestAppService = paymentRequestAppService;
    }

    public virtual async Task<IActionResult> OnPostAsync()
    {
        var paymentRequest = await PaymentRequestAppService.CreateAsync(
            new PaymentRequestCreateDto()
            {
                Products =
                {
                    new PaymentRequestProductCreateDto
                    {
                        PaymentType = PaymentType.Subscription,
                        Name = "Enterprise Plan",
                        Code = "EP",
                        Count = 1,
                        // 在下面放置您创建的 PlanId。
                        PlanId = DemoAppData.Plan_2_Id,
                    }
                }
            });

        return LocalRedirectPreserveMethod("/Payment/GatewaySelection?paymentRequestId=" + paymentRequest.Id);
    }
}

要跟踪订阅是继续还是已取消,您应该保留 SubscriptionId,所有事件都包含它。

在本文档中