Files
gamegroup/src/modules/bets/bets.service.ts

303 lines
9.1 KiB
TypeScript
Raw Normal View History

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<Bet>,
@InjectRepository(Appointment)
private appointmentRepository: Repository<Appointment>,
@InjectRepository(Point)
private pointRepository: Repository<Point>,
@InjectRepository(GroupMember)
private groupMemberRepository: Repository<GroupMember>,
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();
}
}
}