Files
bw-mini-app-server/src/payment/services/unified-payment.service.ts
imeepos 489756b7a0 feat: 集成Google OAuth认证和完善Stripe支付系统
- 新增Google OAuth 2.0认证模块
  - 实现Google Strategy和AuthController
  - 添加Google平台适配器和用户统一管理
  - 支持OAuth回调和token交换机制

- 完善Stripe支付集成
  - 新增Stripe支付适配器和控制器
  - 实现webhook事件处理和支付确认
  - 支持多币种和国际化支付

- 扩展平台类型支持
  - 在PlatformType中新增GOOGLE和STRIPE
  - 更新适配器工厂注册机制
  - 完善跨平台用户身份管理

- 增强依赖和配置
  - 添加@nestjs/passport和passport-google-oauth20
  - 更新支付方法枚举和货币支持
  - 完善订单号生成和回调URL构建
2025-09-26 15:26:37 +08:00

790 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
* @param currency 货币类型可选默认CNY
* @param customAmount 自定义金额(可选,覆盖配置价格)
* @returns 支付订单结果
*/
async createPaymentOrder(
platform: PlatformType,
userId: string,
businessType: PaymentBusinessType,
businessId: string,
platformUserId: string,
currency: string = 'CNY',
customAmount?: number,
): 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: customAmount || businessConfig.unitPrice,
currency: currency,
description: businessConfig.description,
userId,
platformUserId,
businessType,
businessId,
expireMinutes: 30, // 默认30分钟过期
metadata: {
businessName: businessConfig.name,
platform,
currency,
},
notifyUrl: this.buildNotifyUrl(platform),
};
// 6. 获取支付适配器并创建订单
const adapter = this.paymentAdapterFactory.getAdapter(
platform,
paymentMethod,
);
console.log(orderData)
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;
case PlatformType.STRIPE:
return PaymentMethod.STRIPE_CARD;
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()}`;
}
/**
* 构建回调通知URL
* @param platform 支付平台
* @returns 回调URL
*/
private buildNotifyUrl(platform: PlatformType): string {
const baseUrl = this.configService.get('SERVER_URL') || 'http://localhost:3000';
switch (platform) {
case PlatformType.WECHAT:
return `${baseUrl}/api/v1/payment/wechat/callback`;
case PlatformType.BYTEDANCE:
return `${baseUrl}/api/v1/payment/bytedance/callback`;
case PlatformType.STRIPE:
return `${baseUrl}/api/v1/payment/stripe/webhook`;
default:
return `${baseUrl}/api/v1/payment/${platform.toLowerCase()}/callback`;
}
}
}