From c928ee0f65a7b86ec71bbabd583db753640bb16e Mon Sep 17 00:00:00 2001 From: wangziqi Date: Thu, 9 Jul 2026 16:31:50 +0800 Subject: [PATCH] feat(rbac): add user-ding-mapping CRUD endpoints --- apps/server/src/rbac/dto/rbac.dto.ts | 20 ++++++++++++- apps/server/src/rbac/rbac.controller.ts | 27 ++++++++++++++++++ apps/server/src/rbac/rbac.module.ts | 4 +-- apps/server/src/rbac/rbac.service.ts | 37 +++++++++++++++++++++++-- 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/apps/server/src/rbac/dto/rbac.dto.ts b/apps/server/src/rbac/dto/rbac.dto.ts index 262ff38..9395630 100644 --- a/apps/server/src/rbac/dto/rbac.dto.ts +++ b/apps/server/src/rbac/dto/rbac.dto.ts @@ -1,4 +1,4 @@ -import { IsString, MinLength, IsOptional, IsArray, IsBoolean } from 'class-validator'; +import { IsString, MinLength, IsOptional, IsArray, IsBoolean, IsInt, IsNotEmpty } from 'class-validator'; export class CreateRoleDto { @IsString() @@ -79,3 +79,21 @@ export class UpdateProfileDto { @IsString() qualifications?: string; } + +export class CreateUserDingMappingDto { + @IsInt() + @IsNotEmpty() + userId: number; + + @IsString() + @IsNotEmpty() + dingUserId: string; + + @IsOptional() + @IsString() + dingName?: string; + + @IsOptional() + @IsString() + dingMobile?: string; +} diff --git a/apps/server/src/rbac/rbac.controller.ts b/apps/server/src/rbac/rbac.controller.ts index 06d1e96..ca7f67c 100644 --- a/apps/server/src/rbac/rbac.controller.ts +++ b/apps/server/src/rbac/rbac.controller.ts @@ -19,6 +19,7 @@ import { UpdateUserDto, ResetPasswordDto, UpdateProfileDto, + CreateUserDingMappingDto, } from './dto/rbac.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { RequirePermission } from '../auth/decorators/permission.decorator'; @@ -298,6 +299,32 @@ export class RbacController { } } + + // ---- 钉钉用户绑定 ---- + + @Get('user-ding-mappings') + @RequirePermission('user:view') + async getUserDingMappings() { + return this.rbacService.getUserDingMappings(); + } + + @Get('user-ding-mappings/unbound-users') + @RequirePermission('user:view') + async getUnboundUsers() { + return this.rbacService.getUnboundUsers(); + } + + @Post('user-ding-mappings') + @RequirePermission('user:edit') + async createUserDingMapping(@Body() dto: CreateUserDingMappingDto) { + return this.rbacService.createUserDingMapping(dto); + } + + @Delete('user-ding-mappings/:id') + @RequirePermission('user:delete') + async deleteUserDingMapping(@Param('id') id: string) { + return this.rbacService.deleteUserDingMapping(+id); + } // ---- 教师工作台 ---- @Get('teacher-workspace') diff --git a/apps/server/src/rbac/rbac.module.ts b/apps/server/src/rbac/rbac.module.ts index dd27fe6..4317fcd 100644 --- a/apps/server/src/rbac/rbac.module.ts +++ b/apps/server/src/rbac/rbac.module.ts @@ -1,12 +1,12 @@ import { Module, OnModuleInit, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student } from '../entities'; +import { Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, UserDingMapping } from '../entities'; import { RbacService } from './rbac.service'; import { RbacController } from './rbac.controller'; import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [TypeOrmModule.forFeature([Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student]), forwardRef(() => AuthModule)], + imports: [TypeOrmModule.forFeature([Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, UserDingMapping]), forwardRef(() => AuthModule)], controllers: [RbacController], providers: [RbacService], exports: [RbacService], diff --git a/apps/server/src/rbac/rbac.service.ts b/apps/server/src/rbac/rbac.service.ts index e42d6b5..9a4b226 100644 --- a/apps/server/src/rbac/rbac.service.ts +++ b/apps/server/src/rbac/rbac.service.ts @@ -1,8 +1,8 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, In } from 'typeorm'; import * as bcrypt from 'bcryptjs'; -import { Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student } from '../entities'; +import { Permission, Role, User, Class, ClassStudent, ClassTeacher, ClassSchedule, Student, UserDingMapping } from '../entities'; const PRESET_PERMISSIONS: Array<{ code: string; name: string; group: string }> = [ { code: 'dashboard:view', name: '查看数据面板', group: 'dashboard' }, @@ -171,6 +171,7 @@ export class RbacService { @InjectRepository(ClassTeacher) private classTeacherRepo: Repository, @InjectRepository(ClassSchedule) private classScheduleRepo: Repository, @InjectRepository(Student) private studentRepo: Repository, + @InjectRepository(UserDingMapping) private mappingRepo: Repository, ) {} async seedData(): Promise { @@ -246,6 +247,38 @@ export class RbacService { this.logger.log(`种子数据初始化完成: ${allPerms.length} 权限点, ${allRoles.length} 角色`); } + // ---- 钉钉用户绑定 ---- + + async getUserDingMappings(): Promise { + return this.mappingRepo.find({ relations: ['user'] }); + } + + async createUserDingMapping(dto: { userId: number; dingUserId: string; dingName?: string; dingMobile?: string }) { + const existing = await this.mappingRepo.findOne({ where: { dingUserId: dto.dingUserId } }); + if (existing) throw new ConflictException(`钉钉用户 ${dto.dingUserId} 已绑定到本地用户 #${existing.userId}`); + const userExisting = await this.mappingRepo.findOne({ where: { userId: dto.userId } }); + if (userExisting) throw new ConflictException(`本地用户 #${dto.userId} 已绑定到钉钉用户 ${userExisting.dingUserId}`); + const mapping = this.mappingRepo.create(dto); + return this.mappingRepo.save(mapping); + } + + async deleteUserDingMapping(id: number) { + const mapping = await this.mappingRepo.findOne({ where: { id } }); + if (!mapping) throw new NotFoundException('绑定记录不存在'); + await this.mappingRepo.remove(mapping); + return { success: true }; + } + + async getUnboundUsers(): Promise<{ id: number; username: string; name: string }[]> { + return this.userRepo + .createQueryBuilder('u') + .leftJoin(UserDingMapping, 'm', 'm.userId = u.id') + .where('m.id IS NULL') + .andWhere('u.isArchived = false') + .select(['u.id', 'u.username', 'u.name']) + .getMany(); + } + async findAllRoles(): Promise { return this.roleRepo.find({ relations: ['permissions'],