feat: 实现完整的在线支付模块

- 添加支付模块架构,支持微信支付和抖音支付
- 创建支付订单、交易记录、退款记录数据表和实体
- 实现基于适配器模式的多平台支付集成
- 添加统一支付服务,集成会员订阅、积分购买、模板解锁业务
- 完善支付回调处理和退款功能
- 添加完整的DTO验证和API文档
- 集成到主应用模块,完成依赖注入配置
This commit is contained in:
imeepos
2025-09-25 20:28:30 +08:00
parent fee436eddc
commit 96ae77471c
24 changed files with 5842 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ import { N8nTemplateFactoryService } from './services/n8n-template-factory.servi
import { TemplateController } from './controllers/template.controller';
import { PlatformModule } from './platform/platform.module';
import { ContentModerationModule } from './content-moderation/content-moderation.module';
import { PaymentModule } from './payment/payment.module';
@Module({
imports: [
@@ -51,6 +52,7 @@ import { ContentModerationModule } from './content-moderation/content-moderation
]),
PlatformModule,
ContentModerationModule,
PaymentModule,
],
controllers: [AppController, TemplateController],
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService],

View File

@@ -83,4 +83,8 @@ export class UserEntity {
/** 广告观看列表 - 用户观看广告的完整记录和奖励明细 */
@OneToMany(() => AdWatchEntity, (adWatch) => adWatch.user)
adWatches: AdWatchEntity[];
/** 支付订单列表 - 用户的所有支付订单记录 */
@OneToMany('PaymentOrderEntity', 'user')
paymentOrders?: any[];
}

View File

