test: harden business boundary conditions

This commit is contained in:
2026-07-15 00:03:55 +08:00
parent 17a5046ea0
commit b1f35f9d1a
65 changed files with 2311 additions and 293 deletions

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull, Not, MoreThanOrEqual, In } from 'typeorm';
import { Room } from '../entities/room.entity';
@@ -40,8 +40,7 @@ export class DashboardService {
}
async getStats(accessibleClassIds?: number[]) {
const today = new Date();
const todayStr = today.toISOString().slice(0, 10);
const todayStr = this.getChinaDate(new Date());
const currentMonth = todayStr.slice(0, 7); // YYYY-MM
const totalRooms = await this.roomRepo.count({ where: { status: Not('archived') } });
@@ -180,6 +179,10 @@ export class DashboardService {
accessibleClassIds?: number[],
) {
if (accessibleClassIds) {
if (accessibleClassIds.length === 0) {
qb.andWhere('1 = 0');
return;
}
qb.andWhere(`${alias}.classId IN (:...accessibleClassIds)`, { accessibleClassIds });
}
}
@@ -260,6 +263,7 @@ export class DashboardService {
// 甘特图数据:每个宿舍的入住时间线
async getGanttData(query?: { periodStart?: string; periodEnd?: string; building?: string }) {
this.assertPeriodRange(query?.periodStart, query?.periodEnd);
const qb = this.occRepo
.createQueryBuilder('o')
.leftJoinAndSelect('o.student', 'student')
@@ -302,6 +306,7 @@ export class DashboardService {
}
// 费用统计
async getExpenseStats(periodStart?: string, periodEnd?: string) {
this.assertPeriodRange(periodStart, periodEnd);
const qb = this.expRepo
.createQueryBuilder('e')
.select('e.expenseType', 'type')
@@ -314,6 +319,7 @@ export class DashboardService {
// 各宿舍费用排行
async getRoomExpenseRanking(periodStart?: string, periodEnd?: string) {
this.assertPeriodRange(periodStart, periodEnd);
const qb = this.expRepo
.createQueryBuilder('e')
.leftJoin('e.room', 'room')
@@ -368,7 +374,7 @@ export class DashboardService {
where: { status: 'available' as const },
order: { building: 'ASC', name: 'ASC' },
});
const today = new Date().toISOString().slice(0, 10);
const today = this.getChinaDate(new Date());
const schedQb = this.scheduleRepo
.createQueryBuilder('s')
.select('s.classroomId', 'classroomId')
@@ -400,12 +406,31 @@ export class DashboardService {
}));
}
private assertPeriodRange(periodStart?: string, periodEnd?: string) {
if (periodStart && periodEnd && periodStart > periodEnd) {
throw new BadRequestException('结束日期不能早于开始日期');
}
}
private getChinaDate(date: Date): string {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts(date);
const values = Object.fromEntries(
parts.filter((part) => part.type !== 'literal').map((part) => [part.type, part.value]),
);
return `${values.year}-${values.month}-${values.day}`;
}
async getClassroomUtilizationStats() {
const totalClassrooms = await this.classroomRepo.count({
where: { status: 'available' as const },
});
const today = new Date().toISOString().slice(0, 10);
const today = this.getChinaDate(new Date());
// Count classrooms with active schedules today
const schedQb = this.scheduleRepo