包含 NestJS 后端、三端前端、Prisma 数据模型、结算引擎测试与 PRD 文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
223 lines
6.5 KiB
TypeScript
223 lines
6.5 KiB
TypeScript
import { Injectable, BadRequestException } from '@nestjs/common';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { Decimal } from '@prisma/client/runtime/library';
|
|
import { generateTransactionId } from '../common/decorators';
|
|
|
|
@Injectable()
|
|
export class WalletService {
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
async getWallet(userId: bigint) {
|
|
const wallet = await this.prisma.wallet.findUnique({ where: { userId } });
|
|
if (!wallet) throw new BadRequestException('Wallet not found');
|
|
return wallet;
|
|
}
|
|
|
|
async createWallet(userId: bigint, currency = 'USD') {
|
|
return this.prisma.wallet.create({
|
|
data: { userId, currency },
|
|
});
|
|
}
|
|
|
|
private async lockWallet(tx: Parameters<Parameters<PrismaService['$transaction']>[0]>[0], userId: bigint) {
|
|
const wallets = await tx.$queryRaw<Array<{ id: bigint; available_balance: Decimal; frozen_balance: Decimal; version: number }>>`
|
|
SELECT id, available_balance, frozen_balance, version FROM wallets WHERE user_id = ${userId} FOR UPDATE
|
|
`;
|
|
if (!wallets.length) throw new BadRequestException('Wallet not found');
|
|
return wallets[0];
|
|
}
|
|
|
|
async deposit(
|
|
userId: bigint,
|
|
amount: Decimal | number,
|
|
operatorId: bigint,
|
|
remark?: string,
|
|
referenceId?: string,
|
|
) {
|
|
const amt = new Decimal(amount);
|
|
if (amt.lte(0)) throw new BadRequestException('Amount must be positive');
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
const w = await this.lockWallet(tx, userId);
|
|
const balanceBefore = new Decimal(w.available_balance);
|
|
const balanceAfter = balanceBefore.add(amt);
|
|
|
|
await tx.wallet.update({
|
|
where: { id: w.id },
|
|
data: {
|
|
availableBalance: balanceAfter,
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
|
|
await tx.walletTransaction.create({
|
|
data: {
|
|
transactionId: generateTransactionId(),
|
|
userId,
|
|
walletId: w.id,
|
|
transactionType: 'MANUAL_DEPOSIT',
|
|
amount: amt,
|
|
balanceBefore,
|
|
balanceAfter,
|
|
frozenBefore: w.frozen_balance,
|
|
frozenAfter: w.frozen_balance,
|
|
referenceType: 'DEPOSIT',
|
|
referenceId,
|
|
operatorId,
|
|
remark,
|
|
},
|
|
});
|
|
|
|
return { balanceAfter };
|
|
});
|
|
}
|
|
|
|
async withdraw(
|
|
userId: bigint,
|
|
amount: Decimal | number,
|
|
operatorId: bigint,
|
|
remark?: string,
|
|
referenceId?: string,
|
|
) {
|
|
const amt = new Decimal(amount);
|
|
if (amt.lte(0)) throw new BadRequestException('Amount must be positive');
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
const w = await this.lockWallet(tx, userId);
|
|
const balanceBefore = new Decimal(w.available_balance);
|
|
if (balanceBefore.lt(amt)) throw new BadRequestException('Insufficient balance');
|
|
const balanceAfter = balanceBefore.sub(amt);
|
|
|
|
await tx.wallet.update({
|
|
where: { id: w.id },
|
|
data: {
|
|
availableBalance: balanceAfter,
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
|
|
await tx.walletTransaction.create({
|
|
data: {
|
|
transactionId: generateTransactionId(),
|
|
userId,
|
|
walletId: w.id,
|
|
transactionType: 'MANUAL_WITHDRAW',
|
|
amount: amt.neg(),
|
|
balanceBefore,
|
|
balanceAfter,
|
|
frozenBefore: w.frozen_balance,
|
|
frozenAfter: w.frozen_balance,
|
|
referenceType: 'WITHDRAW',
|
|
referenceId,
|
|
operatorId,
|
|
remark,
|
|
},
|
|
});
|
|
|
|
return { balanceAfter };
|
|
});
|
|
}
|
|
|
|
async freezeForBet(userId: bigint, stake: Decimal | number, betId: string) {
|
|
const amt = new Decimal(stake);
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
const w = await this.lockWallet(tx, userId);
|
|
const avail = new Decimal(w.available_balance);
|
|
if (avail.lt(amt)) throw new BadRequestException('Insufficient balance');
|
|
|
|
const balanceAfter = avail.sub(amt);
|
|
const frozenAfter = new Decimal(w.frozen_balance).add(amt);
|
|
|
|
await tx.wallet.update({
|
|
where: { id: w.id },
|
|
data: {
|
|
availableBalance: balanceAfter,
|
|
frozenBalance: frozenAfter,
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
|
|
await tx.walletTransaction.create({
|
|
data: {
|
|
transactionId: generateTransactionId(),
|
|
userId,
|
|
walletId: w.id,
|
|
transactionType: 'BET_FREEZE',
|
|
amount: amt.neg(),
|
|
balanceBefore: avail,
|
|
balanceAfter,
|
|
frozenBefore: w.frozen_balance,
|
|
frozenAfter,
|
|
referenceType: 'BET',
|
|
referenceId: betId,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async settleBet(
|
|
userId: bigint,
|
|
stake: Decimal,
|
|
payout: Decimal,
|
|
betId: string,
|
|
result: 'WIN' | 'LOSE' | 'PUSH' | 'VOID' | 'HALF_WIN' | 'HALF_LOSE',
|
|
) {
|
|
const txTypeMap: Record<string, string> = {
|
|
WIN: 'BET_SETTLE_WIN',
|
|
LOSE: 'BET_SETTLE_LOSE',
|
|
PUSH: 'BET_SETTLE_PUSH',
|
|
VOID: 'BET_VOID_REFUND',
|
|
HALF_WIN: 'BET_SETTLE_WIN',
|
|
HALF_LOSE: 'BET_SETTLE_LOSE',
|
|
};
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
const w = await this.lockWallet(tx, userId);
|
|
const avail = new Decimal(w.available_balance);
|
|
const frozen = new Decimal(w.frozen_balance);
|
|
const frozenAfter = frozen.sub(stake);
|
|
const balanceAfter = avail.add(payout);
|
|
|
|
await tx.wallet.update({
|
|
where: { id: w.id },
|
|
data: {
|
|
availableBalance: balanceAfter,
|
|
frozenBalance: frozenAfter.lt(0) ? new Decimal(0) : frozenAfter,
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
|
|
await tx.walletTransaction.create({
|
|
data: {
|
|
transactionId: generateTransactionId(),
|
|
userId,
|
|
walletId: w.id,
|
|
transactionType: txTypeMap[result] || 'BET_SETTLE_WIN',
|
|
amount: payout,
|
|
balanceBefore: avail,
|
|
balanceAfter,
|
|
frozenBefore: frozen,
|
|
frozenAfter: frozenAfter.lt(0) ? new Decimal(0) : frozenAfter,
|
|
referenceType: 'BET',
|
|
referenceId: betId,
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
async getTransactions(userId: bigint, page = 1, pageSize = 20) {
|
|
const skip = (page - 1) * pageSize;
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.walletTransaction.findMany({
|
|
where: { userId },
|
|
orderBy: { createdAt: 'desc' },
|
|
skip,
|
|
take: pageSize,
|
|
}),
|
|
this.prisma.walletTransaction.count({ where: { userId } }),
|
|
]);
|
|
return { items, total, page, pageSize };
|
|
}
|
|
}
|