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'); throw new BadRequestException('records array must not be empty');
} }
const entities = dto.records.map((r) => // Batch-load student departmentIds
this.attendanceRepo.create({ 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, studentId: r.studentId,
classId: r.classId ?? undefined, classId: r.classId ?? undefined,
attendanceDate: r.attendanceDate, attendanceDate: r.attendanceDate,
@@ -45,8 +50,10 @@ export class AttendanceService {
status: r.status, status: r.status,
remark: r.remark, remark: r.remark,
source: r.source || 'manual', source: r.source || 'manual',
}), });
); entity.departmentId = deptMap.get(r.studentId)!;
return entity;
});
const saved = await this.attendanceRepo.save(entities); const saved = await this.attendanceRepo.save(entities);
return { count: saved.length, records: saved }; return { count: saved.length, records: saved };

View File

@@ -1,6 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm'; import { Repository, In, DataSource } from 'typeorm';
import { Bill } from '../entities/bill.entity'; import { Bill } from '../entities/bill.entity';
import { BillItem } from '../entities/bill-item.entity'; import { BillItem } from '../entities/bill-item.entity';
import { RoomExpense } from '../entities/room-expense.entity'; import { RoomExpense } from '../entities/room-expense.entity';
@@ -177,10 +177,19 @@ export class BillsService {
}); });
} }
// 合并所有涉及的学生 // 合并所有涉及的学生
const allStudentIds = new Set([...studentBillData.keys(), ...personalMap.keys()]); 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[] = []; const bills: Bill[] = [];
for (const studentId of allStudentIds) { for (const studentId of allStudentIds) {
const shared = studentBillData.get(studentId)?.shared || 0; const shared = studentBillData.get(studentId)?.shared || 0;
@@ -195,6 +204,7 @@ export class BillsService {
personalAmount: personal, personalAmount: personal,
totalAmount: total, totalAmount: total,
status: 'draft', status: 'draft',
departmentId: studentDeptMap.get(studentId),
}); });
const savedBill = await this.billRepo.save(bill); 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) { async update(id: number, dto: UpdateRentalDto) {

View File

@@ -34,7 +34,9 @@ export class ClassroomsService {
async create(dto: CreateClassroomDto) { async create(dto: CreateClassroomDto) {
const exists = await this.repo.findOne({ where: { name: dto.name } }); const exists = await this.repo.findOne({ where: { name: dto.name } });
if (exists) throw new BadRequestException(`教室 ${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) { async update(id: number, dto: UpdateClassroomDto) {

View File

@@ -32,6 +32,10 @@ export class CreateClassroomDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
notes?: string; notes?: string;
@IsOptional()
@IsInt()
departmentId?: number;
} }
export class UpdateClassroomDto { 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 { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { Deposit } from '../entities/deposit.entity'; import { Deposit } from '../entities/deposit.entity';
import { Student } from '../entities/student.entity';
import { DepositInstallment } from '../entities/deposit-installment.entity'; import { DepositInstallment } from '../entities/deposit-installment.entity';
import { CampusScope } from '../common/campus-scope'; import { CampusScope } from '../common/campus-scope';
import { CreateDepositDto, RefundDepositDto, CreateDepositWithInstallmentsDto } from './dto/deposit.dto'; import { CreateDepositDto, RefundDepositDto, CreateDepositWithInstallmentsDto } from './dto/deposit.dto';
@@ -13,6 +14,8 @@ export class DepositsService {
@InjectRepository(Deposit) private repo: Repository<Deposit>, @InjectRepository(Deposit) private repo: Repository<Deposit>,
@InjectRepository(DepositInstallment) @InjectRepository(DepositInstallment)
private installmentRepo: Repository<DepositInstallment>, private installmentRepo: Repository<DepositInstallment>,
@InjectRepository(Student)
private studentRepo: Repository<Student>,
private readonly scope: CampusScope, private readonly scope: CampusScope,
) {} ) {}
@@ -36,6 +39,8 @@ export class DepositsService {
} }
async create(dto: CreateDepositDto, userId?: number) { 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({ const deposit = this.repo.create({
studentId: dto.studentId, studentId: dto.studentId,
amount: dto.amount, amount: dto.amount,
@@ -44,15 +49,18 @@ export class DepositsService {
status: 'paid', status: 'paid',
recordedBy: userId, recordedBy: userId,
}); });
deposit.departmentId = student.departmentId;
if (dto instanceof CreateDepositWithInstallmentsDto && dto.installments?.length) { if (dto instanceof CreateDepositWithInstallmentsDto && dto.installments?.length) {
deposit.installments = dto.installments.map((i) => deposit.installments = dto.installments.map((i) => {
this.installmentRepo.create({ const inst = this.installmentRepo.create({
amount: i.amount, amount: i.amount,
dueDate: i.dueDate, dueDate: i.dueDate,
status: 'pending', status: 'pending',
}), });
); inst.departmentId = student.departmentId;
return inst;
});
} }
return this.repo.save(deposit); return this.repo.save(deposit);

View File

@@ -1,6 +1,6 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository, In } from 'typeorm';
import { RoomExpense } from '../entities/room-expense.entity'; import { RoomExpense } from '../entities/room-expense.entity';
import { PersonalExpense } from '../entities/personal-expense.entity'; import { PersonalExpense } from '../entities/personal-expense.entity';
import { Room } from '../entities/room.entity'; import { Room } from '../entities/room.entity';
@@ -25,12 +25,19 @@ export class ExpensesService {
// 宿舍费用 // 宿舍费用
async createRoomExpense(dto: CreateRoomExpenseDto, userId?: number) { 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) { async batchCreateRoomExpenses(dto: BatchRoomExpenseDto, userId?: number) {
const entities = dto.expenses.map((e) => const roomIds = [...new Set(dto.expenses.map((e) => e.roomId))];
this.roomExpRepo.create({ 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, roomId: e.roomId,
expenseType: e.expenseType, expenseType: e.expenseType,
amount: e.amount, amount: e.amount,
@@ -38,8 +45,10 @@ export class ExpensesService {
periodStart: dto.periodStart, periodStart: dto.periodStart,
periodEnd: dto.periodEnd, periodEnd: dto.periodEnd,
recordedBy: userId, recordedBy: userId,
}), });
); entity.departmentId = roomDeptMap.get(e.roomId)!;
return entity;
});
return this.roomExpRepo.save(entities); return this.roomExpRepo.save(entities);
} }
@@ -82,7 +91,11 @@ export class ExpensesService {
// 个人附加费 // 个人附加费
async createPersonalExpense(dto: CreatePersonalExpenseDto, userId?: number) { 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 }) { async findPersonalExpenses(query?: { studentId?: number }) {

View File

@@ -1,12 +1,16 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { seedDefaultCampus } from './departments/seed';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); app.setGlobalPrefix('api');
app.enableCors(); app.enableCors();
app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true }));
const dataSource = app.get(DataSource);
await seedDefaultCampus(dataSource);
await app.listen(process.env.PORT ?? 3003); await app.listen(process.env.PORT ?? 3003);
console.log(`Server running on http://localhost:${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, tenantId: dto.tenantId,
notes: dto.notes, notes: dto.notes,
}); });
occ.departmentId = room.departmentId;
const saved = await this.repo.save(occ); const saved = await this.repo.save(occ);
// 首位入住者确定房间性别 // 首位入住者确定房间性别

View File

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

View File

@@ -100,7 +100,9 @@ export class RoomsService {
} }
async create(dto: CreateRoomDto) { 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) { async update(id: number, dto: UpdateRoomDto) {

View File

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

View File

@@ -1,7 +1,7 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { ClassSchedule } from '../entities'; import { ClassSchedule, Class } from '../entities';
import { CampusScope } from '../common/campus-scope'; import { CampusScope } from '../common/campus-scope';
import { import {
CreateScheduleDto, CreateScheduleDto,
@@ -16,6 +16,7 @@ export class SchedulesService {
@InjectRepository(ClassSchedule) @InjectRepository(ClassSchedule)
private readonly scheduleRepo: Repository<ClassSchedule>, private readonly scheduleRepo: Repository<ClassSchedule>,
private readonly scope: CampusScope, private readonly scope: CampusScope,
@InjectRepository(Class) private readonly classRepo: Repository<Class>,
) {} ) {}
async findAll(query: QueryScheduleDto) { 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); await this.checkConflict(dto.classroomId, dto.weekDay, dto.startTime, dto.endTime, dto.startDate, dto.endDate);
const schedule = this.scheduleRepo.create(dto); 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); const saved = await this.scheduleRepo.save(schedule);
return this.findOne(saved.id); 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 { export class CreateStudentDto {
@IsString() @IsString()
@@ -39,6 +39,13 @@ export class CreateStudentDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
supervisor?: string; supervisor?: string;
@IsOptional()
@IsInt()
departmentId?: number;
@IsOptional()
@IsInt()
classId?: number;
} }
export class UpdateStudentDto { export class UpdateStudentDto {

View File

@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like, Not, In } from 'typeorm'; import { Repository, Like, Not, In } from 'typeorm';
import { CampusScope } from '../common/campus-scope'; import { CampusScope } from '../common/campus-scope';
import { Student } from '../entities/student.entity'; import { Student } from '../entities/student.entity';
import { Class } from '../entities/class.entity';
import { ClassStudent } from '../entities/class-student.entity'; import { ClassStudent } from '../entities/class-student.entity';
import { AttendanceRecord } from '../entities/attendance-record.entity'; import { AttendanceRecord } from '../entities/attendance-record.entity';
import { CreateStudentDto, UpdateStudentDto } from './dto/student.dto'; import { CreateStudentDto, UpdateStudentDto } from './dto/student.dto';
@@ -13,6 +14,7 @@ export class StudentsService {
constructor( constructor(
@InjectRepository(Student) private repo: Repository<Student>, @InjectRepository(Student) private repo: Repository<Student>,
@InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>, @InjectRepository(ClassStudent) private classStudentRepo: Repository<ClassStudent>,
@InjectRepository(Class) private classRepo: Repository<Class>,
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>, @InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
private readonly scope: CampusScope, private readonly scope: CampusScope,
) {} ) {}
@@ -39,7 +41,14 @@ export class StudentsService {
} }
async create(dto: CreateStudentDto) { 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) { async update(id: number, dto: UpdateStudentDto) {