feat: add Attendance module with batch, summary, calendar, and dingtalk endpoints
This commit is contained in:
@@ -20,6 +20,8 @@ import {
|
|||||||
Permission,
|
Permission,
|
||||||
Role,
|
Role,
|
||||||
ClassSchedule,
|
ClassSchedule,
|
||||||
|
AttendanceRecord,
|
||||||
|
DingAttendanceRaw,
|
||||||
} from './entities';
|
} from './entities';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { RbacModule } from './rbac/rbac.module';
|
import { RbacModule } from './rbac/rbac.module';
|
||||||
@@ -35,6 +37,7 @@ import { DepositsModule } from './deposits/deposits.module';
|
|||||||
import { ClassroomsModule } from './classrooms/classrooms.module';
|
import { ClassroomsModule } from './classrooms/classrooms.module';
|
||||||
import { ClassesModule } from './classes/classes.module';
|
import { ClassesModule } from './classes/classes.module';
|
||||||
import { TenantsModule } from './tenants/tenants.module';
|
import { TenantsModule } from './tenants/tenants.module';
|
||||||
|
import { AttendanceModule } from './attendance/attendance.module';
|
||||||
import { SchedulesModule } from './schedules/schedules.module';
|
import { SchedulesModule } from './schedules/schedules.module';
|
||||||
import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.module';
|
import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.module';
|
||||||
|
|
||||||
@@ -69,6 +72,8 @@ import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.mo
|
|||||||
Permission,
|
Permission,
|
||||||
Role,
|
Role,
|
||||||
ClassSchedule,
|
ClassSchedule,
|
||||||
|
AttendanceRecord,
|
||||||
|
DingAttendanceRaw,
|
||||||
];
|
];
|
||||||
if (dbType === 'mysql') {
|
if (dbType === 'mysql') {
|
||||||
return {
|
return {
|
||||||
@@ -102,6 +107,7 @@ import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.mo
|
|||||||
OperationLogsModule,
|
OperationLogsModule,
|
||||||
DepositsModule,
|
DepositsModule,
|
||||||
ClassroomsModule,
|
ClassroomsModule,
|
||||||
|
AttendanceModule,
|
||||||
ClassesModule,
|
ClassesModule,
|
||||||
TenantsModule,
|
TenantsModule,
|
||||||
SchedulesModule,
|
SchedulesModule,
|
||||||
|
|||||||
97
apps/server/src/attendance/attendance.controller.ts
Normal file
97
apps/server/src/attendance/attendance.controller.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
UseGuards,
|
||||||
|
Request,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { AttendanceService } from './attendance.service';
|
||||||
|
import {
|
||||||
|
BatchCreateAttendanceDto,
|
||||||
|
AttendanceSummaryQueryDto,
|
||||||
|
AttendanceCalendarQueryDto,
|
||||||
|
QueryDingRawDto,
|
||||||
|
MatchDingRecordDto,
|
||||||
|
} from './dto/attendance.dto';
|
||||||
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||||
|
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||||
|
import { extractRequestInfo } from '../common/request-utils';
|
||||||
|
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Controller()
|
||||||
|
export class AttendanceController {
|
||||||
|
constructor(
|
||||||
|
private readonly service: AttendanceService,
|
||||||
|
private readonly logService: OperationLogsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// ── Batch create attendance records ──
|
||||||
|
@Post('attendance-records/batch')
|
||||||
|
@RequirePermission('attendance:create')
|
||||||
|
async batchCreate(
|
||||||
|
@Body() dto: BatchCreateAttendanceDto,
|
||||||
|
@Request() req: any,
|
||||||
|
) {
|
||||||
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
|
const result = await this.service.batchCreate(dto);
|
||||||
|
await this.logService.log({
|
||||||
|
userId: req.user?.id,
|
||||||
|
username: req.user?.username,
|
||||||
|
module: '考勤管理',
|
||||||
|
action: '批量录入考勤',
|
||||||
|
detail: `共 ${result.count} 条`,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Attendance summary ──
|
||||||
|
@Get('attendance-records/summary')
|
||||||
|
@RequirePermission('attendance:view')
|
||||||
|
getSummary(@Query() query: AttendanceSummaryQueryDto) {
|
||||||
|
return this.service.getSummary(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Attendance calendar ──
|
||||||
|
@Get('attendance-records/calendar')
|
||||||
|
@RequirePermission('attendance:view')
|
||||||
|
getCalendar(@Query() query: AttendanceCalendarQueryDto) {
|
||||||
|
return this.service.getCalendar(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DingAttendance raw records ──
|
||||||
|
@Get('ding-attendance-raw')
|
||||||
|
@RequirePermission('attendance:view')
|
||||||
|
getDingRaw(@Query() query: QueryDingRawDto) {
|
||||||
|
return this.service.getDingRaw(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Match a dingtalk record to a student ──
|
||||||
|
@Post('ding-attendance-raw/:id/match')
|
||||||
|
@RequirePermission('attendance:edit')
|
||||||
|
async matchDingRecord(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() dto: MatchDingRecordDto,
|
||||||
|
@Request() req: any,
|
||||||
|
) {
|
||||||
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||||
|
const result = await this.service.matchDingRecord(+id, dto);
|
||||||
|
await this.logService.log({
|
||||||
|
userId: req.user?.id,
|
||||||
|
username: req.user?.username,
|
||||||
|
module: '考勤管理',
|
||||||
|
action: '匹配考勤记录',
|
||||||
|
targetId: +id,
|
||||||
|
targetType: 'dingAttendanceRaw',
|
||||||
|
detail: `匹配到学生 ${dto.studentId}`,
|
||||||
|
ipAddress,
|
||||||
|
userAgent,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
apps/server/src/attendance/attendance.module.ts
Normal file
17
apps/server/src/attendance/attendance.module.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AttendanceRecord, DingAttendanceRaw, Student, Class } from '../entities';
|
||||||
|
import { AttendanceService } from './attendance.service';
|
||||||
|
import { AttendanceController } from './attendance.controller';
|
||||||
|
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([AttendanceRecord, DingAttendanceRaw, Student, Class]),
|
||||||
|
OperationLogsModule,
|
||||||
|
],
|
||||||
|
controllers: [AttendanceController],
|
||||||
|
providers: [AttendanceService],
|
||||||
|
exports: [AttendanceService],
|
||||||
|
})
|
||||||
|
export class AttendanceModule {}
|
||||||
162
apps/server/src/attendance/attendance.service.ts
Normal file
162
apps/server/src/attendance/attendance.service.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, In, Between } from 'typeorm';
|
||||||
|
import { AttendanceRecord, DingAttendanceRaw } from '../entities';
|
||||||
|
import {
|
||||||
|
BatchCreateAttendanceDto,
|
||||||
|
AttendanceSummaryQueryDto,
|
||||||
|
AttendanceCalendarQueryDto,
|
||||||
|
QueryDingRawDto,
|
||||||
|
MatchDingRecordDto,
|
||||||
|
} from './dto/attendance.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AttendanceService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AttendanceRecord)
|
||||||
|
private attendanceRepo: Repository<AttendanceRecord>,
|
||||||
|
@InjectRepository(DingAttendanceRaw)
|
||||||
|
private dingRawRepo: Repository<DingAttendanceRaw>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// ── Batch create attendance records ──
|
||||||
|
async batchCreate(dto: BatchCreateAttendanceDto) {
|
||||||
|
if (!dto.records || dto.records.length === 0) {
|
||||||
|
throw new BadRequestException('records array must not be empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entities = dto.records.map((r) =>
|
||||||
|
this.attendanceRepo.create({
|
||||||
|
studentId: r.studentId,
|
||||||
|
classId: r.classId ?? undefined,
|
||||||
|
attendanceDate: r.attendanceDate,
|
||||||
|
session: r.session,
|
||||||
|
status: r.status,
|
||||||
|
remark: r.remark,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const saved = await this.attendanceRepo.save(entities);
|
||||||
|
return { count: saved.length, records: saved };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Attendance summary ──
|
||||||
|
async getSummary(query: AttendanceSummaryQueryDto) {
|
||||||
|
const qb = this.attendanceRepo.createQueryBuilder('ar');
|
||||||
|
|
||||||
|
if (query.classId) {
|
||||||
|
qb.andWhere('ar.classId = :classId', { classId: query.classId });
|
||||||
|
}
|
||||||
|
if (query.dateFrom) {
|
||||||
|
qb.andWhere('ar.attendanceDate >= :dateFrom', { dateFrom: query.dateFrom });
|
||||||
|
}
|
||||||
|
if (query.dateTo) {
|
||||||
|
qb.andWhere('ar.attendanceDate <= :dateTo', { dateTo: query.dateTo });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await qb.getMany();
|
||||||
|
|
||||||
|
const total = rows.length;
|
||||||
|
const present = rows.filter((r) => r.status === 'present').length;
|
||||||
|
const late = rows.filter((r) => r.status === 'late').length;
|
||||||
|
const absent = rows.filter((r) => r.status === 'absent').length;
|
||||||
|
const leave = rows.filter((r) => r.status === 'leave').length;
|
||||||
|
const presentRate = total > 0 ? Number(((present / total) * 100).toFixed(1)) : 0;
|
||||||
|
|
||||||
|
return { total, present, late, absent, leave, presentRate };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Attendance calendar ──
|
||||||
|
async getCalendar(query: AttendanceCalendarQueryDto) {
|
||||||
|
const { classId, weekStart } = query;
|
||||||
|
|
||||||
|
if (!weekStart) {
|
||||||
|
// Default to the Monday of the current week
|
||||||
|
const now = new Date();
|
||||||
|
const day = now.getDay();
|
||||||
|
const diff = day === 0 ? -6 : 1 - day; // Monday offset
|
||||||
|
const monday = new Date(now);
|
||||||
|
monday.setDate(now.getDate() + diff);
|
||||||
|
const mondayStr = monday.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
return this.buildCalendar(classId, mondayStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.buildCalendar(classId, weekStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildCalendar(classId: number, weekStart: string) {
|
||||||
|
// Compute weekEnd (Sunday = weekStart + 6 days)
|
||||||
|
const start = new Date(weekStart);
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setDate(start.getDate() + 6);
|
||||||
|
const endStr = end.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
// Fetch attendance records for the week
|
||||||
|
const records = await this.attendanceRepo.find({
|
||||||
|
where: {
|
||||||
|
classId,
|
||||||
|
attendanceDate: Between(weekStart, endStr),
|
||||||
|
},
|
||||||
|
relations: ['student'],
|
||||||
|
order: { attendanceDate: 'ASC', session: 'ASC' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group by studentId
|
||||||
|
const studentMap = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
studentId: number;
|
||||||
|
studentName: string;
|
||||||
|
days: Array<{ date: string; session: string; status: string }>;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
for (const r of records) {
|
||||||
|
if (!studentMap.has(r.studentId)) {
|
||||||
|
studentMap.set(r.studentId, {
|
||||||
|
studentId: r.studentId,
|
||||||
|
studentName: r.student?.name ?? `Student#${r.studentId}`,
|
||||||
|
days: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
studentMap.get(r.studentId)!.days.push({
|
||||||
|
date: r.attendanceDate,
|
||||||
|
session: r.session,
|
||||||
|
status: r.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(studentMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DingAttendance raw records ──
|
||||||
|
async getDingRaw(query: QueryDingRawDto) {
|
||||||
|
const where: any = {};
|
||||||
|
if (query.matchStatus) {
|
||||||
|
where.matchStatus = query.matchStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dingRawRepo.find({
|
||||||
|
where,
|
||||||
|
relations: ['matchedStudent'],
|
||||||
|
order: { attendanceDate: 'DESC', checkInTime: 'ASC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Match a dingtalk record to a student ──
|
||||||
|
async matchDingRecord(id: number, dto: MatchDingRecordDto) {
|
||||||
|
const record = await this.dingRawRepo.findOne({ where: { id } });
|
||||||
|
if (!record) {
|
||||||
|
throw new NotFoundException(`DingAttendanceRaw ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
record.matchedStudentId = dto.studentId;
|
||||||
|
record.matchStatus = '已匹配';
|
||||||
|
return this.dingRawRepo.save(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
apps/server/src/attendance/dto/attendance.dto.ts
Normal file
85
apps/server/src/attendance/dto/attendance.dto.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsInt,
|
||||||
|
IsDateString,
|
||||||
|
IsIn,
|
||||||
|
ValidateNested,
|
||||||
|
IsNotEmpty,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class AttendanceRecordItem {
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
studentId: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
classId?: number;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
attendanceDate: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['morning', 'afternoon', 'evening'])
|
||||||
|
@IsNotEmpty()
|
||||||
|
session: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['present', 'late', 'absent', 'leave'])
|
||||||
|
@IsNotEmpty()
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
remark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BatchCreateAttendanceDto {
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => AttendanceRecordItem)
|
||||||
|
records: AttendanceRecordItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AttendanceSummaryQueryDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
classId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
dateFrom?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
dateTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AttendanceCalendarQueryDto {
|
||||||
|
@IsInt()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsNotEmpty()
|
||||||
|
classId: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
weekStart?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryDingRawDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@IsIn(['未处理', '已匹配', '待匹配'])
|
||||||
|
matchStatus?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatchDingRecordDto {
|
||||||
|
@IsInt()
|
||||||
|
@IsNotEmpty()
|
||||||
|
studentId: number;
|
||||||
|
}
|
||||||
52
apps/server/src/entities/attendance-record.entity.ts
Normal file
52
apps/server/src/entities/attendance-record.entity.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Student } from './student.entity';
|
||||||
|
import { Class } from './class.entity';
|
||||||
|
|
||||||
|
@Entity('attendance_records')
|
||||||
|
@Index(['classId', 'attendanceDate'])
|
||||||
|
@Index(['studentId', 'attendanceDate'])
|
||||||
|
export class AttendanceRecord {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ name: 'student_id', type: 'integer' })
|
||||||
|
studentId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => Student, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'student_id' })
|
||||||
|
student: Student;
|
||||||
|
|
||||||
|
@Column({ name: 'class_id', type: 'integer', nullable: true })
|
||||||
|
classId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => Class, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'class_id' })
|
||||||
|
class: Class;
|
||||||
|
|
||||||
|
@Column({ name: 'attendance_date', type: 'date' })
|
||||||
|
attendanceDate: string;
|
||||||
|
|
||||||
|
@Column({ length: 20 })
|
||||||
|
session: string;
|
||||||
|
|
||||||
|
@Column({ length: 20 })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@Column({ length: 200, nullable: true })
|
||||||
|
remark: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
65
apps/server/src/entities/ding-attendance-raw.entity.ts
Normal file
65
apps/server/src/entities/ding-attendance-raw.entity.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Student } from './student.entity';
|
||||||
|
|
||||||
|
@Entity('ding_attendance_raw')
|
||||||
|
@Index(['attendanceDate'])
|
||||||
|
@Index(['matchStatus'])
|
||||||
|
export class DingAttendanceRaw {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ name: 'ding_user_id', length: 100 })
|
||||||
|
dingUserId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'user_name', length: 100 })
|
||||||
|
userName: string;
|
||||||
|
|
||||||
|
@Column({ name: 'attendance_date', type: 'date' })
|
||||||
|
attendanceDate: string;
|
||||||
|
|
||||||
|
@Column({ name: 'ding_id', length: 100, unique: true })
|
||||||
|
dingId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'check_in_time', type: 'datetime', nullable: true })
|
||||||
|
checkInTime: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'check_out_time', type: 'datetime', nullable: true })
|
||||||
|
checkOutTime: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'attendance_type', length: 20 })
|
||||||
|
attendanceType: string;
|
||||||
|
|
||||||
|
@Column({ name: 'time_result', length: 20 })
|
||||||
|
timeResult: string;
|
||||||
|
|
||||||
|
@Column({ name: 'location_result', length: 20, nullable: true })
|
||||||
|
locationResult: string;
|
||||||
|
|
||||||
|
@Column({ name: 'match_status', length: 20, default: '未处理' })
|
||||||
|
matchStatus: string;
|
||||||
|
|
||||||
|
@Column({ name: 'matched_student_id', type: 'integer', nullable: true })
|
||||||
|
matchedStudentId: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => Student, { onDelete: 'SET NULL', nullable: true })
|
||||||
|
@JoinColumn({ name: 'matched_student_id' })
|
||||||
|
matchedStudent: Student;
|
||||||
|
|
||||||
|
@Column({ name: 'raw_data', type: 'text', nullable: true })
|
||||||
|
rawData: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@@ -17,3 +17,5 @@ export { Class, ClassType, ClassStatus } from './class.entity';
|
|||||||
export { ClassStudent } from './class-student.entity';
|
export { ClassStudent } from './class-student.entity';
|
||||||
export { ClassTeacher, TeacherRoleType } from './class-teacher.entity';
|
export { ClassTeacher, TeacherRoleType } from './class-teacher.entity';
|
||||||
export { ClassSchedule, ScheduleType } from './class-schedule.entity';
|
export { ClassSchedule, ScheduleType } from './class-schedule.entity';
|
||||||
|
export { AttendanceRecord } from './attendance-record.entity';
|
||||||
|
export { DingAttendanceRaw } from './ding-attendance-raw.entity';
|
||||||
|
|||||||
Reference in New Issue
Block a user