test: harden business boundary conditions
This commit is contained in:
@@ -42,6 +42,8 @@ export class ExpensesService {
|
||||
|
||||
// 宿舍费用
|
||||
async createRoomExpense(dto: CreateRoomExpenseDto, userId?: number) {
|
||||
this.assertValidPeriod(dto.periodStart, dto.periodEnd);
|
||||
this.assertPositiveAmount(dto.amount);
|
||||
const room = await this.roomRepo.findOne({ where: { id: dto.roomId } });
|
||||
if (!room) throw new NotFoundException('宿舍不存在');
|
||||
const entity = this.roomExpRepo.create({ ...dto, recordedBy: userId });
|
||||
@@ -49,6 +51,12 @@ export class ExpensesService {
|
||||
}
|
||||
|
||||
async batchCreateRoomExpenses(dto: BatchRoomExpenseDto, userId?: number) {
|
||||
this.assertValidPeriod(dto.periodStart, dto.periodEnd);
|
||||
if (!dto.expenses?.length) throw new BadRequestException('请至少填写一条费用');
|
||||
dto.expenses.forEach((expense) => this.assertPositiveAmount(expense.amount));
|
||||
const roomIds = [...new Set(dto.expenses.map((expense) => expense.roomId))];
|
||||
const existingRooms = await this.roomRepo.find({ where: { id: In(roomIds) }, select: ['id'] });
|
||||
if (existingRooms.length !== roomIds.length) throw new NotFoundException('部分宿舍不存在');
|
||||
const entities = dto.expenses.map((e) => {
|
||||
const entity = this.roomExpRepo.create({
|
||||
roomId: e.roomId,
|
||||
@@ -83,11 +91,14 @@ export class ExpensesService {
|
||||
}
|
||||
|
||||
async batchDeleteRoomExpenses(ids: number[]) {
|
||||
if (!ids || ids.length === 0) throw new BadRequestException('请选择要删除的记录');
|
||||
const uniqueIds = [...new Set(ids || [])];
|
||||
if (uniqueIds.length === 0) throw new BadRequestException('请选择要删除的记录');
|
||||
const existing = await this.roomExpRepo.find({ where: { id: In(uniqueIds) }, select: ['id'] });
|
||||
if (existing.length !== uniqueIds.length) throw new NotFoundException('部分费用记录不存在');
|
||||
const result = await this.roomExpRepo
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where('id IN (:...ids)', { ids })
|
||||
.where('id IN (:...ids)', { ids: uniqueIds })
|
||||
.execute();
|
||||
return { message: '批量删除成功', deleted: result.affected || 0 };
|
||||
}
|
||||
@@ -95,12 +106,40 @@ export class ExpensesService {
|
||||
async updateRoomExpense(id: number, dto: Partial<CreateRoomExpenseDto>) {
|
||||
const e = await this.roomExpRepo.findOne({ where: { id } });
|
||||
if (!e) throw new NotFoundException('费用记录不存在');
|
||||
const periodStart = dto.periodStart ?? e.periodStart;
|
||||
const periodEnd = dto.periodEnd ?? e.periodEnd;
|
||||
this.assertValidPeriod(periodStart, periodEnd);
|
||||
if (dto.amount !== undefined) this.assertPositiveAmount(dto.amount);
|
||||
if (dto.roomId !== undefined && dto.roomId !== e.roomId) {
|
||||
const room = await this.roomRepo.findOne({ where: { id: dto.roomId } });
|
||||
if (!room) throw new NotFoundException('宿舍不存在');
|
||||
}
|
||||
Object.assign(e, dto);
|
||||
return this.roomExpRepo.save(e);
|
||||
}
|
||||
|
||||
private assertPositiveAmount(amount: number) {
|
||||
if (!Number.isFinite(amount) || Math.abs(amount * 100 - Math.round(amount * 100)) > 1e-8) {
|
||||
throw new BadRequestException('费用金额最多保留两位小数');
|
||||
}
|
||||
if (amount <= 0) throw new BadRequestException('费用金额必须大于0');
|
||||
}
|
||||
|
||||
private assertValidPeriod(periodStart: string, periodEnd: string) {
|
||||
if (!this.isValidDate(periodStart) || !this.isValidDate(periodEnd) || periodEnd < periodStart) {
|
||||
throw new BadRequestException('账期无效,结束日期不能早于开始日期');
|
||||
}
|
||||
}
|
||||
|
||||
private isValidDate(value: string) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return false;
|
||||
const date = new Date(`${value}T00:00:00Z`);
|
||||
return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
async createStudentUtilityBill(dto: CreateStudentUtilityBillDto, userId?: number) {
|
||||
if (dto.periodEnd < dto.periodStart) throw new BadRequestException('账期结束日期不能早于开始日期');
|
||||
this.assertValidPeriod(dto.periodStart, dto.periodEnd);
|
||||
this.assertPositiveAmount(dto.amount);
|
||||
const student = await this.studentRepo.findOne({ where: { id: dto.studentId } });
|
||||
if (!student) throw new NotFoundException('学生不存在');
|
||||
const expense = await this.personalExpRepo.save(
|
||||
@@ -125,6 +164,7 @@ export class ExpensesService {
|
||||
|
||||
// 个人附加费
|
||||
async createPersonalExpense(dto: CreatePersonalExpenseDto, userId?: number) {
|
||||
this.assertPositiveAmount(dto.amount);
|
||||
const student = await this.studentRepo.findOne({ where: { id: dto.studentId } });
|
||||
if (!student) throw new NotFoundException('学生不存在');
|
||||
const entity = this.personalExpRepo.create({ ...dto, recordedBy: userId });
|
||||
@@ -144,16 +184,23 @@ export class ExpensesService {
|
||||
async deletePersonalExpense(id: number) {
|
||||
const e = await this.personalExpRepo.findOne({ where: { id } });
|
||||
if (!e) throw new NotFoundException('费用记录不存在');
|
||||
if (e.billId) throw new BadRequestException('已计入账单的个人费用不能删除,请先取消账单');
|
||||
await this.personalExpRepo.delete(id);
|
||||
return { message: '删除成功' };
|
||||
}
|
||||
|
||||
async batchDeletePersonalExpenses(ids: number[]) {
|
||||
if (!ids || ids.length === 0) throw new BadRequestException('请选择要删除的记录');
|
||||
const uniqueIds = [...new Set(ids || [])];
|
||||
if (uniqueIds.length === 0) throw new BadRequestException('请选择要删除的记录');
|
||||
const existing = await this.personalExpRepo.find({ where: { id: In(uniqueIds) } });
|
||||
if (existing.length !== uniqueIds.length) throw new NotFoundException('部分费用记录不存在');
|
||||
if (existing.some((expense) => expense.billId)) {
|
||||
throw new BadRequestException('选中记录包含已计入账单的个人费用');
|
||||
}
|
||||
const result = await this.personalExpRepo
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where('id IN (:...ids)', { ids })
|
||||
.where('id IN (:...ids)', { ids: uniqueIds })
|
||||
.execute();
|
||||
return { message: '批量删除成功', deleted: result.affected || 0 };
|
||||
}
|
||||
@@ -161,6 +208,12 @@ export class ExpensesService {
|
||||
async updatePersonalExpense(id: number, dto: Partial<CreatePersonalExpenseDto>) {
|
||||
const e = await this.personalExpRepo.findOne({ where: { id } });
|
||||
if (!e) throw new NotFoundException('费用记录不存在');
|
||||
if (e.billId) throw new BadRequestException('已计入账单的个人费用不能修改,请先取消账单');
|
||||
if (dto.amount !== undefined) this.assertPositiveAmount(dto.amount);
|
||||
if (dto.studentId !== undefined && dto.studentId !== e.studentId) {
|
||||
const student = await this.studentRepo.findOne({ where: { id: dto.studentId } });
|
||||
if (!student) throw new NotFoundException('学生不存在');
|
||||
}
|
||||
Object.assign(e, dto);
|
||||
return this.personalExpRepo.save(e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user