feat(admin,api,player): 结算预览分页、统计图表与返水限额

完善结算计算与预览 API(含后端分页),加强管理端结算/返水/权限,并优化玩家端投注单与队徽展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-05 13:54:33 +08:00
parent 6264b8806c
commit efff7c27e6
40 changed files with 3560 additions and 578 deletions

View File

@@ -1,8 +1,11 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../shared/prisma/prisma.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateTransactionId } from '../../shared/common/decorators';
type TxClient = Prisma.TransactionClient;
@Injectable()
export class WalletService {
constructor(private prisma: PrismaService) {}
@@ -19,7 +22,7 @@ export class WalletService {
});
}
private async lockWallet(tx: Parameters<Parameters<PrismaService['$transaction']>[0]>[0], userId: bigint) {
private async lockWallet(tx: TxClient, 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
`;
@@ -163,24 +166,25 @@ export class WalletService {
payout: Decimal,
betId: string,
result: 'WIN' | 'LOSE' | 'PUSH' | 'VOID' | 'HALF_WIN' | 'HALF_LOSE',
tx?: TxClient,
) {
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',
};
const run = async (client: TxClient) => {
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 w = await this.lockWallet(client, 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({
await client.wallet.update({
where: { id: w.id },
data: {
availableBalance: balanceAfter,
@@ -189,7 +193,7 @@ export class WalletService {
},
});
await tx.walletTransaction.create({
await client.walletTransaction.create({
data: {
transactionId: generateTransactionId(),
userId,
@@ -204,7 +208,55 @@ export class WalletService {
referenceId: betId,
},
});
});
};
if (tx) return run(tx);
return this.prisma.$transaction(run);
}
/** 重结算差额调整delta = 新派彩 - 原派彩,允许扣回导致负余额 */
async applyResettleDelta(
userId: bigint,
delta: Decimal,
betNo: string,
tx?: TxClient,
) {
if (delta.eq(0)) return;
const run = async (client: TxClient) => {
const w = await this.lockWallet(client, userId);
const avail = new Decimal(w.available_balance);
const balanceAfter = avail.add(delta);
const txType = delta.gt(0) ? 'BET_SETTLE_WIN' : 'RESETTLE_REVERSE';
await client.wallet.update({
where: { id: w.id },
data: {
availableBalance: balanceAfter,
version: { increment: 1 },
},
});
await client.walletTransaction.create({
data: {
transactionId: generateTransactionId(),
userId,
walletId: w.id,
transactionType: txType,
amount: delta,
balanceBefore: avail,
balanceAfter,
frozenBefore: w.frozen_balance,
frozenAfter: w.frozen_balance,
referenceType: 'BET',
referenceId: betNo,
remark: 'Resettlement adjustment',
},
});
};
if (tx) return run(tx);
return this.prisma.$transaction(run);
}
async getTransactions(userId: bigint, page = 1, pageSize = 20) {