forked from wangziqi/gongxue-base
feat: add attendance device SN classroom bindings
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
import { extractRequestInfo } from '../common/request-utils';
|
||||
import { AttendanceDevicesService } from './attendance-devices.service';
|
||||
import { CreateAttendanceDeviceDto, UpdateAttendanceDeviceDto } from './dto/attendance-device.dto';
|
||||
import { AttendanceDeviceStatus } from '../entities';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('attendance-devices')
|
||||
export class AttendanceDevicesController {
|
||||
constructor(
|
||||
private readonly service: AttendanceDevicesService,
|
||||
private readonly logService: OperationLogsService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('classroom:view')
|
||||
findAll(
|
||||
@Query('classroomId') classroomId?: string,
|
||||
@Query('status') status?: AttendanceDeviceStatus | 'active' | 'disabled',
|
||||
) {
|
||||
return this.service.findAll({
|
||||
classroomId: classroomId ? Number(classroomId) : undefined,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('classroom:edit')
|
||||
async create(@Body() dto: CreateAttendanceDeviceDto, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.create(dto);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '考勤机',
|
||||
action: '新增考勤机绑定',
|
||||
targetId: result.id,
|
||||
targetType: 'attendanceDevice',
|
||||
detail: `${result.deviceSn} -> ${result.classroom?.name || result.classroomId}`,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermission('classroom:edit')
|
||||
async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateAttendanceDeviceDto, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.update(id, dto);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '考勤机',
|
||||
action: '编辑考勤机绑定',
|
||||
targetId: id,
|
||||
targetType: 'attendanceDevice',
|
||||
detail: JSON.stringify(dto),
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('classroom:edit')
|
||||
async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.remove(id);
|
||||
await this.logService.log({
|
||||
userId: req.user?.id,
|
||||
username: req.user?.username,
|
||||
module: '考勤机',
|
||||
action: '删除考勤机绑定',
|
||||
targetId: id,
|
||||
targetType: 'attendanceDevice',
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AttendanceDevice, Classroom } from '../entities';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { AttendanceDevicesController } from './attendance-devices.controller';
|
||||
import { AttendanceDevicesService } from './attendance-devices.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AttendanceDevice, Classroom]), OperationLogsModule],
|
||||
controllers: [AttendanceDevicesController],
|
||||
providers: [AttendanceDevicesService],
|
||||
exports: [AttendanceDevicesService],
|
||||
})
|
||||
export class AttendanceDevicesModule {}
|
||||
105
apps/server/src/attendance-devices/attendance-devices.service.ts
Normal file
105
apps/server/src/attendance-devices/attendance-devices.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { AttendanceDevice, AttendanceDeviceStatus, Classroom } from '../entities';
|
||||
import { CreateAttendanceDeviceDto, UpdateAttendanceDeviceDto } from './dto/attendance-device.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AttendanceDevicesService {
|
||||
constructor(
|
||||
@InjectRepository(AttendanceDevice)
|
||||
private readonly repo: Repository<AttendanceDevice>,
|
||||
@InjectRepository(Classroom)
|
||||
private readonly classroomRepo: Repository<Classroom>,
|
||||
) {}
|
||||
|
||||
private normalizeSn(sn: string): string {
|
||||
return sn.trim();
|
||||
}
|
||||
|
||||
private async assertClassroomExists(classroomId: number): Promise<void> {
|
||||
const exists = await this.classroomRepo.exist({ where: { id: classroomId } });
|
||||
if (!exists) throw new BadRequestException('绑定教室不存在');
|
||||
}
|
||||
|
||||
async findAll(query?: { classroomId?: number; status?: AttendanceDeviceStatus | 'active' | 'disabled' }) {
|
||||
const where: Record<string, unknown> = {};
|
||||
if (query?.classroomId) where.classroomId = query.classroomId;
|
||||
if (query?.status) where.status = query.status;
|
||||
return this.repo.find({
|
||||
where,
|
||||
relations: ['classroom'],
|
||||
order: { classroomId: 'ASC', deviceName: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const device = await this.repo.findOne({ where: { id }, relations: ['classroom'] });
|
||||
if (!device) throw new NotFoundException('考勤机不存在');
|
||||
return device;
|
||||
}
|
||||
|
||||
async create(dto: CreateAttendanceDeviceDto) {
|
||||
const deviceSn = this.normalizeSn(dto.deviceSn);
|
||||
await this.assertClassroomExists(dto.classroomId);
|
||||
const exists = await this.repo.findOne({ where: { deviceSn } });
|
||||
if (exists) throw new BadRequestException(`SN ${deviceSn} 已绑定教室`);
|
||||
const saved = await this.repo.save(
|
||||
this.repo.create({
|
||||
...dto,
|
||||
deviceSn,
|
||||
deviceName: dto.deviceName.trim(),
|
||||
status: dto.status ?? AttendanceDeviceStatus.ACTIVE,
|
||||
}),
|
||||
);
|
||||
return this.findOne(saved.id);
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateAttendanceDeviceDto) {
|
||||
const device = await this.repo.findOne({ where: { id } });
|
||||
if (!device) throw new NotFoundException('考勤机不存在');
|
||||
const patch: Partial<AttendanceDevice> = { ...dto };
|
||||
if (dto.classroomId != null) await this.assertClassroomExists(dto.classroomId);
|
||||
if (dto.deviceSn != null) {
|
||||
const deviceSn = this.normalizeSn(dto.deviceSn);
|
||||
const exists = await this.repo.findOne({ where: { deviceSn } });
|
||||
if (exists && exists.id !== id) throw new BadRequestException(`SN ${deviceSn} 已绑定教室`);
|
||||
patch.deviceSn = deviceSn;
|
||||
}
|
||||
if (dto.deviceName != null) patch.deviceName = dto.deviceName.trim();
|
||||
await this.repo.update(id, patch);
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const device = await this.repo.findOne({ where: { id } });
|
||||
if (!device) throw new NotFoundException('考勤机不存在');
|
||||
await this.repo.delete(id);
|
||||
return { message: '已删除' };
|
||||
}
|
||||
|
||||
async findActiveBySn(deviceSns: string[]) {
|
||||
const sns = [...new Set(deviceSns.map((sn) => this.normalizeSn(sn)).filter(Boolean))];
|
||||
if (sns.length === 0) return new Map<string, AttendanceDevice>();
|
||||
const devices = await this.repo.find({
|
||||
where: { deviceSn: In(sns), status: AttendanceDeviceStatus.ACTIVE },
|
||||
relations: ['classroom'],
|
||||
});
|
||||
return new Map(devices.map((device) => [device.deviceSn, device]));
|
||||
}
|
||||
|
||||
async findActiveByClassroomIds(classroomIds: number[]) {
|
||||
const ids = [...new Set(classroomIds.filter((id) => Number.isFinite(id)))];
|
||||
if (ids.length === 0) return new Map<number, AttendanceDevice>();
|
||||
const devices = await this.repo.find({
|
||||
where: { classroomId: In(ids), status: AttendanceDeviceStatus.ACTIVE },
|
||||
relations: ['classroom'],
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const result = new Map<number, AttendanceDevice>();
|
||||
for (const device of devices) {
|
||||
if (!result.has(device.classroomId)) result.set(device.classroomId, device);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { AttendanceDeviceStatus } from '../../entities/attendance-device.entity';
|
||||
|
||||
export class CreateAttendanceDeviceDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
deviceSn: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
deviceName: string;
|
||||
|
||||
@IsInt()
|
||||
classroomId: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AttendanceDeviceStatus)
|
||||
status?: AttendanceDeviceStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
location?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class UpdateAttendanceDeviceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
deviceSn?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
deviceName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
classroomId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(AttendanceDeviceStatus)
|
||||
status?: AttendanceDeviceStatus;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
location?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user