Spaces:
Sleeping
Sleeping
| 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'; | |
| () | |
| export class PaymentService { | |
| constructor( | |
| (Payment) | |
| private paymentRepository: Repository<Payment>, | |
| (Order) | |
| private orderRepository: Repository<Order>, | |
| (UserCourse) | |
| private userCourseRepository: Repository<UserCourse>, | |
| (User) | |
| private userRepository: Repository<User>, | |
| (Course) | |
| private courseRepository: Repository<Course>, | |
| ) {} | |
| 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 }; | |
| } | |
| } | |