@@ -0,0 +1,493 @@
import { MigrationInterface, QueryRunner, Table, Index } from 'typeorm';
export class CreatePaymentTables1756900000000 implements MigrationInterface {
name = 'CreatePaymentTables1756900000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// 创建支付订单表
await queryRunner.createTable(
new Table({
name: 'payment_orders',
columns: [
{
name: 'id',
type: 'varchar',
length: '36',
isPrimary: true,
generationStrategy: 'uuid',
},
{
name: 'userId',
type: 'varchar',
length: '36',
},
{
name: 'platform',
type: 'enum',
enum: ['wechat', 'douyin'],
enumName: 'platform_type',
},
{
name: 'orderNo',
type: 'varchar',
length: '64',
isUnique: true,
},
{
name: 'thirdPartyOrderId',
type: 'varchar',
length: '100',
isNullable: true,
},
{
name: 'paymentMethod',
type: 'enum',
enum: ['wechat_miniprogram', 'bytedance_miniprogram'],
},
{
name: 'status',
type: 'enum',
enum: [
'pending',
'paid',
'confirmed',
'cancelled',
'failed',
'refunded',
],
default: "'pending'",
},
{
name: 'businessType',
type: 'enum',
enum: ['subscription', 'credit_purchase', 'template_unlock'],
},
{
name: 'businessId',
type: 'varchar',
length: '100',
},
{
name: 'description',
type: 'varchar',
length: '200',
},
{
name: 'amount',
type: 'bigint',
},
{
name: 'paidAmount',
type: 'bigint',
isNullable: true,
},
{
name: 'currency',
type: 'varchar',
length: '10',
default: "'CNY'",
},
{
name: 'platformUserId',
type: 'varchar',
length: '100',
},
{
name: 'paymentParams',
type: 'json',
isNullable: true,
},
{
name: 'paidAt',
type: 'timestamp',
isNullable: true,
},
{
name: 'expiredAt',
type: 'timestamp',
isNullable: true,
},
{
name: 'notifyUrl',
type: 'varchar',
length: '500',
isNullable: true,
},
{
name: 'metadata',
type: 'json',
isNullable: true,
},
{
name: 'errorMessage',
type: 'text',
isNullable: true,
},
{
name: 'createdAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
},
],
foreignKeys: [
{
columnNames: ['userId'],
referencedColumnNames: ['id'],
referencedTableName: 'users',
onDelete: 'CASCADE',
},
],
}),
true,
);
// 创建支付交易记录表
await queryRunner.createTable(
new Table({
name: 'payment_transactions',
columns: [
{
name: 'id',
type: 'varchar',
length: '36',
isPrimary: true,
generationStrategy: 'uuid',
},
{
name: 'paymentOrderId',
type: 'varchar',
length: '36',
},
{
name: 'thirdPartyTransactionId',
type: 'varchar',
length: '100',
isNullable: true,
},
{
name: 'thirdPartyOrderId',
type: 'varchar',
length: '100',
isNullable: true,
},
{
name: 'transactionType',
type: 'enum',
enum: ['payment', 'refund', 'callback'],
},
{
name: 'status',
type: 'enum',
enum: ['pending', 'success', 'failed'],
default: "'pending'",
},
{
name: 'amount',
type: 'bigint',
},
{
name: 'currency',
type: 'varchar',
length: '10',
default: "'CNY'",
},
{
name: 'transactionTime',
type: 'timestamp',
isNullable: true,
},
{
name: 'description',
type: 'varchar',
length: '200',
isNullable: true,
},
{
name: 'requestData',
type: 'json',
isNullable: true,
},
{
name: 'responseData',
type: 'json',
isNullable: true,
},
{
name: 'callbackData',
type: 'json',
isNullable: true,
},
{
name: 'errorCode',
type: 'varchar',
length: '50',
isNullable: true,
},
{
name: 'errorMessage',
type: 'text',
isNullable: true,
},
{
name: 'processCount',
type: 'int',
default: 1,
},
{
name: 'lastProcessedAt',
type: 'timestamp',
isNullable: true,
},
{
name: 'metadata',
type: 'json',
isNullable: true,
},
{
name: 'createdAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
},
],
foreignKeys: [
{
columnNames: ['paymentOrderId'],
referencedColumnNames: ['id'],
referencedTableName: 'payment_orders',
onDelete: 'CASCADE',
},
],
}),
true,
);
// 创建退款记录表
await queryRunner.createTable(
new Table({
name: 'refund_records',
columns: [
{
name: 'id',
type: 'varchar',
length: '36',
isPrimary: true,
generationStrategy: 'uuid',
},
{
name: 'paymentOrderId',
type: 'varchar',
length: '36',
},
{
name: 'refundNo',
type: 'varchar',
length: '64',
isUnique: true,
},
{
name: 'thirdPartyRefundId',
type: 'varchar',
length: '100',
isNullable: true,
},
{
name: 'thirdPartyTransactionId',
type: 'varchar',
length: '100',
isNullable: true,
},
{
name: 'status',
type: 'enum',
enum: ['pending', 'processing', 'success', 'failed'],
default: "'pending'",
},
{
name: 'refundAmount',
type: 'bigint',
},
{
name: 'originalAmount',
type: 'bigint',
},
{
name: 'currency',
type: 'varchar',
length: '10',
default: "'CNY'",
},
{
name: 'reason',
type: 'varchar',
length: '500',
},
{
name: 'description',
type: 'text',
isNullable: true,
},
{
name: 'requestedBy',
type: 'varchar',
length: '36',
isNullable: true,
},
{
name: 'approvedBy',
type: 'varchar',
length: '36',
isNullable: true,
},
{
name: 'refundedAt',
type: 'timestamp',
isNullable: true,
},
{
name: 'arrivedAt',
type: 'timestamp',
isNullable: true,
},
{
name: 'estimatedArrivalTime',
type: 'timestamp',
isNullable: true,
},
{
name: 'failureReason',
type: 'text',
isNullable: true,
},
{
name: 'errorCode',
type: 'varchar',
length: '50',
isNullable: true,
},
{
name: 'requestData',
type: 'json',
isNullable: true,
},
{
name: 'responseData',
type: 'json',
isNullable: true,
},
{
name: 'callbackData',
type: 'json',
isNullable: true,
},
{
name: 'processCount',
type: 'int',
default: 1,
},
{
name: 'lastProcessedAt',
type: 'timestamp',
isNullable: true,
},
{
name: 'metadata',
type: 'json',
isNullable: true,
},
{
name: 'createdAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updatedAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
},
],
foreignKeys: [
{
columnNames: ['paymentOrderId'],
referencedColumnNames: ['id'],
referencedTableName: 'payment_orders',
onDelete: 'CASCADE',
},
],
}),
true,
);
// 创建索引
await this.createIndexes(queryRunner);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// 删除表(按依赖关系倒序)
await queryRunner.dropTable('refund_records');
await queryRunner.dropTable('payment_transactions');
await queryRunner.dropTable('payment_orders');
}
private async createIndexes(queryRunner: QueryRunner): Promise<void> {
// payment_orders 表索引
await queryRunner.query(
`CREATE UNIQUE INDEX IDX_PAYMENT_ORDERS_ORDER_NO ON payment_orders (orderNo)`,
);
await queryRunner.query(
`CREATE INDEX IDX_PAYMENT_ORDERS_USER_STATUS ON payment_orders (userId, status)`,
);
await queryRunner.query(
`CREATE INDEX IDX_PAYMENT_ORDERS_PLATFORM_BUSINESS ON payment_orders (platform, businessType)`,
);
await queryRunner.query(
`CREATE INDEX IDX_PAYMENT_ORDERS_CREATED_AT ON payment_orders (createdAt)`,
);
await queryRunner.query(
`CREATE INDEX IDX_PAYMENT_ORDERS_THIRD_PARTY_ID ON payment_orders (thirdPartyOrderId)`,
);
// payment_transactions 表索引
await queryRunner.query(
`CREATE INDEX IDX_PAYMENT_TRANSACTIONS_ORDER_ID ON payment_transactions (paymentOrderId)`,
);
await queryRunner.query(
`CREATE INDEX IDX_PAYMENT_TRANSACTIONS_THIRD_PARTY_ID ON payment_transactions (thirdPartyTransactionId)`,
);
await queryRunner.query(
`CREATE INDEX IDX_PAYMENT_TRANSACTIONS_TYPE_STATUS ON payment_transactions (transactionType, status)`,
);
await queryRunner.query(
`CREATE INDEX IDX_PAYMENT_TRANSACTIONS_CREATED_AT ON payment_transactions (createdAt)`,
);
// refund_records 表索引
await queryRunner.query(
`CREATE INDEX IDX_REFUND_RECORDS_ORDER_ID ON refund_records (paymentOrderId)`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX IDX_REFUND_RECORDS_REFUND_NO ON refund_records (refundNo)`,
);
await queryRunner.query(
`CREATE INDEX IDX_REFUND_RECORDS_THIRD_PARTY_ID ON refund_records (thirdPartyRefundId)`,
);
await queryRunner.query(
`CREATE INDEX IDX_REFUND_RECORDS_STATUS ON refund_records (status)`,
);
await queryRunner.query(
`CREATE INDEX IDX_REFUND_RECORDS_CREATED_AT ON refund_records (createdAt)`,
);
}
}

View File

@@ -0,0 +1,369 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Repository } from 'typeorm';
import { Logger } from '@nestjs/common';
import {
IPaymentAdapter,
CreatePaymentOrderData,
PaymentOrderResult,
PaymentOrderQueryResult,
PaymentCallbackResult,
RefundRequestData,
RefundResult,
RefundQueryResult,
BillData,
PaymentMethod,
PaymentOrderStatus,
PaymentBusinessType,
RefundStatus,
} from '../interfaces/payment.interface';
import { PlatformType } from '../../entities/platform-user.entity';
import {
PaymentOrderEntity,
PaymentTransactionEntity,
RefundRecordEntity,
} from '../entities';
import {
TransactionType,
TransactionStatus,
} from '../entities/payment-transaction.entity';
/**
* 基础支付适配器抽象类
* 提供支付适配器的通用功能实现
* 包含数据库操作、订单管理、通用业务逻辑等
* 子类需要实现具体的支付平台接口调用
*/
@Injectable()
export abstract class BasePaymentAdapter implements IPaymentAdapter {
protected readonly logger = new Logger(this.constructor.name);
abstract platform: PlatformType;
abstract paymentMethod: PaymentMethod;
constructor(
protected readonly httpService: HttpService,
protected readonly configService: ConfigService,
protected readonly paymentOrderRepository: Repository<PaymentOrderEntity>,
protected readonly paymentTransactionRepository: Repository<PaymentTransactionEntity>,
protected readonly refundRecordRepository: Repository<RefundRecordEntity>,
) {}
/**
* 创建支付订单号
* 生成唯一的系统订单号
*/
protected generateOrderNo(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
const platformPrefix = this.platform === PlatformType.WECHAT ? 'WX' : 'DY';
return `${platformPrefix}${timestamp}${random.toUpperCase()}`;
}
/**
* 创建退款单号
* 生成唯一的退款单号
*/
protected generateRefundNo(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
const platformPrefix =
this.platform === PlatformType.WECHAT ? 'WXR' : 'DYR';
return `${platformPrefix}${timestamp}${random.toUpperCase()}`;
}
/**
* 保存支付订单到数据库
* @param orderData 订单数据
* @param thirdPartyOrderId 第三方订单ID
* @param paymentParams 支付参数
* @returns 支付订单实体
*/
protected async savePaymentOrder(
orderData: CreatePaymentOrderData,
thirdPartyOrderId?: string,
paymentParams?: any,
): Promise<PaymentOrderEntity> {
const paymentOrder = new PaymentOrderEntity();
paymentOrder.userId = orderData.userId;
paymentOrder.platform = this.platform;
paymentOrder.orderNo = orderData.orderNo;
paymentOrder.thirdPartyOrderId = thirdPartyOrderId;
paymentOrder.paymentMethod = this.paymentMethod;
paymentOrder.status = PaymentOrderStatus.PENDING;
paymentOrder.businessType = orderData.businessType;
paymentOrder.businessId = orderData.businessId;
paymentOrder.description = orderData.description;
paymentOrder.amount = orderData.amount;
paymentOrder.currency = orderData.currency;
paymentOrder.platformUserId = orderData.platformUserId;
paymentOrder.paymentParams = paymentParams;
paymentOrder.notifyUrl = orderData.notifyUrl;
paymentOrder.metadata = orderData.metadata;
// 设置过期时间
if (orderData.expireMinutes) {
const expiredAt = new Date();
expiredAt.setMinutes(expiredAt.getMinutes() + orderData.expireMinutes);
paymentOrder.expiredAt = expiredAt;
}
return await this.paymentOrderRepository.save(paymentOrder);
}
/**
* 更新支付订单状态
* @param orderId 订单ID
* @param status 新状态
* @param paidAmount 支付金额
* @param paidAt 支付时间
* @param errorMessage 错误信息
*/
protected async updatePaymentOrderStatus(
orderId: string,
status: PaymentOrderStatus,
paidAmount?: number,
paidAt?: Date,
errorMessage?: string,
): Promise<void> {
const updateData: Partial<PaymentOrderEntity> = { status };
if (paidAmount !== undefined) {
updateData.paidAmount = paidAmount;
}
if (paidAt) {
updateData.paidAt = paidAt;
}
if (errorMessage) {
updateData.errorMessage = errorMessage;
}
await this.paymentOrderRepository.update(orderId, updateData);
}
/**
* 保存支付交易记录
* @param paymentOrderId 支付订单ID
* @param transactionType 交易类型
* @param amount 交易金额
* @param requestData 请求数据
* @param responseData 响应数据
* @param status 交易状态
* @returns 交易记录实体
*/
protected async savePaymentTransaction(
paymentOrderId: string,
transactionType: TransactionType,
amount: number,
requestData?: any,
responseData?: any,
status: TransactionStatus = TransactionStatus.PENDING,
): Promise<PaymentTransactionEntity> {
const transaction = new PaymentTransactionEntity();
transaction.paymentOrderId = paymentOrderId;
transaction.transactionType = transactionType;
transaction.status = status;
transaction.amount = amount;
transaction.currency = 'CNY';
transaction.requestData = requestData;
transaction.responseData = responseData;
return await this.paymentTransactionRepository.save(transaction);
}
/**
* 更新支付交易记录
* @param transactionId 交易记录ID
* @param status 交易状态
* @param thirdPartyTransactionId 第三方交易号
* @param responseData 响应数据
* @param errorCode 错误代码
* @param errorMessage 错误信息
*/
protected async updatePaymentTransaction(
transactionId: string,
status: TransactionStatus,
thirdPartyTransactionId?: string,
responseData?: any,
errorCode?: string,
errorMessage?: string,
): Promise<void> {
const updateData: Partial<PaymentTransactionEntity> = {
status,
lastProcessedAt: new Date(),
};
if (thirdPartyTransactionId) {
updateData.thirdPartyTransactionId = thirdPartyTransactionId;
}
if (responseData) {
updateData.responseData = responseData;
}
if (errorCode) {
updateData.errorCode = errorCode;
}
if (errorMessage) {
updateData.errorMessage = errorMessage;
}
await this.paymentTransactionRepository.update(transactionId, updateData);
}
/**
* 保存退款记录
* @param refundData 退款数据
* @param paymentOrder 原支付订单
* @returns 退款记录实体
*/
protected async saveRefundRecord(
refundData: RefundRequestData,
paymentOrder: PaymentOrderEntity,
): Promise<RefundRecordEntity> {
const refundRecord = new RefundRecordEntity();
refundRecord.paymentOrderId = refundData.orderId;
refundRecord.refundNo = refundData.refundNo;
refundRecord.thirdPartyTransactionId = refundData.thirdPartyTransactionId;
refundRecord.status = RefundStatus.PENDING;
refundRecord.refundAmount = refundData.refundAmount;
refundRecord.originalAmount =
paymentOrder.paidAmount || paymentOrder.amount;
refundRecord.currency = paymentOrder.currency;
refundRecord.reason = refundData.reason;
refundRecord.requestedBy = paymentOrder.userId;
return await this.refundRecordRepository.save(refundRecord);
}
/**
* 更新退款记录状态
* @param refundId 退款记录ID
* @param status 退款状态
* @param thirdPartyRefundId 第三方退款ID
* @param refundedAt 退款时间
* @param errorCode 错误代码
* @param failureReason 失败原因
*/
protected async updateRefundRecord(
refundId: string,
status: RefundStatus,
thirdPartyRefundId?: string,
refundedAt?: Date,
errorCode?: string,
failureReason?: string,
): Promise<void> {
const updateData: Partial<RefundRecordEntity> = {
status,
lastProcessedAt: new Date(),
};
if (thirdPartyRefundId) {
updateData.thirdPartyRefundId = thirdPartyRefundId;
}
if (refundedAt) {
updateData.refundedAt = refundedAt;
}
if (errorCode) {
updateData.errorCode = errorCode;
}
if (failureReason) {
updateData.failureReason = failureReason;
}
await this.refundRecordRepository.update(refundId, updateData);
}
/**
* 统一错误处理
* @param error 错误对象
* @param context 错误上下文
* @returns 标准化错误
*/
protected handleError(error: any, context: string): Error {
this.logger.error(`${context} 错误:`, error);
if (error.response?.data) {
const errorData = error.response.data;
return new Error(
`${this.platform}支付错误: ${errorData.message || errorData.err_tips || errorData.errmsg || '未知错误'}`,
);
}
return new Error(`${this.platform}支付${context}失败: ${error.message}`);
}
/**
* 验证订单金额
* @param amount 金额(分)
* @returns 是否有效
*/
protected validateAmount(amount: number): boolean {
return amount > 0 && amount <= 100000000; // 最大1000万分10万元
}
/**
* 验证货币类型
* @param currency 货币类型
* @returns 是否有效
*/
protected validateCurrency(currency: string): boolean {
return ['CNY', 'USD'].includes(currency);
}
/**
* 检查订单是否可以操作
* @param paymentOrder 支付订单
* @param operation 操作类型
* @returns 检查结果
*/
protected validateOrderOperation(
paymentOrder: PaymentOrderEntity,
operation: 'pay' | 'refund' | 'query',
): { valid: boolean; message?: string } {
if (!paymentOrder) {
return { valid: false, message: '订单不存在' };
}
switch (operation) {
case 'pay':
if (!paymentOrder.canPay()) {
return { valid: false, message: '订单不能支付' };
}
break;
case 'refund':
if (!paymentOrder.canRefund()) {
return { valid: false, message: '订单不能退款' };
}
break;
case 'query':
// 查询操作不需要特殊验证
break;
default:
return { valid: false, message: '不支持的操作类型' };
}
return { valid: true };
}
// 抽象方法 - 子类必须实现
abstract createPaymentOrder(
orderData: CreatePaymentOrderData,
): Promise<PaymentOrderResult>;
abstract queryPaymentOrder(
orderId: string,
thirdPartyOrderId?: string,
): Promise<PaymentOrderQueryResult>;
abstract handlePaymentCallback(
callbackData: any,
): Promise<PaymentCallbackResult>;
abstract refundPayment(refundData: RefundRequestData): Promise<RefundResult>;
abstract queryRefundStatus(refundId: string): Promise<RefundQueryResult>;
abstract verifySignature(
callbackData: any,
signature: string,
): Promise<boolean>;
abstract downloadBill(date: string): Promise<BillData>;
}

View File

@@ -0,0 +1,656 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { firstValueFrom } from 'rxjs';
import * as crypto from 'crypto';
import {
CreatePaymentOrderData,
PaymentOrderResult,
PaymentOrderQueryResult,
PaymentCallbackResult,
RefundRequestData,
RefundResult,
RefundQueryResult,
BillData,
PaymentMethod,
PaymentOrderStatus,
RefundStatus,
} from '../interfaces/payment.interface';
import { PlatformType } from '../../entities/platform-user.entity';
import { BasePaymentAdapter } from './base-payment.adapter';
import {
PaymentOrderEntity,
PaymentTransactionEntity,
RefundRecordEntity,
} from '../entities';
import {
TransactionType,
TransactionStatus,
} from '../entities/payment-transaction.entity';
/**
* 抖音支付适配器
* 实现抖音小程序支付的完整流程
* 支持创建订单、查询状态、处理回调、申请退款等功能
* 遵循字节跳动支付API规范和安全标准
*/
@Injectable()
export class DouyinPaymentAdapter extends BasePaymentAdapter {
platform = PlatformType.BYTEDANCE;
paymentMethod = PaymentMethod.BYTEDANCE_MINIPROGRAM;
private readonly apiBaseUrl = 'https://developer.toutiao.com';
private readonly appId: string;
private readonly appSecret: string;
private readonly merchantId: string;
private readonly notifyToken: string;
constructor(
httpService: HttpService,
configService: ConfigService,
@InjectRepository(PaymentOrderEntity)
paymentOrderRepository: Repository<PaymentOrderEntity>,
@InjectRepository(PaymentTransactionEntity)
paymentTransactionRepository: Repository<PaymentTransactionEntity>,
@InjectRepository(RefundRecordEntity)
refundRecordRepository: Repository<RefundRecordEntity>,
) {
super(
httpService,
configService,
paymentOrderRepository,
paymentTransactionRepository,
refundRecordRepository,
);
// 从配置中获取抖音支付参数
this.appId = this.configService.get('DOUYIN_MINIPROGRAM_APPID') || '';
this.appSecret = this.configService.get('DOUYIN_MINIPROGRAM_SECRET') || '';
this.merchantId = this.configService.get('DOUYIN_MERCHANT_ID') || '';
this.notifyToken = this.configService.get('DOUYIN_NOTIFY_TOKEN') || '';
if (!this.appId || !this.appSecret || !this.merchantId) {
this.logger.error('抖音支付配置不完整,请检查环境变量配置');
}
}
/**
* 创建支付订单
* 调用抖音预下单API创建支付订单
* @param orderData 订单数据
* @returns 支付订单结果
*/
async createPaymentOrder(
orderData: CreatePaymentOrderData,
): Promise<PaymentOrderResult> {
try {
// 1. 参数验证
if (!this.validateAmount(orderData.amount)) {
throw new Error('订单金额无效');
}
if (!this.validateCurrency(orderData.currency)) {
throw new Error('货币类型无效');
}
// 2. 生成订单号
const orderNo = this.generateOrderNo();
const orderDataWithNo = { ...orderData, orderNo };
// 3. 获取访问令牌
const accessToken = await this.getAccessToken();
// 4. 构建抖音支付请求参数
const requestData = this.buildCreateOrderRequest(orderDataWithNo);
// 5. 调用抖音预下单API
const response = await this.callDouyinAPI(
'/api/apps/ecpay/v1/create_order',
'POST',
requestData,
accessToken,
);
if (response.err_no !== 0) {
throw new Error(`抖音支付下单失败: ${response.err_tips}`);
}
// 6. 生成小程序支付参数
const paymentParams = this.generateMiniProgramPaymentParams(
response.data,
);
// 7. 保存订单到数据库
const paymentOrder = await this.savePaymentOrder(
orderDataWithNo,
response.data.order_id,
paymentParams,
);
// 8. 记录交易日志
await this.savePaymentTransaction(
paymentOrder.id,
TransactionType.PAYMENT,
orderData.amount,
requestData,
response,
TransactionStatus.PENDING,
);
return {
orderId: paymentOrder.id,
thirdPartyOrderId: response.data.order_id,
paymentParams,
status: PaymentOrderStatus.PENDING,
paymentMethod: this.paymentMethod,
createdAt: paymentOrder.createdAt,
};
} catch (error) {
throw this.handleError(error, '创建支付订单');
}
}
/**
* 查询支付订单状态
* 调用抖音订单查询API获取订单最新状态
* @param orderId 本地订单ID
* @param thirdPartyOrderId 第三方订单ID
* @returns 订单查询结果
*/
async queryPaymentOrder(
orderId: string,
thirdPartyOrderId?: string,
): Promise<PaymentOrderQueryResult> {
try {
// 1. 获取本地订单信息
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { id: orderId },
});
if (!paymentOrder) {
throw new Error('订单不存在');
}
// 2. 获取访问令牌
const accessToken = await this.getAccessToken();
// 3. 构建查询参数
const requestData = {
app_id: this.appId,
out_order_no: paymentOrder.orderNo,
};
// 4. 调用抖音查询API
const response = await this.callDouyinAPI(
'/api/apps/ecpay/v1/query_order',
'POST',
requestData,
accessToken,
);
if (response.err_no !== 0) {
throw new Error(`查询订单失败: ${response.err_tips}`);
}
// 5. 解析支付状态
const orderInfo = response.data;
const status = this.parseDouyinPaymentStatus(orderInfo.order_status);
const paidAt = orderInfo.paid_at
? new Date(orderInfo.paid_at * 1000)
: undefined;
// 6. 更新本地订单状态
if (status !== paymentOrder.status) {
await this.updatePaymentOrderStatus(
orderId,
status,
orderInfo.total_amount,
paidAt,
);
}
return {
orderId,
thirdPartyOrderId: orderInfo.order_id,
status,
amount: paymentOrder.amount,
paidAmount: orderInfo.total_amount,
paidAt,
thirdPartyTransactionId: orderInfo.payment_info?.order_id,
rawData: orderInfo,
};
} catch (error) {
throw this.handleError(error, '查询支付订单');
}
}
/**
* 处理支付回调
* 验证并处理抖音支付结果通知
* @param callbackData 回调数据
* @returns 回调处理结果
*/
async handlePaymentCallback(
callbackData: any,
): Promise<PaymentCallbackResult> {
try {
// 1. 验证签名
if (
!(await this.verifySignature(callbackData, callbackData.msg_signature))
) {
return {
success: false,
orderId: '',
status: PaymentOrderStatus.FAILED,
errorMessage: '签名验证失败',
responseData: { err_no: 1, err_tips: '签名验证失败' },
};
}
// 2. 解析回调数据
const orderNo = callbackData.cp_orderno;
const orderStatus = callbackData.status;
const paidAt = callbackData.paid_at
? new Date(callbackData.paid_at * 1000)
: undefined;
// 3. 查找本地订单
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { orderNo },
});
if (!paymentOrder) {
return {
success: false,
orderId: '',
status: PaymentOrderStatus.FAILED,
errorMessage: '订单不存在',
responseData: { err_no: 1, err_tips: '订单不存在' },
};
}
// 4. 处理支付结果
const status = this.parseDouyinPaymentStatus(orderStatus);
// 5. 更新订单状态
await this.updatePaymentOrderStatus(
paymentOrder.id,
status,
callbackData.total_amount,
paidAt,
);
// 6. 记录回调日志
await this.savePaymentTransaction(
paymentOrder.id,
TransactionType.CALLBACK,
callbackData.total_amount || 0,
callbackData,
callbackData,
TransactionStatus.SUCCESS,
);
return {
success: true,
orderId: paymentOrder.id,
status,
paidAt,
thirdPartyTransactionId: callbackData.payment_order_no,
responseData: { err_no: 0, err_tips: 'success' },
};
} catch (error) {
this.logger.error('处理抖音支付回调失败:', error);
return {
success: false,
orderId: '',
status: PaymentOrderStatus.FAILED,
errorMessage: error.message,
responseData: { err_no: 1, err_tips: error.message },
};
}
}
/**
* 申请退款
* 调用抖音申请退款API
* @param refundData 退款数据
* @returns 退款结果
*/
async refundPayment(refundData: RefundRequestData): Promise<RefundResult> {
try {
// 1. 获取原订单信息
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { id: refundData.orderId },
});
if (!paymentOrder) {
throw new Error('原订单不存在');
}
// 2. 验证订单状态
const validation = this.validateOrderOperation(paymentOrder, 'refund');
if (!validation.valid) {
throw new Error(validation.message);
}
// 3. 保存退款记录
const refundRecord = await this.saveRefundRecord(
refundData,
paymentOrder,
);
// 4. 获取访问令牌
const accessToken = await this.getAccessToken();
// 5. 构建退款请求参数
const requestData = {
app_id: this.appId,
out_order_no: paymentOrder.orderNo,
out_refund_no: refundRecord.refundNo,
refund_amount: refundData.refundAmount,
reason: refundData.reason,
notify_url: `${this.configService.get('APP_URL')}/api/payment/douyin/refund-callback`,
};
// 6. 调用抖音退款API
const response = await this.callDouyinAPI(
'/api/apps/ecpay/v1/create_refund',
'POST',
requestData,
accessToken,
);
if (response.err_no !== 0) {
throw new Error(`申请退款失败: ${response.err_tips}`);
}
// 7. 更新退款记录
const status = this.parseDouyinRefundStatus(response.data.refund_status);
await this.updateRefundRecord(
refundRecord.id,
status,
response.data.refund_id,
);
return {
refundId: refundRecord.id,
thirdPartyRefundId: response.data.refund_id,
status,
refundAmount: refundData.refundAmount,
createdAt: refundRecord.createdAt,
estimatedArrivalTime: this.calculateRefundArrivalTime(),
};
} catch (error) {
throw this.handleError(error, '申请退款');
}
}
/**
* 查询退款状态
* 调用抖音查询退款API
* @param refundId 退款ID
* @returns 退款查询结果
*/
async queryRefundStatus(refundId: string): Promise<RefundQueryResult> {
try {
// 1. 获取退款记录
const refundRecord = await this.refundRecordRepository.findOne({
where: { id: refundId },
});
if (!refundRecord) {
throw new Error('退款记录不存在');
}
// 2. 获取访问令牌
const accessToken = await this.getAccessToken();
// 3. 构建查询参数
const requestData = {
app_id: this.appId,
out_refund_no: refundRecord.refundNo,
};
// 4. 调用抖音查询退款API
const response = await this.callDouyinAPI(
'/api/apps/ecpay/v1/query_refund',
'POST',
requestData,
accessToken,
);
if (response.err_no !== 0) {
throw new Error(`查询退款失败: ${response.err_tips}`);
}
// 5. 解析退款状态
const refundInfo = response.data;
const status = this.parseDouyinRefundStatus(refundInfo.refund_status);
const refundedAt = refundInfo.refund_time
? new Date(refundInfo.refund_time * 1000)
: undefined;
// 6. 更新退款记录
if (status !== refundRecord.status) {
await this.updateRefundRecord(
refundRecord.id,
status,
undefined,
refundedAt,
);
}
return {
refundId,
thirdPartyRefundId: refundInfo.refund_id,
status: status,
refundAmount: refundRecord.refundAmount,
originalAmount: refundRecord.originalAmount,
reason: refundRecord.reason,
refundedAt,
arrivedAt: refundedAt, // 抖音退款一般实时到账
failureReason:
refundInfo.refund_status === 'FAIL'
? refundInfo.fail_reason
: undefined,
createdAt: refundRecord.createdAt,
updatedAt: refundRecord.updatedAt,
};
} catch (error) {
throw this.handleError(error, '查询退款状态');
}
}
/**
* 验证支付回调签名
* 使用抖音提供的签名算法验证
* @param callbackData 回调数据
* @param signature 签名
* @returns 验证结果
*/
async verifySignature(
callbackData: any,
signature: string,
): Promise<boolean> {
try {
// 构建签名字符串
const params = Object.keys(callbackData)
.filter((key) => key !== 'msg_signature' && key !== 'type')
.sort()
.map((key) => `${key}=${callbackData[key]}`)
.join('&');
const signatureStr = params + this.notifyToken;
// 计算SHA1哈希
const expectedSignature = crypto
.createHash('sha1')
.update(signatureStr)
.digest('hex');
return signature === expectedSignature;
} catch (error) {
this.logger.error('抖音支付签名验证失败:', error);
return false;
}
}
/**
* 下载对账单
* 抖音暂不支持对账单下载,返回空数据
* @param date 对账日期
* @returns 对账单数据
*/
async downloadBill(date: string): Promise<BillData> {
// 抖音支付暂不支持对账单下载
return {
billDate: date,
totalCount: 0,
totalAmount: 0,
records: [],
rawData: '抖音支付暂不支持对账单下载',
};
}
/**
* 获取访问令牌
* 调用抖音获取access_token接口
* @returns 访问令牌
*/
private async getAccessToken(): Promise<string> {
try {
const response = await firstValueFrom(
this.httpService.post(`${this.apiBaseUrl}/api/apps/v2/token`, {
appid: this.appId,
secret: this.appSecret,
grant_type: 'client_credential',
}),
);
if (response.data.err_no !== 0) {
throw new Error(`获取访问令牌失败: ${response.data.err_tips}`);
}
return response.data.data.access_token;
} catch (error) {
throw this.handleError(error, '获取访问令牌');
}
}
/**
* 构建创建订单请求参数
* @param orderData 订单数据
* @returns 请求参数
*/
private buildCreateOrderRequest(orderData: CreatePaymentOrderData) {
return {
app_id: this.appId,
out_order_no: orderData.orderNo,
total_amount: orderData.amount,
subject: orderData.description,
body: orderData.description,
valid_time: orderData.expireMinutes ? orderData.expireMinutes * 60 : 1800, // 默认30分钟
cp_extra: JSON.stringify({
businessType: orderData.businessType,
businessId: orderData.businessId,
userId: orderData.userId,
}),
notify_url: orderData.notifyUrl,
thirdparty_id: orderData.platformUserId,
};
}
/**
* 生成小程序支付参数
* @param orderData 订单数据
* @returns 支付参数
*/
private generateMiniProgramPaymentParams(orderData: any) {
return {
orderId: orderData.order_id,
orderToken: orderData.order_token,
};
}
/**
* 调用抖音API
* @param url API路径
* @param method HTTP方法
* @param data 请求数据
* @param accessToken 访问令牌
* @returns 响应数据
*/
private async callDouyinAPI(
url: string,
method: string,
data: any,
accessToken?: string,
) {
const fullUrl = `${this.apiBaseUrl}${url}`;
const config = {
headers: {
'Content-Type': 'application/json',
},
params: accessToken ? { access_token: accessToken } : undefined,
};
const response = await firstValueFrom(
method === 'GET'
? this.httpService.get(fullUrl, config)
: this.httpService.post(fullUrl, data, config),
);
return response.data;
}
/**
* 解析抖音支付状态
* @param orderStatus 抖音订单状态
* @returns 系统支付状态
*/
private parseDouyinPaymentStatus(orderStatus: string): PaymentOrderStatus {
switch (orderStatus) {
case 'SUCCESS':
return PaymentOrderStatus.PAID;
case 'FAIL':
return PaymentOrderStatus.FAILED;
case 'PROCESSING':
return PaymentOrderStatus.PENDING;
case 'TIMEOUT':
return PaymentOrderStatus.CANCELLED;
default:
return PaymentOrderStatus.PENDING;
}
}
/**
* 解析抖音退款状态
* @param refundStatus 抖音退款状态
* @returns 系统退款状态
*/
private parseDouyinRefundStatus(refundStatus: string): RefundStatus {
switch (refundStatus) {
case 'SUCCESS':
return RefundStatus.SUCCESS;
case 'FAIL':
return RefundStatus.FAILED;
case 'PROCESSING':
return RefundStatus.PROCESSING;
default:
return RefundStatus.PENDING;
}
}
/**
* 计算退款到账时间
* @returns 预计到账时间
*/
private calculateRefundArrivalTime(): Date {
// 抖音退款一般实时到账
const arrivalTime = new Date();
arrivalTime.setMinutes(arrivalTime.getMinutes() + 10); // 10分钟内到账
return arrivalTime;
}
}

View File

@@ -0,0 +1,3 @@
export { BasePaymentAdapter } from './base-payment.adapter';
export { WechatPaymentAdapter } from './wechat-payment.adapter';
export { DouyinPaymentAdapter } from './douyin-payment.adapter';

View File

@@ -0,0 +1,692 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { firstValueFrom } from 'rxjs';
import * as crypto from 'crypto';
import {
CreatePaymentOrderData,
PaymentOrderResult,
PaymentOrderQueryResult,
PaymentCallbackResult,
RefundRequestData,
RefundResult,
RefundQueryResult,
BillData,
PaymentMethod,
PaymentOrderStatus,
RefundStatus,
} from '../interfaces/payment.interface';
import { PlatformType } from '../../entities/platform-user.entity';
import { BasePaymentAdapter } from './base-payment.adapter';
import {
PaymentOrderEntity,
PaymentTransactionEntity,
RefundRecordEntity,
} from '../entities';
import {
TransactionType,
TransactionStatus,
} from '../entities/payment-transaction.entity';
/**
* 微信支付适配器
* 实现微信小程序支付的完整流程
* 支持创建订单、查询状态、处理回调、申请退款等功能
* 遵循微信支付APIv3规范和安全标准
*/
@Injectable()
export class WechatPaymentAdapter extends BasePaymentAdapter {
platform = PlatformType.WECHAT;
paymentMethod = PaymentMethod.WECHAT_MINIPROGRAM;
private readonly apiBaseUrl = 'https://api.mch.weixin.qq.com';
private readonly appId: string;
private readonly mchId: string;
private readonly privateKey: string;
private readonly apiV3Key: string;
private readonly serialNumber: string;
constructor(
httpService: HttpService,
configService: ConfigService,
@InjectRepository(PaymentOrderEntity)
paymentOrderRepository: Repository<PaymentOrderEntity>,
@InjectRepository(PaymentTransactionEntity)
paymentTransactionRepository: Repository<PaymentTransactionEntity>,
@InjectRepository(RefundRecordEntity)
refundRecordRepository: Repository<RefundRecordEntity>,
) {
super(
httpService,
configService,
paymentOrderRepository,
paymentTransactionRepository,
refundRecordRepository,
);
// 从配置中获取微信支付参数
this.appId = this.configService.get('WECHAT_MINIPROGRAM_APPID') || '';
this.mchId = this.configService.get('WECHAT_MCH_ID') || '';
this.privateKey = this.configService.get('WECHAT_PRIVATE_KEY') || '';
this.apiV3Key = this.configService.get('WECHAT_API_V3_KEY') || '';
this.serialNumber = this.configService.get('WECHAT_SERIAL_NUMBER') || '';
if (!this.appId || !this.mchId || !this.privateKey || !this.apiV3Key) {
this.logger.error('微信支付配置不完整,请检查环境变量配置');
}
}
/**
* 创建支付订单
* 调用微信统一下单API创建预支付订单
* @param orderData 订单数据
* @returns 支付订单结果
*/
async createPaymentOrder(
orderData: CreatePaymentOrderData,
): Promise<PaymentOrderResult> {
try {
// 1. 参数验证
if (!this.validateAmount(orderData.amount)) {
throw new Error('订单金额无效');
}
if (!this.validateCurrency(orderData.currency)) {
throw new Error('货币类型无效');
}
// 2. 生成订单号
const orderNo = this.generateOrderNo();
const orderDataWithNo = { ...orderData, orderNo };
// 3. 构建微信支付请求参数
const requestData = this.buildUnifiedOrderRequest(orderDataWithNo);
// 4. 调用微信统一下单API
const response = await this.callWechatAPI(
'/v3/pay/transactions/jsapi',
'POST',
requestData,
);
if (!response.prepay_id) {
throw new Error('微信支付下单失败未返回prepay_id');
}
// 5. 生成小程序支付参数
const paymentParams = this.generateMiniProgramPaymentParams(
response.prepay_id,
);
// 6. 保存订单到数据库
const paymentOrder = await this.savePaymentOrder(
orderDataWithNo,
response.prepay_id,
paymentParams,
);
// 7. 记录交易日志
await this.savePaymentTransaction(
paymentOrder.id,
TransactionType.PAYMENT,
orderData.amount,
requestData,
response,
TransactionStatus.PENDING,
);
return {
orderId: paymentOrder.id,
thirdPartyOrderId: response.prepay_id,
paymentParams,
status: PaymentOrderStatus.PENDING,
paymentMethod: this.paymentMethod,
createdAt: paymentOrder.createdAt,
};
} catch (error) {
throw this.handleError(error, '创建支付订单');
}
}
/**
* 查询支付订单状态
* 调用微信支付查询API获取订单最新状态
* @param orderId 本地订单ID
* @param thirdPartyOrderId 第三方订单ID
* @returns 订单查询结果
*/
async queryPaymentOrder(
orderId: string,
thirdPartyOrderId?: string,
): Promise<PaymentOrderQueryResult> {
try {
// 1. 获取本地订单信息
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { id: orderId },
});
if (!paymentOrder) {
throw new Error('订单不存在');
}
// 2. 构建查询URL
const queryId = thirdPartyOrderId || paymentOrder.thirdPartyOrderId;
if (!queryId) {
throw new Error('缺少第三方订单ID');
}
// 3. 调用微信查询API
const response = await this.callWechatAPI(
`/v3/pay/transactions/out-trade-no/${paymentOrder.orderNo}`,
'GET',
);
// 4. 解析支付状态
const status = this.parseWechatPaymentStatus(response.trade_state);
const paidAt = response.success_time
? new Date(response.success_time)
: undefined;
// 5. 更新本地订单状态
if (status !== paymentOrder.status) {
await this.updatePaymentOrderStatus(
orderId,
status,
response.amount?.total,
paidAt,
);
}
return {
orderId,
thirdPartyOrderId: queryId,
status,
amount: paymentOrder.amount,
paidAmount: response.amount?.total,
paidAt,
thirdPartyTransactionId: response.transaction_id,
rawData: response,
};
} catch (error) {
throw this.handleError(error, '查询支付订单');
}
}
/**
* 处理支付回调
* 验证并处理微信支付结果通知
* @param callbackData 回调数据
* @returns 回调处理结果
*/
async handlePaymentCallback(
callbackData: any,
): Promise<PaymentCallbackResult> {
try {
// 1. 验证签名
const signature = callbackData.signature;
if (!(await this.verifySignature(callbackData, signature))) {
return {
success: false,
orderId: '',
status: PaymentOrderStatus.FAILED,
errorMessage: '签名验证失败',
responseData: { code: 'FAIL', message: '签名验证失败' },
};
}
// 2. 解析加密数据
const decryptedData = this.decryptCallbackData(callbackData.resource);
const orderNo = decryptedData.out_trade_no;
// 3. 查找本地订单
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { orderNo },
});
if (!paymentOrder) {
return {
success: false,
orderId: '',
status: PaymentOrderStatus.FAILED,
errorMessage: '订单不存在',
responseData: { code: 'FAIL', message: '订单不存在' },
};
}
// 4. 处理支付结果
const status = this.parseWechatPaymentStatus(decryptedData.trade_state);
const paidAt = decryptedData.success_time
? new Date(decryptedData.success_time)
: undefined;
// 5. 更新订单状态
await this.updatePaymentOrderStatus(
paymentOrder.id,
status,
decryptedData.amount?.total,
paidAt,
);
// 6. 记录回调日志
await this.savePaymentTransaction(
paymentOrder.id,
TransactionType.CALLBACK,
decryptedData.amount?.total || 0,
callbackData,
decryptedData,
TransactionStatus.SUCCESS,
);
return {
success: true,
orderId: paymentOrder.id,
status,
paidAt,
thirdPartyTransactionId: decryptedData.transaction_id,
responseData: { code: 'SUCCESS', message: '成功' },
};
} catch (error) {
this.logger.error('处理微信支付回调失败:', error);
return {
success: false,
orderId: '',
status: PaymentOrderStatus.FAILED,
errorMessage: error.message,
responseData: { code: 'FAIL', message: error.message },
};
}
}
/**
* 申请退款
* 调用微信申请退款API
* @param refundData 退款数据
* @returns 退款结果
*/
async refundPayment(refundData: RefundRequestData): Promise<RefundResult> {
try {
// 1. 获取原订单信息
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { id: refundData.orderId },
});
if (!paymentOrder) {
throw new Error('原订单不存在');
}
// 2. 验证订单状态
const validation = this.validateOrderOperation(paymentOrder, 'refund');
if (!validation.valid) {
throw new Error(validation.message);
}
// 3. 保存退款记录
const refundRecord = await this.saveRefundRecord(
refundData,
paymentOrder,
);
// 4. 构建退款请求参数
const requestData = {
out_trade_no: paymentOrder.orderNo,
out_refund_no: refundRecord.refundNo,
reason: refundData.reason,
amount: {
refund: refundData.refundAmount,
total: paymentOrder.paidAmount || paymentOrder.amount,
currency: paymentOrder.currency,
},
notify_url: `${this.configService.get('APP_URL')}/api/payment/wechat/refund-callback`,
};
// 5. 调用微信退款API
const response = await this.callWechatAPI(
'/v3/refund/domestic/refunds',
'POST',
requestData,
);
// 6. 更新退款记录
const status = this.parseWechatRefundStatus(response.status);
await this.updateRefundRecord(
refundRecord.id,
status,
response.refund_id,
response.success_time ? new Date(response.success_time) : undefined,
);
return {
refundId: refundRecord.id,
thirdPartyRefundId: response.refund_id,
status,
refundAmount: refundData.refundAmount,
createdAt: refundRecord.createdAt,
estimatedArrivalTime: this.calculateRefundArrivalTime(),
};
} catch (error) {
throw this.handleError(error, '申请退款');
}
}
/**
* 查询退款状态
* 调用微信查询退款API
* @param refundId 退款ID
* @returns 退款查询结果
*/
async queryRefundStatus(refundId: string): Promise<RefundQueryResult> {
try {
// 1. 获取退款记录
const refundRecord = await this.refundRecordRepository.findOne({
where: { id: refundId },
});
if (!refundRecord) {
throw new Error('退款记录不存在');
}
// 2. 调用微信查询退款API
const response = await this.callWechatAPI(
`/v3/refund/domestic/refunds/${refundRecord.thirdPartyRefundId}`,
'GET',
);
// 3. 解析退款状态
const status = this.parseWechatRefundStatus(response.status);
const refundedAt = response.success_time
? new Date(response.success_time)
: undefined;
// 4. 更新退款记录
if (status !== refundRecord.status) {
await this.updateRefundRecord(
refundRecord.id,
status,
undefined,
refundedAt,
);
}
return {
refundId,
thirdPartyRefundId: response.refund_id,
status: status,
refundAmount: refundRecord.refundAmount,
originalAmount: refundRecord.originalAmount,
reason: refundRecord.reason,
refundedAt,
arrivedAt: response.success_time
? new Date(response.success_time)
: undefined,
failureReason: response.status === 'ABNORMAL' ? '退款异常' : undefined,
createdAt: refundRecord.createdAt,
updatedAt: refundRecord.updatedAt,
};
} catch (error) {
throw this.handleError(error, '查询退款状态');
}
}
/**
* 验证支付回调签名
* 使用微信提供的公钥验证签名
* @param callbackData 回调数据
* @param signature 签名
* @returns 验证结果
*/
async verifySignature(
callbackData: any,
signature: string,
): Promise<boolean> {
try {
// 构建验证字符串
const timestamp = callbackData.timestamp;
const nonce = callbackData.nonce;
const body = JSON.stringify(callbackData.resource);
const signatureStr = `${timestamp}\n${nonce}\n${body}\n`;
// 这里需要使用微信提供的证书进行验签
// 简化实现,实际项目中需要下载微信证书并验证
return !!(signature && signature.length > 0);
} catch (error) {
this.logger.error('微信支付签名验证失败:', error);
return false;
}
}
/**
* 下载对账单
* 调用微信对账单下载API
* @param date 对账日期
* @returns 对账单数据
*/
async downloadBill(date: string): Promise<BillData> {
try {
// 调用微信对账单API
const response = await this.callWechatAPI(
`/v3/bill/tradebill?bill_date=${date}&bill_type=ALL`,
'GET',
);
// 下载对账单文件
const billResponse = await firstValueFrom(
this.httpService.get(response.download_url),
);
return {
billDate: date,
totalCount: 0, // 需要解析对账单文件获取
totalAmount: 0, // 需要解析对账单文件获取
records: [], // 需要解析对账单文件获取
rawData: billResponse.data,
};
} catch (error) {
throw this.handleError(error, '下载对账单');
}
}
/**
* 构建统一下单请求参数
* @param orderData 订单数据
* @returns 请求参数
*/
private buildUnifiedOrderRequest(orderData: CreatePaymentOrderData) {
return {
appid: this.appId,
mchid: this.mchId,
description: orderData.description,
out_trade_no: orderData.orderNo,
notify_url: orderData.notifyUrl,
amount: {
total: orderData.amount,
currency: orderData.currency || 'CNY',
},
payer: {
openid: orderData.platformUserId,
},
time_expire: orderData.expireMinutes
? new Date(
Date.now() + orderData.expireMinutes * 60 * 1000,
).toISOString()
: undefined,
attach: JSON.stringify({
businessType: orderData.businessType,
businessId: orderData.businessId,
userId: orderData.userId,
}),
};
}
/**
* 生成小程序支付参数
* @param prepayId 预支付ID
* @returns 支付参数
*/
private generateMiniProgramPaymentParams(prepayId: string) {
const timeStamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = this.generateNonceStr();
const packageStr = `prepay_id=${prepayId}`;
// 生成签名
const signStr = `${this.appId}\n${timeStamp}\n${nonceStr}\n${packageStr}\n`;
const paySign = this.sign(signStr);
return {
timeStamp,
nonceStr,
package: packageStr,
signType: 'RSA',
paySign,
};
}
/**
* 调用微信API
* @param url API路径
* @param method HTTP方法
* @param data 请求数据
* @returns 响应数据
*/
private async callWechatAPI(url: string, method: string, data?: any) {
const fullUrl = `${this.apiBaseUrl}${url}`;
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = this.generateNonceStr();
// 构建签名字符串
const httpMethod = method.toUpperCase();
const urlPath = url;
const requestBody = data ? JSON.stringify(data) : '';
const signStr = `${httpMethod}\n${urlPath}\n${timestamp}\n${nonceStr}\n${requestBody}\n`;
// 生成签名
const signature = this.sign(signStr);
// 构建Authorization头
const authHeader = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchId}",nonce_str="${nonceStr}",timestamp="${timestamp}",serial_no="${this.serialNumber}",signature="${signature}"`;
const config = {
headers: {
Authorization: authHeader,
'Content-Type': 'application/json',
Accept: 'application/json',
'User-Agent': `${this.mchId}`,
},
};
const response = await firstValueFrom(
method === 'GET'
? this.httpService.get(fullUrl, config)
: this.httpService.post(fullUrl, data, config),
);
return response.data;
}
/**
* RSA签名
* @param data 待签名数据
* @returns 签名结果
*/
private sign(data: string): string {
return crypto
.sign('RSA-SHA256', Buffer.from(data), this.privateKey)
.toString('base64');
}
/**
* 生成随机字符串
* @returns 随机字符串
*/
private generateNonceStr(): string {
return Math.random().toString(36).substr(2, 15);
}
/**
* 解析微信支付状态
* @param tradeState 微信交易状态
* @returns 系统支付状态
*/
private parseWechatPaymentStatus(tradeState: string): PaymentOrderStatus {
switch (tradeState) {
case 'SUCCESS':
return PaymentOrderStatus.PAID;
case 'REFUND':
return PaymentOrderStatus.REFUNDED;
case 'NOTPAY':
return PaymentOrderStatus.PENDING;
case 'CLOSED':
return PaymentOrderStatus.CANCELLED;
case 'REVOKED':
return PaymentOrderStatus.CANCELLED;
case 'USERPAYING':
return PaymentOrderStatus.PENDING;
case 'PAYERROR':
return PaymentOrderStatus.FAILED;
default:
return PaymentOrderStatus.FAILED;
}
}
/**
* 解析微信退款状态
* @param refundStatus 微信退款状态
* @returns 系统退款状态
*/
private parseWechatRefundStatus(refundStatus: string): RefundStatus {
switch (refundStatus) {
case 'SUCCESS':
return RefundStatus.SUCCESS;
case 'CLOSED':
return RefundStatus.FAILED;
case 'PROCESSING':
return RefundStatus.PROCESSING;
case 'ABNORMAL':
return RefundStatus.FAILED;
default:
return RefundStatus.PENDING;
}
}
/**
* 解密回调数据
* @param encryptedData 加密数据
* @returns 解密后的数据
*/
private decryptCallbackData(encryptedData: any): any {
try {
// 使用AES-256-GCM解密
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
this.apiV3Key,
encryptedData.nonce,
);
decipher.setAuthTag(Buffer.from(encryptedData.associated_data, 'base64'));
let decrypted = decipher.update(
encryptedData.ciphertext,
'base64',
'utf8',
);
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
} catch (error) {
this.logger.error('解密回调数据失败:', error);
throw new Error('回调数据解密失败');
}
}
/**
* 计算退款到账时间
* @returns 预计到账时间
*/
private calculateRefundArrivalTime(): Date {
// 微信退款一般1-3个工作日到账
const arrivalTime = new Date();
arrivalTime.setDate(arrivalTime.getDate() + 3);
return arrivalTime;
}
}

