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

@@ -3,6 +3,10 @@ import { PrismaService } from '../../../shared/prisma/prisma.service';
import { WalletService } from '../../ledger/wallet.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBatchNo } from '../../../shared/common/decorators';
import {
resolveCashbackRateForBet,
type CashbackRuleRow,
} from './cashback-rate.resolver';
@Injectable()
export class CashbackService {
@@ -12,37 +16,98 @@ export class CashbackService {
) {}
async previewBatch(periodStart: Date, periodEnd: Date) {
const settledBets = await this.prisma.bet.findMany({
where: {
status: { in: ['WON', 'LOST', 'SETTLED'] },
settledAt: { gte: periodStart, lte: periodEnd },
},
include: { user: { include: { agentProfile: true } } },
});
const [settledBets, rules, agentProfiles] = await Promise.all([
this.prisma.bet.findMany({
where: {
status: { in: ['WON', 'LOST'] },
settledAt: { gte: periodStart, lte: periodEnd },
},
include: {
user: { select: { id: true, parentId: true } },
selections: { select: { marketType: true } },
},
}),
this.prisma.cashbackRule.findMany({ where: { isActive: true } }),
this.prisma.agentProfile.findMany({
select: { userId: true, cashbackRate: true },
}),
]);
const playerStakes = new Map<string, { userId: bigint; stake: Decimal; rate: Decimal }>();
for (const bet of settledBets) {
if (bet.status === 'PUSH' || bet.status === 'VOID') continue;
const key = bet.userId.toString();
const existing = playerStakes.get(key) ?? {
userId: bet.userId,
stake: new Decimal(0),
rate: new Decimal(0.01),
};
existing.stake = existing.stake.add(bet.stake);
playerStakes.set(key, existing);
}
const items = Array.from(playerStakes.values()).map((p) => ({
userId: p.userId,
effectiveStake: p.stake,
rate: p.rate,
amount: p.stake.mul(p.rate),
const agentRateById = new Map(
agentProfiles.map((p) => [p.userId.toString(), new Decimal(p.cashbackRate)]),
);
const ruleRows: CashbackRuleRow[] = rules.map((r) => ({
targetType: r.targetType,
targetId: r.targetId,
rate: new Decimal(r.rate),
marketType: r.marketType,
}));
const totalAmount = items.reduce((s, i) => s.add(i.amount), new Decimal(0));
const playerAgg = new Map<
string,
{ userId: bigint; stake: Decimal; amount: Decimal }
>();
for (const bet of settledBets) {
const agentId = bet.user.parentId;
const agentDefaultRate = agentId
? agentRateById.get(agentId.toString()) ?? new Decimal(0)
: new Decimal(0);
const marketTypes = bet.selections.map((s) => s.marketType);
const rate = resolveCashbackRateForBet({
userId: bet.userId,
agentId,
marketTypes,
agentDefaultRate,
rules: ruleRows,
});
if (rate.lte(0)) continue;
const key = bet.userId.toString();
const existing = playerAgg.get(key) ?? {
userId: bet.userId,
stake: new Decimal(0),
amount: new Decimal(0),
};
existing.stake = existing.stake.add(bet.stake);
existing.amount = existing.amount.add(bet.stake.mul(rate));
playerAgg.set(key, existing);
}
const items = Array.from(playerAgg.values())
.map((p) => ({
userId: p.userId,
effectiveStake: p.stake,
rate: p.stake.gt(0) ? p.amount.div(p.stake) : new Decimal(0),
amount: p.amount,
}))
.sort((a, b) => (b.amount.gt(a.amount) ? 1 : a.amount.gt(b.amount) ? -1 : 0));
const userIds = items.map((i) => i.userId);
const users =
userIds.length > 0
? await this.prisma.user.findMany({
where: { id: { in: userIds } },
select: {
id: true,
username: true,
parent: { select: { username: true } },
},
})
: [];
const userById = new Map(users.map((u) => [u.id.toString(), u]));
const enrichedItems = items.map((item) => {
const user = userById.get(item.userId.toString());
return {
...item,
username: user?.username ?? '',
agentUsername: user?.parent?.username ?? null,
};
});
const totalAmount = enrichedItems.reduce((s, i) => s.add(i.amount), new Decimal(0));
const batch = await this.prisma.cashbackBatch.create({
data: {
@@ -55,7 +120,7 @@ export class CashbackService {
},
});
for (const item of items) {
for (const item of enrichedItems) {
await this.prisma.cashbackItem.create({
data: {
batchId: batch.id,
@@ -67,7 +132,7 @@ export class CashbackService {
});
}
return { batch, items, totalAmount };
return { batch, items: enrichedItems, totalAmount };
}
async confirmBatch(batchId: bigint, operatorId: bigint) {