import { Injectable, NotFoundException, BadRequestException, ForbiddenException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { Bet } from '../../entities/bet.entity'; import { Appointment } from '../../entities/appointment.entity'; import { Point } from '../../entities/point.entity'; import { GroupMember } from '../../entities/group-member.entity'; import { CreateBetDto, SettleBetDto } from './dto/bet.dto'; import { BetStatus, GroupMemberRole, AppointmentStatus } from '../../common/enums'; import { ErrorCode, ErrorMessage } from '../../common/interfaces/response.interface'; @Injectable() export class BetsService { constructor( @InjectRepository(Bet) private betRepository: Repository, @InjectRepository(Appointment) private appointmentRepository: Repository, @InjectRepository(Point) private pointRepository: Repository, @InjectRepository(GroupMember) private groupMemberRepository: Repository, private dataSource: DataSource, ) {} /** * 创建竞猜下注 */ async create(userId: string, createDto: CreateBetDto) { const { appointmentId, amount, betOption } = createDto; // 使用事务确保数据一致性 const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // 验证预约存在 const appointment = await queryRunner.manager.findOne(Appointment, { where: { id: appointmentId }, }); if (!appointment) { throw new NotFoundException({ code: ErrorCode.APPOINTMENT_NOT_FOUND, message: ErrorMessage[ErrorCode.APPOINTMENT_NOT_FOUND], }); } // 验证预约状态 if (appointment.status !== AppointmentStatus.PENDING) { throw new BadRequestException({ code: ErrorCode.INVALID_OPERATION, message: '预约已结束,无法下注', }); } // 验证用户积分是否足够 const balance = await queryRunner.manager .createQueryBuilder(Point, 'point') .select('SUM(point.amount)', 'total') .where('point.userId = :userId', { userId }) .andWhere('point.groupId = :groupId', { groupId: appointment.groupId }) .getRawOne(); const currentBalance = parseInt(balance.total || '0'); if (currentBalance < amount) { throw new BadRequestException({ code: ErrorCode.INSUFFICIENT_POINTS, message: '积分不足', }); } // 检查是否已下注 const existingBet = await queryRunner.manager.findOne(Bet, { where: { appointmentId, userId }, }); if (existingBet) { throw new BadRequestException({ code: ErrorCode.INVALID_OPERATION, message: '已下注,不能重复下注', }); } const bet = queryRunner.manager.create(Bet, { appointmentId, userId, betOption, amount, }); const savedBet = await queryRunner.manager.save(Bet, bet); // 扣除积分 const pointRecord = queryRunner.manager.create(Point, { userId, groupId: appointment.groupId, amount: -amount, reason: '竞猜下注', description: `预约: ${appointment.title}`, relatedId: savedBet.id, }); await queryRunner.manager.save(Point, pointRecord); await queryRunner.commitTransaction(); return savedBet; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } /** * 查询预约的所有竞猜 */ async findAll(appointmentId: string) { const bets = await this.betRepository.find({ where: { appointmentId }, relations: ['user'], order: { createdAt: 'DESC' }, }); // 统计各选项的下注情况 const stats = bets.reduce((acc, bet) => { if (!acc[bet.betOption]) { acc[bet.betOption] = { count: 0, totalAmount: 0 }; } acc[bet.betOption].count++; acc[bet.betOption].totalAmount += bet.amount; return acc; }, {}); return { bets, stats, totalBets: bets.length, totalAmount: bets.reduce((sum, bet) => sum + bet.amount, 0), }; } /** * 结算竞猜(管理员) */ async settle(userId: string, appointmentId: string, settleDto: SettleBetDto) { const { winningOption } = settleDto; // 验证预约存在 const appointment = await this.appointmentRepository.findOne({ where: { id: appointmentId }, }); if (!appointment) { throw new NotFoundException({ code: ErrorCode.APPOINTMENT_NOT_FOUND, message: ErrorMessage[ErrorCode.APPOINTMENT_NOT_FOUND], }); } // 验证权限 const membership = await this.groupMemberRepository.findOne({ where: { groupId: appointment.groupId, userId }, }); if (!membership || (membership.role !== GroupMemberRole.ADMIN && membership.role !== GroupMemberRole.OWNER)) { throw new ForbiddenException({ code: ErrorCode.NO_PERMISSION, message: '需要管理员权限', }); } // 使用事务确保数据一致性 const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // 获取所有下注 const bets = await queryRunner.manager.find(Bet, { where: { appointmentId }, }); // 计算总奖池和赢家总下注 const totalPool = bets.reduce((sum, bet) => sum + bet.amount, 0); const winningBets = bets.filter((bet) => bet.betOption === winningOption); const winningTotal = winningBets.reduce((sum, bet) => sum + bet.amount, 0); if (winningTotal === 0) { throw new BadRequestException({ code: ErrorCode.INVALID_OPERATION, message: '没有人下注该选项', }); } // 按比例分配奖池,修复精度损失问题 let distributedAmount = 0; for (let i = 0; i < winningBets.length; i++) { const bet = winningBets[i]; let winAmount: number; if (i === winningBets.length - 1) { // 最后一个赢家获得剩余所有积分,避免精度损失 winAmount = totalPool - distributedAmount; } else { winAmount = Math.floor((bet.amount / winningTotal) * totalPool); distributedAmount += winAmount; } bet.winAmount = winAmount; bet.status = BetStatus.WON; // 返还积分 const pointRecord = queryRunner.manager.create(Point, { userId: bet.userId, groupId: appointment.groupId, amount: winAmount, reason: '竞猜获胜', description: `预约: ${appointment.title}`, relatedId: bet.id, }); await queryRunner.manager.save(Point, pointRecord); await queryRunner.manager.save(Bet, bet); } // 更新输家状态 for (const bet of bets) { if (bet.betOption !== winningOption) { bet.status = BetStatus.LOST; await queryRunner.manager.save(Bet, bet); } } await queryRunner.commitTransaction(); return { message: '结算成功', winningOption, totalPool, winners: winningBets.length, }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } /** * 取消竞猜(预约取消时) */ async cancel(appointmentId: string) { // 使用事务确保数据一致性 const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const bets = await queryRunner.manager.find(Bet, { where: { appointmentId }, relations: ['appointment'], }); for (const bet of bets) { if (bet.status === BetStatus.PENDING) { bet.status = BetStatus.CANCELLED; await queryRunner.manager.save(Bet, bet); // 退还积分 const pointRecord = queryRunner.manager.create(Point, { userId: bet.userId, groupId: bet.appointment.groupId, amount: bet.amount, reason: '竞猜取消退款', description: `预约: ${bet.appointment.title}`, relatedId: bet.id, }); await queryRunner.manager.save(Point, pointRecord); } } await queryRunner.commitTransaction(); return { message: '竞猜已取消,积分已退还' }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } }