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

@@ -0,0 +1,114 @@
import { Prisma } from '@prisma/client';
import type { PrismaService } from '../prisma/prisma.service';
const INVITE_CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const INVITE_CODE_LENGTH = 8;
export const INVITE_STATUS_ACTIVE = 'ACTIVE';
export const INVITE_STATUS_REVOKED = 'REVOKED';
export function normalizeInviteCode(input: string): string {
return input.trim().toUpperCase();
}
export function generateInviteCodeCandidate(): string {
let code = '';
for (let i = 0; i < INVITE_CODE_LENGTH; i++) {
code += INVITE_CODE_CHARS[Math.floor(Math.random() * INVITE_CODE_CHARS.length)];
}
return code;
}
type InviteCodeDb = Pick<PrismaService, 'user' | 'userInvite'>;
function isInviteCodeUniqueViolation(err: unknown): boolean {
return err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002';
}
async function isCodeTaken(db: InviteCodeDb, code: string): Promise<boolean> {
const [userHit, inviteHit] = await Promise.all([
db.user.findUnique({ where: { inviteCode: code }, select: { id: true } }),
db.userInvite.findUnique({ where: { code }, select: { id: true } }),
]);
return Boolean(userHit || inviteHit);
}
export async function generateUniqueInviteCode(
db: InviteCodeDb,
maxAttempts = 12,
): Promise<string> {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const code = generateInviteCodeCandidate();
if (!(await isCodeTaken(db, code))) return code;
}
throw new Error('Failed to generate unique invite code');
}
/** Create history record and set as the user's current invite code. */
export async function assignInviteCodeWithHistory(
db: InviteCodeDb,
sponsorId: bigint,
cashbackRate?: Prisma.Decimal | null,
): Promise<string> {
for (let attempt = 0; attempt < 12; attempt++) {
const candidate = generateInviteCodeCandidate();
if (await isCodeTaken(db, candidate)) continue;
try {
await db.userInvite.create({
data: {
code: candidate,
sponsorId,
status: INVITE_STATUS_ACTIVE,
...(cashbackRate != null ? { cashbackRate } : {}),
},
});
await db.user.update({
where: { id: sponsorId },
data: { inviteCode: candidate },
});
return candidate;
} catch (err) {
if (!isInviteCodeUniqueViolation(err)) throw err;
}
}
throw new Error('Failed to assign invite code');
}
/** Revoke active codes and assign a new one with history. */
export async function rotateUserInviteCode(
db: InviteCodeDb,
userId: bigint,
cashbackRate?: Prisma.Decimal | null,
): Promise<string> {
await db.userInvite.updateMany({
where: { sponsorId: userId, status: INVITE_STATUS_ACTIVE },
data: { status: INVITE_STATUS_REVOKED, revokedAt: new Date() },
});
return assignInviteCodeWithHistory(db, userId, cashbackRate);
}
/** Assign a stable invite code once per staff user (admin/agent). */
export async function ensureUserInviteCode(db: InviteCodeDb, userId: bigint): Promise<string> {
const existing = await db.user.findUnique({
where: { id: userId },
select: { inviteCode: true },
});
if (existing?.inviteCode) {
const history = await db.userInvite.findUnique({
where: { code: existing.inviteCode },
select: { id: true },
});
if (!history) {
await db.userInvite.create({
data: {
code: existing.inviteCode,
sponsorId: userId,
status: INVITE_STATUS_ACTIVE,
},
});
}
return existing.inviteCode;
}
return assignInviteCodeWithHistory(db, userId);
}