import { Injectable, NotFoundException, BadRequestException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as crypto from 'crypto'; import { Payment, PaymentStatus, PayType } from '../entities/payment.entity'; import { Order, OrderStatus, OrderType } from '../entities/order.entity'; import { UserCourse } from '../entities/user-course.entity'; import { PreparePaymentDto } from './dto/prepare-payment.dto'; import { User } from '../entities/user.entity'; import { Course } from '../entities/course.entity'; @Injectable() export class PaymentService { constructor( @InjectRepository(Payment) private paymentRepository: Repository, @InjectRepository(Order) private orderRepository: Repository, @InjectRepository(UserCourse) private userCourseRepository: Repository, @InjectRepository(User) private userRepository: Repository, @InjectRepository(Course) private courseRepository: Repository, ) {} async prepare(userId: number, preparePaymentDto: PreparePaymentDto) { const orderIdNum = parseInt(preparePaymentDto.orderId, 10); const order = await this.orderRepository.findOne({ where: { id: orderIdNum, userId }, }); if (!order) { throw new NotFoundException('Order not found'); } if (order.status !== OrderStatus.PENDING) { throw new BadRequestException('Order is not in pending status'); } const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 1000000)}`; const payment = this.paymentRepository.create({ orderId: order.id, paymentNo, payType: preparePaymentDto.payType, amount: order.amount, status: PaymentStatus.PENDING, }); await this.paymentRepository.save(payment); // Mock payment params if (preparePaymentDto.payType === PayType.WECHAT) { return { payParams: 'mock_wechat_pay_params', paymentNo }; } else { return { orderInfo: 'mock_alipay_order_string', paymentNo }; } } // Mock webhook handler async handleCallback(paymentNo: string, status: 'SUCCESS' | 'FAILED') { const payment = await this.paymentRepository.findOne({ where: { paymentNo }, relations: ['order'], }); if (!payment || payment.status !== PaymentStatus.PENDING) { return { success: false, message: 'Invalid payment or already processed', }; } if (status === 'SUCCESS') { payment.status = PaymentStatus.SUCCESS; payment.completedAt = new Date(); await this.paymentRepository.save(payment); const order = payment.order; order.status = OrderStatus.PAID; order.paidAt = new Date(); await this.orderRepository.save(order); // Only grant access if it's a purchase order if (order.orderType === OrderType.PURCHASE) { // Send email with drive link instead of generating access token const user = await this.userRepository.findOne({ where: { id: order.userId } }); const course = await this.courseRepository.findOne({ where: { id: order.courseId } }); if (user && course && course.driveLink) { // In a real application, you would use a mail service like Nodemailer here console.log('----------------------------------------'); console.log(`[MOCK EMAIL SERVICE] Sending course materials...`); console.log(`To: ${user.email}`); console.log(`Subject: 您购买的课程【${course.title}】资料`); console.log(`Body: 感谢您的购买!您的课程资料链接如下:\n${course.driveLink}`); console.log('----------------------------------------'); } // We still create a UserCourse record to mark that the user has purchased it, // but the accessToken isn't strictly needed anymore for viewing the content. const expiredAt = new Date(); expiredAt.setFullYear(expiredAt.getFullYear() + 1); // 1 year access const accessToken = crypto.randomBytes(32).toString('hex'); const userCourse = this.userCourseRepository.create({ userId: order.userId, courseId: order.courseId, accessToken, expiredAt, }); // Handle duplicate purchase gracefully try { await this.userCourseRepository.save(userCourse); } catch { // Already purchased, maybe extend expiration } } else if (order.orderType === OrderType.VIP) { // Upgrade user to VIP const user = await this.userRepository.findOne({ where: { id: order.userId } }); if (user) { user.isVip = true; await this.userRepository.save(user); } } } else { payment.status = PaymentStatus.FAILED; await this.paymentRepository.save(payment); } return { success: true }; } }