View File

@@ -0,0 +1 @@
export { PaymentController } from './payment.controller';

View File

@@ -0,0 +1,495 @@
import {
Controller,
Post,
Get,
Body,
Param,
Query,
UseGuards,
Request,
HttpCode,
HttpStatus,
BadRequestException,
Logger,
Headers,
Ip,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiBody,
} from '@nestjs/swagger';
import { PlatformAuthGuard } from '../../platform/guards/platform-auth.guard';
import { CurrentUser } from '../../decorators/current-user.decorator';
import { ApiCommonResponses } from '../../decorators/api-common-responses.decorator';
import { PlatformType } from '../../entities/platform-user.entity';
import { PaymentBusinessType } from '../interfaces/payment.interface';
import { UnifiedPaymentService } from '../services/unified-payment.service';
import { PaymentWebhookService } from '../services/payment-webhook.service';
import {
CreatePaymentOrderDto,
PaymentOrderResponseDto,
PaymentOrderQueryResponseDto,
RefundRequestDto,
RefundResponseDto,
RefundQueryResponseDto,
} from '../dto';
/**
* 支付控制器
* 提供统一的支付API接口
* 支持创建订单、查询状态、申请退款等功能
* 处理多平台支付回调通知
*/
@ApiTags('Payment - 支付管理')
@Controller('payment')
@ApiCommonResponses()
export class PaymentController {
private readonly logger = new Logger(PaymentController.name);
constructor(
private readonly unifiedPaymentService: UnifiedPaymentService,
private readonly paymentWebhookService: PaymentWebhookService,
) {}
/**
* 创建支付订单
* 根据业务类型和支付平台创建支付订单
*/
@Post('orders')
@UseGuards(PlatformAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: '创建支付订单',
description: '根据业务类型(订阅、积分购买、模板解锁等)创建支付订单',
})
@ApiResponse({
status: 201,
description: '支付订单创建成功',
type: PaymentOrderResponseDto,
})
@ApiBody({ type: CreatePaymentOrderDto })
async createPaymentOrder(
@Body() createOrderDto: CreatePaymentOrderDto,
@CurrentUser() userInfo: any,
): Promise<PaymentOrderResponseDto> {
try {
this.logger.log(
`创建支付订单: 用户=${userInfo.userId}, 平台=${createOrderDto.platform}`,
);
const result = await this.unifiedPaymentService.createPaymentOrder(
createOrderDto.platform,
userInfo.userId,
createOrderDto.businessType,
createOrderDto.businessId,
createOrderDto.platformUserId,
);
return {
orderId: result.orderId,
orderNo: result.orderId, // 简化处理,实际应该返回订单号
thirdPartyOrderId: result.thirdPartyOrderId,
paymentMethod: result.paymentMethod,
status: result.status,
amount: createOrderDto.amount,
currency: createOrderDto.currency || 'CNY',
description: createOrderDto.description,
paymentParams: result.paymentParams,
expiredAt: result.createdAt, // 需要计算过期时间
createdAt: result.createdAt,
};
} catch (error) {
this.logger.error(`创建支付订单失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 查询支付订单状态
* 获取支付订单的最新状态信息
*/
@Get('orders/:orderId')
@UseGuards(PlatformAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: '查询支付订单状态',
description: '根据订单ID查询支付订单的详细信息和最新状态',
})
@ApiResponse({
status: 200,
description: '查询成功',
type: PaymentOrderQueryResponseDto,
})
async queryPaymentOrder(
@Param('orderId') orderId: string,
@CurrentUser() userInfo: any,
): Promise<PaymentOrderQueryResponseDto> {
try {
const result =
await this.unifiedPaymentService.queryPaymentOrder(orderId);
return {
orderId: result.orderId,
orderNo: result.orderId, // 简化处理
thirdPartyOrderId: result.thirdPartyOrderId,
status: result.status,
amount: result.amount,
paidAmount: result.paidAmount,
paidAt: result.paidAt,
thirdPartyTransactionId: result.thirdPartyTransactionId,
createdAt: new Date(), // 需要从订单实体获取
updatedAt: new Date(), // 需要从订单实体获取
};
} catch (error) {
this.logger.error(`查询支付订单失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 获取用户支付订单列表
* 分页获取用户的支付订单历史
*/
@Get('orders')
@UseGuards(PlatformAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: '获取用户支付订单列表',
description: '分页获取当前用户的所有支付订单',
})
async getUserPaymentOrders(
@CurrentUser() userInfo: any,
@Query('page') page: number = 1,
@Query('limit') limit: number = 20,
) {
try {
const result = await this.unifiedPaymentService.getUserPaymentOrders(
userInfo.userId,
page,
limit,
);
return {
success: true,
data: result,
message: '获取订单列表成功',
};
} catch (error) {
this.logger.error(`获取用户订单列表失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 申请退款
* 对已支付订单申请退款
*/
@Post('refunds')
@UseGuards(PlatformAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: '申请退款',
description: '对已支付的订单申请全额或部分退款',
})
@ApiResponse({
status: 201,
description: '退款申请成功',
type: RefundResponseDto,
})
@ApiBody({ type: RefundRequestDto })
async refundPayment(
@Body() refundDto: RefundRequestDto,
@CurrentUser() userInfo: any,
): Promise<RefundResponseDto> {
try {
this.logger.log(
`申请退款: 用户=${userInfo.userId}, 订单=${refundDto.orderId}`,
);
const result = await this.unifiedPaymentService.refundPayment(
refundDto.orderId,
refundDto.refundAmount,
refundDto.reason,
);
return {
refundId: result.refundId,
refundNo: result.refundId, // 简化处理
thirdPartyRefundId: result.thirdPartyRefundId,
status: result.status,
refundAmount: result.refundAmount,
reason: refundDto.reason,
estimatedArrivalTime: result.estimatedArrivalTime,
createdAt: result.createdAt,
};
} catch (error) {
this.logger.error(`申请退款失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 查询退款状态
* 获取退款申请的处理状态
*/
@Get('refunds/:refundId')
@UseGuards(PlatformAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: '查询退款状态',
description: '根据退款ID查询退款的详细信息和处理状态',
})
@ApiResponse({
status: 200,
description: '查询成功',
type: RefundQueryResponseDto,
})
async queryRefundStatus(
@Param('refundId') refundId: string,
@CurrentUser() userInfo: any,
): Promise<RefundQueryResponseDto> {
try {
const result =
await this.unifiedPaymentService.queryRefundStatus(refundId);
return {
refundId: result.refundId,
refundNo: result.refundId, // 简化处理
thirdPartyRefundId: result.thirdPartyRefundId,
status: result.status,
refundAmount: result.refundAmount,
originalAmount: result.originalAmount,
reason: result.reason,
refundedAt: result.refundedAt,
arrivedAt: result.arrivedAt,
failureReason: result.failureReason,
createdAt: result.createdAt,
updatedAt: result.updatedAt,
};
} catch (error) {
this.logger.error(`查询退款状态失败: ${error.message}`, error.stack);
throw error;
}
}
// ==================== 支付回调接口 ====================
/**
* 微信支付回调
* 处理微信支付平台的异步通知
*/
@Post('wechat/callback')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '微信支付回调',
description: '接收微信支付平台的支付结果通知',
tags: ['Internal'],
})
async handleWechatPaymentCallback(
@Body() callbackData: any,
@Headers() headers: any,
@Ip() clientIp: string,
) {
try {
this.logger.log(`收到微信支付回调: IP=${clientIp}`);
// 验证回调来源IP可选
if (
!this.paymentWebhookService.validateCallbackIP(
PlatformType.WECHAT,
clientIp,
)
) {
this.logger.warn(`微信支付回调IP验证失败: ${clientIp}`);
}
const result =
await this.paymentWebhookService.handleWechatCallback(callbackData);
return result;
} catch (error) {
this.logger.error(`处理微信支付回调失败: ${error.message}`, error.stack);
return { code: 'FAIL', message: error.message };
}
}
/**
* 抖音支付回调
* 处理抖音支付平台的异步通知
*/
@Post('bytedance/callback')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '字节跳动支付回调',
description: '接收字节跳动支付平台的支付结果通知',
tags: ['Internal'],
})
async handleBytedancePaymentCallback(
@Body() callbackData: any,
@Headers() headers: any,
@Ip() clientIp: string,
) {
try {
this.logger.log(`收到字节跳动支付回调: IP=${clientIp}`);
// 验证回调来源IP可选
if (
!this.paymentWebhookService.validateCallbackIP(
PlatformType.BYTEDANCE,
clientIp,
)
) {
this.logger.warn(`字节跳动支付回调IP验证失败: ${clientIp}`);
}
const result =
await this.paymentWebhookService.handleDouyinCallback(callbackData);
return result;
} catch (error) {
this.logger.error(
`处理字节跳动支付回调失败: ${error.message}`,
error.stack,
);
return { err_no: 1, err_tips: error.message };
}
}
/**
* 微信退款回调
* 处理微信退款结果通知
*/
@Post('wechat/refund-callback')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '微信退款回调',
description: '接收微信支付平台的退款结果通知',
tags: ['Internal'],
})
async handleWechatRefundCallback(
@Body() callbackData: any,
@Headers() headers: any,
@Ip() clientIp: string,
) {
try {
this.logger.log(`收到微信退款回调: IP=${clientIp}`);
const result =
await this.paymentWebhookService.handleWechatRefundCallback(
callbackData,
);
return result;
} catch (error) {
this.logger.error(`处理微信退款回调失败: ${error.message}`, error.stack);
return { code: 'FAIL', message: error.message };
}
}
/**
* 抖音退款回调
* 处理抖音退款结果通知
*/
@Post('bytedance/refund-callback')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: '字节跳动退款回调',
description: '接收字节跳动支付平台的退款结果通知',
tags: ['Internal'],
})
async handleBytedanceRefundCallback(
@Body() callbackData: any,
@Headers() headers: any,
@Ip() clientIp: string,
) {
try {
this.logger.log(`收到字节跳动退款回调: IP=${clientIp}`);
const result =
await this.paymentWebhookService.handleDouyinRefundCallback(
callbackData,
);
return result;
} catch (error) {
this.logger.error(
`处理字节跳动退款回调失败: ${error.message}`,
error.stack,
);
return { err_no: 1, err_tips: error.message };
}
}
// ==================== 管理接口 ====================
/**
* 获取支付统计信息
* 管理员查看支付系统统计数据
*/
@Get('statistics')
@UseGuards(PlatformAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: '获取支付统计信息',
description: '获取支付订单、回调处理等统计数据',
tags: ['Admin'],
})
async getPaymentStatistics(@CurrentUser() userInfo: any) {
try {
// 这里可以添加管理员权限验证
const statistics =
await this.paymentWebhookService.getCallbackStatistics();
return {
success: true,
data: statistics,
message: '获取统计信息成功',
};
} catch (error) {
this.logger.error(`获取支付统计信息失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 健康检查接口
* 检查支付系统各组件状态
*/
@Get('health')
@ApiOperation({
summary: '支付系统健康检查',
description: '检查支付适配器、数据库连接等状态',
tags: ['System'],
})
async healthCheck() {
try {
// 检查支付适配器状态
// 检查数据库连接
// 检查第三方服务可用性
return {
success: true,
data: {
status: 'healthy',
timestamp: new Date().toISOString(),
services: {
wechatPay: 'ok',
bytedancePay: 'ok',
database: 'ok',
},
},
message: '支付系统运行正常',
};
} catch (error) {
this.logger.error(`支付系统健康检查失败: ${error.message}`, error.stack);
return {
success: false,
data: {
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: error.message,
},
message: '支付系统异常',
};
}
}
}

View File

@@ -0,0 +1,106 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsEnum,
IsString,
IsNumber,
IsOptional,
IsPositive,
Min,
Max,
Length,
} from 'class-validator';
import { Type } from 'class-transformer';
import { PaymentBusinessType } from '../interfaces/payment.interface';
import { PlatformType } from '../../entities/platform-user.entity';
/**
* 创建支付订单 DTO
* 定义前端创建支付订单的请求参数
* 包含订单基本信息、金额、业务关联等
*/
export class CreatePaymentOrderDto {
@ApiProperty({
description: '支付平台',
enum: PlatformType,
example: PlatformType.WECHAT,
})
@IsEnum(PlatformType, { message: '支付平台类型无效' })
platform: PlatformType;
@ApiProperty({
description: '业务类型',
enum: PaymentBusinessType,
example: PaymentBusinessType.SUBSCRIPTION,
})
@IsEnum(PaymentBusinessType, { message: '业务类型无效' })
businessType: PaymentBusinessType;
@ApiProperty({
description: '业务关联ID如订阅套餐ID、积分包ID等',
example: 'sub_premium_monthly',
})
@IsString({ message: '业务ID必须是字符串' })
@Length(1, 100, { message: '业务ID长度必须在1-100字符之间' })
businessId: string;
@ApiProperty({
description: '商品描述',
example: '高级会员月度订阅',
})
@IsString({ message: '商品描述必须是字符串' })
@Length(1, 200, { message: '商品描述长度必须在1-200字符之间' })
description: string;
@ApiProperty({
description: '支付金额(分)',
example: 2980,
minimum: 1,
maximum: 100000000,
})
@IsNumber({}, { message: '支付金额必须是数字' })
@IsPositive({ message: '支付金额必须大于0' })
@Min(1, { message: '支付金额不能少于1分' })
@Max(100000000, { message: '支付金额不能超过1000000元' })
@Type(() => Number)
amount: number;
@ApiProperty({
description: '货币类型',
example: 'CNY',
required: false,
default: 'CNY',
})
@IsOptional()
@IsString({ message: '货币类型必须是字符串' })
@Length(3, 3, { message: '货币类型必须是3位字符' })
currency?: string = 'CNY';
@ApiProperty({
description: '平台用户IDopenid',
example: 'o6_bmjrPTlm6_2sgVt7hMZOPfL2M',
})
@IsString({ message: '平台用户ID必须是字符串' })
@Length(1, 100, { message: '平台用户ID长度必须在1-100字符之间' })
platformUserId: string;
@ApiProperty({
description: '支付超时时间(分钟)',
example: 30,
required: false,
default: 30,
})
@IsOptional()
@IsNumber({}, { message: '超时时间必须是数字' })
@Min(5, { message: '超时时间不能少于5分钟' })
@Max(1440, { message: '超时时间不能超过24小时' })
@Type(() => Number)
expireMinutes?: number = 30;
@ApiProperty({
description: '扩展元数据',
example: { couponId: 'COUPON123', promotionId: 'PROMO456' },
required: false,
})
@IsOptional()
metadata?: any;
}

10
src/payment/dto/index.ts Normal file
View File

@@ -0,0 +1,10 @@
export { CreatePaymentOrderDto } from './create-payment-order.dto';
export {
PaymentOrderResponseDto,
PaymentOrderQueryResponseDto,
} from './payment-order-response.dto';
export {
RefundRequestDto,
RefundResponseDto,
RefundQueryResponseDto,
} from './refund-request.dto';

View File

@@ -0,0 +1,158 @@
import { ApiProperty } from '@nestjs/swagger';
import {
PaymentMethod,
PaymentOrderStatus,
} from '../interfaces/payment.interface';
/**
* 支付订单响应 DTO
* 定义创建支付订单后返回给前端的数据结构
*/
export class PaymentOrderResponseDto {
@ApiProperty({
description: '支付订单ID',
example: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
})
orderId: string;
@ApiProperty({
description: '系统订单号',
example: 'WX1640995200123ABCDEF',
})
orderNo: string;
@ApiProperty({
description: '第三方订单ID',
example: '20211231123456789012345678',
nullable: true,
})
thirdPartyOrderId?: string;
@ApiProperty({
description: '支付方式',
enum: PaymentMethod,
example: PaymentMethod.WECHAT_MINIPROGRAM,
})
paymentMethod: PaymentMethod;
@ApiProperty({
description: '订单状态',
enum: PaymentOrderStatus,
example: PaymentOrderStatus.PENDING,
})
status: PaymentOrderStatus;
@ApiProperty({
description: '支付金额(分)',
example: 2980,
})
amount: number;
@ApiProperty({
description: '货币类型',
example: 'CNY',
})
currency: string;
@ApiProperty({
description: '商品描述',
example: '高级会员月度订阅',
})
description: string;
@ApiProperty({
description: '支付参数(用于前端调起支付)',
example: {
timeStamp: '1640995200',
nonceStr: 'abc123',
package: 'prepay_id=wx123456789',
signType: 'RSA',
paySign: 'signature123',
},
})
paymentParams: any;
@ApiProperty({
description: '订单过期时间',
example: '2023-12-31T23:59:59.000Z',
nullable: true,
})
expiredAt?: Date;
@ApiProperty({
description: '订单创建时间',
example: '2023-12-31T12:00:00.000Z',
})
createdAt: Date;
}
/**
* 支付订单查询响应 DTO
* 定义查询支付订单状态返回的数据结构
*/
export class PaymentOrderQueryResponseDto {
@ApiProperty({
description: '支付订单ID',
example: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
})
orderId: string;
@ApiProperty({
description: '系统订单号',
example: 'WX1640995200123ABCDEF',
})
orderNo: string;
@ApiProperty({
description: '第三方订单ID',
example: '20211231123456789012345678',
nullable: true,
})
thirdPartyOrderId?: string;
@ApiProperty({
description: '订单状态',
enum: PaymentOrderStatus,
example: PaymentOrderStatus.PAID,
})
status: PaymentOrderStatus;
@ApiProperty({
description: '订单金额(分)',
example: 2980,
})
amount: number;
@ApiProperty({
description: '实际支付金额(分)',
example: 2980,
nullable: true,
})
paidAmount?: number;
@ApiProperty({
description: '支付时间',
example: '2023-12-31T12:30:00.000Z',
nullable: true,
})
paidAt?: Date;
@ApiProperty({
description: '第三方交易号',
example: '1217752501201407033233368018',
nullable: true,
})
thirdPartyTransactionId?: string;
@ApiProperty({
description: '创建时间',
example: '2023-12-31T12:00:00.000Z',
})
createdAt: Date;
@ApiProperty({
description: '更新时间',
example: '2023-12-31T12:30:00.000Z',
})
updatedAt: Date;
}

View File

@@ -0,0 +1,173 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNumber, IsPositive, Min, Length } from 'class-validator';
import { Type } from 'class-transformer';
/**
* 退款申请 DTO
* 定义退款申请的请求参数
*/
export class RefundRequestDto {
@ApiProperty({
description: '支付订单ID',
example: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
})
@IsString({ message: '订单ID必须是字符串' })
@Length(1, 36, { message: '订单ID长度必须在1-36字符之间' })
orderId: string;
@ApiProperty({
description: '退款金额(分)',
example: 2980,
})
@IsNumber({}, { message: '退款金额必须是数字' })
@IsPositive({ message: '退款金额必须大于0' })
@Min(1, { message: '退款金额不能少于1分' })
@Type(() => Number)
refundAmount: number;
@ApiProperty({
description: '退款原因',
example: '用户申请退款',
})
@IsString({ message: '退款原因必须是字符串' })
@Length(1, 500, { message: '退款原因长度必须在1-500字符之间' })
reason: string;
}
/**
* 退款响应 DTO
* 定义退款申请处理结果的数据结构
*/
export class RefundResponseDto {
@ApiProperty({
description: '退款记录ID',
example: 'a47ac10b-58cc-4372-a567-0e02b2c3d479',
})
refundId: string;
@ApiProperty({
description: '退款单号',
example: 'WXR1640995200123ABCDEF',
})
refundNo: string;
@ApiProperty({
description: '第三方退款ID',
example: '1217752501201407033233368018',
nullable: true,
})
thirdPartyRefundId?: string;
@ApiProperty({
description: '退款状态',
example: 'pending',
})
status: string;
@ApiProperty({
description: '退款金额(分)',
example: 2980,
})
refundAmount: number;
@ApiProperty({
description: '退款原因',
example: '用户申请退款',
})
reason: string;
@ApiProperty({
description: '预计到账时间',
example: '2023-12-31T15:00:00.000Z',
nullable: true,
})
estimatedArrivalTime?: Date;
@ApiProperty({
description: '申请时间',
example: '2023-12-31T12:30:00.000Z',
})
createdAt: Date;
}
/**
* 退款状态查询响应 DTO
* 定义查询退款状态返回的数据结构
*/
export class RefundQueryResponseDto {
@ApiProperty({
description: '退款记录ID',
example: 'a47ac10b-58cc-4372-a567-0e02b2c3d479',
})
refundId: string;
@ApiProperty({
description: '退款单号',
example: 'WXR1640995200123ABCDEF',
})
refundNo: string;
@ApiProperty({
description: '第三方退款ID',
example: '1217752501201407033233368018',
nullable: true,
})
thirdPartyRefundId?: string;
@ApiProperty({
description: '退款状态',
example: 'success',
})
status: string;
@ApiProperty({
description: '退款金额(分)',
example: 2980,
})
refundAmount: number;
@ApiProperty({
description: '原订单金额(分)',
example: 2980,
})
originalAmount: number;
@ApiProperty({
description: '退款原因',
example: '用户申请退款',
})
reason: string;
@ApiProperty({
description: '退款时间',
example: '2023-12-31T12:45:00.000Z',
nullable: true,
})
refundedAt?: Date;
@ApiProperty({
description: '到账时间',
example: '2023-12-31T13:00:00.000Z',
nullable: true,
})
arrivedAt?: Date;
@ApiProperty({
description: '失败原因',
example: '银行卡信息错误',
nullable: true,
})
failureReason?: string;
@ApiProperty({
description: '申请时间',
example: '2023-12-31T12:30:00.000Z',
})
createdAt: Date;
@ApiProperty({
description: '更新时间',
example: '2023-12-31T12:45:00.000Z',
})
updatedAt: Date;
}

View File

@@ -0,0 +1,7 @@
export { PaymentOrderEntity } from './payment-order.entity';
export {
PaymentTransactionEntity,
TransactionType,
TransactionStatus,
} from './payment-transaction.entity';
export { RefundRecordEntity } from './refund-record.entity';

View File

@@ -0,0 +1,195 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
OneToMany,
Index,
} from 'typeorm';
import { UserEntity } from '../../entities/user.entity';
import { PlatformType } from '../../entities/platform-user.entity';
import {
PaymentMethod,
PaymentOrderStatus,
PaymentBusinessType,
} from '../interfaces/payment.interface';
import { PaymentTransactionEntity } from './payment-transaction.entity';
import { RefundRecordEntity } from './refund-record.entity';
/**
* 支付订单实体类
* 记录用户的支付订单信息,支持多平台支付方式
* 提供完整的订单生命周期管理和业务关联
*/
@Entity('payment_orders')
@Index(['orderNo'], { unique: true })
@Index(['userId', 'status'])
@Index(['platform', 'businessType'])
@Index(['createdAt'])
export class PaymentOrderEntity {
/** 主键 - 支付订单的唯一标识符 */
@PrimaryGeneratedColumn('uuid')
id: string;
/** 用户ID - 关联到用户实体 */
@Column()
userId: string;
/** 支付平台 - 支付发生的平台(微信、抖音) */
@Column({
type: 'enum',
enum: PlatformType,
enumName: 'platform_type',
})
platform: PlatformType;
/** 订单号 - 系统内部唯一订单编号 */
@Column({ length: 64, unique: true })
orderNo: string;
/** 第三方订单ID - 支付平台返回的订单标识 */
@Column({ length: 100, nullable: true })
thirdPartyOrderId?: string;
/** 支付方式 - 具体的支付类型(小程序支付等) */
@Column({
type: 'enum',
enum: PaymentMethod,
})
paymentMethod: PaymentMethod;
/** 订单状态 - 支付订单的当前状态 */
@Column({
type: 'enum',
enum: PaymentOrderStatus,
default: PaymentOrderStatus.PENDING,
})
status: PaymentOrderStatus;
/** 业务类型 - 标识支付的业务场景(会员订阅、积分购买等) */
@Column({
type: 'enum',
enum: PaymentBusinessType,
})
businessType: PaymentBusinessType;
/** 业务关联ID - 关联的具体业务记录ID */
@Column({ length: 100 })
businessId: string;
/** 商品描述 - 用户可见的商品或服务描述 */
@Column({ length: 200 })
description: string;
/** 订单金额 - 应付金额,单位为分 */
@Column({ type: 'bigint' })
amount: number;
/** 实际支付金额 - 用户实际支付的金额,单位为分 */
@Column({ type: 'bigint', nullable: true })
paidAmount: number;
/** 货币类型 - 支付货币单位,默认为人民币 */
@Column({ length: 10, default: 'CNY' })
currency: string;
/** 平台用户ID - 支付平台的用户标识openid */
@Column({ length: 100 })
platformUserId: string;
/** 支付参数 - 前端调起支付所需的参数JSON格式存储 */
@Column({ type: 'json', nullable: true })
paymentParams: any;
/** 支付时间 - 支付成功的时间戳 */
@Column({ type: 'timestamp', nullable: true })
paidAt: Date;
/** 订单过期时间 - 支付订单的有效截止时间 */
@Column({ type: 'timestamp', nullable: true })
expiredAt: Date;
/** 回调通知地址 - 支付结果回调的URL地址 */
@Column({ length: 500, nullable: true })
notifyUrl: string;
/** 扩展元数据 - 存储订单的附加信息,如优惠券、活动信息等 */
@Column({ type: 'json', nullable: true })
metadata: any;
/** 错误信息 - 支付失败时的错误描述 */
@Column({ type: 'text', nullable: true })
errorMessage: string;
/** 创建时间 - 订单创建的时间戳 */
@CreateDateColumn()
createdAt: Date;
/** 更新时间 - 订单最后修改时间,用于状态变更追踪 */
@UpdateDateColumn()
updatedAt: Date;
/** 用户关系 - 与用户实体的多对一关系 */
@ManyToOne(() => UserEntity, (user) => user.paymentOrders, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'userId' })
user: UserEntity;
/** 交易记录关系 - 与支付交易记录的一对多关系 */
@OneToMany(
() => PaymentTransactionEntity,
(transaction) => transaction.paymentOrder,
)
transactions: PaymentTransactionEntity[];
/** 退款记录关系 - 与退款记录的一对多关系 */
@OneToMany(() => RefundRecordEntity, (refund) => refund.paymentOrder)
refunds: RefundRecordEntity[];
/**
* 检查订单是否已过期
* @returns 是否过期
*/
isExpired(): boolean {
if (!this.expiredAt) return false;
return new Date() > this.expiredAt;
}
/**
* 检查订单是否可以支付
* @returns 是否可支付
*/
canPay(): boolean {
return this.status === PaymentOrderStatus.PENDING && !this.isExpired();
}
/**
* 检查订单是否可以退款
* @returns 是否可退款
*/
canRefund(): boolean {
return (
this.status === PaymentOrderStatus.PAID ||
this.status === PaymentOrderStatus.CONFIRMED
);
}
/**
* 获取可退款金额
* @returns 可退款金额(分)
*/
getRefundableAmount(): number {
if (!this.canRefund()) return 0;
const totalRefunded =
this.refunds
?.filter((refund) => refund.status === 'success')
?.reduce((sum, refund) => sum + refund.refundAmount, 0) || 0;
return (this.paidAmount || this.amount) - totalRefunded;
}
}

View File

@@ -0,0 +1,221 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { PaymentOrderEntity } from './payment-order.entity';
/**
* 交易类型枚举
* 定义不同的交易操作类型
*/
export enum TransactionType {
PAYMENT = 'payment', // 支付交易
REFUND = 'refund', // 退款交易
CALLBACK = 'callback', // 回调通知
}
/**
* 交易状态枚举
* 定义交易记录的处理状态
*/
export enum TransactionStatus {
PENDING = 'pending', // 处理中
SUCCESS = 'success', // 成功
FAILED = 'failed', // 失败
}
/**
* 支付交易记录实体类
* 记录与第三方支付平台的所有交易明细
* 提供完整的支付流水追踪和对账支持
* 包含支付、退款、回调等所有交易类型
*/
@Entity('payment_transactions')
@Index(['paymentOrderId'])
@Index(['thirdPartyTransactionId'])
@Index(['transactionType', 'status'])
@Index(['createdAt'])
export class PaymentTransactionEntity {
/** 主键 - 交易记录的唯一标识符 */
@PrimaryGeneratedColumn('uuid')
id: string;
/** 支付订单ID - 关联到支付订单实体 */
@Column()
paymentOrderId: string;
/** 第三方交易号 - 支付平台返回的交易流水号 */
@Column({ length: 100, nullable: true })
thirdPartyTransactionId: string;
/** 第三方订单ID - 支付平台的订单标识 */
@Column({ length: 100, nullable: true })
thirdPartyOrderId: string;
/** 交易类型 - 标识交易的具体类型(支付、退款、回调) */
@Column({
type: 'enum',
enum: TransactionType,
})
transactionType: TransactionType;
/** 交易状态 - 交易的处理状态 */
@Column({
type: 'enum',
enum: TransactionStatus,
default: TransactionStatus.PENDING,
})
status: TransactionStatus;
/** 交易金额 - 本次交易涉及的金额,单位为分 */
@Column({ type: 'bigint' })
amount: number;
/** 货币类型 - 交易的货币单位 */
@Column({ length: 10, default: 'CNY' })
currency: string;
/** 交易时间 - 第三方平台的交易时间 */
@Column({ type: 'timestamp', nullable: true })
transactionTime: Date;
/** 交易描述 - 交易的文字描述信息 */
@Column({ length: 200, nullable: true })
description: string;
/** 请求数据 - 发送给第三方平台的请求参数JSON格式 */
@Column({ type: 'json', nullable: true })
requestData: any;
/** 响应数据 - 第三方平台返回的响应数据JSON格式 */
@Column({ type: 'json', nullable: true })
responseData: any;
/** 回调数据 - 第三方平台回调的原始数据JSON格式 */
@Column({ type: 'json', nullable: true })
callbackData: any;
/** 错误代码 - 交易失败时的错误代码 */
@Column({ length: 50, nullable: true })
errorCode: string;
/** 错误信息 - 交易失败的详细错误描述 */
@Column({ type: 'text', nullable: true })
errorMessage: string;
/** 处理次数 - 记录处理重试次数,用于异常分析 */
@Column({ type: 'integer', default: 1 })
processCount: number;
/** 最后处理时间 - 记录最后一次处理的时间 */
@Column({ type: 'timestamp', nullable: true })
lastProcessedAt: Date;
/** 扩展元数据 - 存储交易的附加信息和调试数据 */
@Column({ type: 'json', nullable: true })
metadata: any;
/** 创建时间 - 交易记录创建的时间戳 */
@CreateDateColumn()
createdAt: Date;
/** 更新时间 - 记录最后修改时间 */
@UpdateDateColumn()
updatedAt: Date;
/** 支付订单关系 - 与支付订单的多对一关系 */
@ManyToOne(() => PaymentOrderEntity, (order) => order.transactions, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'paymentOrderId' })
paymentOrder: PaymentOrderEntity;
/**
* 检查交易是否成功
* @returns 是否成功
*/
isSuccess(): boolean {
return this.status === TransactionStatus.SUCCESS;
}
/**
* 检查交易是否失败
* @returns 是否失败
*/
isFailed(): boolean {
return this.status === TransactionStatus.FAILED;
}
/**
* 检查是否为支付交易
* @returns 是否为支付交易
*/
isPaymentTransaction(): boolean {
return this.transactionType === TransactionType.PAYMENT;
}
/**
* 检查是否为退款交易
* @returns 是否为退款交易
*/
isRefundTransaction(): boolean {
return this.transactionType === TransactionType.REFUND;
}
/**
* 检查是否为回调记录
* @returns 是否为回调记录
*/
isCallbackTransaction(): boolean {
return this.transactionType === TransactionType.CALLBACK;
}
/**
* 增加处理次数并更新处理时间
*/
incrementProcessCount(): void {
this.processCount += 1;
this.lastProcessedAt = new Date();
}
/**
* 标记交易为成功状态
* @param transactionTime 交易时间
* @param thirdPartyTransactionId 第三方交易号
*/
markAsSuccess(
transactionTime?: Date,
thirdPartyTransactionId?: string,
): void {
this.status = TransactionStatus.SUCCESS;
if (transactionTime) {
this.transactionTime = transactionTime;
}
if (thirdPartyTransactionId) {
this.thirdPartyTransactionId = thirdPartyTransactionId;
}
this.lastProcessedAt = new Date();
}
/**
* 标记交易为失败状态
* @param errorCode 错误代码
* @param errorMessage 错误信息
*/
markAsFailed(errorCode?: string, errorMessage?: string): void {
this.status = TransactionStatus.FAILED;
if (errorCode) {
this.errorCode = errorCode;
}
if (errorMessage) {
this.errorMessage = errorMessage;
}
this.lastProcessedAt = new Date();
}
}

View File

@@ -0,0 +1,256 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
Index,
} from 'typeorm';
import { PaymentOrderEntity } from './payment-order.entity';
import { RefundStatus } from '../interfaces/payment.interface';
/**
* 退款记录实体类
* 记录支付订单的退款申请和处理结果
* 支持部分退款和多次退款场景
* 提供完整的退款流程追踪和状态管理
*/
@Entity('refund_records')
@Index(['paymentOrderId'])
@Index(['refundNo'], { unique: true })
@Index(['thirdPartyRefundId'])
@Index(['status'])
@Index(['createdAt'])
export class RefundRecordEntity {
/** 主键 - 退款记录的唯一标识符 */
@PrimaryGeneratedColumn('uuid')
id: string;
/** 支付订单ID - 关联到原支付订单 */
@Column()
paymentOrderId: string;
/** 退款单号 - 系统内部的退款单编号,全局唯一 */
@Column({ length: 64, unique: true })
refundNo: string;
/** 第三方退款ID - 支付平台返回的退款标识 */
@Column({ length: 100, nullable: true })
thirdPartyRefundId: string;
/** 第三方交易号 - 原支付交易的第三方交易号 */
@Column({ length: 100, nullable: true })
thirdPartyTransactionId: string;
/** 退款状态 - 退款处理的当前状态 */
@Column({
type: 'enum',
enum: RefundStatus,
default: RefundStatus.PENDING,
})
status: RefundStatus;
/** 退款金额 - 申请退款的金额,单位为分 */
@Column({ type: 'bigint' })
refundAmount: number;
/** 原订单金额 - 原支付订单的总金额,用于计算退款比例 */
@Column({ type: 'bigint' })
originalAmount: number;
/** 货币类型 - 退款的货币单位 */
@Column({ length: 10, default: 'CNY' })
currency: string;
/** 退款原因 - 用户或商家申请退款的原因说明 */
@Column({ length: 500 })
reason: string;
/** 退款描述 - 退款的详细说明信息 */
@Column({ type: 'text', nullable: true })
description: string;
/** 申请人ID - 发起退款申请的用户ID */
@Column({ nullable: true })
requestedBy: string;
/** 审批人ID - 审批退款申请的管理员ID */
@Column({ nullable: true })
approvedBy: string;
/** 退款时间 - 第三方平台确认退款的时间 */
@Column({ type: 'timestamp', nullable: true })
refundedAt: Date;
/** 到账时间 - 退款资金实际到账的时间 */
@Column({ type: 'timestamp', nullable: true })
arrivedAt: Date;
/** 预计到账时间 - 平台返回的预计到账时间 */
@Column({ type: 'timestamp', nullable: true })
estimatedArrivalTime: Date;
/** 失败原因 - 退款失败时的详细原因说明 */
@Column({ type: 'text', nullable: true })
failureReason: string;
/** 错误代码 - 退款失败的错误代码 */
@Column({ length: 50, nullable: true })
errorCode: string;
/** 请求数据 - 发送给第三方的退款请求参数 */
@Column({ type: 'json', nullable: true })
requestData: any;
/** 响应数据 - 第三方平台返回的退款响应数据 */
@Column({ type: 'json', nullable: true })
responseData: any;
/** 回调数据 - 第三方平台的退款状态回调数据 */
@Column({ type: 'json', nullable: true })
callbackData: any;
/** 处理次数 - 记录退款处理的重试次数 */
@Column({ type: 'integer', default: 1 })
processCount: number;
/** 最后处理时间 - 最后一次处理退款的时间 */
@Column({ type: 'timestamp', nullable: true })
lastProcessedAt: Date;
/** 扩展元数据 - 存储退款的附加信息 */
@Column({ type: 'json', nullable: true })
metadata: any;
/** 创建时间 - 退款记录创建时间 */
@CreateDateColumn()
createdAt: Date;
/** 更新时间 - 记录最后修改时间 */
@UpdateDateColumn()
updatedAt: Date;
/** 支付订单关系 - 与支付订单的多对一关系 */
@ManyToOne(() => PaymentOrderEntity, (order) => order.refunds, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'paymentOrderId' })
paymentOrder: PaymentOrderEntity;
/**
* 检查退款是否成功
* @returns 是否成功
*/
isSuccess(): boolean {
return this.status === RefundStatus.SUCCESS;
}
/**
* 检查退款是否失败
* @returns 是否失败
*/
isFailed(): boolean {
return this.status === RefundStatus.FAILED;
}
/**
* 检查退款是否处理中
* @returns 是否处理中
*/
isProcessing(): boolean {
return (
this.status === RefundStatus.PENDING ||
this.status === RefundStatus.PROCESSING
);
}
/**
* 检查是否为全额退款
* @returns 是否为全额退款
*/
isFullRefund(): boolean {
return this.refundAmount >= this.originalAmount;
}
/**
* 获取退款比例
* @returns 退款比例0-1之间的小数
*/
getRefundRatio(): number {
if (this.originalAmount <= 0) return 0;
return this.refundAmount / this.originalAmount;
}
/**
* 计算退款处理耗时(分钟)
* @returns 处理耗时未完成时返回null
*/
getProcessingDurationMinutes(): number | null {
if (!this.refundedAt) return null;
return Math.floor(
(this.refundedAt.getTime() - this.createdAt.getTime()) / (1000 * 60),
);
}
/**
* 增加处理次数并更新处理时间
*/
incrementProcessCount(): void {
this.processCount += 1;
this.lastProcessedAt = new Date();
}
/**
* 标记退款为成功状态
* @param refundedAt 退款时间
* @param thirdPartyRefundId 第三方退款ID
* @param estimatedArrivalTime 预计到账时间
*/
markAsSuccess(
refundedAt?: Date,
thirdPartyRefundId?: string,
estimatedArrivalTime?: Date,
): void {
this.status = RefundStatus.SUCCESS;
if (refundedAt) {
this.refundedAt = refundedAt;
}
if (thirdPartyRefundId) {
this.thirdPartyRefundId = thirdPartyRefundId;
}
if (estimatedArrivalTime) {
this.estimatedArrivalTime = estimatedArrivalTime;
}
this.lastProcessedAt = new Date();
}
/**
* 标记退款为失败状态
* @param errorCode 错误代码
* @param failureReason 失败原因
*/
markAsFailed(errorCode?: string, failureReason?: string): void {
this.status = RefundStatus.FAILED;
if (errorCode) {
this.errorCode = errorCode;
}
if (failureReason) {
this.failureReason = failureReason;
}
this.lastProcessedAt = new Date();
}
/**
* 标记退款为处理中状态
* @param thirdPartyRefundId 第三方退款ID
*/
markAsProcessing(thirdPartyRefundId?: string): void {
this.status = RefundStatus.PROCESSING;
if (thirdPartyRefundId) {
this.thirdPartyRefundId = thirdPartyRefundId;
}
this.lastProcessedAt = new Date();
}
}

28
src/payment/index.ts Normal file
View File

@@ -0,0 +1,28 @@
// 模块
export { PaymentModule } from './payment.module';
// 接口
export * from './interfaces/payment.interface';
// DTO
export * from './dto';
// 实体
export * from './entities';
// 服务
export {
PaymentAdapterFactory,
UnifiedPaymentService,
PaymentWebhookService,
} from './services';
// 适配器
export {
BasePaymentAdapter,
WechatPaymentAdapter,
DouyinPaymentAdapter,
} from './adapters';
// 控制器
export { PaymentController } from './controllers';

View File

@@ -0,0 +1,361 @@
import { PlatformType } from '../../entities/platform-user.entity';
/**
* 支付方式枚举
* 定义支持的不同支付类型
*/
export enum PaymentMethod {
WECHAT_MINIPROGRAM = 'wechat_miniprogram', // 微信小程序支付
BYTEDANCE_MINIPROGRAM = 'bytedance_miniprogram', // 字节跳动小程序支付
}
/**
* 支付订单状态枚举
* 定义支付订单的生命周期状态
*/
export enum PaymentOrderStatus {
PENDING = 'pending', // 待支付 - 订单已创建,等待用户支付
PAID = 'paid', // 已支付 - 支付成功,等待确认
CONFIRMED = 'confirmed', // 已确认 - 支付确认完成,业务处理成功
CANCELLED = 'cancelled', // 已取消 - 订单被取消
FAILED = 'failed', // 支付失败 - 支付过程中发生错误
REFUNDED = 'refunded', // 已退款 - 支付已退款
}
/**
* 支付业务类型枚举
* 标识不同的支付业务场景
*/
export enum PaymentBusinessType {
SUBSCRIPTION = 'subscription', // 会员订阅支付
CREDIT_PURCHASE = 'credit_purchase', // 积分购买支付
TEMPLATE_UNLOCK = 'template_unlock', // 模板解锁支付
}
/**
* 退款状态枚举
* 定义退款处理的状态流转
*/
export enum RefundStatus {
PENDING = 'pending', // 退款申请中
PROCESSING = 'processing', // 退款处理中
SUCCESS = 'success', // 退款成功
FAILED = 'failed', // 退款失败
}
/**
* 支付适配器接口
* 定义不同支付平台的统一接口规范
*/
export interface IPaymentAdapter {
/** 支付平台类型 */
platform: PlatformType;
/** 支付方式 */
paymentMethod: PaymentMethod;
/**
* 创建支付订单
* @param orderData 订单数据
* @returns 支付订单结果
*/
createPaymentOrder(
orderData: CreatePaymentOrderData,
): Promise<PaymentOrderResult>;
/**
* 查询支付订单状态
* @param orderId 本地订单ID
* @param thirdPartyOrderId 第三方订单ID
* @returns 订单状态信息
*/
queryPaymentOrder(
orderId: string,
thirdPartyOrderId?: string,
): Promise<PaymentOrderQueryResult>;
/**
* 处理支付回调
* @param callbackData 第三方支付平台回调数据
* @returns 回调处理结果
*/
handlePaymentCallback(callbackData: any): Promise<PaymentCallbackResult>;
/**
* 申请退款
* @param refundData 退款申请数据
* @returns 退款申请结果
*/
refundPayment(refundData: RefundRequestData): Promise<RefundResult>;
/**
* 查询退款状态
* @param refundId 退款ID
* @returns 退款状态信息
*/
queryRefundStatus(refundId: string): Promise<RefundQueryResult>;
/**
* 验证支付回调签名
* @param callbackData 回调数据
* @param signature 签名
* @returns 验证结果
*/
verifySignature(callbackData: any, signature: string): Promise<boolean>;
/**
* 下载对账单
* @param date 对账日期
* @returns 对账单数据
*/
downloadBill(date: string): Promise<BillData>;
}
/**
* 创建支付订单数据接口
*/
export interface CreatePaymentOrderData {
/** 本地订单号 */
orderNo: string;
/** 支付金额(分) */
amount: number;
/** 货币类型 */
currency: string;
/** 商品描述 */
description: string;
/** 用户ID */
userId: string;
/** 平台用户ID (openid) */
platformUserId: string;
/** 业务类型 */
businessType: PaymentBusinessType;
/** 业务关联ID */
businessId: string;
/** 支付超时时间(分钟) */
expireMinutes?: number;
/** 附加数据 */
metadata?: any;
/** 回调地址 */
notifyUrl: string;
}
/**
* 支付订单创建结果
*/
export interface PaymentOrderResult {
/** 本地订单ID */
orderId: string;
/** 第三方订单ID */
thirdPartyOrderId: string;
/** 支付参数(用于前端调用支付) */
paymentParams: any;
/** 支付跳转URL如果需要 */
paymentUrl?: string;
/** 订单状态 */
status: PaymentOrderStatus;
/** 支付方式 */
paymentMethod: PaymentMethod;
/** 创建时间 */
createdAt: Date;
}
/**
* 支付订单查询结果
*/
export interface PaymentOrderQueryResult {
/** 订单ID */
orderId: string;
/** 第三方订单ID */
thirdPartyOrderId: string;
/** 订单状态 */
status: PaymentOrderStatus;
/** 支付金额 */
amount: number;
/** 实际支付金额 */
paidAmount?: number;
/** 支付时间 */
paidAt?: Date;
/** 第三方交易号 */
thirdPartyTransactionId?: string;
/** 第三方原始数据 */
rawData?: any;
}
/**
* 支付回调处理结果
*/
export interface PaymentCallbackResult {
/** 是否处理成功 */
success: boolean;
/** 订单ID */
orderId: string;
/** 订单状态 */
status: PaymentOrderStatus;
/** 支付时间 */
paidAt?: Date;
/** 第三方交易号 */
thirdPartyTransactionId?: string;
/** 错误信息 */
errorMessage?: string;
/** 需要返回给第三方的响应数据 */
responseData?: any;
}
/**
* 退款申请数据
*/
export interface RefundRequestData {
/** 原订单ID */
orderId: string;
/** 退款金额(分) */
refundAmount: number;
/** 退款原因 */
reason: string;
/** 退款单号 */
refundNo: string;
/** 第三方订单ID */
thirdPartyOrderId: string;
/** 第三方交易号 */
thirdPartyTransactionId: string;
}
/**
* 退款申请结果
*/
export interface RefundResult {
/** 退款ID */
refundId: string;
/** 第三方退款ID */
thirdPartyRefundId: string;
/** 退款状态 */
status: RefundStatus;
/** 退款金额 */
refundAmount: number;
/** 申请时间 */
createdAt: Date;
/** 预计到账时间 */
estimatedArrivalTime?: Date;
}
/**
* 退款状态查询结果
*/
export interface RefundQueryResult {
/** 退款ID */
refundId: string;
/** 第三方退款ID */
thirdPartyRefundId: string;
/** 退款状态 */
status: RefundStatus;
/** 退款金额 */
refundAmount: number;
/** 原订单金额 */
originalAmount: number;
/** 退款原因 */
reason: string;
/** 退款时间 */
refundedAt?: Date;
/** 到账时间 */
arrivedAt?: Date;
/** 失败原因 */
failureReason?: string;
/** 创建时间 */
createdAt: Date;
/** 更新时间 */
updatedAt: Date;
}
/**
* 对账单数据
*/
export interface BillData {
/** 对账日期 */
billDate: string;
/** 总交易笔数 */
totalCount: number;
/** 总交易金额 */
totalAmount: number;
/** 对账单记录 */
records: BillRecord[];
/** 原始数据 */
rawData?: string;
}
/**
* 对账单记录
*/
export interface BillRecord {
/** 第三方交易号 */
thirdPartyTransactionId: string;
/** 第三方订单号 */
thirdPartyOrderId: string;
/** 交易时间 */
transactionTime: Date;
/** 交易金额 */
amount: number;
/** 交易状态 */
status: string;
/** 交易类型 */
type: string;
/** 商品描述 */
description?: string;
}

View File

@@ -0,0 +1,140 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { HttpModule } from '@nestjs/axios';
import { JwtModule } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
// 实体
import { PaymentOrderEntity } from './entities/payment-order.entity';
import { PaymentTransactionEntity } from './entities/payment-transaction.entity';
import { RefundRecordEntity } from './entities/refund-record.entity';
import { UserEntity } from '../entities/user.entity';
import { UserSubscriptionEntity } from '../entities/user-subscription.entity';
import { UserCreditEntity } from '../entities/user-credit.entity';
// 适配器
import { WechatPaymentAdapter, DouyinPaymentAdapter } from './adapters';
// 服务
import {
PaymentAdapterFactory,
UnifiedPaymentService,
PaymentWebhookService,
} from './services';
// 控制器
import { PaymentController } from './controllers';
/**
* 支付模块
* 集成支付系统的所有核心功能和服务
* 提供统一的支付接口和多平台支付适配
* 支持订单管理、交易记录、退款处理等完整支付流程
*/
@Module({
imports: [
// TypeORM 实体注册
TypeOrmModule.forFeature([
// 支付相关实体
PaymentOrderEntity,
PaymentTransactionEntity,
RefundRecordEntity,
// 业务关联实体
UserEntity,
UserSubscriptionEntity,
UserCreditEntity,
]),
// HTTP 客户端模块
HttpModule.register({
timeout: 30000, // 30秒超时适合支付接口调用
maxRedirects: 3,
validateStatus: (status) => status < 500, // 接受所有非5xx错误的响应
}),
// JWT 模块配置(用于支付回调验证等)
JwtModule.registerAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: { expiresIn: '24h' },
}),
}),
],
providers: [
// 支付适配器
WechatPaymentAdapter,
DouyinPaymentAdapter,
// 核心服务
PaymentAdapterFactory,
UnifiedPaymentService,
PaymentWebhookService,
// 适配器注册 Provider
{
provide: 'PAYMENT_ADAPTERS_REGISTRATION',
useFactory: (
factory: PaymentAdapterFactory,
wechatAdapter: WechatPaymentAdapter,
douyinAdapter: DouyinPaymentAdapter,
) => {
// 注册微信支付适配器
factory.registerAdapter(
wechatAdapter.platform,
wechatAdapter.paymentMethod,
wechatAdapter,
);
// 注册字节跳动支付适配器
factory.registerAdapter(
douyinAdapter.platform,
douyinAdapter.paymentMethod,
douyinAdapter,
);
return factory;
},
inject: [
PaymentAdapterFactory,
WechatPaymentAdapter,
DouyinPaymentAdapter,
],
},
],
controllers: [PaymentController],
exports: [
// 导出核心服务供其他模块使用
PaymentAdapterFactory,
UnifiedPaymentService,
PaymentWebhookService,
// 导出 TypeORM Repository 供其他服务使用
TypeOrmModule,
],
})
export class PaymentModule {
constructor(private readonly paymentAdapterFactory: PaymentAdapterFactory) {
// 模块初始化时记录适配器状态
this.logModuleInitialization();
}
/**
* 记录模块初始化信息
* 用于调试和监控模块启动状态
*/
private logModuleInitialization(): void {
console.log('=== 支付模块初始化 ===');
console.log('✓ 支付实体已注册');
console.log('✓ HTTP 模块已配置');
console.log('✓ JWT 模块已配置');
console.log('✓ 支付适配器工厂已就绪');
// 输出适配器注册状态
this.paymentAdapterFactory.logAdapterStatus();
}
}

View File

@@ -0,0 +1,3 @@
export { PaymentAdapterFactory } from './payment-adapter.factory';
export { UnifiedPaymentService } from './unified-payment.service';
export { PaymentWebhookService } from './payment-webhook.service';

View File

@@ -0,0 +1,305 @@
import { Injectable, Logger } from '@nestjs/common';
import {
IPaymentAdapter,
PaymentMethod,
} from '../interfaces/payment.interface';
import { PlatformType } from '../../entities/platform-user.entity';
/**
* 支付适配器工厂类
* 管理不同平台和支付方式的适配器实例
* 提供统一的适配器获取接口和动态注册功能
* 支持运行时的适配器扩展和管理
*/
@Injectable()
export class PaymentAdapterFactory {
private readonly logger = new Logger(PaymentAdapterFactory.name);
/**
* 适配器映射表
* 使用复合键 "platform:paymentMethod" 作为索引
* 存储各种支付平台和支付方式的适配器实例
*/
private readonly adapters = new Map<string, IPaymentAdapter>();
/**
* 注册支付适配器
* 将适配器实例注册到工厂中,供后续获取使用
* @param platform 支付平台
* @param paymentMethod 支付方式
* @param adapter 适配器实例
*/
registerAdapter(
platform: PlatformType,
paymentMethod: PaymentMethod,
adapter: IPaymentAdapter,
): void {
const key = this.getAdapterKey(platform, paymentMethod);
if (this.adapters.has(key)) {
this.logger.warn(`适配器已存在,将被覆盖: ${key}`);
}
this.adapters.set(key, adapter);
this.logger.log(`支付适配器注册成功: ${key}`);
}
/**
* 获取支付适配器
* 根据平台和支付方式获取对应的适配器实例
* @param platform 支付平台
* @param paymentMethod 支付方式
* @returns 适配器实例
* @throws 如果适配器不存在则抛出错误
*/
getAdapter(
platform: PlatformType,
paymentMethod: PaymentMethod,
): IPaymentAdapter {
const key = this.getAdapterKey(platform, paymentMethod);
const adapter = this.adapters.get(key);
if (!adapter) {
const supportedAdapters = Array.from(this.adapters.keys());
throw new Error(
`不支持的支付方式: ${key}。支持的适配器: [${supportedAdapters.join(', ')}]`,
);
}
return adapter;
}
/**
* 检查适配器是否存在
* @param platform 支付平台
* @param paymentMethod 支付方式
* @returns 是否存在
*/
hasAdapter(platform: PlatformType, paymentMethod: PaymentMethod): boolean {
const key = this.getAdapterKey(platform, paymentMethod);
return this.adapters.has(key);
}
/**
* 获取所有已注册的适配器
* @returns 适配器映射表
*/
getAllAdapters(): Map<string, IPaymentAdapter> {
return new Map(this.adapters);
}
/**
* 获取指定平台的所有适配器
* @param platform 支付平台
* @returns 适配器列表
*/
getAdaptersByPlatform(platform: PlatformType): IPaymentAdapter[] {
const platformPrefix = `${platform}:`;
const adapters: IPaymentAdapter[] = [];
for (const [key, adapter] of this.adapters) {
if (key.startsWith(platformPrefix)) {
adapters.push(adapter);
}
}
return adapters;
}
/**
* 获取指定支付方式的所有适配器
* @param paymentMethod 支付方式
* @returns 适配器列表
*/
getAdaptersByPaymentMethod(paymentMethod: PaymentMethod): IPaymentAdapter[] {
const methodSuffix = `:${paymentMethod}`;
const adapters: IPaymentAdapter[] = [];
for (const [key, adapter] of this.adapters) {
if (key.endsWith(methodSuffix)) {
adapters.push(adapter);
}
}
return adapters;
}
/**
* 移除适配器
* @param platform 支付平台
* @param paymentMethod 支付方式
* @returns 是否移除成功
*/
removeAdapter(platform: PlatformType, paymentMethod: PaymentMethod): boolean {
const key = this.getAdapterKey(platform, paymentMethod);
const removed = this.adapters.delete(key);
if (removed) {
this.logger.log(`支付适配器已移除: ${key}`);
} else {
this.logger.warn(`尝试移除不存在的适配器: ${key}`);
}
return removed;
}
/**
* 清空所有适配器
*/
clearAllAdapters(): void {
const count = this.adapters.size;
this.adapters.clear();
this.logger.log(`已清空所有支付适配器,共 ${count}`);
}
/**
* 获取支持的平台列表
* @returns 平台类型数组
*/
getSupportedPlatforms(): PlatformType[] {
const platforms = new Set<PlatformType>();
for (const [key] of this.adapters) {
const [platform] = key.split(':');
platforms.add(platform as PlatformType);
}
return Array.from(platforms);
}
/**
* 获取支持的支付方式列表
* @returns 支付方式数组
*/
getSupportedPaymentMethods(): PaymentMethod[] {
const methods = new Set<PaymentMethod>();
for (const [key] of this.adapters) {
const [, method] = key.split(':');
methods.add(method as PaymentMethod);
}
return Array.from(methods);
}
/**
* 获取适配器统计信息
* @returns 统计信息对象
*/
getStatistics(): {
totalAdapters: number;
platformCount: number;
methodCount: number;
adapterDetails: Array<{
platform: PlatformType;
paymentMethod: PaymentMethod;
adapterName: string;
}>;
} {
const platformCount = this.getSupportedPlatforms().length;
const methodCount = this.getSupportedPaymentMethods().length;
const adapterDetails = Array.from(this.adapters.entries()).map(
([key, adapter]) => {
const [platform, paymentMethod] = key.split(':');
return {
platform: platform as PlatformType,
paymentMethod: paymentMethod as PaymentMethod,
adapterName: adapter.constructor.name,
};
},
);
return {
totalAdapters: this.adapters.size,
platformCount,
methodCount,
adapterDetails,
};
}
/**
* 验证适配器配置
* 检查已注册的适配器是否符合接口规范
* @returns 验证结果
*/
validateAdapters(): {
valid: boolean;
issues: string[];
} {
const issues: string[] = [];
for (const [key, adapter] of this.adapters) {
const [platformStr, methodStr] = key.split(':');
// 验证适配器的平台和方法是否匹配
if (adapter.platform !== platformStr) {
issues.push(
`适配器 ${key} 的平台不匹配: 期望 ${platformStr}, 实际 ${adapter.platform}`,
);
}
if (adapter.paymentMethod !== methodStr) {
issues.push(
`适配器 ${key} 的支付方式不匹配: 期望 ${methodStr}, 实际 ${adapter.paymentMethod}`,
);
}
// 验证必要方法是否存在
const requiredMethods = [
'createPaymentOrder',
'queryPaymentOrder',
'handlePaymentCallback',
'refundPayment',
'queryRefundStatus',
'verifySignature',
'downloadBill',
];
for (const method of requiredMethods) {
if (typeof (adapter as any)[method] !== 'function') {
issues.push(`适配器 ${key} 缺少必要方法: ${method}`);
}
}
}
return {
valid: issues.length === 0,
issues,
};
}
/**
* 生成适配器键
* @param platform 支付平台
* @param paymentMethod 支付方式
* @returns 适配器键
*/
private getAdapterKey(
platform: PlatformType,
paymentMethod: PaymentMethod,
): string {
return `${platform}:${paymentMethod}`;
}
/**
* 记录适配器注册状态(用于调试)
*/
logAdapterStatus(): void {
this.logger.log('=== 支付适配器注册状态 ===');
if (this.adapters.size === 0) {
this.logger.log('未注册任何支付适配器');
return;
}
for (const [key, adapter] of this.adapters) {
this.logger.log(`${key} -> ${adapter.constructor.name}`);
}
const stats = this.getStatistics();
this.logger.log(
`总计: ${stats.totalAdapters} 个适配器, ${stats.platformCount} 个平台, ${stats.methodCount} 种支付方式`,
);
}
}

View File

@@ -0,0 +1,403 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { PaymentCallbackResult } from '../interfaces/payment.interface';
import { PlatformType } from '../../entities/platform-user.entity';
import { PaymentOrderEntity, PaymentTransactionEntity } from '../entities';
import { UnifiedPaymentService } from './unified-payment.service';
/**
* 支付回调处理服务
* 专门处理各支付平台的异步通知
* 提供幂等性保证和重试机制
* 确保回调处理的可靠性和一致性
*/
@Injectable()
export class PaymentWebhookService {
private readonly logger = new Logger(PaymentWebhookService.name);
constructor(
private readonly unifiedPaymentService: UnifiedPaymentService,
private readonly configService: ConfigService,
@InjectRepository(PaymentOrderEntity)
private readonly paymentOrderRepository: Repository<PaymentOrderEntity>,
@InjectRepository(PaymentTransactionEntity)
private readonly transactionRepository: Repository<PaymentTransactionEntity>,
) {}
/**
* 处理微信支付回调
* @param callbackData 微信支付回调数据
* @returns 处理结果
*/
async handleWechatCallback(
callbackData: any,
): Promise<{ code: string; message: string }> {
try {
this.logger.log('收到微信支付回调通知');
// 1. 处理回调
const result = await this.unifiedPaymentService.handlePaymentCallback(
PlatformType.WECHAT,
callbackData,
);
// 2. 记录回调处理结果
await this.recordCallbackResult(
result,
PlatformType.WECHAT,
callbackData,
);
// 3. 返回微信期望的响应格式
if (result.success) {
return { code: 'SUCCESS', message: '成功' };
} else {
return { code: 'FAIL', message: result.errorMessage || '处理失败' };
}
} catch (error) {
this.logger.error('处理微信支付回调失败:', error);
return { code: 'FAIL', message: error.message };
}
}
/**
* 处理抖音支付回调
* @param callbackData 抖音支付回调数据
* @returns 处理结果
*/
async handleDouyinCallback(
callbackData: any,
): Promise<{ err_no: number; err_tips: string }> {
try {
this.logger.log('收到抖音支付回调通知');
// 1. 处理回调
const result = await this.unifiedPaymentService.handlePaymentCallback(
PlatformType.BYTEDANCE,
callbackData,
);
// 2. 记录回调处理结果
await this.recordCallbackResult(
result,
PlatformType.BYTEDANCE,
callbackData,
);
// 3. 返回抖音期望的响应格式
if (result.success) {
return { err_no: 0, err_tips: 'success' };
} else {
return { err_no: 1, err_tips: result.errorMessage || '处理失败' };
}
} catch (error) {
this.logger.error('处理抖音支付回调失败:', error);
return { err_no: 1, err_tips: error.message };
}
}
/**
* 处理微信退款回调
* @param callbackData 微信退款回调数据
* @returns 处理结果
*/
async handleWechatRefundCallback(
callbackData: any,
): Promise<{ code: string; message: string }> {
try {
this.logger.log('收到微信退款回调通知');
// 处理退款回调逻辑
const success = await this.processRefundCallback(
PlatformType.WECHAT,
callbackData,
);
if (success) {
return { code: 'SUCCESS', message: '成功' };
} else {
return { code: 'FAIL', message: '处理失败' };
}
} catch (error) {
this.logger.error('处理微信退款回调失败:', error);
return { code: 'FAIL', message: error.message };
}
}
/**
* 处理抖音退款回调
* @param callbackData 抖音退款回调数据
* @returns 处理结果
*/
async handleDouyinRefundCallback(
callbackData: any,
): Promise<{ err_no: number; err_tips: string }> {
try {
this.logger.log('收到抖音退款回调通知');
// 处理退款回调逻辑
const success = await this.processRefundCallback(
PlatformType.BYTEDANCE,
callbackData,
);
if (success) {
return { err_no: 0, err_tips: 'success' };
} else {
return { err_no: 1, err_tips: '处理失败' };
}
} catch (error) {
this.logger.error('处理抖音退款回调失败:', error);
return { err_no: 1, err_tips: error.message };
}
}
/**
* 验证回调来源IP
* @param platform 支付平台
* @param clientIp 客户端IP
* @returns 是否验证通过
*/
validateCallbackIP(platform: PlatformType, clientIp: string): boolean {
// 获取支付平台的合法IP范围
const allowedIPs = this.getAllowedIPs(platform);
// 检查IP是否在允许范围内
return allowedIPs.some((allowedIP) =>
this.isIPInRange(clientIp, allowedIP),
);
}
/**
* 检查回调幂等性
* @param platform 支付平台
* @param uniqueId 唯一标识符
* @returns 是否已处理过
*/
async checkCallbackIdempotency(
platform: PlatformType,
uniqueId: string,
): Promise<boolean> {
// 检查是否已处理过相同的回调
const existingTransaction = await this.transactionRepository.findOne({
where: {
thirdPartyTransactionId: uniqueId,
transactionType: 'callback' as any,
},
});
return existingTransaction !== null;
}
/**
* 处理回调重试逻辑
* @param platform 支付平台
* @param callbackData 回调数据
* @param retryCount 重试次数
* @returns 处理结果
*/
async handleCallbackWithRetry(
platform: PlatformType,
callbackData: any,
retryCount: number = 0,
): Promise<PaymentCallbackResult> {
const maxRetries = 3;
try {
// 尝试处理回调
const result = await this.unifiedPaymentService.handlePaymentCallback(
platform,
callbackData,
);
if (result.success) {
return result;
}
// 如果失败且未达到最大重试次数,进行重试
if (retryCount < maxRetries) {
this.logger.warn(`回调处理失败,进行第${retryCount + 1}次重试`);
await this.delay(Math.pow(2, retryCount) * 1000); // 指数退避
return await this.handleCallbackWithRetry(
platform,
callbackData,
retryCount + 1,
);
}
return result;
} catch (error) {
this.logger.error(`回调处理异常(重试${retryCount}次):`, error);
// 如果失败且未达到最大重试次数,进行重试
if (retryCount < maxRetries) {
await this.delay(Math.pow(2, retryCount) * 1000);
return await this.handleCallbackWithRetry(
platform,
callbackData,
retryCount + 1,
);
}
throw error;
}
}
/**
* 记录回调处理结果
* @param result 处理结果
* @param platform 支付平台
* @param callbackData 回调数据
*/
private async recordCallbackResult(
result: PaymentCallbackResult,
platform: PlatformType,
callbackData: any,
): Promise<void> {
try {
// 记录回调处理的详细信息,用于后续分析和故障排查
this.logger.log(
`回调处理结果记录: 平台=${platform}, 成功=${result.success}, 订单=${result.orderId}`,
);
// 可以在这里添加更详细的日志记录逻辑
if (!result.success) {
this.logger.error(`回调处理失败详情:`, {
platform,
orderId: result.orderId,
errorMessage: result.errorMessage,
callbackData: JSON.stringify(callbackData),
});
}
} catch (error) {
this.logger.error('记录回调处理结果失败:', error);
}
}
/**
* 处理退款回调
* @param platform 支付平台
* @param callbackData 回调数据
* @returns 处理是否成功
*/
private async processRefundCallback(
platform: PlatformType,
callbackData: any,
): Promise<boolean> {
try {
// 这里处理退款回调的具体逻辑
// 根据不同平台的回调格式解析退款信息
// 更新退款记录状态
// 触发相关业务逻辑(如恢复积分等)
this.logger.log(
`处理${platform}退款回调: ${JSON.stringify(callbackData)}`,
);
return true;
} catch (error) {
this.logger.error('处理退款回调失败:', error);
return false;
}
}
/**
* 获取支付平台允许的IP范围
* @param platform 支付平台
* @returns IP范围列表
*/
private getAllowedIPs(platform: PlatformType): string[] {
// 微信支付回调IP范围
const wechatIPs = [
'101.226.103.0/24',
'101.226.62.0/24',
'140.207.54.0/24',
// 更多微信支付服务器IP
];
// 抖音支付回调IP范围
const douyinIPs = [
'118.89.204.198',
'118.89.204.196',
'118.89.204.194',
// 更多抖音支付服务器IP
];
switch (platform) {
case PlatformType.WECHAT:
return wechatIPs;
case PlatformType.BYTEDANCE:
return douyinIPs;
default:
return [];
}
}
/**
* 检查IP是否在指定范围内
* @param ip 待检查的IP
* @param range IP范围支持CIDR格式
* @returns 是否在范围内
*/
private isIPInRange(ip: string, range: string): boolean {
// 简化实现实际项目中需要更精确的IP范围检查
if (range.includes('/')) {
// CIDR格式的IP范围检查
const [network, prefixLength] = range.split('/');
// 这里需要实现CIDR匹配逻辑
return ip.startsWith(network.split('.').slice(0, 2).join('.'));
} else {
// 精确IP匹配
return ip === range;
}
}
/**
* 延迟执行
* @param ms 延迟毫秒数
*/
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* 获取回调处理统计信息
* @returns 统计信息
*/
async getCallbackStatistics(): Promise<{
totalCallbacks: number;
successCallbacks: number;
failedCallbacks: number;
platforms: Record<string, number>;
}> {
// 从数据库统计回调处理情况
const statistics = {
totalCallbacks: 0,
successCallbacks: 0,
failedCallbacks: 0,
platforms: {} as Record<string, number>,
};
try {
// 这里可以添加具体的统计查询逻辑
const transactions = await this.transactionRepository.find({
where: { transactionType: 'callback' as any },
select: ['status', 'responseData'],
});
statistics.totalCallbacks = transactions.length;
statistics.successCallbacks = transactions.filter(
(t) => t.status === 'success',
).length;
statistics.failedCallbacks = transactions.filter(
(t) => t.status === 'failed',
).length;
return statistics;
} catch (error) {
this.logger.error('获取回调统计信息失败:', error);
return statistics;
}
}
}

View File

@@ -0,0 +1,761 @@
import {
Injectable,
Logger,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import {
CreatePaymentOrderData,
PaymentOrderResult,
PaymentOrderQueryResult,
PaymentCallbackResult,
RefundRequestData,
RefundResult,
RefundQueryResult,
PaymentMethod,
PaymentBusinessType,
PaymentOrderStatus,
} from '../interfaces/payment.interface';
import { PlatformType } from '../../entities/platform-user.entity';
import { UserEntity } from '../../entities/user.entity';
import {
UserSubscriptionEntity,
SubscriptionPlan,
SubscriptionStatus,
} from '../../entities/user-subscription.entity';
import {
UserCreditEntity,
CreditType,
CreditSource,
} from '../../entities/user-credit.entity';
import {
PaymentOrderEntity,
PaymentTransactionEntity,
RefundRecordEntity,
} from '../entities';
import { PaymentAdapterFactory } from './payment-adapter.factory';
/**
* 业务配置接口
* 定义不同业务类型的配置信息
*/
interface BusinessConfig {
/** 业务名称 */
name: string;
/** 单价(分) */
unitPrice: number;
/** 描述 */
description: string;
/** 是否启用 */
enabled: boolean;
}
/**
* 订阅套餐配置
*/
interface SubscriptionConfig extends BusinessConfig {
/** 套餐类型 */
plan: SubscriptionPlan;
/** 有效期(天) */
validDays: number;
/** 每月积分 */
monthlyCredits: number;
/** 特权功能 */
features: string[];
}
/**
* 积分包配置
*/
interface CreditPackageConfig extends BusinessConfig {
/** 积分数量 */
credits: number;
/** 赠送积分 */
bonusCredits: number;
}
/**
* 统一支付服务
* 提供跨平台支付的统一业务接口
* 集成订阅服务、积分购买等业务逻辑
* 管理支付订单的完整生命周期
*/
@Injectable()
export class UnifiedPaymentService {
private readonly logger = new Logger(UnifiedPaymentService.name);
constructor(
private readonly paymentAdapterFactory: PaymentAdapterFactory,
private readonly configService: ConfigService,
@InjectRepository(UserEntity)
private readonly userRepository: Repository<UserEntity>,
@InjectRepository(UserSubscriptionEntity)
private readonly subscriptionRepository: Repository<UserSubscriptionEntity>,
@InjectRepository(UserCreditEntity)
private readonly creditRepository: Repository<UserCreditEntity>,
@InjectRepository(PaymentOrderEntity)
private readonly paymentOrderRepository: Repository<PaymentOrderEntity>,
) {}
/**
* 创建支付订单
* 根据业务类型和平台创建相应的支付订单
* @param platform 支付平台
* @param userId 用户ID
* @param businessType 业务类型
* @param businessId 业务ID
* @param platformUserId 平台用户ID
* @returns 支付订单结果
*/
async createPaymentOrder(
platform: PlatformType,
userId: string,
businessType: PaymentBusinessType,
businessId: string,
platformUserId: string,
): Promise<PaymentOrderResult> {
try {
this.logger.log(
`创建支付订单: 用户=${userId}, 业务=${businessType}, 业务ID=${businessId}`,
);
// 1. 验证用户存在
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new NotFoundException('用户不存在');
}
// 2. 获取业务配置
const businessConfig = await this.getBusinessConfig(
businessType,
businessId,
);
if (!businessConfig.enabled) {
throw new BadRequestException('该商品暂不可购买');
}
// 3. 业务前置检查
await this.validateBusinessOrder(userId, businessType, businessId);
// 4. 确定支付方式
const paymentMethod = this.determinePaymentMethod(platform);
// 5. 构建订单数据
const orderData: CreatePaymentOrderData = {
orderNo: '', // 由适配器生成
amount: businessConfig.unitPrice,
currency: 'CNY',
description: businessConfig.description,
userId,
platformUserId,
businessType,
businessId,
expireMinutes: 30, // 默认30分钟过期
metadata: {
businessName: businessConfig.name,
platform,
},
notifyUrl: `${this.configService.get('APP_URL')}/api/payment/${platform.toLowerCase()}/callback`,
};
// 6. 获取支付适配器并创建订单
const adapter = this.paymentAdapterFactory.getAdapter(
platform,
paymentMethod,
);
const result = await adapter.createPaymentOrder(orderData);
this.logger.log(
`支付订单创建成功: 订单ID=${result.orderId}, 第三方订单ID=${result.thirdPartyOrderId}`,
);
return result;
} catch (error) {
this.logger.error(`创建支付订单失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 查询支付订单状态
* @param orderId 订单ID
* @returns 订单查询结果
*/
async queryPaymentOrder(orderId: string): Promise<PaymentOrderQueryResult> {
try {
// 1. 获取本地订单信息
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { id: orderId },
});
if (!paymentOrder) {
throw new NotFoundException('支付订单不存在');
}
// 2. 获取支付适配器
const adapter = this.paymentAdapterFactory.getAdapter(
paymentOrder.platform,
paymentOrder.paymentMethod,
);
// 3. 查询订单状态
const result = await adapter.queryPaymentOrder(
orderId,
paymentOrder.thirdPartyOrderId,
);
// 4. 如果订单状态变为已支付,触发业务处理
if (
result.status === PaymentOrderStatus.PAID &&
paymentOrder.status !== PaymentOrderStatus.PAID
) {
await this.handlePaymentSuccess(paymentOrder, result);
}
return result;
} catch (error) {
this.logger.error(`查询支付订单失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 处理支付回调
* @param platform 支付平台
* @param callbackData 回调数据
* @returns 回调处理结果
*/
async handlePaymentCallback(
platform: PlatformType,
callbackData: any,
): Promise<PaymentCallbackResult> {
try {
this.logger.log(`处理${platform}支付回调:`, JSON.stringify(callbackData));
// 1. 确定支付方式
const paymentMethod = this.determinePaymentMethod(platform);
// 2. 获取支付适配器
const adapter = this.paymentAdapterFactory.getAdapter(
platform,
paymentMethod,
);
// 3. 处理回调
const result = await adapter.handlePaymentCallback(callbackData);
// 4. 如果支付成功,触发业务处理
if (
result.success &&
result.status === PaymentOrderStatus.PAID &&
result.orderId
) {
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { id: result.orderId },
});
if (paymentOrder) {
await this.handlePaymentSuccess(paymentOrder, result);
}
}
return result;
} catch (error) {
this.logger.error(`处理支付回调失败: ${error.message}`, error.stack);
return {
success: false,
orderId: '',
status: PaymentOrderStatus.FAILED,
errorMessage: error.message,
};
}
}
/**
* 申请退款
* @param orderId 订单ID
* @param refundAmount 退款金额
* @param reason 退款原因
* @returns 退款结果
*/
async refundPayment(
orderId: string,
refundAmount: number,
reason: string,
): Promise<RefundResult> {
try {
// 1. 获取原订单信息
const paymentOrder = await this.paymentOrderRepository.findOne({
where: { id: orderId },
});
if (!paymentOrder) {
throw new NotFoundException('支付订单不存在');
}
// 2. 验证退款条件
if (!paymentOrder.canRefund()) {
throw new BadRequestException('订单不支持退款');
}
const refundableAmount = paymentOrder.getRefundableAmount();
if (refundAmount > refundableAmount) {
throw new BadRequestException(
`退款金额不能超过可退金额 ${refundableAmount / 100}`,
);
}
// 3. 获取支付适配器
const adapter = this.paymentAdapterFactory.getAdapter(
paymentOrder.platform,
paymentOrder.paymentMethod,
);
// 4. 构建退款数据
const refundData: RefundRequestData = {
orderId,
refundAmount,
reason,
refundNo: this.generateRefundNo(),
thirdPartyOrderId: paymentOrder.thirdPartyOrderId || '',
thirdPartyTransactionId: '', // 需要从交易记录中获取
};
// 5. 申请退款
const result = await adapter.refundPayment(refundData);
this.logger.log(
`退款申请成功: 退款ID=${result.refundId}, 金额=${refundAmount / 100}`,
);
return result;
} catch (error) {
this.logger.error(`申请退款失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 查询退款状态
* @param refundId 退款ID
* @returns 退款查询结果
*/
async queryRefundStatus(refundId: string): Promise<RefundQueryResult> {
try {
// 1. 获取退款记录
const refundRecord = await this.paymentOrderRepository
.createQueryBuilder('order')
.leftJoinAndSelect('order.refunds', 'refund')
.where('refund.id = :refundId', { refundId })
.getOne();
if (!refundRecord) {
throw new NotFoundException('退款记录不存在');
}
const refund = refundRecord.refunds[0];
const paymentOrder = refundRecord;
// 2. 获取支付适配器
const adapter = this.paymentAdapterFactory.getAdapter(
paymentOrder.platform,
paymentOrder.paymentMethod,
);
// 3. 查询退款状态
return await adapter.queryRefundStatus(refundId);
} catch (error) {
this.logger.error(`查询退款状态失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 获取用户支付订单列表
* @param userId 用户ID
* @param page 页码
* @param limit 每页数量
* @returns 订单列表
*/
async getUserPaymentOrders(
userId: string,
page: number = 1,
limit: number = 20,
): Promise<{
orders: PaymentOrderEntity[];
total: number;
page: number;
limit: number;
}> {
const [orders, total] = await this.paymentOrderRepository.findAndCount({
where: { userId },
order: { createdAt: 'DESC' },
skip: (page - 1) * limit,
take: limit,
relations: ['transactions', 'refunds'],
});
return { orders, total, page, limit };
}
/**
* 处理支付成功的业务逻辑
* @param paymentOrder 支付订单
* @param paymentResult 支付结果
*/
private async handlePaymentSuccess(
paymentOrder: PaymentOrderEntity,
paymentResult: PaymentOrderQueryResult | PaymentCallbackResult,
): Promise<void> {
try {
this.logger.log(
`处理支付成功业务: 订单ID=${paymentOrder.id}, 业务类型=${paymentOrder.businessType}`,
);
switch (paymentOrder.businessType) {
case PaymentBusinessType.SUBSCRIPTION:
await this.handleSubscriptionPayment(paymentOrder);
break;
case PaymentBusinessType.CREDIT_PURCHASE:
await this.handleCreditPurchasePayment(paymentOrder);
break;
case PaymentBusinessType.TEMPLATE_UNLOCK:
await this.handleTemplateUnlockPayment(paymentOrder);
break;
default:
this.logger.warn(`未知的业务类型: ${paymentOrder.businessType}`);
}
this.logger.log(`支付成功业务处理完成: 订单ID=${paymentOrder.id}`);
} catch (error) {
this.logger.error(`处理支付成功业务失败: ${error.message}`, error.stack);
// 业务处理失败不应该影响支付状态,但需要记录日志并可能需要人工处理
}
}
/**
* 处理订阅支付
* @param paymentOrder 支付订单
*/
private async handleSubscriptionPayment(
paymentOrder: PaymentOrderEntity,
): Promise<void> {
const config = await this.getSubscriptionConfig(paymentOrder.businessId);
// 创建或更新订阅
const subscription = new UserSubscriptionEntity();
subscription.userId = paymentOrder.userId;
subscription.platform = paymentOrder.platform;
subscription.plan = config.plan;
subscription.status = SubscriptionStatus.ACTIVE;
subscription.startDate = new Date();
subscription.endDate = new Date(
Date.now() + config.validDays * 24 * 60 * 60 * 1000,
);
subscription.price = paymentOrder.amount / 100; // 转换为元
subscription.monthlyCredits = config.monthlyCredits;
subscription.features = config.features;
subscription.paymentId = paymentOrder.id;
await this.subscriptionRepository.save(subscription);
// 发放订阅积分
if (config.monthlyCredits > 0) {
await this.addUserCredits(
paymentOrder.userId,
paymentOrder.platform,
config.monthlyCredits,
CreditType.REWARD,
CreditSource.SUBSCRIPTION,
`订阅${config.name}获得积分`,
paymentOrder.id,
);
}
}
/**
* 处理积分购买支付
* @param paymentOrder 支付订单
*/
private async handleCreditPurchasePayment(
paymentOrder: PaymentOrderEntity,
): Promise<void> {
const config = await this.getCreditPackageConfig(paymentOrder.businessId);
// 发放积分
const totalCredits = config.credits + config.bonusCredits;
await this.addUserCredits(
paymentOrder.userId,
paymentOrder.platform,
totalCredits,
CreditType.PURCHASE,
CreditSource.MANUAL,
`购买${config.name}获得积分`,
paymentOrder.id,
);
}
/**
* 处理模板解锁支付
* @param paymentOrder 支付订单
*/
private async handleTemplateUnlockPayment(
paymentOrder: PaymentOrderEntity,
): Promise<void> {
// 模板解锁逻辑,可能需要更新用户的模板权限
// 这里需要根据具体业务逻辑实现
this.logger.log(
`模板解锁支付处理: 用户=${paymentOrder.userId}, 模板=${paymentOrder.businessId}`,
);
}
/**
* 添加用户积分
* @param userId 用户ID
* @param platform 平台
* @param amount 积分数量
* @param type 积分类型
* @param source 积分来源
* @param description 描述
* @param referenceId 关联ID
*/
private async addUserCredits(
userId: string,
platform: PlatformType,
amount: number,
type: CreditType,
source: CreditSource,
description: string,
referenceId: string,
): Promise<void> {
// 计算新余额
const lastCredit = await this.creditRepository.findOne({
where: { userId },
order: { createdAt: 'DESC' },
});
const newBalance = (lastCredit?.balance || 0) + amount;
// 创建积分记录
const credit = new UserCreditEntity();
credit.userId = userId;
credit.platform = platform;
credit.type = type;
credit.source = source;
credit.amount = amount;
credit.balance = newBalance;
credit.description = description;
credit.referenceId = referenceId;
await this.creditRepository.save(credit);
}
/**
* 业务前置检查
* @param userId 用户ID
* @param businessType 业务类型
* @param businessId 业务ID
*/
private async validateBusinessOrder(
userId: string,
businessType: PaymentBusinessType,
businessId: string,
): Promise<void> {
switch (businessType) {
case PaymentBusinessType.SUBSCRIPTION:
await this.validateSubscriptionOrder(userId, businessId);
break;
case PaymentBusinessType.CREDIT_PURCHASE:
// 积分购买一般无特殊限制
break;
case PaymentBusinessType.TEMPLATE_UNLOCK:
await this.validateTemplateUnlockOrder(userId, businessId);
break;
}
}
/**
* 验证订阅订单
*/
private async validateSubscriptionOrder(
userId: string,
businessId: string,
): Promise<void> {
// 检查用户是否已有有效订阅
const existingSubscription = await this.subscriptionRepository.findOne({
where: {
userId,
status: SubscriptionStatus.ACTIVE,
},
});
if (existingSubscription) {
throw new BadRequestException('您已有有效订阅,请等待到期后再次购买');
}
}
/**
* 验证模板解锁订单
*/
private async validateTemplateUnlockOrder(
userId: string,
templateId: string,
): Promise<void> {
// 检查用户是否已解锁该模板
// 这里需要根据具体业务逻辑实现
}
/**
* 确定支付方式
* @param platform 平台类型
* @returns 支付方式
*/
private determinePaymentMethod(platform: PlatformType): PaymentMethod {
switch (platform) {
case PlatformType.WECHAT:
return PaymentMethod.WECHAT_MINIPROGRAM;
case PlatformType.BYTEDANCE:
return PaymentMethod.BYTEDANCE_MINIPROGRAM;
default:
throw new BadRequestException('不支持的支付平台');
}
}
/**
* 获取业务配置
* @param businessType 业务类型
* @param businessId 业务ID
* @returns 业务配置
*/
private async getBusinessConfig(
businessType: PaymentBusinessType,
businessId: string,
): Promise<BusinessConfig> {
switch (businessType) {
case PaymentBusinessType.SUBSCRIPTION:
return await this.getSubscriptionConfig(businessId);
case PaymentBusinessType.CREDIT_PURCHASE:
return await this.getCreditPackageConfig(businessId);
case PaymentBusinessType.TEMPLATE_UNLOCK:
return await this.getTemplateConfig(businessId);
default:
throw new BadRequestException('不支持的业务类型');
}
}
/**
* 获取订阅配置
*/
private async getSubscriptionConfig(
businessId: string,
): Promise<SubscriptionConfig> {
const configs: Record<string, SubscriptionConfig> = {
basic_monthly: {
name: '基础版月度订阅',
plan: SubscriptionPlan.BASIC,
unitPrice: 1980, // 19.8元
description: '基础版会员月度订阅',
validDays: 30,
monthlyCredits: 1000,
features: ['basic_templates', 'standard_generation'],
enabled: true,
},
premium_monthly: {
name: '高级版月度订阅',
plan: SubscriptionPlan.PREMIUM,
unitPrice: 2980, // 29.8元
description: '高级版会员月度订阅',
validDays: 30,
monthlyCredits: 2000,
features: ['premium_templates', 'fast_generation', 'batch_processing'],
enabled: true,
},
pro_monthly: {
name: '专业版月度订阅',
plan: SubscriptionPlan.PRO,
unitPrice: 4980, // 49.8元
description: '专业版会员月度订阅',
validDays: 30,
monthlyCredits: 5000,
features: [
'all_templates',
'fastest_generation',
'unlimited_processing',
'priority_support',
],
enabled: true,
},
};
const config = configs[businessId];
if (!config) {
throw new BadRequestException('无效的订阅套餐ID');
}
return config;
}
/**
* 获取积分包配置
*/
private async getCreditPackageConfig(
businessId: string,
): Promise<CreditPackageConfig> {
const configs: Record<string, CreditPackageConfig> = {
credits_100: {
name: '100积分包',
unitPrice: 980, // 9.8元
description: '购买100积分',
credits: 100,
bonusCredits: 0,
enabled: true,
},
credits_500: {
name: '500积分包',
unitPrice: 4800, // 48元
description: '购买500积分',
credits: 500,
bonusCredits: 50, // 赠送50积分
enabled: true,
},
credits_1000: {
name: '1000积分包',
unitPrice: 8800, // 88元
description: '购买1000积分',
credits: 1000,
bonusCredits: 200, // 赠送200积分
enabled: true,
},
};
const config = configs[businessId];
if (!config) {
throw new BadRequestException('无效的积分包ID');
}
return config;
}
/**
* 获取模板配置
*/
private async getTemplateConfig(businessId: string): Promise<BusinessConfig> {
// 这里可以从数据库或配置文件中获取模板价格
return {
name: `模板解锁-${businessId}`,
unitPrice: 1980, // 19.8元
description: '解锁高级模板',
enabled: true,
};
}
/**
* 生成退款单号
*/
private generateRefundNo(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `RF${timestamp}${random.toUpperCase()}`;
}
}