fix: harden financial transaction boundaries
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In } from 'typeorm';
|
||||
import { DataSource, In, Repository } from 'typeorm';
|
||||
import { RoomExpense } from '../entities/room-expense.entity';
|
||||
import { PersonalExpense } from '../entities/personal-expense.entity';
|
||||
import { Room } from '../entities/room.entity';
|
||||
@@ -23,6 +23,7 @@ export class ExpensesService {
|
||||
@InjectRepository(Room) private roomRepo: Repository<Room>,
|
||||
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
||||
private billsService: BillsService,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async getFormLookups() {
|
||||
@@ -87,6 +88,8 @@ export class ExpensesService {
|
||||
async deleteRoomExpense(id: number) {
|
||||
const e = await this.roomExpRepo.findOne({ where: { id } });
|
||||
if (!e) throw new NotFoundException('费用记录不存在');
|
||||
const billed = await this.dataSource?.getRepository('bill_items').count({ where: { roomExpenseId: id } });
|
||||
if (billed) throw new BadRequestException('已计入账单的宿舍费用不能归档,请先取消账单');
|
||||
if (e.status === 'archived') throw new BadRequestException('费用记录已归档');
|
||||
await this.roomExpRepo.update(id, { status: 'archived' });
|
||||
return { message: '已归档' };
|
||||
@@ -96,6 +99,8 @@ export class ExpensesService {
|
||||
const uniqueIds = [...new Set(ids || [])];
|
||||
if (uniqueIds.length === 0) throw new BadRequestException('请选择要归档的记录');
|
||||
const existing = await this.roomExpRepo.find({ where: { id: In(uniqueIds) }, select: ['id'] });
|
||||
const billed = await this.dataSource?.getRepository('bill_items').count({ where: { roomExpenseId: In(uniqueIds) } });
|
||||
if (billed) throw new BadRequestException('选中记录包含已计入账单的宿舍费用');
|
||||
if (existing.length !== uniqueIds.length) throw new NotFoundException('部分费用记录不存在');
|
||||
const result = await this.roomExpRepo
|
||||
.createQueryBuilder()
|
||||
@@ -108,6 +113,8 @@ export class ExpensesService {
|
||||
|
||||
async updateRoomExpense(id: number, dto: Partial<CreateRoomExpenseDto>) {
|
||||
const e = await this.roomExpRepo.findOne({ where: { id } });
|
||||
const billed = await this.dataSource?.getRepository('bill_items').count({ where: { roomExpenseId: id } });
|
||||
if (billed) throw new BadRequestException('已计入账单的宿舍费用不能修改,请先取消账单');
|
||||
if (!e) throw new NotFoundException('费用记录不存在');
|
||||
const periodStart = dto.periodStart ?? e.periodStart;
|
||||
const periodEnd = dto.periodEnd ?? e.periodEnd;
|
||||
@@ -145,24 +152,16 @@ export class ExpensesService {
|
||||
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(
|
||||
this.personalExpRepo.create({
|
||||
studentId: dto.studentId,
|
||||
expenseType: dto.expenseType,
|
||||
amount: dto.amount,
|
||||
expenseDate: dto.periodEnd,
|
||||
description: dto.description || (dto.expenseType === 'water' ? '学生水费' : '学生电费'),
|
||||
recordedBy: userId,
|
||||
billId: null,
|
||||
}),
|
||||
);
|
||||
try {
|
||||
const bill = await this.billsService.createImmediatePersonalBill(expense, dto.periodStart, dto.periodEnd, userId);
|
||||
return { expense, bill };
|
||||
} catch (error) {
|
||||
await this.personalExpRepo.delete(expense.id);
|
||||
throw error;
|
||||
}
|
||||
const expense = {
|
||||
studentId: dto.studentId,
|
||||
expenseType: dto.expenseType,
|
||||
amount: dto.amount,
|
||||
expenseDate: dto.periodEnd,
|
||||
description: dto.description || (dto.expenseType === 'water' ? '学生水费' : '学生电费'),
|
||||
recordedBy: userId,
|
||||
billId: null,
|
||||
} as PersonalExpense;
|
||||
return this.billsService.createImmediatePersonalBill(expense, dto.periodStart, dto.periodEnd, userId);
|
||||
}
|
||||
|
||||
// 个人附加费
|
||||
@@ -303,45 +302,50 @@ export class ExpensesService {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 幂等:先删除该房间在同一周期已有的水/电费用记录,避免重复导入产生脏数据
|
||||
await this.roomExpRepo
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where('roomId = :roomId', { roomId: room.id })
|
||||
.andWhere('periodStart = :ps AND periodEnd = :pe', { ps: periodStart, pe: periodEnd })
|
||||
.andWhere('expenseType IN (:...types)', { types: ['water', 'electricity'] })
|
||||
.execute();
|
||||
const existing = await this.roomExpRepo.find({
|
||||
where: [
|
||||
{ importKey: `${room.id}:${periodStart}:${periodEnd}:electricity` },
|
||||
{ importKey: `${room.id}:${periodStart}:${periodEnd}:water` },
|
||||
],
|
||||
});
|
||||
const byType = new Map(existing.map((expense) => [expense.expenseType, expense]));
|
||||
|
||||
let savedAny = false;
|
||||
// 导入电费
|
||||
if (row.electricityFee > 0) {
|
||||
await this.roomExpRepo.save(
|
||||
this.roomExpRepo.create({
|
||||
roomId: room.id,
|
||||
expenseType: 'electricity',
|
||||
amount: row.electricityFee,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
description: `电量${row.electricityAmount}kWh`,
|
||||
recordedBy: userId,
|
||||
}),
|
||||
);
|
||||
const expense = byType.get('electricity') || this.roomExpRepo.create({
|
||||
roomId: room.id,
|
||||
expenseType: 'electricity',
|
||||
periodStart,
|
||||
periodEnd,
|
||||
importKey: `${room.id}:${periodStart}:${periodEnd}:electricity`,
|
||||
});
|
||||
if (expense.id && await this.dataSource?.getRepository('bill_items').count({ where: { roomExpenseId: expense.id } })) {
|
||||
throw new BadRequestException('该周期电费已计入账单,不能覆盖');
|
||||
}
|
||||
expense.amount = row.electricityFee;
|
||||
expense.description = `电量${row.electricityAmount}kWh`;
|
||||
expense.recordedBy = userId!;
|
||||
await this.roomExpRepo.save(expense);
|
||||
savedAny = true;
|
||||
}
|
||||
|
||||
// 导入水费
|
||||
if (row.waterFee > 0) {
|
||||
await this.roomExpRepo.save(
|
||||
this.roomExpRepo.create({
|
||||
roomId: room.id,
|
||||
expenseType: 'water',
|
||||
amount: row.waterFee,
|
||||
periodStart,
|
||||
periodEnd,
|
||||
description: `用水${row.waterAmount}吨`,
|
||||
recordedBy: userId,
|
||||
}),
|
||||
);
|
||||
const expense = byType.get('water') || this.roomExpRepo.create({
|
||||
roomId: room.id,
|
||||
expenseType: 'water',
|
||||
periodStart,
|
||||
periodEnd,
|
||||
importKey: `${room.id}:${periodStart}:${periodEnd}:water`,
|
||||
});
|
||||
if (expense.id && await this.dataSource?.getRepository('bill_items').count({ where: { roomExpenseId: expense.id } })) {
|
||||
throw new BadRequestException('该周期水费已计入账单,不能覆盖');
|
||||
}
|
||||
expense.amount = row.waterFee;
|
||||
expense.description = `用水${row.waterAmount}吨`;
|
||||
expense.recordedBy = userId!;
|
||||
await this.roomExpRepo.save(expense);
|
||||
savedAny = true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user