diff --git a/apps/server/src/attendance/attendance.service.ts b/apps/server/src/attendance/attendance.service.ts index bffbb24..b5a2cb5 100644 --- a/apps/server/src/attendance/attendance.service.ts +++ b/apps/server/src/attendance/attendance.service.ts @@ -36,8 +36,13 @@ export class AttendanceService { throw new BadRequestException('records array must not be empty'); } - const entities = dto.records.map((r) => - this.attendanceRepo.create({ + // Batch-load student departmentIds + const ids = [...new Set(dto.records.map((r) => r.studentId))]; + const students = await this.studentRepo.find({ where: { id: In(ids) } }); + const deptMap = new Map(students.map((s) => [s.id, s.departmentId])); + + const entities = dto.records.map((r) => { + const entity = this.attendanceRepo.create({ studentId: r.studentId, classId: r.classId ?? undefined, attendanceDate: r.attendanceDate, @@ -45,8 +50,10 @@ export class AttendanceService { status: r.status, remark: r.remark, source: r.source || 'manual', - }), - ); + }); + entity.departmentId = deptMap.get(r.studentId)!; + return entity; + }); const saved = await this.attendanceRepo.save(entities); return { count: saved.length, records: saved }; diff --git a/apps/server/src/bills/bills.service.ts b/apps/server/src/bills/bills.service.ts index 4d91e68..516682b 100644 --- a/apps/server/src/bills/bills.service.ts +++ b/apps/server/src/bills/bills.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; +import { Repository, In, DataSource } from 'typeorm'; import { Bill } from '../entities/bill.entity'; import { BillItem } from '../entities/bill-item.entity'; import { RoomExpense } from '../entities/room-expense.entity'; @@ -177,10 +177,19 @@ export class BillsService { }); } + // 合并所有涉及的学生 const allStudentIds = new Set([...studentBillData.keys(), ...personalMap.keys()]); - // 生成账单 + // Batch-load student departmentIds + const studentIdsArr = [...allStudentIds]; + const studentDeptMap = new Map(); + if (studentIdsArr.length > 0) { + const studentsData: { id: number; department_id: number | null }[] = await this.dataSource.query( + `SELECT id, department_id FROM students WHERE id IN (${studentIdsArr.join(',')})` + ); + for (const s of studentsData) if (s.department_id != null) studentDeptMap.set(s.id, s.department_id); + } const bills: Bill[] = []; for (const studentId of allStudentIds) { const shared = studentBillData.get(studentId)?.shared || 0; @@ -195,6 +204,7 @@ export class BillsService { personalAmount: personal, totalAmount: total, status: 'draft', + departmentId: studentDeptMap.get(studentId), }); const savedBill = await this.billRepo.save(bill); diff --git a/apps/server/src/classroom-rentals/classroom-rentals.service.ts b/apps/server/src/classroom-rentals/classroom-rentals.service.ts index 3416304..9e79881 100644 --- a/apps/server/src/classroom-rentals/classroom-rentals.service.ts +++ b/apps/server/src/classroom-rentals/classroom-rentals.service.ts @@ -118,7 +118,9 @@ export class ClassroomRentalsService { })), }); } - return this.repo.save(this.repo.create({ ...dto, createdBy: userId, status: 'active' })); + const rental = this.repo.create({ ...dto, createdBy: userId, status: 'active' }); + rental.departmentId = classroom.departmentId; + return this.repo.save(rental); } async update(id: number, dto: UpdateRentalDto) { diff --git a/apps/server/src/classrooms/classrooms.service.ts b/apps/server/src/classrooms/classrooms.service.ts index bdc2a4d..4abef72 100644 --- a/apps/server/src/classrooms/classrooms.service.ts +++ b/apps/server/src/classrooms/classrooms.service.ts @@ -34,7 +34,9 @@ export class ClassroomsService { async create(dto: CreateClassroomDto) { const exists = await this.repo.findOne({ where: { name: dto.name } }); if (exists) throw new BadRequestException(`教室 ${dto.name} 已存在`); - return this.repo.save(this.repo.create(dto)); + const entity = this.repo.create(dto); + if (dto.departmentId) entity.departmentId = dto.departmentId; + return this.repo.save(entity); } async update(id: number, dto: UpdateClassroomDto) { diff --git a/apps/server/src/classrooms/dto/classroom.dto.ts b/apps/server/src/classrooms/dto/classroom.dto.ts index 90dcb5b..8033a2a 100644 --- a/apps/server/src/classrooms/dto/classroom.dto.ts +++ b/apps/server/src/classrooms/dto/classroom.dto.ts @@ -32,6 +32,10 @@ export class CreateClassroomDto { @IsOptional() @IsString() notes?: string; + + @IsOptional() + @IsInt() + departmentId?: number; } export class UpdateClassroomDto { diff --git a/apps/server/src/departments/seed.ts b/apps/server/src/departments/seed.ts new file mode 100644 index 0000000..85d0245 --- /dev/null +++ b/apps/server/src/departments/seed.ts @@ -0,0 +1,23 @@ +import { DataSource } from 'typeorm'; + +export async function seedDefaultCampus(dataSource: DataSource) { + const deptRepo = dataSource.getRepository('departments'); + const userDeptRepo = dataSource.getRepository('user_departments'); + + const existing = await deptRepo.count(); + if (existing > 0) { console.log('Departments exist, skip seed'); return; } + + const campus = await deptRepo.save({ name: '主校区', type: 'campus', sortOrder: 0 }); + + const tables = ['students','rooms','classrooms','class_schedules','attendance_records','room_expenses','personal_expenses','occupancies','bills','deposits','deposit_installments','classroom_rentals']; + for (const table of tables) { + await dataSource.query(`UPDATE ${table} SET department_id = ? WHERE department_id IS NULL`, [campus.id]); + } + + const users = await dataSource.query('SELECT id FROM users'); + for (const user of users) { + await userDeptRepo.save({ userId: user.id, departmentId: campus.id, isDefault: true }); + } + + console.log('Seed: default campus created, data backfilled'); +} diff --git a/apps/server/src/deposits/deposits.service.ts b/apps/server/src/deposits/deposits.service.ts index 692c572..ee9367e 100644 --- a/apps/server/src/deposits/deposits.service.ts +++ b/apps/server/src/deposits/deposits.service.ts @@ -2,6 +2,7 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/comm import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Deposit } from '../entities/deposit.entity'; +import { Student } from '../entities/student.entity'; import { DepositInstallment } from '../entities/deposit-installment.entity'; import { CampusScope } from '../common/campus-scope'; import { CreateDepositDto, RefundDepositDto, CreateDepositWithInstallmentsDto } from './dto/deposit.dto'; @@ -13,6 +14,8 @@ export class DepositsService { @InjectRepository(Deposit) private repo: Repository, @InjectRepository(DepositInstallment) private installmentRepo: Repository, + @InjectRepository(Student) + private studentRepo: Repository, private readonly scope: CampusScope, ) {} @@ -36,6 +39,8 @@ export class DepositsService { } async create(dto: CreateDepositDto, userId?: number) { + const student = await this.studentRepo.findOne({ where: { id: dto.studentId } }); + if (!student) throw new NotFoundException('学生不存在'); const deposit = this.repo.create({ studentId: dto.studentId, amount: dto.amount, @@ -44,15 +49,18 @@ export class DepositsService { status: 'paid', recordedBy: userId, }); + deposit.departmentId = student.departmentId; if (dto instanceof CreateDepositWithInstallmentsDto && dto.installments?.length) { - deposit.installments = dto.installments.map((i) => - this.installmentRepo.create({ + deposit.installments = dto.installments.map((i) => { + const inst = this.installmentRepo.create({ amount: i.amount, dueDate: i.dueDate, status: 'pending', - }), - ); + }); + inst.departmentId = student.departmentId; + return inst; + }); } return this.repo.save(deposit); diff --git a/apps/server/src/expenses/expenses.service.ts b/apps/server/src/expenses/expenses.service.ts index 6fc6916..13701ea 100644 --- a/apps/server/src/expenses/expenses.service.ts +++ b/apps/server/src/expenses/expenses.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { Repository, In } from 'typeorm'; import { RoomExpense } from '../entities/room-expense.entity'; import { PersonalExpense } from '../entities/personal-expense.entity'; import { Room } from '../entities/room.entity'; @@ -25,12 +25,19 @@ export class ExpensesService { // 宿舍费用 async createRoomExpense(dto: CreateRoomExpenseDto, userId?: number) { - return this.roomExpRepo.save(this.roomExpRepo.create({ ...dto, recordedBy: userId })); + const room = await this.roomRepo.findOne({ where: { id: dto.roomId } }); + if (!room) throw new NotFoundException('宿舍不存在'); + const entity = this.roomExpRepo.create({ ...dto, recordedBy: userId }); + entity.departmentId = room.departmentId; + return this.roomExpRepo.save(entity); } async batchCreateRoomExpenses(dto: BatchRoomExpenseDto, userId?: number) { - const entities = dto.expenses.map((e) => - this.roomExpRepo.create({ + const roomIds = [...new Set(dto.expenses.map((e) => e.roomId))]; + const rooms = await this.roomRepo.find({ where: { id: In(roomIds) } }); + const roomDeptMap = new Map(rooms.map((r) => [r.id, r.departmentId])); + const entities = dto.expenses.map((e) => { + const entity = this.roomExpRepo.create({ roomId: e.roomId, expenseType: e.expenseType, amount: e.amount, @@ -38,8 +45,10 @@ export class ExpensesService { periodStart: dto.periodStart, periodEnd: dto.periodEnd, recordedBy: userId, - }), - ); + }); + entity.departmentId = roomDeptMap.get(e.roomId)!; + return entity; + }); return this.roomExpRepo.save(entities); } @@ -82,7 +91,11 @@ export class ExpensesService { // 个人附加费 async createPersonalExpense(dto: CreatePersonalExpenseDto, userId?: number) { - return this.personalExpRepo.save(this.personalExpRepo.create({ ...dto, recordedBy: userId })); + const student = await this.studentRepo.findOne({ where: { id: dto.studentId } }); + if (!student) throw new NotFoundException('学生不存在'); + const entity = this.personalExpRepo.create({ ...dto, recordedBy: userId }); + entity.departmentId = student.departmentId; + return this.personalExpRepo.save(entity); } async findPersonalExpenses(query?: { studentId?: number }) { diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 04ea33e..ecf94f5 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -1,12 +1,16 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; +import { DataSource } from 'typeorm'; import { AppModule } from './app.module'; +import { seedDefaultCampus } from './departments/seed'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); app.enableCors(); app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); + const dataSource = app.get(DataSource); + await seedDefaultCampus(dataSource); await app.listen(process.env.PORT ?? 3003); console.log(`Server running on http://localhost:${process.env.PORT ?? 3003}`); } diff --git a/apps/server/src/occupancies/occupancies.service.ts b/apps/server/src/occupancies/occupancies.service.ts index 5973046..ee8d964 100644 --- a/apps/server/src/occupancies/occupancies.service.ts +++ b/apps/server/src/occupancies/occupancies.service.ts @@ -75,6 +75,7 @@ export class OccupanciesService { tenantId: dto.tenantId, notes: dto.notes, }); + occ.departmentId = room.departmentId; const saved = await this.repo.save(occ); // 首位入住者确定房间性别 diff --git a/apps/server/src/rooms/dto/room.dto.ts b/apps/server/src/rooms/dto/room.dto.ts index 4dac4dc..1fe9cd1 100644 --- a/apps/server/src/rooms/dto/room.dto.ts +++ b/apps/server/src/rooms/dto/room.dto.ts @@ -27,6 +27,10 @@ export class CreateRoomDto { @IsOptional() @IsNumber() monthlyRate?: number; + + @IsOptional() + @IsInt() + departmentId?: number; } export class UpdateRoomDto { diff --git a/apps/server/src/rooms/rooms.service.ts b/apps/server/src/rooms/rooms.service.ts index a2273dd..8fb0ac8 100644 --- a/apps/server/src/rooms/rooms.service.ts +++ b/apps/server/src/rooms/rooms.service.ts @@ -100,7 +100,9 @@ export class RoomsService { } async create(dto: CreateRoomDto) { - return this.repo.save(this.repo.create(dto)); + const entity = this.repo.create(dto); + if (dto.departmentId) entity.departmentId = dto.departmentId; + return this.repo.save(entity); } async update(id: number, dto: UpdateRoomDto) { diff --git a/apps/server/src/schedules/dto/schedule.dto.ts b/apps/server/src/schedules/dto/schedule.dto.ts index 3d9d40a..3d6869f 100644 --- a/apps/server/src/schedules/dto/schedule.dto.ts +++ b/apps/server/src/schedules/dto/schedule.dto.ts @@ -60,6 +60,10 @@ export class CreateScheduleDto { @IsOptional() @IsString() notes?: string; + + @IsOptional() + @IsInt() + departmentId?: number; } export class UpdateScheduleDto { diff --git a/apps/server/src/schedules/schedules.service.ts b/apps/server/src/schedules/schedules.service.ts index 6a4eb39..c2785fc 100644 --- a/apps/server/src/schedules/schedules.service.ts +++ b/apps/server/src/schedules/schedules.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ClassSchedule } from '../entities'; +import { ClassSchedule, Class } from '../entities'; import { CampusScope } from '../common/campus-scope'; import { CreateScheduleDto, @@ -16,6 +16,7 @@ export class SchedulesService { @InjectRepository(ClassSchedule) private readonly scheduleRepo: Repository, private readonly scope: CampusScope, + @InjectRepository(Class) private readonly classRepo: Repository, ) {} async findAll(query: QueryScheduleDto) { @@ -47,6 +48,12 @@ export class SchedulesService { await this.checkConflict(dto.classroomId, dto.weekDay, dto.startTime, dto.endTime, dto.startDate, dto.endDate); const schedule = this.scheduleRepo.create(dto); + if (dto.departmentId) { + schedule.departmentId = dto.departmentId; + } else if (dto.classId) { + const cls = await this.classRepo.findOne({ where: { id: dto.classId } }); + if (cls) schedule.departmentId = cls.departmentId; + } const saved = await this.scheduleRepo.save(schedule); return this.findOne(saved.id); } diff --git a/apps/server/src/students/dto/student.dto.ts b/apps/server/src/students/dto/student.dto.ts index 78002ad..0d23ab9 100644 --- a/apps/server/src/students/dto/student.dto.ts +++ b/apps/server/src/students/dto/student.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional, IsEnum, IsNumber } from 'class-validator'; +import { IsString, IsOptional, IsEnum, IsNumber, IsInt } from 'class-validator'; export class CreateStudentDto { @IsString() @@ -39,6 +39,13 @@ export class CreateStudentDto { @IsOptional() @IsString() supervisor?: string; + @IsOptional() + @IsInt() + departmentId?: number; + + @IsOptional() + @IsInt() + classId?: number; } export class UpdateStudentDto { diff --git a/apps/server/src/students/students.service.ts b/apps/server/src/students/students.service.ts index 7d6bc20..86c6456 100644 --- a/apps/server/src/students/students.service.ts +++ b/apps/server/src/students/students.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Like, Not, In } from 'typeorm'; import { CampusScope } from '../common/campus-scope'; import { Student } from '../entities/student.entity'; +import { Class } from '../entities/class.entity'; import { ClassStudent } from '../entities/class-student.entity'; import { AttendanceRecord } from '../entities/attendance-record.entity'; import { CreateStudentDto, UpdateStudentDto } from './dto/student.dto'; @@ -13,6 +14,7 @@ export class StudentsService { constructor( @InjectRepository(Student) private repo: Repository, @InjectRepository(ClassStudent) private classStudentRepo: Repository, + @InjectRepository(Class) private classRepo: Repository, @InjectRepository(AttendanceRecord) private attendanceRepo: Repository, private readonly scope: CampusScope, ) {} @@ -39,7 +41,14 @@ export class StudentsService { } async create(dto: CreateStudentDto) { - return this.repo.save(this.repo.create(dto)); + const entity = this.repo.create(dto); + if (dto.departmentId) { + entity.departmentId = dto.departmentId; + } else if (dto.classId) { + const cls = await this.classRepo.findOne({ where: { id: dto.classId } }); + if (cls) entity.departmentId = cls.departmentId; + } + return this.repo.save(entity); } async update(id: number, dto: UpdateStudentDto) {