feat: auto-populate department_id on create + seed default campus with backfill
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -32,6 +32,10 @@ export class CreateClassroomDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
departmentId?: number;
|
||||
}
|
||||
|
||||
export class UpdateClassroomDto {
|
||||
|
||||
23
apps/server/src/departments/seed.ts
Normal file
23
apps/server/src/departments/seed.ts
Normal 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');
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ export class OccupanciesService {
|
||||
tenantId: dto.tenantId,
|
||||
notes: dto.notes,
|
||||
});
|
||||
occ.departmentId = room.departmentId;
|
||||
const saved = await this.repo.save(occ);
|
||||
|
||||
// 首位入住者确定房间性别
|
||||
|
||||
@@ -27,6 +27,10 @@ export class CreateRoomDto {
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
monthlyRate?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
departmentId?: number;
|
||||
}
|
||||
|
||||
export class UpdateRoomDto {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -60,6 +60,10 @@ export class CreateScheduleDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
departmentId?: number;
|
||||
}
|
||||
|
||||
export class UpdateScheduleDto {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user