refactor: remove obsolete scaffolding and dead modules
Drop the unused root hello-world controller, empty CommonModule imports, development seeding code, legacy student report entity, stale DTOs, notification hook, and their placeholder tests.
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import api from '../api';
|
||||
|
||||
interface Notification {
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
content: string;
|
||||
link: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export function useNotifications() {
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [latestNotification, setLatestNotification] = useState<Notification | null>(null);
|
||||
|
||||
const fetchUnreadCount = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.get('/notifications/unread-count') as unknown as { count: number };
|
||||
setUnreadCount(data.count);
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUnreadCount();
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const es = new EventSource(`/api/notifications/stream?token=${encodeURIComponent(token)}`);
|
||||
|
||||
es.onmessage = (event) => {
|
||||
try {
|
||||
const notification = JSON.parse(event.data) as Notification;
|
||||
setUnreadCount((c) => c + 1);
|
||||
setLatestNotification(notification);
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
};
|
||||
|
||||
const pollRef = { current: undefined as number | undefined };
|
||||
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
pollRef.current = setInterval(() => {
|
||||
fetchUnreadCount();
|
||||
}, 60_000);
|
||||
};
|
||||
|
||||
return () => {
|
||||
es.close();
|
||||
if (pollRef.current !== undefined) clearInterval(pollRef.current);
|
||||
};
|
||||
}, [fetchUnreadCount]);
|
||||
|
||||
const markAsRead = useCallback(async (id: number) => {
|
||||
try {
|
||||
await api.put(`/notifications/${id}/read`);
|
||||
setUnreadCount((c) => Math.max(0, c - 1));
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
}, []);
|
||||
|
||||
const markAllAsRead = useCallback(async () => {
|
||||
try {
|
||||
await api.put('/notifications/read-all');
|
||||
setUnreadCount(0);
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { unreadCount, latestNotification, markAsRead, markAllAsRead };
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,6 @@ import { SchedulesModule } from './schedules/schedules.module';
|
||||
import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.module';
|
||||
import { SyncModule } from './sync/sync.module';
|
||||
import { NotificationsModule } from './notifications/notifications.module';
|
||||
import { CommonModule } from './common/common.module';
|
||||
import { ArchiveModule } from './archive/archive.module';
|
||||
import { ExpenseTypesModule } from './expense-types/expense-types.module';
|
||||
|
||||
@@ -161,7 +160,6 @@ import { IntegrationConfigModule } from './integration/config/config.module';
|
||||
ClassroomRentalsModule,
|
||||
SyncModule,
|
||||
NotificationsModule,
|
||||
CommonModule,
|
||||
ArchiveModule,
|
||||
IntegrationConfigModule,
|
||||
ExpenseTypesModule,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { LearningRecord } from '../entities/learning-record.entity';
|
||||
import { ResultArchive } from '../entities/result-archive.entity';
|
||||
import { ArchiveAttachment } from '../entities/archive-attachment.entity';
|
||||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { ArchiveService } from './archive.service';
|
||||
import { ArchiveReportService } from './archive-report.service';
|
||||
@@ -26,7 +25,6 @@ import { ArchiveController } from './archive.controller';
|
||||
ArchiveAttachment,
|
||||
AttendanceRecord,
|
||||
]),
|
||||
CommonModule,
|
||||
NotificationsModule,
|
||||
],
|
||||
controllers: [ArchiveController],
|
||||
|
||||
@@ -5,14 +5,12 @@ import { AttendanceService } from './attendance.service';
|
||||
import { AttendanceImportService } from './attendance-import.service';
|
||||
import { AttendanceController } from './attendance.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { IntegrationModule } from '../integration/integration.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([AttendanceRecord, DingAttendanceRaw, Student, Class, ClassSchedule, ClassStudent, StudentDingMapping]),
|
||||
OperationLogsModule,
|
||||
CommonModule,
|
||||
IntegrationModule,
|
||||
],
|
||||
controllers: [AttendanceController],
|
||||
|
||||
@@ -8,15 +8,3 @@ export class LoginDto {
|
||||
@MinLength(4)
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class RegisterDto {
|
||||
@IsString()
|
||||
username: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(4)
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Bill } from '../entities/bill.entity';
|
||||
import { BillItem } from '../entities/bill-item.entity';
|
||||
@@ -27,7 +26,6 @@ import { BillsController } from './bills.controller';
|
||||
Student,
|
||||
]),
|
||||
NotificationsModule,
|
||||
CommonModule,
|
||||
],
|
||||
controllers: [BillsController],
|
||||
providers: [BillsService, BillsExportService],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Student, StudentDingMapping } from '../entities';
|
||||
import { ClassesService } from './classes.service';
|
||||
import { ClassesController } from './classes.controller';
|
||||
@@ -8,7 +7,7 @@ import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Student, StudentDingMapping]), OperationLogsModule, NotificationsModule, CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Student, StudentDingMapping]), OperationLogsModule, NotificationsModule],
|
||||
controllers: [ClassesController],
|
||||
providers: [ClassesService],
|
||||
exports: [ClassesService],
|
||||
|
||||
@@ -7,10 +7,9 @@ import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||
import { ClassroomRentalsService } from './classroom-rentals.service';
|
||||
import { ClassroomRentalsController } from './classroom-rentals.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ClassroomRental, Classroom, Tenant, ClassSchedule]), OperationLogsModule, CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([ClassroomRental, Classroom, Tenant, ClassSchedule]), OperationLogsModule],
|
||||
controllers: [ClassroomRentalsController],
|
||||
providers: [ClassroomRentalsService],
|
||||
exports: [ClassroomRentalsService],
|
||||
|
||||
@@ -6,10 +6,9 @@ import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||
import { ClassroomsService } from './classrooms.service';
|
||||
import { ClassroomsController } from './classrooms.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Classroom, ClassroomRental, ClassSchedule]), OperationLogsModule, CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([Classroom, ClassroomRental, ClassSchedule]), OperationLogsModule],
|
||||
controllers: [ClassroomsController],
|
||||
providers: [ClassroomsService],
|
||||
exports: [ClassroomsService],
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class CommonModule {}
|
||||
@@ -13,11 +13,10 @@ import { Deposit } from '../entities/deposit.entity';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { ClassTeacher } from '../entities/class-teacher.entity';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { DashboardController } from './dashboard.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Room, Student, Occupancy, Bill, RoomExpense, Classroom, ClassSchedule, AttendanceRecord, Class, Deposit, ClassroomRental, ClassTeacher]), CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([Room, Student, Occupancy, Bill, RoomExpense, Classroom, ClassSchedule, AttendanceRecord, Class, Deposit, ClassroomRental, ClassTeacher])],
|
||||
controllers: [DashboardController],
|
||||
providers: [DashboardService],
|
||||
})
|
||||
|
||||
@@ -4,13 +4,12 @@ import { Student } from '../entities/student.entity';
|
||||
import { Deposit } from '../entities/deposit.entity';
|
||||
import { DepositInstallment } from '../entities/deposit-installment.entity';
|
||||
import { DepositsService } from './deposits.service';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { DepositsController } from './deposits.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Deposit, DepositInstallment, Student]), OperationLogsModule, NotificationsModule, CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([Deposit, DepositInstallment, Student]), OperationLogsModule, NotificationsModule],
|
||||
controllers: [DepositsController],
|
||||
providers: [DepositsService],
|
||||
exports: [DepositsService],
|
||||
|
||||
@@ -24,15 +24,6 @@ export class CreateInstallmentDto {
|
||||
dueDate: string;
|
||||
}
|
||||
|
||||
export class UpdateInstallmentDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
paidDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
status?: string;
|
||||
}
|
||||
|
||||
|
||||
export class RefundDepositDto {
|
||||
@@ -57,9 +48,3 @@ export class CreateDepositWithInstallmentsDto extends CreateDepositDto {
|
||||
@Type(() => CreateInstallmentDto)
|
||||
installments?: CreateInstallmentDto[];
|
||||
}
|
||||
|
||||
export class ApproveRefundDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
@@ -32,5 +32,4 @@ export { ExamScore } from './exam-score.entity';
|
||||
export { LearningRecord } from './learning-record.entity';
|
||||
export { ResultArchive } from './result-archive.entity';
|
||||
export { ArchiveAttachment } from './archive-attachment.entity';
|
||||
export { StudentReport } from './student-report.entity';
|
||||
export { StudentDingMapping } from './student-ding-mapping.entity';
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Student } from './student.entity';
|
||||
|
||||
@Entity('student_reports')
|
||||
export class StudentReport {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'student_id', type: 'integer' })
|
||||
studentId: number;
|
||||
|
||||
@ManyToOne(() => Student, { eager: true })
|
||||
@JoinColumn({ name: 'student_id' })
|
||||
student: Student;
|
||||
|
||||
@Column({ name: 'snapshot_data', type: 'simple-json', nullable: true })
|
||||
snapshotData: Record<string, unknown>;
|
||||
|
||||
@Column({ name: 'html_content', type: 'text', nullable: true })
|
||||
htmlContent: string;
|
||||
|
||||
@Column({ name: 'pdf_path', length: 500, nullable: true })
|
||||
pdfPath: string;
|
||||
|
||||
@Column({ name: 'generated_at', type: 'datetime', nullable: true })
|
||||
generatedAt: Date;
|
||||
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { RoomExpense } from '../entities/room-expense.entity';
|
||||
import { PersonalExpense } from '../entities/personal-expense.entity';
|
||||
import { Room } from '../entities/room.entity';
|
||||
@@ -13,7 +12,6 @@ import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([RoomExpense, PersonalExpense, Room, Student]),
|
||||
OperationLogsModule,
|
||||
CommonModule,
|
||||
],
|
||||
controllers: [ExpensesController],
|
||||
providers: [ExpensesService],
|
||||
|
||||
@@ -10,10 +10,9 @@ import { OccupanciesService } from './occupancies.service';
|
||||
import { OccupanciesController } from './occupancies.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Occupancy, Room, Student, Deposit, Bed, Locker]), OperationLogsModule, NotificationsModule, CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([Occupancy, Room, Student, Deposit, Bed, Locker]), OperationLogsModule, NotificationsModule],
|
||||
controllers: [OccupanciesController],
|
||||
providers: [OccupanciesService],
|
||||
exports: [OccupanciesService],
|
||||
|
||||
@@ -8,10 +8,9 @@ import { Locker } from '../entities/locker.entity';
|
||||
import { RoomsService } from './rooms.service';
|
||||
import { RoomsController } from './rooms.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Room, Occupancy, RoomExpense, Bed, Locker]), OperationLogsModule, CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([Room, Occupancy, RoomExpense, Bed, Locker]), OperationLogsModule],
|
||||
controllers: [RoomsController],
|
||||
providers: [RoomsService],
|
||||
exports: [RoomsService],
|
||||
|
||||
@@ -5,10 +5,9 @@ import { SchedulesService } from './schedules.service';
|
||||
import { SchedulesController } from './schedules.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ClassSchedule, Class, ClassroomRental]), OperationLogsModule, NotificationsModule, CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([ClassSchedule, Class, ClassroomRental]), OperationLogsModule, NotificationsModule],
|
||||
controllers: [SchedulesController],
|
||||
providers: [SchedulesService],
|
||||
exports: [SchedulesService],
|
||||
|
||||
@@ -1,768 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import {
|
||||
User, Role, Permission,
|
||||
Tenant, Student, Room, Classroom, Occupancy,
|
||||
RoomExpense, ExpenseType, Class,
|
||||
ClassStudent, ClassTeacher, ClassSchedule,
|
||||
Bill, BillItem, Deposit,
|
||||
AttendanceRecord, ClassroomRental,
|
||||
StudentProfile, StudentEnrollment, ExamScore,
|
||||
LearningRecord, TeacherRoleType,
|
||||
ClassType, ClassStatus, ScheduleType,
|
||||
} from '../entities';
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────
|
||||
|
||||
function randInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function pick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
// ── service ──────────────────────────────────────────────
|
||||
|
||||
@Injectable()
|
||||
export class SeedDevService {
|
||||
private readonly logger = new Logger(SeedDevService.name);
|
||||
|
||||
private cachedStudents: Student[] = [];
|
||||
private cachedRooms: Room[] = [];
|
||||
private cachedClassrooms: Classroom[] = [];
|
||||
private cachedUsers: User[] = [];
|
||||
private cachedClasses: Class[] = [];
|
||||
private cachedTenants: Tenant[] = [];
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Permission) private permRepo: Repository<Permission>,
|
||||
@InjectRepository(Role) private roleRepo: Repository<Role>,
|
||||
@InjectRepository(User) private userRepo: Repository<User>,
|
||||
@InjectRepository(Tenant) private tenantRepo: Repository<Tenant>,
|
||||
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
||||
@InjectRepository(Room) private roomRepo: Repository<Room>,
|
||||
@InjectRepository(Classroom) private classroomRepo: Repository<Classroom>,
|
||||
@InjectRepository(Occupancy) private occupancyRepo: Repository<Occupancy>,
|
||||
@InjectRepository(RoomExpense) private roomExpenseRepo: Repository<RoomExpense>,
|
||||
@InjectRepository(ExpenseType) private expenseTypeRepo: Repository<ExpenseType>,
|
||||
@InjectRepository(Class) private classRepo: Repository<Class>,
|
||||
@InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
|
||||
@InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
|
||||
@InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
|
||||
@InjectRepository(Bill) private billRepo: Repository<Bill>,
|
||||
@InjectRepository(BillItem) private billItemRepo: Repository<BillItem>,
|
||||
@InjectRepository(Deposit) private depositRepo: Repository<Deposit>,
|
||||
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
|
||||
@InjectRepository(ClassroomRental) private rentalRepo: Repository<ClassroomRental>,
|
||||
@InjectRepository(StudentProfile) private profileRepo: Repository<StudentProfile>,
|
||||
@InjectRepository(StudentEnrollment) private enrollmentRepo: Repository<StudentEnrollment>,
|
||||
@InjectRepository(ExamScore) private examScoreRepo: Repository<ExamScore>,
|
||||
@InjectRepository(LearningRecord) private learningRecordRepo: Repository<LearningRecord>,
|
||||
) {}
|
||||
|
||||
async getUserCount(): Promise<number> {
|
||||
return this.userRepo.count();
|
||||
}
|
||||
|
||||
async getStudentCount(): Promise<number> {
|
||||
return this.studentRepo.count();
|
||||
}
|
||||
|
||||
async seed(): Promise<void> {
|
||||
// ═══════════════════ Layer 0 ══════════════════════════
|
||||
await this.seedPermissions();
|
||||
await this.seedExpenseTypes();
|
||||
|
||||
|
||||
// ═══════════════════ Layer 1 ══════════════════════════
|
||||
await this.seedRoles();
|
||||
await this.seedUsers();
|
||||
|
||||
// ═══════════════════ Layer 2 ══════════════════════════
|
||||
await this.seedTenants();
|
||||
await this.seedRooms();
|
||||
await this.seedClassrooms();
|
||||
await this.seedStudents();
|
||||
|
||||
// ═══════════════════ Layer 3 ══════════════════════════
|
||||
await this.seedClasses();
|
||||
await this.seedOccupancies();
|
||||
await this.seedSchedules();
|
||||
|
||||
// ═══════════════════ Layer 4 ══════════════════════════
|
||||
await this.seedRoomExpenses();
|
||||
await this.seedDeposits();
|
||||
await this.seedBills();
|
||||
|
||||
// ═══════════════════ Layer 5 ══════════════════════════
|
||||
await this.seedAttendance();
|
||||
await this.seedClassroomRentals();
|
||||
|
||||
// ═══════════════════ Layer 6 ══════════════════════════
|
||||
await this.seedProfiles();
|
||||
await this.seedEnrollments();
|
||||
await this.seedExamScores();
|
||||
await this.seedLearningRecords();
|
||||
|
||||
this.logger.log('=== Mock data seeding complete ===');
|
||||
}
|
||||
|
||||
// ── 0a: permissions ───────────────────────────────────
|
||||
|
||||
private async seedPermissions(): Promise<void> {
|
||||
const existing = await this.permRepo.count();
|
||||
if (existing > 0) { this.logger.log(' ⏭ permissions exist, skip'); return; }
|
||||
|
||||
const groups: Record<string, string[]> = {
|
||||
student: ['view', 'create', 'edit', 'delete', 'import', 'export'],
|
||||
room: ['view', 'create', 'edit', 'delete'],
|
||||
occupancy: ['view', 'checkin', 'checkout', 'transfer'],
|
||||
class: ['view', 'create', 'edit', 'delete'],
|
||||
schedule: ['view', 'create', 'edit', 'delete'],
|
||||
classroom: ['view', 'create', 'edit', 'delete'],
|
||||
expense: ['view', 'create', 'edit', 'delete'],
|
||||
bill: ['view', 'generate', 'edit'],
|
||||
deposit: ['view', 'create', 'refund'],
|
||||
tenant: ['view', 'create', 'edit', 'delete'],
|
||||
dashboard: ['view'],
|
||||
rbac: ['view', 'manage'],
|
||||
};
|
||||
|
||||
const nameMap: Record<string, string> = {
|
||||
student: '学生', room: '宿舍', occupancy: '入住', class: '班级',
|
||||
schedule: '排课', classroom: '教室', expense: '费用', bill: '账单',
|
||||
deposit: '押金', tenant: '租赁方', dashboard: '数据面板', rbac: '用户角色',
|
||||
};
|
||||
const actionMap: Record<string, string> = {
|
||||
view: '查看', create: '新增', edit: '编辑', delete: '删除',
|
||||
import: '导入', export: '导出', checkin: '办理入住', checkout: '办理退房',
|
||||
transfer: '调寝', generate: '生成', refund: '退还', manage: '管理',
|
||||
};
|
||||
|
||||
const perms: Array<{ code: string; name: string; group: string }> = [];
|
||||
for (const [group, actions] of Object.entries(groups)) {
|
||||
for (const action of actions) {
|
||||
perms.push({
|
||||
code: `${group}:${action}`,
|
||||
name: `${actionMap[action]}${nameMap[group]}`,
|
||||
group,
|
||||
});
|
||||
}
|
||||
}
|
||||
await this.permRepo.save(perms);
|
||||
this.logger.log(` ✓ ${perms.length} permissions`);
|
||||
}
|
||||
|
||||
// ── 0b: expense types ────────────────────────────────
|
||||
|
||||
private async seedExpenseTypes(): Promise<void> {
|
||||
const types: Array<{ code: string; name: string; category: string; sortOrder: number }> = [
|
||||
{ code: 'electricity', name: '电费', category: 'room', sortOrder: 1 },
|
||||
{ code: 'water', name: '水费', category: 'room', sortOrder: 2 },
|
||||
{ code: 'gas', name: '燃气费', category: 'room', sortOrder: 3 },
|
||||
{ code: 'property', name: '物业费', category: 'room', sortOrder: 4 },
|
||||
{ code: 'internet', name: '网费', category: 'personal', sortOrder: 5 },
|
||||
{ code: 'cleaning', name: '保洁费', category: 'personal', sortOrder: 6 },
|
||||
];
|
||||
await this.expenseTypeRepo.save(types);
|
||||
this.logger.log(` ✓ ${types.length} expense types`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ── 1a: roles ────────────────────────────────────────
|
||||
|
||||
private async seedRoles(): Promise<void> {
|
||||
const existing = await this.roleRepo.count();
|
||||
if (existing > 0) { this.logger.log(' ⏭ roles exist, skip'); return; }
|
||||
|
||||
const allPerms = await this.permRepo.find();
|
||||
const saPerms = allPerms;
|
||||
const adminPerms = allPerms.filter((p) => p.group !== 'rbac');
|
||||
const dormPerms = allPerms.filter((p) =>
|
||||
['room', 'occupancy', 'deposit', 'expense', 'bill', 'dashboard', 'student'].includes(p.group),
|
||||
);
|
||||
const teacherPerms = allPerms.filter((p) =>
|
||||
['student', 'class', 'schedule', 'classroom', 'dashboard'].includes(p.group),
|
||||
);
|
||||
const financePerms = allPerms.filter((p) =>
|
||||
['expense', 'bill', 'deposit', 'tenant', 'dashboard'].includes(p.group),
|
||||
);
|
||||
const operatorPerms = allPerms.filter((p) => p.group !== 'rbac');
|
||||
|
||||
const roles = [
|
||||
{ name: '超级管理员', description: '全部权限', isSystem: true, status: 1, permissions: saPerms },
|
||||
{ name: '管理员', description: '除RBAC外全部权限', isSystem: true, status: 1, permissions: adminPerms },
|
||||
{ name: '宿管', description: '宿舍/入住/押金/费用/账单', isSystem: true, status: 1, permissions: dormPerms },
|
||||
{ name: '班主任', description: '学生/班级/排课/教室', isSystem: true, status: 1, permissions: teacherPerms },
|
||||
{ name: '财务', description: '费用/账单/押金/租赁方', isSystem: true, status: 1, permissions: financePerms },
|
||||
{ name: '操作员', description: '日常操作', isSystem: true, status: 1, permissions: operatorPerms },
|
||||
];
|
||||
|
||||
for (const r of roles) {
|
||||
await this.roleRepo.save(r);
|
||||
}
|
||||
this.logger.log(` ✓ ${roles.length} roles`);
|
||||
}
|
||||
|
||||
// ── 1b: users ───────────────────────────────────────
|
||||
|
||||
private async seedUsers(): Promise<void> {
|
||||
const hash = await bcrypt.hash('123456', 10);
|
||||
const roles = await this.roleRepo.find();
|
||||
const superAdminRole = roles.find((r) => r.name === '超管');
|
||||
const operatorRole = roles.find((r) => r.name === '宿管');
|
||||
|
||||
|
||||
const existingUsernames = new Set((await this.userRepo.find({ select: ['username'] })).map(u => u.username));
|
||||
|
||||
const usersToCreate = [
|
||||
{ username: 'admin', name: '管理员', roles: [superAdminRole!] },
|
||||
{ username: 'jidi', name: '恭学基地管理-微微', roles: [operatorRole!] },
|
||||
{ username: 'jiaoyu', name: '恭学教育', roles: [operatorRole!] },
|
||||
].filter(u => !existingUsernames.has(u.username));
|
||||
|
||||
const saved: User[] = [];
|
||||
for (const u of usersToCreate) {
|
||||
const user = await this.userRepo.save({
|
||||
username: u.username,
|
||||
name: u.name,
|
||||
passwordHash: hash,
|
||||
isActive: true,
|
||||
roles: u.roles,
|
||||
});
|
||||
saved.push(user);
|
||||
}
|
||||
// Load all users for later seed steps to reference
|
||||
this.cachedUsers = await this.userRepo.find();
|
||||
this.logger.log(` ✓ ${usersToCreate.length} new users, ${this.cachedUsers.length} total (password: 123456)`);
|
||||
}
|
||||
|
||||
// ── 2a: tenants ──────────────────────────────────────
|
||||
|
||||
private async seedTenants(): Promise<void> {
|
||||
const tenants = [
|
||||
{ name: '犀牛华安', contact: '陈浩', phone: '18307069952', color: '#36cfc9' },
|
||||
];
|
||||
const saved = await this.tenantRepo.save(tenants);
|
||||
this.cachedTenants = saved;
|
||||
this.logger.log(` ✓ ${saved.length} tenants`);
|
||||
}
|
||||
|
||||
// ── 2b: rooms ────────────────────────────────────────
|
||||
|
||||
private async seedRooms(): Promise<void> {
|
||||
// Real data pattern: 单人间 (2号楼5层) + 四人间 (3/4/5/6号楼1-2层)
|
||||
const rooms: Array<{ roomNumber: string; building: string; floor: number; capacity: number; status: string; roomType: string; gender: string; rentalCategory: string; monthlyRate: number }> = [
|
||||
// 2号楼5层 单人间 pattern (real data: 2-502 through 2-519)
|
||||
{ roomNumber: '2-502', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-503', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-504', building: '2号楼', floor: 5, capacity: 1, status: 'available', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-505', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-506', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-507', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-508', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-509', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-510', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-511', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-512', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-513', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-515', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-516', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-517', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-518', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
{ roomNumber: '2-519', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 },
|
||||
// 1号楼 家庭房
|
||||
{ roomNumber: '1-2-301', building: '1号楼', floor: 3, capacity: 2, status: 'full', roomType: '家庭房', gender: '男', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
// 四人间 pattern
|
||||
{ roomNumber: '3-106', building: '3号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
{ roomNumber: '3-107', building: '3号楼', floor: 1, capacity: 4, status: 'full', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
{ roomNumber: '4-107', building: '4号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
{ roomNumber: '4-111', building: '4号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
{ roomNumber: '4-201', building: '4号楼', floor: 2, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
{ roomNumber: '4-204', building: '4号楼', floor: 2, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
{ roomNumber: '5-109', building: '5号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
{ roomNumber: '6-116', building: '6号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 },
|
||||
];
|
||||
|
||||
const saved = await this.roomRepo.save(rooms);
|
||||
this.cachedRooms = saved;
|
||||
this.logger.log(` ✓ ${saved.length} rooms`);
|
||||
}
|
||||
|
||||
// ── 2c: classrooms ───────────────────────────────────
|
||||
|
||||
private async seedClassrooms(): Promise<void> {
|
||||
const classrooms = [
|
||||
{ name: '102', building: 'a座', floor: 1, capacity: 30, roomType: '大', supervisor: '陈浩' },
|
||||
{ name: '201', building: 'a座', floor: 2, capacity: 30, roomType: '大', supervisor: '刘老师' },
|
||||
{ name: '202', building: 'a座', floor: 2, capacity: 25, roomType: '次大', supervisor: '刘老师' },
|
||||
{ name: '301', building: 'b座', floor: 3, capacity: 20, roomType: '小', supervisor: '黄老师' },
|
||||
];
|
||||
const saved = await this.classroomRepo.save(classrooms);
|
||||
this.cachedClassrooms = saved;
|
||||
this.logger.log(` ✓ ${saved.length} classrooms`);
|
||||
}
|
||||
|
||||
// ── 2d: students ─────────────────────────────────────
|
||||
|
||||
private async seedStudents(): Promise<void> {
|
||||
// Real data pattern: organization fields like 恭学专升本, 26定向, 续住, etc.
|
||||
const students: Array<{ name: string; phone: string; studentNo: string; gender: string; ethnicity: string; organization: string; supervisor: string; status: string }> = [
|
||||
// 26定向 students
|
||||
{ name: '聂天羽', phone: '12345678912', studentNo: '123456', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' },
|
||||
{ name: '艾柯丽努尔·艾买尔江', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '陈昊天', phone: '', studentNo: '', gender: '男', ethnicity: '汉', organization: '续住', supervisor: '糕糕', status: 'active' },
|
||||
{ name: '赵璟涵', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '周子涵', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '秦婧怡', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑文', supervisor: '', status: 'active' },
|
||||
{ name: '石欣欣', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享', supervisor: '', status: 'active' },
|
||||
{ name: '李光铄', phone: '', studentNo: '', gender: '男', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '田芸竹', phone: '12345678920', studentNo: '123464', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' },
|
||||
{ name: '荚欣语', phone: '12345678921', studentNo: '123465', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' },
|
||||
{ name: '刘倬宁', phone: '12345678922', studentNo: '123466', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' },
|
||||
{ name: '柴高星', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑专+专冲', supervisor: '', status: 'active' },
|
||||
{ name: '焦怡菲', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑期+专冲+年前文化', supervisor: '', status: 'active' },
|
||||
{ name: '王姿璇', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '寇星彤', phone: '12345678926', studentNo: '123470', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' },
|
||||
// 续住 students
|
||||
{ name: '於嘉丽', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '续住', supervisor: '泡泡', status: 'active' },
|
||||
{ name: '郑斌', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', status: 'active' },
|
||||
{ name: '仵梓钰', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '续住', supervisor: '方方', status: 'active' },
|
||||
{ name: '覃鼎浩', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '云熙', status: 'active' },
|
||||
{ name: '常智禹', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '方方', status: 'active' },
|
||||
{ name: '郭庆泉', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', status: 'active' },
|
||||
{ name: '孙立欣', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', status: 'active' },
|
||||
{ name: '韩尧祖', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '', supervisor: '', status: 'active' },
|
||||
{ name: '陈亚津', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '', supervisor: '', status: 'active' },
|
||||
{ name: '孟思妍', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '武嘉怡', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '刘禹含', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '杜瑾慧', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' },
|
||||
{ name: '孟凡志', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' },
|
||||
];
|
||||
|
||||
const saved = await this.studentRepo.save(students);
|
||||
this.cachedStudents = saved;
|
||||
this.logger.log(` ✓ ${saved.length} students`);
|
||||
}
|
||||
|
||||
// ── 3a: classes ──────────────────────────────────────
|
||||
|
||||
private async seedClasses(): Promise<void> {
|
||||
const clsData = [
|
||||
{ name: '26定向班', code: 'DX2026-01', classType: ClassType.CULTURE, status: ClassStatus.ACTIVE },
|
||||
{ name: '26尊享班', code: 'ZX2026-01', classType: ClassType.PROFESSIONAL, status: ClassStatus.ACTIVE },
|
||||
{ name: '续住1班', code: 'XZ2026-01', classType: ClassType.CULTURE, status: ClassStatus.ACTIVE },
|
||||
{ name: '恭学专升本1班', code: 'ZS2026-01', classType: ClassType.PROFESSIONAL, status: ClassStatus.ACTIVE },
|
||||
{ name: '暑期文化课', code: 'SQ2026-01', classType: ClassType.SPRINT, status: ClassStatus.ENROLLING },
|
||||
];
|
||||
|
||||
const savedClasses: Class[] = [];
|
||||
for (const c of clsData) {
|
||||
const saved = await this.classRepo.save({
|
||||
...c,
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2026-08-31',
|
||||
maxStudents: 30,
|
||||
});
|
||||
savedClasses.push(saved);
|
||||
}
|
||||
|
||||
// Distribute students
|
||||
const orgMap: Record<string, Student[]> = {};
|
||||
for (const s of this.cachedStudents) {
|
||||
const key = s.organization || 'other';
|
||||
(orgMap[key] ??= []).push(s);
|
||||
}
|
||||
|
||||
const assignments: Record<string, Class> = {
|
||||
'26定向': savedClasses[0],
|
||||
'26尊享': savedClasses[1],
|
||||
'续住': savedClasses[2],
|
||||
'恭学专升本': savedClasses[3],
|
||||
};
|
||||
|
||||
for (const [org, students] of Object.entries(orgMap)) {
|
||||
// Match by prefix
|
||||
const clsKey = Object.keys(assignments).find((k) => org.startsWith(k) || k.startsWith(org));
|
||||
const cls = clsKey ? assignments[clsKey] : pick(savedClasses);
|
||||
for (const s of students) {
|
||||
await this.classStudentRepo.save({
|
||||
classId: cls!.id,
|
||||
studentId: s.id,
|
||||
joinDate: '2026-04-01',
|
||||
status: 'active',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Assign teachers
|
||||
for (const cls of savedClasses) {
|
||||
const teacher = pick(this.cachedUsers);
|
||||
await this.classTeacherRepo.save({
|
||||
classId: cls.id,
|
||||
userId: teacher.id,
|
||||
roleType: TeacherRoleType.HEAD_TEACHER,
|
||||
});
|
||||
await this.classRepo.update(cls.id, { headTeacherId: teacher.id });
|
||||
}
|
||||
|
||||
this.cachedClasses = savedClasses;
|
||||
this.logger.log(` ✓ ${savedClasses.length} classes`);
|
||||
}
|
||||
|
||||
// ── 3b: occupancies ──────────────────────────────────
|
||||
|
||||
private async seedOccupancies(): Promise<void> {
|
||||
// Real data pattern: students mapped to specific rooms
|
||||
const mapping: Array<{ studentIdx: number; roomIdx: number; checkInDate: string; notes: string }> = [
|
||||
{ studentIdx: 0, roomIdx: 14, checkInDate: '2026-04-01', notes: '' }, // 聂天羽 -> 2-516
|
||||
{ studentIdx: 1, roomIdx: 3, checkInDate: '2026-07-12', notes: '定向' }, // 艾柯丽努尔 -> 2-505
|
||||
{ studentIdx: 2, roomIdx: 4, checkInDate: '2026-03-01', notes: '续住' }, // 陈昊天 -> 2-506
|
||||
{ studentIdx: 3, roomIdx: 5, checkInDate: '2026-07-12', notes: '定向' }, // 赵璟涵 -> 2-507
|
||||
{ studentIdx: 4, roomIdx: 6, checkInDate: '2026-07-12', notes: '定向' }, // 周子涵 -> 2-508
|
||||
{ studentIdx: 5, roomIdx: 7, checkInDate: '2026-07-12', notes: '暑期文化+尊享' },// 秦婧怡 -> 2-509
|
||||
{ studentIdx: 6, roomIdx: 12, checkInDate: '2026-09-15', notes: '' }, // 石欣欣 -> 2-513
|
||||
{ studentIdx: 7, roomIdx: 10, checkInDate: '2026-07-12', notes: '定向' }, // 李光铄 -> 2-511
|
||||
{ studentIdx: 8, roomIdx: 13, checkInDate: '2026-09-15', notes: '' }, // 田芸竹 -> 2-515
|
||||
{ studentIdx: 9, roomIdx: 0, checkInDate: '2026-09-15', notes: '' }, // 荚欣语 -> 2-502
|
||||
{ studentIdx: 10, roomIdx: 1, checkInDate: '2026-09-15', notes: '' }, // 刘倬宁 -> 2-503
|
||||
{ studentIdx: 11, roomIdx: 14, checkInDate: '2026-08-12', notes: '' }, // 柴高星 -> 2-516
|
||||
{ studentIdx: 12, roomIdx: 15, checkInDate: '2026-07-12', notes: '定向' }, // 焦怡菲 -> 2-517
|
||||
{ studentIdx: 13, roomIdx: 16, checkInDate: '2026-07-12', notes: '定向' }, // 王姿璇 -> 2-518
|
||||
{ studentIdx: 14, roomIdx: 2, checkInDate: '2026-06-05', notes: '' }, // 寇星彤 -> 2-504
|
||||
{ studentIdx: 15, roomIdx: 19, checkInDate: '2026-04-01', notes: '' }, // 於嘉丽 -> 3-106
|
||||
{ studentIdx: 16, roomIdx: 21, checkInDate: '2026-04-01', notes: '' }, // 郑斌 -> 4-107
|
||||
{ studentIdx: 17, roomIdx: 23, checkInDate: '2026-04-01', notes: '' }, // 仵梓钰 -> 4-201
|
||||
{ studentIdx: 18, roomIdx: 25, checkInDate: '2026-04-01', notes: '' }, // 覃鼎浩 -> 5-109
|
||||
{ studentIdx: 19, roomIdx: 17, checkInDate: '2026-04-01', notes: '' }, // 常智禹 -> 1-2-301
|
||||
{ studentIdx: 20, roomIdx: 8, checkInDate: '2026-05-14', notes: '' }, // 郭庆泉 -> 2-510
|
||||
{ studentIdx: 21, roomIdx: 8, checkInDate: '2026-05-14', notes: '' }, // 孙立欣 -> 2-510
|
||||
{ studentIdx: 22, roomIdx: 18, checkInDate: '2026-06-09', notes: '' }, // 韩尧祖 -> 3-107
|
||||
{ studentIdx: 23, roomIdx: 20, checkInDate: '2026-08-15', notes: '' }, // 陈亚津 -> 4-111
|
||||
{ studentIdx: 24, roomIdx: 26, checkInDate: '2026-07-12', notes: '' }, // 孟思妍 -> 6-116
|
||||
{ studentIdx: 25, roomIdx: 26, checkInDate: '2026-07-12', notes: '' }, // 武嘉怡 -> 6-116
|
||||
{ studentIdx: 26, roomIdx: 26, checkInDate: '2026-07-12', notes: '' }, // 刘禹含 -> 6-116
|
||||
{ studentIdx: 27, roomIdx: 26, checkInDate: '2026-07-12', notes: '' }, // 杜瑾慧 -> 6-116
|
||||
{ studentIdx: 28, roomIdx: 18, checkInDate: '2026-06-04', notes: '' }, // 孟凡志 -> 3-107
|
||||
];
|
||||
|
||||
const occupancies: Array<{ studentId: number; roomId: number; checkInDate: string; billingStartDate: string; rentalType: string; notes: string; checkOutDate?: string; billingEndDate?: string }> = [];
|
||||
for (const m of mapping) {
|
||||
const student = this.cachedStudents[m.studentIdx];
|
||||
const room = this.cachedRooms[m.roomIdx];
|
||||
if (student && room) {
|
||||
occupancies.push({
|
||||
studentId: student.id,
|
||||
roomId: room.id,
|
||||
checkInDate: m.checkInDate,
|
||||
billingStartDate: m.checkInDate,
|
||||
rentalType: room.rentalCategory,
|
||||
notes: m.notes,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// One checkout: 陈昊天 checked out
|
||||
occupancies[2].checkOutDate = '2026-06-09';
|
||||
occupancies[2].billingEndDate = '2026-06-09';
|
||||
|
||||
await this.occupancyRepo.save(occupancies);
|
||||
this.logger.log(` ✓ ${occupancies.length} occupancies`);
|
||||
}
|
||||
|
||||
// ── 3c: class schedules ──────────────────────────────
|
||||
|
||||
private async seedSchedules(): Promise<void> {
|
||||
const subjects = ['数学', '英语', '语文', '专业课', '政治', '历史'];
|
||||
const schedules: Array<{ classId: number; classroomId: number; weekDay: number; startTime: string; endTime: string; startDate: string; endDate: string; subject: string; teacherId: number; scheduleType: string }> = [];
|
||||
|
||||
for (const cls of this.cachedClasses.slice(0, 3)) {
|
||||
for (let day = 1; day <= 5; day++) {
|
||||
schedules.push(
|
||||
{
|
||||
classId: cls.id,
|
||||
classroomId: pick(this.cachedClassrooms).id,
|
||||
weekDay: day,
|
||||
startTime: '08:30',
|
||||
endTime: '10:00',
|
||||
startDate: cls.startDate ?? '2026-04-01',
|
||||
endDate: cls.endDate ?? '2026-08-31',
|
||||
subject: pick(subjects),
|
||||
teacherId: pick(this.cachedUsers).id,
|
||||
scheduleType: ScheduleType.INTERNAL,
|
||||
},
|
||||
{
|
||||
classId: cls.id,
|
||||
classroomId: pick(this.cachedClassrooms).id,
|
||||
weekDay: day,
|
||||
startTime: '10:30',
|
||||
endTime: '12:00',
|
||||
startDate: cls.startDate ?? '2026-04-01',
|
||||
endDate: cls.endDate ?? '2026-08-31',
|
||||
subject: pick(subjects),
|
||||
teacherId: pick(this.cachedUsers).id,
|
||||
scheduleType: ScheduleType.INTERNAL,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await this.scheduleRepo.save(schedules);
|
||||
this.logger.log(` ✓ ${schedules.length} schedules`);
|
||||
}
|
||||
|
||||
// ── 4a: room expenses ────────────────────────────────
|
||||
|
||||
private async seedRoomExpenses(): Promise<void> {
|
||||
// Real data: electricity + water per room, April & May 2026
|
||||
const expenseRooms = [
|
||||
{ roomIdx: 24, electric: { apr: 38.55, may: 57.81 }, water: { apr: 9.80, may: 9.80 } }, // 5-109
|
||||
{ roomIdx: 19, electric: { apr: 44.34, may: 35.16 }, water: { apr: 9.80, may: 4.90 } }, // 3-106
|
||||
{ roomIdx: 17, electric: { apr: 27.09, may: 23.84 }, water: { apr: 4.90, may: 4.90 } }, // 1-2-301
|
||||
{ roomIdx: 21, electric: { apr: 42.79, may: 41.21 }, water: { apr: 9.80, may: 9.80 } }, // 4-107
|
||||
{ roomIdx: 22, electric: { apr: 39.90, may: 31.99 }, water: { apr: 9.80, may: 4.90 } }, // 4-111
|
||||
{ roomIdx: 23, electric: { apr: 101.00, may: 98.18 }, water: { apr: 19.60, may: 19.60 } }, // 4-201
|
||||
];
|
||||
|
||||
const expenses: Array<{ roomId: number; expenseType: string; amount: number; periodStart: string; periodEnd: string; description: string }> = [];
|
||||
|
||||
for (const er of expenseRooms) {
|
||||
const room = this.cachedRooms[er.roomIdx];
|
||||
if (!room) continue;
|
||||
// April
|
||||
expenses.push({
|
||||
roomId: room.id, expenseType: 'electricity', amount: er.electric.apr,
|
||||
periodStart: '2026-04-01', periodEnd: '2026-04-30',
|
||||
description: `电费 - ${room.roomNumber} 4月`,
|
||||
});
|
||||
expenses.push({
|
||||
roomId: room.id, expenseType: 'water', amount: er.water.apr,
|
||||
periodStart: '2026-04-01', periodEnd: '2026-04-30',
|
||||
description: `水费 - ${room.roomNumber} 4月`,
|
||||
});
|
||||
// May
|
||||
expenses.push({
|
||||
roomId: room.id, expenseType: 'electricity', amount: er.electric.may,
|
||||
periodStart: '2026-05-01', periodEnd: '2026-05-31',
|
||||
description: `电费 - ${room.roomNumber} 5月`,
|
||||
});
|
||||
expenses.push({
|
||||
roomId: room.id, expenseType: 'water', amount: er.water.may,
|
||||
periodStart: '2026-05-01', periodEnd: '2026-05-31',
|
||||
description: `水费 - ${room.roomNumber} 5月`,
|
||||
});
|
||||
}
|
||||
|
||||
await this.roomExpenseRepo.save(expenses);
|
||||
this.logger.log(` ✓ ${expenses.length} room expenses`);
|
||||
}
|
||||
|
||||
// ── 4b: deposits ─────────────────────────────────────
|
||||
|
||||
private async seedDeposits(): Promise<void> {
|
||||
// Real data: deposits tied to specific students
|
||||
const depositStudents = [24, 25, 26, 27, 28, 19, 2]; // indices into cachedStudents
|
||||
const deposits: Array<{ studentId: number; amount: number; status: string; paidDate: string }> = [];
|
||||
|
||||
for (const idx of depositStudents) {
|
||||
const s = this.cachedStudents[idx];
|
||||
if (!s) continue;
|
||||
deposits.push({
|
||||
studentId: s.id,
|
||||
amount: 500,
|
||||
status: 'paid',
|
||||
paidDate: s.id <= this.cachedStudents[25].id ? '2026-07-12' : '2026-06-04',
|
||||
});
|
||||
}
|
||||
|
||||
// Manual deposits with custom amounts
|
||||
deposits.push({ studentId: this.cachedStudents[19].id, amount: 200, status: 'paid', paidDate: '2026-03-01' }); // 常智禹
|
||||
deposits.push({ studentId: this.cachedStudents[2].id, amount: 121.60, status: 'paid', paidDate: '2026-03-01' }); // 陈昊天
|
||||
|
||||
await this.depositRepo.save(deposits);
|
||||
this.logger.log(` ✓ ${deposits.length} deposits`);
|
||||
}
|
||||
|
||||
// ── 4c: bills ────────────────────────────────────────
|
||||
|
||||
private async seedBills(): Promise<void> {
|
||||
// Real data: bills for students with occupancies, April & May
|
||||
const billData = [
|
||||
{ studentIdx: 15, roomIdx: 19, sharedApr: 54.14, sharedMay: 40.06 }, // 於嘉丽
|
||||
{ studentIdx: 16, roomIdx: 21, sharedApr: 52.59, sharedMay: 51.01 }, // 郑斌
|
||||
{ studentIdx: 17, roomIdx: 22, sharedApr: 49.70, sharedMay: 36.89 }, // 仵梓钰
|
||||
{ studentIdx: 18, roomIdx: 24, sharedApr: 120.60, sharedMay: 117.78 }, // 覃鼎浩
|
||||
{ studentIdx: 19, roomIdx: 17, sharedApr: 31.99, sharedMay: 28.74 }, // 常智禹
|
||||
{ studentIdx: 2, roomIdx: 4, sharedApr: 48.35, sharedMay: 67.61 }, // 陈昊天
|
||||
];
|
||||
|
||||
for (const bd of billData) {
|
||||
const student = this.cachedStudents[bd.studentIdx];
|
||||
const room = this.cachedRooms[bd.roomIdx];
|
||||
if (!student || !room) continue;
|
||||
|
||||
// April bill
|
||||
const billApr = await this.billRepo.save({
|
||||
studentId: student.id,
|
||||
periodStart: '2026-04-01',
|
||||
periodEnd: '2026-04-30',
|
||||
sharedAmount: bd.sharedApr,
|
||||
personalAmount: 0,
|
||||
totalAmount: bd.sharedApr,
|
||||
status: 'paid',
|
||||
});
|
||||
await this.billItemRepo.save([
|
||||
{ billId: billApr.id, roomId: room.id, expenseType: 'electricity', description: '电费分摊', days: 30, studentAmount: Math.round(bd.sharedApr * 0.82 * 100) / 100 },
|
||||
{ billId: billApr.id, roomId: room.id, expenseType: 'water', description: '水费分摊', days: 30, studentAmount: Math.round(bd.sharedApr * 0.18 * 100) / 100 },
|
||||
]);
|
||||
|
||||
// May bill
|
||||
const billMay = await this.billRepo.save({
|
||||
studentId: student.id,
|
||||
periodStart: '2026-05-01',
|
||||
periodEnd: '2026-05-31',
|
||||
sharedAmount: bd.sharedMay,
|
||||
personalAmount: 0,
|
||||
totalAmount: bd.sharedMay,
|
||||
status: 'paid',
|
||||
});
|
||||
await this.billItemRepo.save([
|
||||
{ billId: billMay.id, roomId: room.id, expenseType: 'electricity', description: '电费分摊', days: 31, studentAmount: Math.round(bd.sharedMay * 0.82 * 100) / 100 },
|
||||
{ billId: billMay.id, roomId: room.id, expenseType: 'water', description: '水费分摊', days: 31, studentAmount: Math.round(bd.sharedMay * 0.18 * 100) / 100 },
|
||||
]);
|
||||
}
|
||||
|
||||
this.logger.log(' ✓ bills + items (April & May 2026)');
|
||||
}
|
||||
|
||||
// ── 5a: attendance ───────────────────────────────────
|
||||
|
||||
private async seedAttendance(): Promise<void> {
|
||||
const statuses = ['present', 'absent', 'late', 'leave'];
|
||||
const records: Array<{ studentId: number; classId: number; attendanceDate: string; session: string; status: string; source: string }> = [];
|
||||
|
||||
for (const cls of this.cachedClasses) {
|
||||
const classStudents = await this.classStudentRepo.find({ where: { classId: cls.id } });
|
||||
for (const cs of classStudents) {
|
||||
// Last 10 weekdays
|
||||
let d = 0;
|
||||
let count = 0;
|
||||
while (count < 10) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - d);
|
||||
const dow = date.getDay();
|
||||
if (dow !== 0 && dow !== 6) {
|
||||
records.push({
|
||||
studentId: cs.studentId,
|
||||
classId: cls.id,
|
||||
attendanceDate: date.toISOString().slice(0, 10),
|
||||
session: 'am',
|
||||
status: pick(statuses),
|
||||
source: 'manual',
|
||||
});
|
||||
count++;
|
||||
}
|
||||
d++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.attendanceRepo.save(records);
|
||||
this.logger.log(` ✓ ${records.length} attendance records`);
|
||||
}
|
||||
|
||||
// ── 5b: classroom rentals ────────────────────────────
|
||||
|
||||
private async seedClassroomRentals(): Promise<void> {
|
||||
if (this.cachedClassrooms.length > 0 && this.cachedTenants.length > 0) {
|
||||
await this.rentalRepo.save({
|
||||
classroomId: this.cachedClassrooms[0].id,
|
||||
tenantId: this.cachedTenants[0].id,
|
||||
startDate: '2026-05-14',
|
||||
endDate: '2026-06-30',
|
||||
totalAmount: 30000,
|
||||
status: 'active',
|
||||
});
|
||||
this.logger.log(' ✓ 1 classroom rental');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 6a: student profiles ─────────────────────────────
|
||||
|
||||
private async seedProfiles(): Promise<void> {
|
||||
const colleges = ['北京大学', '清华大学', '复旦大学', '浙江大学', '南京大学'];
|
||||
const profiles = this.cachedStudents.slice(0, 10).map((s) => ({
|
||||
studentId: s.id,
|
||||
targetCollege: pick(colleges),
|
||||
targetMajor: pick(['计算机科学', '数学', '物理学', '经济学']),
|
||||
subjectDirection: pick(['理科', '文科']),
|
||||
grade: '高三',
|
||||
campusLocation: '主校区',
|
||||
}));
|
||||
await this.profileRepo.save(profiles);
|
||||
this.logger.log(` ✓ ${profiles.length} student profiles`);
|
||||
}
|
||||
|
||||
// ── 6b: student enrollments ──────────────────────────
|
||||
|
||||
private async seedEnrollments(): Promise<void> {
|
||||
const enrollments = this.cachedStudents.slice(0, 15).map((s) => ({
|
||||
studentId: s.id,
|
||||
courseCategory: pick(['文化课', '专业课', '集训']),
|
||||
classType: pick(['全日制', '周末班']),
|
||||
className: pick(['26定向班', '26尊享班', '续住1班']),
|
||||
headTeacher: '张班主任',
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2026-08-31',
|
||||
status: 'active',
|
||||
}));
|
||||
await this.enrollmentRepo.save(enrollments);
|
||||
this.logger.log(` ✓ ${enrollments.length} enrollments`);
|
||||
}
|
||||
|
||||
// ── 6c: exam scores ──────────────────────────────────
|
||||
|
||||
private async seedExamScores(): Promise<void> {
|
||||
const subjects = ['数学', '英语', '语文', '专业课'];
|
||||
const exams = ['月考', '期中考试', '模拟考试'];
|
||||
const scores: Array<{ studentId: number; examType: string; examName: string; subject: string; score: number; examDate: string }> = [];
|
||||
|
||||
for (const s of this.cachedStudents.slice(0, 10)) {
|
||||
for (const exam of exams) {
|
||||
for (const subj of subjects) {
|
||||
scores.push({
|
||||
studentId: s.id,
|
||||
examType: 'exam',
|
||||
examName: exam,
|
||||
subject: subj,
|
||||
score: randInt(50, 100),
|
||||
examDate: `2026-0${randInt(4, 6)}-${String(randInt(1, 28)).padStart(2, '0')}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.examScoreRepo.save(scores);
|
||||
this.logger.log(` ✓ ${scores.length} exam scores`);
|
||||
}
|
||||
|
||||
// ── 6d: learning records ────────────────────────────
|
||||
|
||||
private async seedLearningRecords(): Promise<void> {
|
||||
const types = ['跟进记录', '家长沟通', '学习反馈', '教学建议'];
|
||||
const records = this.cachedStudents.slice(0, 12).map((s) => ({
|
||||
studentId: s.id,
|
||||
recordDate: `2026-0${randInt(5, 6)}-${String(randInt(1, 28)).padStart(2, '0')}`,
|
||||
recordType: pick(types),
|
||||
content: `学习状态:${pick(['良好', '一般', '需加强'])}`,
|
||||
followUpMethod: pick(['电话', '微信', '面谈']),
|
||||
nextStep: '继续跟进',
|
||||
}));
|
||||
await this.learningRecordRepo.save(records);
|
||||
this.logger.log(` ✓ ${records.length} learning records`);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { Module, OnModuleInit, Logger } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SeedDevService } from './seed-dev.service';
|
||||
import {
|
||||
Student, Room, Occupancy, RoomExpense,
|
||||
Bill, BillItem, User, Deposit,
|
||||
Classroom, Tenant, ClassroomRental, Permission, Role,
|
||||
Class, ClassStudent, ClassTeacher, ClassSchedule,
|
||||
AttendanceRecord,
|
||||
StudentProfile, StudentEnrollment, ExamScore, LearningRecord,
|
||||
ExpenseType,
|
||||
} from '../entities';
|
||||
|
||||
const SEED_ENTITIES = [
|
||||
User, Role, Permission,
|
||||
Tenant, Student, Room, Classroom, Occupancy,
|
||||
RoomExpense, ExpenseType, Class,
|
||||
ClassStudent, ClassTeacher, ClassSchedule, Bill, BillItem,
|
||||
Deposit, AttendanceRecord,
|
||||
ClassroomRental, StudentProfile, StudentEnrollment,
|
||||
ExamScore, LearningRecord,
|
||||
];
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature(SEED_ENTITIES)],
|
||||
providers: [SeedDevService],
|
||||
})
|
||||
export class SeedModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(SeedModule.name);
|
||||
|
||||
constructor(private readonly seedService: SeedDevService) {}
|
||||
|
||||
async onModuleInit() {
|
||||
const enabled = process.env['SEED_DEV'] === 'true';
|
||||
const skip = process.env['SEED_DEV_SKIP'] === 'true';
|
||||
|
||||
if (!enabled || skip) {
|
||||
this.logger.log(
|
||||
`Seed skipped: SEED_DEV=${process.env['SEED_DEV']}, NODE_ENV=${process.env['NODE_ENV']}, SKIP=${skip}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const studentCount = await this.seedService.getStudentCount();
|
||||
if (studentCount > 0) {
|
||||
this.logger.log(`Seed skipped: ${studentCount} students already exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log('Starting mock data seeding...');
|
||||
try {
|
||||
await this.seedService.seed();
|
||||
this.logger.log('Mock data seeding completed successfully');
|
||||
} catch (err) {
|
||||
this.logger.error('Mock data seeding failed', err instanceof Error ? err.stack : String(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,13 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Student } from '../entities/student.entity';
|
||||
import { Class } from '../entities/class.entity';
|
||||
import { Tenant } from '../entities/tenant.entity';
|
||||
import { CommonModule } from '../common/common.module';
|
||||
import { ClassStudent } from '../entities/class-student.entity';
|
||||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||||
import { StudentsService } from './students.service';
|
||||
import { StudentsController } from './students.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Student, Class, ClassStudent, AttendanceRecord, Tenant]), CommonModule],
|
||||
imports: [TypeOrmModule.forFeature([Student, Class, ClassStudent, AttendanceRecord, Tenant])],
|
||||
controllers: [StudentsController],
|
||||
providers: [StudentsService],
|
||||
exports: [StudentsService],
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user