feat: auto-populate department_id on create + seed default campus with backfill

This commit is contained in:
2026-07-06 00:05:00 +08:00
parent cd6364268d
commit df61ebbc5b
16 changed files with 130 additions and 23 deletions

View File

@@ -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 };

View File

@@ -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<number, number>();
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);

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -32,6 +32,10 @@ export class CreateClassroomDto {
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
@IsInt()
departmentId?: number;
}
export class UpdateClassroomDto {

View File

@@ -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');
}

View File

@@ -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<Deposit>,
@InjectRepository(DepositInstallment)
private installmentRepo: Repository<DepositInstallment>,
@InjectRepository(Student)
private studentRepo: Repository<Student>,
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);

View File

@@ -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 }) {

View File

@@ -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}`);
}

View File

@@ -75,6 +75,7 @@ export class OccupanciesService {
tenantId: dto.tenantId,
notes: dto.notes,
});
occ.departmentId = room.departmentId;
const saved = await this.repo.save(occ);
// 首位入住者确定房间性别

View File

@@ -27,6 +27,10 @@ export class CreateRoomDto {
@IsOptional()
@IsNumber()
monthlyRate?: number;
@IsOptional()
@IsInt()
departmentId?: number;
}
export class UpdateRoomDto {

View File

@@ -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) {

View File

@@ -60,6 +60,10 @@ export class CreateScheduleDto {
@IsOptional()
@IsString()
notes?: string;
@IsOptional()
@IsInt()
departmentId?: number;
}
export class UpdateScheduleDto {

View File

@@ -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<ClassSchedule>,
private readonly scope: CampusScope,
@InjectRepository(Class) private readonly classRepo: Repository<Class>,
) {}
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);
}

View File

@@ -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 {

View File

@@ -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<Student>,
@InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
@InjectRepository(Class) private classRepo: Repository<Class>,
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
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) {