在本教程中,您将使用 NestJS 微服务构建一个可用于生产环境的 PayPal 支付服务。通过学习本教程,您会了解到如何将支付逻辑独立成专门的微服务、如何利用 gRPC 实现服务之间的通信、如何通过 RabbitMQ 发布支付事件,以及如何使用 Docker 完成整个服务的部署过程。
最终,您将会获得一个可扩展的支付架构,该架构可以在多个业务领域中被重复使用。
目录
引言
并非每个微服务都需要包含支付逻辑。当你在user-service、order-service和billing-service中分散使用PayPal API调用时,就会出现以下问题:
-
重复的PayPal认证信息及SDK代码
-
错误处理机制不一致,且缺乏幂等性
-
支付记录难以被审计
-
在测试环境与生产环境之间切换非常麻烦
解决方案是创建一个专门的支付微服务,负责处理所有与PayPal相关的交互。其他服务通过gRPC与该微服务进行通信,而支付结果会通过RabbitMQ发送给相关业务服务,以便它们能够更新自己的数据。
本指南将使用实际开发环境中的技术栈来讲解这一架构模式:
| 层次 | 技术 |
|---|---|
| 支付服务 | NestJS |
| 服务间通信 | gRPC |
| 事件总线 | RabbitMQ |
| 数据库 | PostgreSQL |
| API接口 | API网关(HTTP) |
| 容器化技术 | Docker Compose |
| PayPal API | Orders v2(创建订单、批准支付、捕获交易信息) |
为什么需要专门的支付服务?
专门的支付服务将所有与支付相关的功能集中到一个地方。各微服务无需直接与PayPal进行交互,只需向该支付服务发起支付请求即可。
这个服务负责处理PayPal认证、订单创建、交易信息捕获、钱包管理、账本记录以及Webhook事件的处理。而业务服务则可以专注于处理诸如学生申请或订阅等业务逻辑。
业务服务只需要了解以下信息:
-
应收取的费用金额
-
付款方是谁
- 这笔支付是用于哪项业务活动(通过
referenceId识别) - 支付完成后应将用户重定向到哪个页面(
returnUrl/cancelUrl) -
NOTStarted — 订单记录已创建在数据库中
-
EXECUTING — PayPal订单已创建,正在等待用户确认
-
SUCCESS — 资金已成功扣收,账本信息已更新,相关事件也已发布
-
FAILED — 扣款失败或用户取消了操作
-
一个PayPal开发者账户
-
对gRPC及消息队列有基本的了解
-
创建订单:后端会生成包含付款金额及退款链接的订单信息
-
用户确认:系统会将用户重定向到PayPal页面,由用户完成支付确认操作
-
资金扣收:后端会扣除经用户确认的款项
-
检查幂等性键,如果已经存在相应的订单,则直接返回该订单。
-
创建一个
payment_events结算记录。 -
创建一条状态为
NOT_STARTED的payment_orders记录。 -
调用
PayPalService.createOrder()来创建支付订单。 -
将订单状态更新为
EXECUTING。 -
将
approveUrl返回给调用者。 -
将订单状态更新为
SUCCESS -
更新卖家的钱包余额
-
创建账本记录(审计追踪)
-
标记结账操作已完成
-
向RabbitMQ发布
payment.{domain}.completed事件 -
身份验证(使用JWT机制)
-
请求校验
-
确保不使用PayPal的认证信息
-
发送请求
POST /applications/:id/pay/applicationfee,其中参数{ "domain": "http://localhost:3000" } -
在浏览器中打开返回的
approveUrl -
使用 PayPal沙箱测试账户 登录
-
审批通过后,发送请求
POST /applications/:id/pay/applicationfee/capture,并提供paypalOrderId -
确认申请状态是否为
PAID -
创建一个新的实时REST API应用
-
复制客户端ID和密钥
-
Docker的
HEALTHCHECK命令 -
Traefik负载均衡器
-
系统运行状态监控工具
-
[ ] 执行
GET /health请求后应返回200状态码 -
[ ] PayPal的日志中应显示
credentialsPresent: true -
[ ] 启动应用程序后,数据库表(如
payment_orders、payment_events等)必须存在 -
[ ] 创建的支付返回信息中必须包含有效的
approveUrl -
[ ] 沙箱环境中的买家能够顺利完成结算流程
-
[ ] 收到退货信息时,
payment_order_status字段应显示“SUCCESS” -
[ ] 在域服务中,应用程序的状态应被标记为“PAID”
-
[ ] RabbitMQ会正确处理类型为
payment.application_completed的事件 -
[ ] 对于重复发生的支付请求,系统能够正确进行处理(保证操作的幂等性)
-
[ ] 当使用100%折扣优惠券时,系统应跳过PayPal的支付流程
-
[ > 生产环境应使用
https://api-m.paypal.com地址 -
所有与PayPal相关的API调用都由同一个支付服务模块负责处理
-
gRPC用于将域服务内部连接到支付服务模块
- 通过使用幂等性机制,可以避免重复收费的情况发生
- 环境变量可用于区分沙箱环境和生产环境,无需修改代码
- 迁移脚本必须针对生产环境的Docker镜像进行编译
- 前端在发送请求时需要指定
domain>参数,这样返回的URL才能在所有环境中正常使用
它们不需要掌握PayPal的认证信息。
架构概述
用户在前端发起支付请求,这些请求会通过API网关被转发到学生服务。该服务使用gRPC与支付服务进行通信,而支付服务则负责处理所有与PayPal相关的交互。
一旦支付完成,支付服务会向RabbitMQ发布事件,这样学生服务就可以异步地更新支付状态。
┌────────────────────────────────────────────────────────────┐
│ 表示层 │
├────────────────────────────────────────────────────────────┤
│ 前端(React) │
└───────────────────────┬────────────────────────────────────┘
│ HTTP
▼
┌────────────────────────────────────────────────────────────┐
│ 网关层 │
├────────────────────────────────────────────────────────────┤
│ student-apigw │
└───────────────────────┬────────────────────────────────────┘
│ gRPC
▼
┌────────────────────────────────────────────────────────────┐
│ 业务逻辑层 │
├────────────────────────────────────────────────────────────┤
│ students-service │
└───────────────────────┬────────────────────────────────────┘
│ gRPC
▼
┌────────────────────────────────────────────────────────────┐
│ 支付层 │
├────────────────────────────────────────────────────────────┤
│ payment-service │
│ │
│ • 创建支付请求 │
│ • 捕获交易信息 │
│ • 管理钱包 │
│ • 更新账本 │
│ • 处理Webhook事件 │
│ • 发布事件通知 │
└──────────────┬───────────────────────┬─────────────────────┘
│ │
│ REST │ RabbitMQ
▼ ▼
┌───────────────┐ ┌────────────────────┐
│ PayPal │ │ payment_events │
│ 结账流程 │ │ 队列 │
└───────────────┘ └─────────┬──────────┘
│
▼
┌────────────────────┐
│ students-service │
│ 事件监听器 │
└────────────────────┘
支付状态机
支付状态机用于描述支付的整个生命周期,跟踪其从创建到完成(或失败)的整个过程。每个状态都能反映支付的当前状况,这样便更便于进行监控、重试操作,以及避免出现无效的操作。
NOT_STARTED → EXECUTING → SUCCESS
└→ FAILED
先决条件
在开始之前,请确保您具备以下条件:
需要了解的PayPal相关概念
在集成PayPal之前,了解一些核心概念会很有帮助。PayPal为开发环境和生产环境提供了不同的环境设置,同时也会规定应用程序应遵循的订单处理流程。
沙盒环境与生产环境
| 环境类型 | API基础地址 | 结账页面地址 |
|---|---|---|
| 沙盒环境(开发用) | https://api-m.sandbox.paypal.com |
https://www.sandbox.paypal.com/checkoutnow?token=... |
| 生产环境(正式使用) | https://api-m.paypal.com |
https://www.paypal.com/checkoutnow?token=... |
请始终在沙盒环境中进行开发,只有在生产环境中才切换到正式环境。
订单API流程(我们的使用方式)
PayPal的Orders v2 API分为三个步骤:
这与旧版的 Payments REST API有所不同。对于新的集成项目来说,Orders v2是推荐的使用方式。
环境变量
PayPal服务会从环境变量中读取配置信息。这种方式能够将敏感的认证信息隐藏在源代码之外,同时也能方便地在沙盒环境和生产环境之间进行切换。
PAYPAL_CLIENT_ID=your_client_id
PAYPAL_CLIENT_SECRET=your_client_secret
PAYPAL_API_BASE=https://api-m.sandbox.paypal.com # 或 https://api-m.paypal.com(用于生产环境)
切勿将真实的凭证提交到Git中。应使用`.env`文件以及Docker环境注入机制。
项目结构
apps/
├── core/
│ └── payment-service/ # 包含所有与PayPal相关的逻辑
│ ├── src/
│ │ ├── app/payment/
│ │ │ ├── paypal/paypal.service.ts
│ │ │ ├── payment.service.ts
│ │ │ ├── paymentgrpc.controller.ts
│ │ │ ├── payment.http.controller.ts
│ │ │ └── eventspayment-events.publisher.ts
│ │ ├── migrations/ # 数据库模式变更相关文件
│ │ └── routes/health.routes.ts
│ └── Dockerfile
├── services/
│ └── students-service/ # 示例领域服务
│ └── src/app/payment/
│ ├── payment-client.service.ts # gRPC客户端代码
│ ├── application-payment.service.ts # 业务逻辑处理代码
│ └── payment-events.consumer.ts # RabbitMQ事件监听器代码
└── gateways/
└── student-apigw/ # 用于前端的HTTP API服务
libs/
└── shared/dto/src/lib/payment/
└── payment.proto # 共享的gRPC协议文件
步骤1 — 创建支付服务
支付服务在同一进程中运行两台服务器。
| 协议类型 | 端口号 | 用途 |
|---|---|---|
| HTTP | 3003 | 用于健康检查、Webhook触发以及管理员API调用 |
| gRPC | 50061 | 用于服务之间的内部通信 |
在同一NestJS应用中,支付服务同时提供了HTTP服务器和gRPC服务器。HTTP服务器负责处理健康检查、Webhook请求以及外部访问请求,而gRPC服务器则用于接收其他微服务发送的内部请求。
// apps/core/payment-service/src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 健康检查路由(位于/api前缀之外)
app.use('/health', healthRouter);
// gRPC微服务
app.connectMicroservice({
transport: Transport.GRPC,
options: {
package: 'payment',
protoPath: join(process.cwd(), 'libs/shared/dto/src/lib/payment/paypal.proto'),
url: `0.0.0.0:${process.env.GRPC_PORT || '50061'}`,
},
});
app.setGlobalPrefix('api');
await app.startAllMicroservices();
await app.listen(process.env.PORT || 3003);
}
在启动过程中,NestJS会同时初始化这两台服务器,从而确保外部客户端与内部服务能够通过相应的协议进行通信。
关键设计原则:HTTP用于处理外部请求及Webhook触发事件;gRPC则用于服务之间的高效、类型化的内部通信。
步骤2 — 定义gRPC协议文件
接下来,你需要创建一个共享的`.proto`文件,以确保所有服务都能使用相同的语言进行交互。
GRPC合约定义了微服务之间共用的API。使用.proto文件可以确保所有服务在与支付服务进行通信时,无论使用何种编程语言,都会采用相同的请求与响应结构。
// libs/shared/dto/src/lib/payment/paypal.proto
syntax = "proto3";
package payment;
service PaymentService {
rpc CreatePayment(CreatePaymentRequest) returns (CreatePaymentResponse) {}
rpc CapturePayment(CapturePaymentRequest) returns (CapturePaymentResponse) {}
rpc GetPaymentStatus(GetPaymentStatusRequest) returns (GetPaymentStatusResponse) {}
rpc ListPayments(List PaymentsRequest) returns (ListPaymentsResponse) {}
}
message CreatePaymentRequest {
string checkout_id = 1;
string payment_order_id = 2;
string domain = 3; // 例如 "application"、"subscription"
string reference_id = 4; // 商业实体ID
string payer_id = 5;
string amount = 6;
string currency = 7;
string buyer_email = 8;
string seller_account = 9;
string payment_category = 10;
string return_url = 11; // 成功支付后PayPal的跳转链接
string cancel_url = 12; // 取消支付时PayPal的跳转链接
string idempotency_key = 13;
string metadata = 14;
string description = 15;
}
message CreatePaymentResponse {
int32 status = 1;
string message = 2;
string payment_order_id = 3;
string paypal_order_id = 4;
string approve_url = 5; // 将用户重定向到此链接
string payment_order_status = 6;
}
domain与reference_id的组合使得某个支付服务能够处理应用程序费用、订阅费用、大学学费等各种类型的付款请求,而无需与任何特定的商业模式发生关联。
步骤3 — 实现PayPal服务
现在,您将创建一个专门的PayPalService类,用于封装PayPal的REST API接口。
我们不会在整个应用程序中直接调用PayPal API,而是将所有与PayPal相关的通信操作都放在这个专门的服务中。这样,认证、订单创建以及支付捕获等逻辑就被集中起来,从而更便于维护。
// apps/core/payment-service/src/app/paypal/paypal.service.ts
@Injectable()
export class PayPalService {
private accessToken: string | null = null;
private tokenExpiresAt = 0;
private get apiBase(): string {
return this.configService.get('PAYPAL_API_BASE')
|| 'https://api-m.sandbox.paypal.com';
}
// 步骤1:获取OAuth访问令牌(缓存直至失效)
private async getAccessToken(): Promise {
const now = Date.now();
if (this.accessToken && now < this.tokenExpiresAt) {
return this.accessToken;
}
const response = await axios.post(
`${this.apiBase}/v1/oauth2/token`,
'grant_type=client_credentials',
{
auth: {
username: this.configService.get('PAYPAL_CLIENT_ID'),
password: this.configService.get('PAYPAL_CLIENT_SECRET'),
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}
);
this.accessToken = response.data.access_token;
this.tokenExpiresAt = now + (response.dataexpires_in - 60) * 1000;
return this.accessToken;
}
// 步骤2:创建PayPal结算订单
async createOrder(input: PayPalCreateOrderInput) {
const token = await this.getAccessToken();
const response = await axios.post(
`${this.apiBase}/v2/checkout/orders`,
{
intent: 'CAPTURE',
purchase_units: [
{
custom_id: input.paymentOrderId,
description: input.description,
amount: {
currency_code: input(currency,
value: input.amount,
},
},
],
application_context: {
return_url: input.returnUrl,
cancel_url: input.cancelUrl,
brand_name: 'YourApp',
user_action: 'PAY_NOW',
},
},
{
headers: {
Authorization: `Bearer ${token}`,
'PayPal-Request-Id': input.idempotencyKey,
},
}
);
const paypalOrderId = response.data.id;
const approveUrl = response.datalinks
?.find((l) => l.rel === 'approve')?.href;
return { paypalOrderId, approveUrl };
}
// 步骤3:捕获已批准的订单
async captureOrder(paypalOrderId: string) {
const token = await this.getAccessToken();
const response = await axios.post(
`${this.apiBase}/v2/checkout/orders/${paypalOrderId}/capture`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
const capture = response.data.purchase_units?.[0]?.payments?.captures?.[0];
return { status: response.data.status, captureId: capture?.id || '' };
}
}
在应用程序启动时,应配置日志记录选项(其中敏感信息会被隐藏),这样你就能一目了然地区分测试环境与生产环境中的数据:
PayPal配置检查:
PAYPAL_API_BASE: https://api-m.paypal.com
PAYPAL_CLIENT_ID: AQb2...aq1M (80个字符)
credentialsPresent: true
environment: live
需要注意的是,访问令牌会被缓存,直到其过期为止。这样一来,在每次进行支付操作时就不需要重新请求OAuth令牌了,这既能提升性能,也能减少不必要的API调用。
步骤4 — 构建支付流程(创建、审批、收款)
创建支付订单
PaymentService.createPayment()会执行以下操作:
async createPayment(input: CreatePaymentPayload) {
// 幂等性检查:防止重复收费
const existing = await this.paymentOrderModel.findOne({
where: { idempotencyKey: input.idempotencyKey },
});
if (existing) return this.buildCreateResponse(existing);
const order = await this.paymentOrderModel.create({
paymentOrderId: input.paymentOrderId,
amount: input.amount,
currency: input(currency,
paymentOrderStatus: PaymentOrderStatus NOT_STARTED,
domain: input.domain,
referenceId: input/referenceId,
// ...
});
const paypalOrder = await this.paypalService.createOrder({
paymentOrderId: order.paymentOrderId,
amount: input.amount,
currency: input.currency,
returnUrl: input.returnUrl,
cancelUrl: input.cancelUrl,
idempotencyKey: input.idempotencyKey,
});
await order.update({
paymentOrderStatus: PaymentOrderStatus.EXECUTING,
paypalOrderId: paypalOrder.paypalOrderId,
});
return {
approveUrl: paypalOrder.approveUrl,
paypalOrderId: paypalOrder/paypalOrderId,
paymentOrderStatus: PaymentOrderStatus.EXECUTING,
};
}
用户在PayPal上进行审批
前端会将用户重定向到approveUrl。PayPal会处理身份验证和审批流程,之后再把用户重定向回你的returnUrl。
收款
在获得批准后,可以调用capturePayment()方法,传入paymentOrderId或paypalOrderId即可:
async capturePayment(paymentOrderId?: string, paypalOrderId?: string) {
const order = await this.findOrder(paymentOrderId, paypalOrderId);
if (order.paymentOrderStatus === PaymentOrderStatus SUCCESS) {
return this.buildCaptureResponse(order); // 收款操作已经完成
}
const capture = await this.paypalService.captureOrder(order/paypalOrderId);
if (capture.status !== 'COMPLETED') {
throw new Error(`PayPal收款状态:${capture.status}`);
}
await this.finalizeSuccessfulPayment(order, capturecaptureId);
return this.buildCaptureResponse(order);
}
finalizeSuccessfulPayment()是在数据库事务中执行的:
步骤5 — 通过gRPC连接领域服务
领域服务(如students-service)从不直接与PayPal进行交互。它们会使用gRPC客户端:
Students Service是通过gRPC客户端与Payment Service进行通信的。它并不会直接调用PayPal的API,而是通过支付服务提供的强类型远程过程来完成相关操作。
// apps/services/students-service/src/app/payment/payment.module.ts
ClientsModule.registerAsync([
name: 'PAYMENT_SERVICE',
useFactory: () => ({
transport: Transport.GRPC,
options: {
package: 'payment',
protoPath: 'libs/shared/dto/src/lib/payment/payment.proto',
url: process.env.PAYMENT_SERVICE_URL || 'payment-service:50061',
},
}),
}])
// payment-client.service.ts
@Injectable()
export class PaymentClientService implements OnModuleInit {
private paymentService: PaymentGrpcService;
constructor(@Inject('PAYMENT_SERVICE') private client: ClientGrpc) {}
onModuleInit() {
this.paymentService = this.client.getService('PaymentService');
}
async createPayment(data: CreatePaymentRequest) {
return firstValueFrom(this.paymentService.CreatePayment(data));
}
async capturePayment(data: { payment_order_id?: string; paypal_order_id?: string }) {
return firstValueFrom(this.paymentService.CapturePayment(data));
}
}
领域服务的业务逻辑示例:
这个示例展示了领域服务如何在将支付处理任务委托给Payment Service之前,先准备特定的业务数据。
// application-payment.service.ts
async initiateTuitionPayment/applicationId: number, options: { domain: string }) {
const application = await this.applicationModel.findByPk(applicationId);
// 根据前端域名生成PayPal的返回URL
const frontendBase = options.domain; // 例如:https://crm.yourapp.com
const returnUrl = `${frontendBase}/payment/successful?applicationId=${application/applicationId}`;
const cancelUrl = `${frontendBase}/payment/failure?applicationId=${application.applicationId}`;
const result = await this.paymentClient.createPayment({
checkout_id: `checkout-app-${application.id}`,
payment_order_id: uuidv4(),
domain: 'application',
reference_id: String(application.id),
payer_id: application.studentId,
amount: finalAmount.toFixed(2),
currency: 'USD',
buyer_email: buyerEmail,
seller_account: `university-${application.universityId}`,
payment_category: 'tuition_deposit',
return_url: returnUrl,
cancel_url: cancelUrl,
idempotency_key: `app-${application.id}-tuition-${uuidv4()`,
});
return {
approveUrl: result.approve_url,
paypalOrderId: result.paypal_order_id,
payment orderId: result.payment_order_id,
};
}
域服务仍然负责处理业务规则,而支付服务则负责处理具体的支付流程。
重要提示:前端必须将自己所在的域名作为domain参数进行发送,这样返回的URL才能指向正确的环境(开发环境中为localhost,生产环境中为对应的production URL)。
步骤6 — 添加API网关层
API网关会向前端暴露HTTP接口,并将请求转发给相应的域服务:
POST /applications/:id/pay/applicationfee
Body: { "domain": "https://crm.yourapp.com", "couponCode": "SAVE10" }
student-apigw → students-service (gRPC) → payment-service (gRPC) → PayPal
API网关的功能包括:
在完成PayPal支付流程后,需要发送一个额外的请求来捕获相关数据:
POST /applications/:id/pay/applicationfee/capture
Body: { "paypalOrderId": "PAYPAL_ORDER_ID_FROM_URL" }
步骤7 — 使用RabbitMQ发布支付事件
RabbitMQ能够实现服务之间的异步通信。在支付成功后,支付服务只需发布一个事件,然后由其他相关服务自行处理这个事件即可。
当支付数据被成功捕获后,支付服务会发布一个事件:
payment-events.publisher.ts
async publishCompleted(event: PaymentCompletedEvent) {
const pattern = `payment.${event.domain}.completed`; // 例如:payment.application_completed
this.eventsClient.emit(pattern, { ...event, eventId: uuidv4() });
}
每个域服务都会订阅与其业务领域相关的支付事件。例如,学生服务会监听payment/application Completed事件,从而将学生的申请状态标记为“已支付”。
payment-events.consumer.ts (students-service)
@EventPattern('payment.application_completed')
async handlePaymentCompleted(@Payload() data: PaymentCompletedPayload) {
await thisapplicationPaymentService.handlePaymentCompletedEvent(data);
// 将申请状态标记为“已支付”,并记录支付历史
}
这种设计使得支付完成的处理过程与域服务的更新操作相互独立。即使students-service暂时无法正常运行,也可以通过RabbitMQ队列来重新处理这些事件。
标记订单为已支付的两种方式
| 方式 | 适用场景 |
|---|---|
| 同步捕获 | 前端在完成PayPal支付流程后,立即调用捕获接口 |
| 异步事件处理 | RabbitMQ消费者在收到支付服务发布的事件后,再更新域服务的状态 |
同时使用这两种方式(并且确保操作具有幂等性),就能保证系统的可靠性:同步处理路径能够立即提供用户反馈,而异步处理路径则起到了安全保障的作用。
步骤 8 — 数据库架构与迁移
支付服务拥有自己独立的数据库架构。每个表都有特定的功能,这使得支付记录、金融交易数据以及Webhook处理过程能够与其他业务系统相互隔离。
| 表名 | 用途 |
|---|---|
payment_events |
结账会话信息(买家/卖家相关信息) |
payment_orders |
使用PayPal ID进行的各项支付尝试记录 |
ledger_entries |
财务审计轨迹记录 |
wallets |
卖家余额查询功能 |
processed_webhooks |
用于去除重复的Webhook请求 |
coupons / coupon_redemptions |
折扣码信息(可选) |
sequelize_meta |
用于追踪迁移过程的信息 |
生产环境中的迁移注意事项
在正式的生产环境中使用的Docker镜像中,迁移相关的.ts文件是不可直接使用的,除非你将这些文件编译成JavaScript代码并复制到镜像中:
# Dockerfile — 为生产环境编译迁移脚本
RUN pnpm exec tsc --project apps/core/payment-service/tsconfig.migrations.json
COPY --from=builder /app/dist/apps/core/payment-service/migrations ./migrations
如果不执行上述操作,日志中会显示“Executed 0 migrations”,并且不会创建任何数据库表。
在首次进行部署之前,请先创建相应的数据库用户:
CREATE USER payment_user WITH PASSWORD 'payment_pass';
CREATE DATABASE payment_db;
GRANT ALL PRIVILEGES ON DATABASE payment_db TO payment_user;
步骤 9 — 本地开发环境配置(使用Docker)
环境变量设置(.env文件)
PAYPAL_CLIENT_ID=your_sandbox_client_id
PAYPAL_CLIENT_SECRET=your_sandbox_client_secret
PAYPAL_API_BASE=https://api-m.sandbox.paypal.com
在本节中,我们将使用Docker Compose来配置支付服务的本地开发环境。这样的设置能够为测试支付功能提供完整的环境,而无需将系统部署到生产环境中。
Docker Compose(本地环境配置)
以下配置文件可以同时启动支付服务及其所需的依赖项,包括PostgreSQL和RabbitMQ:
payment-service:
build:
dockerfile: apps/core/payment-service/Dockerfile.dev
ports:
- '3003:3003' # HTTP接口
- '50061:50061' # gRPC接口
environment:
- PAYPAL_API_BASE=https://api-m.sandbox.paypal.com
- PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
- PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}
- DB_HOST=postgres
- DB_NAME=payment_db
- DB_USER=payment_user
- DB_PASSWORD=payment_pass
- RABBITMQ_URL=amqp://rabbitmq:5672
students-service:
environment:
- PAYMENT_SERVICE_URL=payment-service:50061
depends_on:
payment-service:
condition: service_healthy
启动服务
配置完成后,先启动各个容器,确认所有服务都能正常运行,然后再测试支付流程。
docker compose up -d payment-service students-service student-apigw
检查服务状态
curl http://localhost:3003/health
# 返回结果示例:{"status":"healthy","service":"payment-service",...}
测试支付流程
步骤10——生产环境部署
在本地完成所有测试后,下一步就是将支付服务部署到生产环境中。主要区别在于使用PayPal Live的认证信息、生产环境的配置变量以及专为生产环境准备的Docker镜像。
PayPal Live认证信息
生产环境配置文件 `.env`(仅保存在服务器上,永不提交更改)
PAYPAL_CLIENT_ID=your_live_client_id
PAYPAL_CLIENT_SECRET=your_live_secret
PAYPAL_API_BASE=https://api-m.paypal.com
Docker Compose配置(生产环境版)
payment-service:
build:
dockerfile: apps/core/payment-service/Dockerfile
environment:
- NODE_ENV=production
- PAYPAL_API_BASE=${PAYPAL_API_BASE:-https://api-m.paypal.com}
- PAYPAL_CLIENT_ID=${PAYPAL_CLIENT_ID}
- PAYPAL_CLIENT_SECRET=${PAYPAL_CLIENT_SECRET}
- DB_HOST=${DB_HOST}
- DB_NAME=payment_db
- DB_USER=payment_user
- DB_PASSWORD=payment_pass
- RABBITMQ_URL=amqp://${RABBITMQ_USER}:${RABBITMQ_PASS}@rabbitmq:5672
labels:
- 'traefik.http.routers.payment.rule=Host(`payment-service.yourapp.com`)'
students-service:
environment:
- PAYMENT_SERVICE_URL=payment-service:50061
depends_on:
payment-service:
condition: service_healthy
部署命令
docker compose -f docker-compose.prod.yml build --no-cache payment-service
docker compose -f docker-compose.prod.yml up -d payment-service students-service
验证生产环境配置
curl https://payment-service.yourapp.com/health
docker logs -f apply-goal-payment-service
# 查找以下内容:
# environment: live
# 找到8个待执行的迁移任务
# 已执行了8个迁移操作
生产环境中的前端域名配置
在发起支付请求时,前端必须发送生产环境的CRM地址:
localhost。这个设置决定了PayPal在完成支付后会将用户重定向到哪个地址。
步骤11——健康检查与监控
健康检查功能允许Docker、Traefik等编排工具验证支付服务是否正常运行。对这些接口进行监控有助于及时发现故障,从而提高应用程序的可靠性。
// GET /health
{ "status": "healthy", "service": "payment-service", "timestamp": "...", "version": "1.0.0" }
PayPal的凭证验证会在程序启动时通过PayPalService.logConfiguration()自动执行。
完整的请求流程示例
场景:学生为大学申请支付学费。
1. 前端
POST /applications/42/pay/applicationfee
Body: { "domain": "https://crm.yourapp.com" }
│
▼
2. student-apigw(HTTP → gRPC)
InitiateApplicationTuitionPayment(applicationId: 42)
│
▼
3. students-service
- 验证该申请是否已经支付过
- 计算应支付的学费金额
- 可选地通过gRPC接口验证优惠券的有效性
- 根据提供的域名生成返回地址(returnUrl)和取消链接cancelUrl
- 调用payment-service的CreatePayment接口(gRPC)
│
▼
4. payment-service
- 创建支付订单记录(执行中)
- 向PayPal发送POST请求,路径为/v2/checkout/orders
- 返回批准链接approveUrl
│
▼
5. 前端将用户重定向到approveUrl页面(进行PayPal结算)
│
▼
6. 用户完成支付确认 → PayPal将用户重定向到returnUrl页面
│
▼
7. 前端
POST /applications/42/pay/applicationfee/capture
Body: { "paypalOrderId": "PAYPAL_ORDER_ID" }
│
▼
8. payment-service
- 向/v2/checkout/orders/{id}/capture发送POST请求
- 更新订单状态 → 操作成功
- 更新用户的钱包信息及账本记录
- 将支付完成的状态信息发布到RabbitMQ队列中
│
▼
9. students-service(事件监听器)
- 将申请的状态设置为PAID
- 将支付信息记录到application_payments表中
优惠券支持(可选功能)
在创建PayPal订单之前,需要通过gRPC接口验证优惠券的有效性:
const validation = await this.paymentClient.validateCoupon({
code: 'SAVE20',
universityId: application.universityId,
originalAmount: 500,
paymentType: 'application_fee',
});
const finalAmount = validation.data.finalAmount;
// 如果优惠券可以覆盖100%的费用,那么就完全跳过PayPal这个支付环节
if (finalAmount <= 0) {
await this.markApplicationPaid(applicationId, { amount: 0, source: 'coupon' });
return { paymentOrderStatus: 'COMPLETED' };
}
优惠券相关的逻辑都存储在payment-service模块中,因此折扣规则是集中管理的。
PayPal Webhooks(可选但推荐使用)
你可以在PayPal的控制面板中注册一个Webhook地址:
https://payment-service.yourapp.com/api/v1/payments/webhooks/paypal
支付服务会处理以下事件:
| 事件类型 | 对应操作 |
|---|---|
CHECKOUT_ORDER.APPROVED |
自动捕获订单信息 |
PAYMENT.CAPTURE.COMPLETED |
如果尚未完成支付,则最终确认付款 |
通过processed_webhooks表来消除重复处理的现象,从而避免同一事件被多次处理。
测试检查清单
总结
在微服务架构中集成PayPal,主要遵循以下原则:
RabbitMQ用于传递支付结果,从而保证各域服务之间的解耦
这种架构具有很好的扩展性:只需为新的支付类型(如订阅服务、代理费用、大学服务费等)指定不同的domain>和payment_category>参数,即可实现相应的集成功能,而无需修改PayPal相关的代码。

