Spaces:
Sleeping
Sleeping
File size: 4,956 Bytes
8268e91 73746a8 426f2a4 73746a8 426f2a4 73746a8 426f2a4 73746a8 f45e448 8268e91 73746a8 8268e91 73746a8 426f2a4 73746a8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 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<Payment>,
@InjectRepository(Order)
private orderRepository: Repository<Order>,
@InjectRepository(UserCourse)
private userCourseRepository: Repository<UserCourse>,
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(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 };
}
}
|