feat: add student wallet utility billing

This commit is contained in:
2026-07-14 20:39:32 +08:00
parent b480070e69
commit c75a08affe
29 changed files with 986 additions and 548 deletions

View File

@@ -5,7 +5,7 @@ import { Bill } from '../entities/bill.entity';
import { BillItem } from '../entities/bill-item.entity';
import { Deposit } from '../entities/deposit.entity';
import * as ExcelJS from 'exceljs';
import PDFDocument from 'pdfkit';
import * as PDFDocument from 'pdfkit';
import { Response } from 'express';
@Injectable()
@@ -34,20 +34,6 @@ export class BillsExportService {
if (query.status) qb.andWhere('b.status = :status', { status: query.status });
const bills = await qb.getMany();
// 查询涉及学生当前押金余额。账单生成时不冻结押金,导出只展示实时余额和已实际扣款。
const studentIds = Array.from(new Set(bills.map((b) => b.studentId)));
const depMap = new Map<number, number>();
if (studentIds.length > 0) {
const deposits = await this.depositRepo
.createQueryBuilder('d')
.where('d.studentId IN (:...ids)', { ids: studentIds })
.andWhere('d.status = :status', { status: 'paid' })
.getMany();
for (const d of deposits) {
depMap.set(d.studentId, (depMap.get(d.studentId) || 0) + Number(d.amount || 0));
}
}
const workbook = new ExcelJS.Workbook();
workbook.creator = '恭学教育基地管理系统';
@@ -60,8 +46,8 @@ export class BillsExportService {
{ header: '分摊费用', key: 'shared', width: 12 },
{ header: '个人费用', key: 'personal', width: 12 },
{ header: '总金额', key: 'total', width: 12 },
{ header: '可用押金', key: 'deposit', width: 12 },
{ header: '已扣押金', key: 'depositDeducted', width: 12 },
{ header: '已扣余额', key: 'paidAmount', width: 12 },
{ header: '待补缴', key: 'outstandingAmount', width: 12 },
{ header: '状态', key: 'status', width: 10 },
{ header: '生成时间', key: 'generatedAt', width: 20 },
];
@@ -70,12 +56,13 @@ export class BillsExportService {
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
const statusMap: Record<string, string> = {
draft: '草稿',
unpaid: '待支付',
partially_paid: '部分支付',
paid: '已结清',
cancelled: '已取消',
};
for (const bill of bills) {
const total = Number(bill.totalAmount || 0);
const dep = Number((depMap.get(bill.studentId) || 0).toFixed(2));
ws.addRow({
id: bill.id,
studentName: (bill as any).student?.name || '-',
@@ -83,8 +70,8 @@ export class BillsExportService {
shared: Number(bill.sharedAmount),
personal: Number(bill.personalAmount),
total,
deposit: dep,
depositDeducted: Number(bill.depositDeductedAmount || 0),
paidAmount: Number(bill.paidAmount || 0),
outstandingAmount: Number(bill.outstandingAmount || 0),
status: statusMap[bill.status] || bill.status,
generatedAt: bill.generatedAt ? new Date(bill.generatedAt).toLocaleString('zh-CN') : '',
});
@@ -142,15 +129,9 @@ export class BillsExportService {
return;
}
// 查询该学生的当前可用押金。草稿账单只展示余额,不预生成抵扣金额。
const deposits = await this.depositRepo
.createQueryBuilder('d')
.where('d.studentId = :sid', { sid: bill.studentId })
.andWhere('d.status = :status', { status: 'paid' })
.getMany();
const availableDeposit = deposits.reduce((s, d) => s + Number(d.amount || 0), 0);
const totalAmount = Number(bill.totalAmount || 0);
const depositDeducted = Number(bill.depositDeductedAmount || 0);
const paidAmount = Number(bill.paidAmount || 0);
const outstandingAmount = Number(bill.outstandingAmount || 0);
const doc = new PDFDocument({ size: 'A4', margin: 50 });
res.setHeader('Content-Type', 'application/pdf');
@@ -185,8 +166,10 @@ export class BillsExportService {
}
const statusMap: Record<string, string> = {
draft: '草稿',
unpaid: '待支付',
partially_paid: '部分支付',
paid: '已结清',
cancelled: '已取消',
};
// 标题
@@ -216,18 +199,8 @@ export class BillsExportService {
.fillColor('#007AFF')
.text(`应付总额: ¥${totalAmount.toFixed(2)}`);
doc.moveDown(0.3);
if (availableDeposit > 0 || depositDeducted > 0) {
doc
.fontSize(11)
.fillColor('#52C41A')
.text(`可用押金: ¥${availableDeposit.toFixed(2)}`);
if (depositDeducted > 0) {
doc
.fontSize(11)
.fillColor('#FA8C16')
.text(`已扣押金: -¥${depositDeducted.toFixed(2)}`);
}
}
doc.fontSize(11).fillColor('#389E0D').text(`已扣余额: ¥${paidAmount.toFixed(2)}`);
doc.fontSize(14).fillColor(outstandingAmount > 0 ? '#FF3B30' : '#389E0D').text(`待补缴: ¥${outstandingAmount.toFixed(2)}`);
doc.moveDown(1);
// 明细表格