feat: 管理端 RBAC 权限体系与员工管理

新增多角色权限控制(赛事/财务/客服管理员),支持员工 CRUD、路由菜单按权限显隐、审计日志范围过滤;登录返回角色与权限列表。玩家端赛事列表增加静默刷新避免图片闪烁。Seed 补充演示员工账号与充值相关权限。附带 RBAC/审计范围单元测试及 UAT 文档更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-15 17:52:39 +08:00
parent be5b4a4921
commit 567ec9ec8a
44 changed files with 1717 additions and 125 deletions

View File

@@ -24,6 +24,7 @@ import { jsonResponse } from '../../shared/common/filters';
import { appBadRequest, appForbidden } from '../../shared/common/app-error';
import { getUploadRoot } from '../../shared/uploads/upload-paths';
import { UsersService } from '../../domains/identity/users.service';
import { AdminStaffService } from '../../domains/identity/admin-staff.service';
import { AgentsService } from '../../domains/agent/agents.service';
import { WalletService } from '../../domains/ledger/wallet.service';
import { MatchesService } from '../../domains/catalog/matches.service';
@@ -255,6 +256,40 @@ class UpdatePlayerAdminDto {
cashbackRate?: number | null;
}
class CreateStaffDto {
@IsString()
username!: string;
@IsString()
@MinLength(8)
password!: string;
@IsString()
roleCode!: string;
}
class UpdateStaffDto {
@IsOptional()
@IsIn(['ACTIVE', 'SUSPENDED', 'DISABLED'])
status?: string;
@IsOptional()
@IsString()
roleCode?: string;
@IsOptional()
@IsString()
@MinLength(8)
password?: string;
}
class ResetPlayerPasswordDto {
@IsOptional()
@IsString()
@MinLength(8)
password?: string;
}
class PlatformDirectCashbackSettingsDto {
@IsOptional()
@IsNumber()
@@ -1168,6 +1203,7 @@ export class AdminController {
private databaseReset: DatabaseResetService,
private smokeTests: SmokeTestService,
private depositService: DepositService,
private staff: AdminStaffService,
) {}
@Get('dashboard')
@@ -1178,7 +1214,7 @@ export class AdminController {
}
@Get('users/page-init')
@RequirePermissions(P.agentsView)
@RequirePermissions(P.agentsView, P.usersView)
async getUsersPageInit() {
const [
playerSettings,
@@ -1380,6 +1416,84 @@ export class AdminController {
return jsonResponse(detail);
}
@Post('users/:id/reset-password')
@RequirePermissions(P.usersResetPassword)
async resetPlayerPassword(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
@Body() dto: ResetPlayerPasswordDto,
) {
const { password } = await this.staff.resetPlayerPassword(BigInt(id), dto.password);
await this.audit.log({
operatorId,
operatorType: 'ADMIN',
action: 'RESET_PLAYER_PASSWORD',
module: 'USERS',
targetId: id,
});
const detail = await this.users.getPlayerAdminDetail(BigInt(id));
return jsonResponse({ ...detail, password });
}
@Get('staff/roles')
@RequirePermissions(P.settings)
async listStaffRoles() {
const roles = await this.staff.listRoles();
return jsonResponse(roles);
}
@Get('staff')
@RequirePermissions(P.settings)
async listStaff(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('keyword') keyword?: string,
) {
const result = await this.staff.listStaff(
page ? parseInt(page, 10) : 1,
pageSize ? parseInt(pageSize, 10) : 20,
keyword,
);
return jsonResponse(result);
}
@Post('staff')
@RequirePermissions(P.settings)
async createStaff(
@CurrentUser('id') operatorId: bigint,
@Body() dto: CreateStaffDto,
) {
const created = await this.staff.createStaff(dto);
await this.audit.log({
operatorId,
operatorType: 'ADMIN',
action: 'CREATE_STAFF',
module: 'STAFF',
targetId: created.id,
afterData: { username: created.username, role: created.role },
});
return jsonResponse(created);
}
@Patch('staff/:id')
@RequirePermissions(P.settings)
async updateStaff(
@CurrentUser('id') operatorId: bigint,
@Param('id') id: string,
@Body() dto: UpdateStaffDto,
) {
const updated = await this.staff.updateStaff(BigInt(id), dto);
await this.audit.log({
operatorId,
operatorType: 'ADMIN',
action: 'UPDATE_STAFF',
module: 'STAFF',
targetId: id,
afterData: JSON.stringify({ status: dto.status, roleCode: dto.roleCode }),
});
return jsonResponse(updated);
}
@Delete('users/:id')
@RequirePermissions(P.usersCreate)
async deletePlayer(
@@ -2803,6 +2917,9 @@ export class AdminController {
@Get('audit-logs')
@RequirePermissions(P.audit)
async auditLogs(
@CurrentUser('id') viewerId: bigint,
@CurrentUser('role') viewerRole: string | undefined,
@CurrentUser('userType') viewerUserType: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('module') module?: string,
@@ -2811,6 +2928,7 @@ export class AdminController {
page ? parseInt(page, 10) : 1,
pageSize ? parseInt(pageSize, 10) : 10,
module || undefined,
{ viewerId, viewerRole, viewerUserType },
);
return jsonResponse(result);
}