feat: 手动充值、邀请码注册与后台管理增强

新增玩家手动充值全流程(收款方式配置、充值下单/审核、钱包上分),
支持邀请码注册、邀请历史与专属返水率;完善后台代理/玩家管理与响应式操作栏,
并补充前台注册、充值页及多语言错误码。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 12:20:11 +08:00
parent 618fb49511
commit 10485ecfaf
98 changed files with 7908 additions and 856 deletions

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../../shared/prisma/prisma.service';
import { WalletService } from '../../ledger/wallet.service';
import { SystemConfigService } from '../../../shared/config/system-config.service';
import { Decimal } from '@prisma/client/runtime/library';
import { generateBatchNo } from '../../../shared/common/decorators';
import { appBadRequest } from '../../../shared/common/app-error';
@@ -33,6 +34,7 @@ export class CashbackService {
constructor(
private prisma: PrismaService,
private wallet: WalletService,
private systemConfig: SystemConfigService,
) {}
/** 已被待发放/已发放返水批次占用的注单 */
@@ -55,14 +57,15 @@ export class CashbackService {
eligibleBetCount: number;
skippedClaimedCount: number;
}> {
const [settledBets, rules, agentProfiles, claimedBetIds] = await Promise.all([
const [settledBets, rules, agentProfiles, claimedBetIds, platformDirectRateRaw] =
await Promise.all([
this.prisma.bet.findMany({
where: {
status: { in: ['WON', 'LOST'] },
settledAt: { gte: periodStart, lte: periodEnd },
},
include: {
user: { select: { id: true, parentId: true } },
user: { select: { id: true, parentId: true, inviteSponsorId: true } },
selections: { select: { marketType: true } },
},
}),
@@ -71,11 +74,46 @@ export class CashbackService {
select: { userId: true, cashbackRate: true },
}),
this.loadClaimedBetIds(),
this.systemConfig.getPlatformDirectCashbackSettings(),
]);
const platformDirectDefaultRate = new Decimal(platformDirectRateRaw.platformDirectRate);
const adminInviteDefaultRate = new Decimal(platformDirectRateRaw.adminInviteRate);
const agentRateById = new Map(
agentProfiles.map((p) => [p.userId.toString(), new Decimal(p.cashbackRate)]),
);
const sponsorTypeById = new Map<string, string>();
const sponsorIds = [
...new Set(
settledBets
.map((b) => b.user.inviteSponsorId)
.filter((id): id is bigint => id != null),
),
];
if (sponsorIds.length > 0) {
const sponsors = await this.prisma.user.findMany({
where: { id: { in: sponsorIds } },
select: { id: true, userType: true },
});
for (const sponsor of sponsors) {
sponsorTypeById.set(sponsor.id.toString(), sponsor.userType);
}
}
const resolveDefaultRate = (user: {
parentId: bigint | null;
inviteSponsorId: bigint | null;
}) => {
if (user.parentId) {
return agentRateById.get(user.parentId.toString()) ?? new Decimal(0);
}
if (user.inviteSponsorId) {
const sponsorType = sponsorTypeById.get(user.inviteSponsorId.toString());
if (sponsorType === 'ADMIN') return adminInviteDefaultRate;
}
return platformDirectDefaultRate;
};
const ruleRows: CashbackRuleRow[] = rules.map((r) => ({
targetType: r.targetType,
targetId: r.targetId,
@@ -93,9 +131,7 @@ export class CashbackService {
for (const bet of settledBets) {
const agentId = bet.user.parentId;
const agentDefaultRate = agentId
? agentRateById.get(agentId.toString()) ?? new Decimal(0)
: new Decimal(0);
const agentDefaultRate = resolveDefaultRate(bet.user);
const marketTypes = bet.selections.map((s) => s.marketType);
const rate = resolveCashbackRateForBet({
userId: bet.userId,
@@ -538,4 +574,64 @@ export class CashbackService {
createdAt: item.createdAt,
}));
}
async getPlayerCustomCashbackRate(userId: bigint): Promise<Decimal | null> {
const rule = await this.prisma.cashbackRule.findFirst({
where: {
targetType: 'USER',
targetId: userId,
isActive: true,
marketType: null,
},
orderBy: { updatedAt: 'desc' },
});
if (!rule) return null;
const rate = new Decimal(rule.rate);
return rate.gt(0) ? rate : null;
}
async setPlayerCustomCashbackRate(userId: bigint, rate: Decimal | null) {
await this.prisma.$transaction(async (tx) => {
await tx.cashbackRule.updateMany({
where: { targetType: 'USER', targetId: userId },
data: { isActive: false },
});
if (rate && rate.gt(0)) {
await tx.cashbackRule.create({
data: {
name: `Player ${userId.toString()}`,
targetType: 'USER',
targetId: userId,
rate,
isActive: true,
},
});
}
});
}
async resolvePlayerDefaultCashbackRate(params: {
parentId: bigint | null;
inviteSponsorId?: bigint | null;
}): Promise<Decimal> {
if (params.parentId) {
const profile = await this.prisma.agentProfile.findUnique({
where: { userId: params.parentId },
select: { cashbackRate: true },
});
return profile ? new Decimal(profile.cashbackRate) : new Decimal(0);
}
const settings = await this.systemConfig.getPlatformDirectCashbackSettings();
if (params.inviteSponsorId) {
const sponsor = await this.prisma.user.findUnique({
where: { id: params.inviteSponsorId },
select: { userType: true },
});
if (sponsor?.userType === 'ADMIN') {
return new Decimal(settings.adminInviteRate);
}
}
return new Decimal(settings.platformDirectRate);
}
}