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

@@ -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()}`;
}
}