diff --git a/apps/admin/src/hooks/useNotifications.ts b/apps/admin/src/hooks/useNotifications.ts deleted file mode 100644 index 34aca03..0000000 --- a/apps/admin/src/hooks/useNotifications.ts +++ /dev/null @@ -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(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 }; -} diff --git a/apps/server/src/app.controller.spec.ts b/apps/server/src/app.controller.spec.ts deleted file mode 100644 index d22f389..0000000 --- a/apps/server/src/app.controller.spec.ts +++ /dev/null @@ -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); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/apps/server/src/app.controller.ts b/apps/server/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/apps/server/src/app.controller.ts +++ /dev/null @@ -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(); - } -} diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 2f51d35..fc2a7ec 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -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, diff --git a/apps/server/src/app.service.ts b/apps/server/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/apps/server/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/apps/server/src/archive/archive.module.ts b/apps/server/src/archive/archive.module.ts index 2d0e463..d755a89 100644 --- a/apps/server/src/archive/archive.module.ts +++ b/apps/server/src/archive/archive.module.ts @@ -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], diff --git a/apps/server/src/attendance/attendance.module.ts b/apps/server/src/attendance/attendance.module.ts index eeccbe4..847ba0d 100644 --- a/apps/server/src/attendance/attendance.module.ts +++ b/apps/server/src/attendance/attendance.module.ts @@ -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], diff --git a/apps/server/src/auth/dto/auth.dto.ts b/apps/server/src/auth/dto/auth.dto.ts index d9c1308..b32a24b 100644 --- a/apps/server/src/auth/dto/auth.dto.ts +++ b/apps/server/src/auth/dto/auth.dto.ts @@ -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; -} diff --git a/apps/server/src/bills/bills.module.ts b/apps/server/src/bills/bills.module.ts index 239a7dc..14dfd6c 100644 --- a/apps/server/src/bills/bills.module.ts +++ b/apps/server/src/bills/bills.module.ts @@ -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], diff --git a/apps/server/src/classes/classes.module.ts b/apps/server/src/classes/classes.module.ts index 8fc4e2e..a4d0ee6 100644 --- a/apps/server/src/classes/classes.module.ts +++ b/apps/server/src/classes/classes.module.ts @@ -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], diff --git a/apps/server/src/classroom-rentals/classroom-rentals.module.ts b/apps/server/src/classroom-rentals/classroom-rentals.module.ts index 1319e22..cd6e53c 100644 --- a/apps/server/src/classroom-rentals/classroom-rentals.module.ts +++ b/apps/server/src/classroom-rentals/classroom-rentals.module.ts @@ -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], diff --git a/apps/server/src/classrooms/classrooms.module.ts b/apps/server/src/classrooms/classrooms.module.ts index e77f72d..888edc4 100644 --- a/apps/server/src/classrooms/classrooms.module.ts +++ b/apps/server/src/classrooms/classrooms.module.ts @@ -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], diff --git a/apps/server/src/common/common.module.ts b/apps/server/src/common/common.module.ts deleted file mode 100644 index ac198ba..0000000 --- a/apps/server/src/common/common.module.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Module } from '@nestjs/common'; - -@Module({}) -export class CommonModule {} diff --git a/apps/server/src/dashboard/dashboard.module.ts b/apps/server/src/dashboard/dashboard.module.ts index 26da52f..b864386 100644 --- a/apps/server/src/dashboard/dashboard.module.ts +++ b/apps/server/src/dashboard/dashboard.module.ts @@ -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], }) diff --git a/apps/server/src/deposits/deposits.module.ts b/apps/server/src/deposits/deposits.module.ts index f7a9f6c..5c49211 100644 --- a/apps/server/src/deposits/deposits.module.ts +++ b/apps/server/src/deposits/deposits.module.ts @@ -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], diff --git a/apps/server/src/deposits/dto/deposit.dto.ts b/apps/server/src/deposits/dto/deposit.dto.ts index 2dd48b9..1b140b6 100644 --- a/apps/server/src/deposits/dto/deposit.dto.ts +++ b/apps/server/src/deposits/dto/deposit.dto.ts @@ -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; -} diff --git a/apps/server/src/entities/index.ts b/apps/server/src/entities/index.ts index c7e0b7e..388dda5 100644 --- a/apps/server/src/entities/index.ts +++ b/apps/server/src/entities/index.ts @@ -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'; diff --git a/apps/server/src/entities/student-report.entity.ts b/apps/server/src/entities/student-report.entity.ts deleted file mode 100644 index e741c76..0000000 --- a/apps/server/src/entities/student-report.entity.ts +++ /dev/null @@ -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; - - @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; -} diff --git a/apps/server/src/expenses/expenses.module.ts b/apps/server/src/expenses/expenses.module.ts index 3e172f6..2a71e90 100644 --- a/apps/server/src/expenses/expenses.module.ts +++ b/apps/server/src/expenses/expenses.module.ts @@ -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], diff --git a/apps/server/src/occupancies/occupancies.module.ts b/apps/server/src/occupancies/occupancies.module.ts index a8f1bb5..e812e8c 100644 --- a/apps/server/src/occupancies/occupancies.module.ts +++ b/apps/server/src/occupancies/occupancies.module.ts @@ -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], diff --git a/apps/server/src/rooms/rooms.module.ts b/apps/server/src/rooms/rooms.module.ts index b5959c6..5ce2996 100644 --- a/apps/server/src/rooms/rooms.module.ts +++ b/apps/server/src/rooms/rooms.module.ts @@ -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], diff --git a/apps/server/src/schedules/schedules.module.ts b/apps/server/src/schedules/schedules.module.ts index a3a4624..0b0c7cb 100644 --- a/apps/server/src/schedules/schedules.module.ts +++ b/apps/server/src/schedules/schedules.module.ts @@ -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], diff --git a/apps/server/src/seed/seed-dev.service.ts b/apps/server/src/seed/seed-dev.service.ts deleted file mode 100644 index 1b62b2a..0000000 --- a/apps/server/src/seed/seed-dev.service.ts +++ /dev/null @@ -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(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, - @InjectRepository(Role) private roleRepo: Repository, - @InjectRepository(User) private userRepo: Repository, - @InjectRepository(Tenant) private tenantRepo: Repository, - @InjectRepository(Student) private studentRepo: Repository, - @InjectRepository(Room) private roomRepo: Repository, - @InjectRepository(Classroom) private classroomRepo: Repository, - @InjectRepository(Occupancy) private occupancyRepo: Repository, - @InjectRepository(RoomExpense) private roomExpenseRepo: Repository, - @InjectRepository(ExpenseType) private expenseTypeRepo: Repository, - @InjectRepository(Class) private classRepo: Repository, - @InjectRepository(ClassStudent) private classStudentRepo: Repository, - @InjectRepository(ClassTeacher) private classTeacherRepo: Repository, - @InjectRepository(ClassSchedule) private scheduleRepo: Repository, - @InjectRepository(Bill) private billRepo: Repository, - @InjectRepository(BillItem) private billItemRepo: Repository, - @InjectRepository(Deposit) private depositRepo: Repository, - @InjectRepository(AttendanceRecord) private attendanceRepo: Repository, - @InjectRepository(ClassroomRental) private rentalRepo: Repository, - @InjectRepository(StudentProfile) private profileRepo: Repository, - @InjectRepository(StudentEnrollment) private enrollmentRepo: Repository, - @InjectRepository(ExamScore) private examScoreRepo: Repository, - @InjectRepository(LearningRecord) private learningRecordRepo: Repository, - ) {} - - async getUserCount(): Promise { - return this.userRepo.count(); - } - - async getStudentCount(): Promise { - return this.studentRepo.count(); - } - - async seed(): Promise { - // ═══════════════════ 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 { - const existing = await this.permRepo.count(); - if (existing > 0) { this.logger.log(' ⏭ permissions exist, skip'); return; } - - const groups: Record = { - 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 = { - student: '学生', room: '宿舍', occupancy: '入住', class: '班级', - schedule: '排课', classroom: '教室', expense: '费用', bill: '账单', - deposit: '押金', tenant: '租赁方', dashboard: '数据面板', rbac: '用户角色', - }; - const actionMap: Record = { - 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 { - 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 { - 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 { - 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 { - 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 { - // 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 { - 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 { - // 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 { - 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 = {}; - for (const s of this.cachedStudents) { - const key = s.organization || 'other'; - (orgMap[key] ??= []).push(s); - } - - const assignments: Record = { - '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 { - // 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 { - 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 { - // 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 { - // 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 { - // 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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`); - } -} diff --git a/apps/server/src/seed/seed.module.ts b/apps/server/src/seed/seed.module.ts deleted file mode 100644 index 5590696..0000000 --- a/apps/server/src/seed/seed.module.ts +++ /dev/null @@ -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)); - } - } -} diff --git a/apps/server/src/students/students.module.ts b/apps/server/src/students/students.module.ts index 05d961a..f89e0c4 100644 --- a/apps/server/src/students/students.module.ts +++ b/apps/server/src/students/students.module.ts @@ -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], diff --git a/apps/server/test/app.e2e-spec.ts b/apps/server/test/app.e2e-spec.ts deleted file mode 100644 index 7f28aa9..0000000 --- a/apps/server/test/app.e2e-spec.ts +++ /dev/null @@ -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; - - 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(); - }); -});