chore: commit oxfmt formatting changes and verify artifacts

This commit is contained in:
2026-07-02 15:55:09 +08:00
parent 34726cc46d
commit 4f4cee157a
77 changed files with 5576 additions and 1400 deletions

View File

@@ -19,8 +19,12 @@ export class BillsExportService {
/**
* 导出账单列表为 Excel
*/
async exportExcel(query: { periodStart?: string; periodEnd?: string; studentId?: number; status?: string }, res: Response) {
const qb = this.billRepo.createQueryBuilder('b')
async exportExcel(
query: { periodStart?: string; periodEnd?: string; studentId?: number; status?: string },
res: Response,
) {
const qb = this.billRepo
.createQueryBuilder('b')
.leftJoinAndSelect('b.student', 'student')
.leftJoinAndSelect('b.items', 'items')
.orderBy('b.generatedAt', 'DESC');
@@ -34,7 +38,8 @@ export class BillsExportService {
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')
const deposits = await this.depositRepo
.createQueryBuilder('d')
.where('d.studentId IN (:...ids)', { ids: studentIds })
.andWhere('d.status = :status', { status: 'paid' })
.getMany();
@@ -65,7 +70,11 @@ export class BillsExportService {
ws.getRow(1).font = { bold: true };
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
const statusMap: Record<string, string> = { draft: '草稿', confirmed: '已确认', paid: '已结清' };
const statusMap: Record<string, string> = {
draft: '草稿',
confirmed: '已确认',
paid: '已结清',
};
for (const bill of bills) {
const total = Number(bill.totalAmount || 0);
const dep = Number((depMap.get(bill.studentId) || 0).toFixed(2));
@@ -116,7 +125,10 @@ export class BillsExportService {
}
}
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
);
res.setHeader('Content-Disposition', `attachment; filename=bills_${Date.now()}.xlsx`);
await workbook.xlsx.write(res);
res.end();
@@ -126,11 +138,18 @@ export class BillsExportService {
* 导出单个学生的 PDF 账单
*/
async exportStudentPdf(billId: number, res: Response) {
const bill = await this.billRepo.findOne({ where: { id: billId }, relations: ['student', 'items'] });
if (!bill) { res.status(404).json({ message: '账单不存在' }); return; }
const bill = await this.billRepo.findOne({
where: { id: billId },
relations: ['student', 'items'],
});
if (!bill) {
res.status(404).json({ message: '账单不存在' });
return;
}
// 查询该学生的可用押金(已缴未退)
const deposits = await this.depositRepo.createQueryBuilder('d')
const deposits = await this.depositRepo
.createQueryBuilder('d')
.where('d.studentId = :sid', { sid: bill.studentId })
.andWhere('d.status = :status', { status: 'paid' })
.getMany();
@@ -146,12 +165,12 @@ export class BillsExportService {
// 注册中文字体(优先使用系统字体,兼容 macOS 和 Linux
const fontPaths = [
'/System/Library/Fonts/PingFang.ttc', // macOS
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', // Linux Noto
'/System/Library/Fonts/PingFang.ttc', // macOS
'/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', // Linux Noto
'/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc',
'/usr/share/fonts/noto-cjk/NotoSansSC-Regular.otf',
'/usr/share/fonts/wqy-microhei/wqy-microhei.ttc', // Linux WenQuanYi
'/usr/share/fonts/wqy-microhei/wqy-microhei.ttc', // Linux WenQuanYi
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
];
let fontRegistered = false;
@@ -171,12 +190,19 @@ export class BillsExportService {
doc.font('Helvetica');
}
const statusMap: Record<string, string> = { draft: '草稿', confirmed: '已确认', paid: '已结清' };
const statusMap: Record<string, string> = {
draft: '草稿',
confirmed: '已确认',
paid: '已结清',
};
// 标题
doc.fontSize(20).text('恭学教育基地水电费账单', { align: 'center' });
doc.moveDown(0.5);
doc.fontSize(10).fillColor('#666').text(`生成时间: ${new Date().toLocaleString('zh-CN')}`, { align: 'center' });
doc
.fontSize(10)
.fillColor('#666')
.text(`生成时间: ${new Date().toLocaleString('zh-CN')}`, { align: 'center' });
doc.moveDown(1);
// 基本信息
@@ -192,12 +218,24 @@ export class BillsExportService {
doc.fontSize(12);
doc.text(`分摊费用: ¥${Number(bill.sharedAmount).toFixed(2)}`);
doc.text(`个人费用: ¥${Number(bill.personalAmount).toFixed(2)}`);
doc.fontSize(14).fillColor('#007AFF').text(`应付总额: ¥${totalAmount.toFixed(2)}`);
doc
.fontSize(14)
.fillColor('#007AFF')
.text(`应付总额: ¥${totalAmount.toFixed(2)}`);
doc.moveDown(0.3);
if (availableDeposit > 0) {
doc.fontSize(11).fillColor('#52C41A').text(`可用押金: ¥${availableDeposit.toFixed(2)}`);
doc.fontSize(11).fillColor('#FA8C16').text(`押金抵扣: -¥${depositApplied.toFixed(2)}`);
doc.fontSize(14).fillColor('#FF3B30').text(`抵扣后应付: ¥${amountAfterDeposit.toFixed(2)}`);
doc
.fontSize(11)
.fillColor('#52C41A')
.text(`可用押金: ¥${availableDeposit.toFixed(2)}`);
doc
.fontSize(11)
.fillColor('#FA8C16')
.text(`押金抵扣: -¥${depositApplied.toFixed(2)}`);
doc
.fontSize(14)
.fillColor('#FF3B30')
.text(`抵扣后应付: ¥${amountAfterDeposit.toFixed(2)}`);
}
doc.moveDown(1);
@@ -226,16 +264,23 @@ export class BillsExportService {
const y = doc.y;
x = 50;
doc.fontSize(9).fillColor('#000');
doc.text(item.expenseType || '', x, y, { width: colWidths[0] }); x += colWidths[0];
doc.text(item.description || '', x, y, { width: colWidths[1] }); x += colWidths[1];
doc.text(String(item.days || 0), x, y, { width: colWidths[2] }); x += colWidths[2];
doc.text(String(item.totalRoomDays || 0), x, y, { width: colWidths[3] }); x += colWidths[3];
doc.text(item.expenseType || '', x, y, { width: colWidths[0] });
x += colWidths[0];
doc.text(item.description || '', x, y, { width: colWidths[1] });
x += colWidths[1];
doc.text(String(item.days || 0), x, y, { width: colWidths[2] });
x += colWidths[2];
doc.text(String(item.totalRoomDays || 0), x, y, { width: colWidths[3] });
x += colWidths[3];
doc.text(Number(item.studentAmount).toFixed(2), x, y, { width: colWidths[4] });
doc.moveDown(0.8);
}
doc.moveDown(2);
doc.fontSize(8).fillColor('#999').text('本账单由恭学教育基地管理系统自动生成', { align: 'center' });
doc
.fontSize(8)
.fillColor('#999')
.text('本账单由恭学教育基地管理系统自动生成', { align: 'center' });
doc.end();
}

View File

@@ -1,4 +1,17 @@
import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards, Request, Res, Req } from '@nestjs/common';
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
Query,
UseGuards,
Request,
Res,
Req,
} from '@nestjs/common';
import { BillsService } from './bills.service';
import { BillsExportService } from './bills-export.service';
import { GenerateBillsDto, UpdateBillStatusDto } from './dto/bill.dto';
@@ -11,14 +24,26 @@ import type { Response } from 'express';
@UseGuards(JwtAuthGuard)
@Controller('bills')
export class BillsController {
constructor(private service: BillsService, private exportService: BillsExportService, private logService: OperationLogsService) {}
constructor(
private service: BillsService,
private exportService: BillsExportService,
private logService: OperationLogsService,
) {}
@Post('generate')
@RequirePermission('bill:generate')
async generateBills(@Body() dto: GenerateBillsDto, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.generateBills(dto);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单', action: '生成账单', detail: `周期 ${dto.periodStart}~${dto.periodEnd}, 生成 ${result.count}`, ipAddress, userAgent });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '账单',
action: '生成账单',
detail: `周期 ${dto.periodStart}~${dto.periodEnd}, 生成 ${result.count}`,
ipAddress,
userAgent,
});
return result;
}
@@ -31,7 +56,8 @@ export class BillsController {
@Query('status') status?: string,
) {
return this.service.findAll({
periodStart, periodEnd,
periodStart,
periodEnd,
studentId: studentId ? +studentId : undefined,
status,
});
@@ -45,10 +71,23 @@ export class BillsController {
@Put(':id/status')
@RequirePermission('bill:confirm')
async updateStatus(@Param('id') id: string, @Body() dto: UpdateBillStatusDto, @Request() req: any) {
async updateStatus(
@Param('id') id: string,
@Body() dto: UpdateBillStatusDto,
@Request() req: any,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.updateStatus(+id, dto);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单', action: `状态变更为${dto.status}`, targetId: +id, targetType: 'bill', ipAddress, userAgent });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '账单',
action: `状态变更为${dto.status}`,
targetId: +id,
targetType: 'bill',
ipAddress,
userAgent,
});
return result;
}
@@ -57,7 +96,15 @@ export class BillsController {
async batchUpdateStatus(@Body() body: { ids: number[]; status: string }, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.batchUpdateStatus(body.ids, body.status);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单', action: `批量状态变更为${body.status}`, detail: `IDs: ${body.ids.join(',')}`, ipAddress, userAgent });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '账单',
action: `批量状态变更为${body.status}`,
detail: `IDs: ${body.ids.join(',')}`,
ipAddress,
userAgent,
});
return result;
}
@@ -66,7 +113,16 @@ export class BillsController {
async remove(@Param('id') id: string, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.remove(+id);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单', action: '删除账单', targetId: +id, targetType: 'bill', ipAddress, userAgent });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '账单',
action: '删除账单',
targetId: +id,
targetType: 'bill',
ipAddress,
userAgent,
});
return result;
}
@@ -75,7 +131,15 @@ export class BillsController {
async batchRemove(@Body() body: { ids: number[] }, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.batchRemove(body.ids);
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单', action: '批量删除账单', detail: `IDs: ${body.ids.join(',')}`, ipAddress, userAgent });
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '账单',
action: '批量删除账单',
detail: `IDs: ${body.ids.join(',')}`,
ipAddress,
userAgent,
});
return result;
}
@@ -90,19 +154,40 @@ export class BillsController {
@Req() req?: any,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
await this.logService.log({ userId: req?.user?.id, username: req?.user?.username, module: '账单', action: '导出Excel', detail: `筛选: 周期${periodStart || '全部'}~${periodEnd || '全部'}, 状态${status || '全部'}`, ipAddress, userAgent });
return this.exportService.exportExcel({
periodStart, periodEnd,
studentId: studentId ? +studentId : undefined,
status,
}, res!);
await this.logService.log({
userId: req?.user?.id,
username: req?.user?.username,
module: '账单',
action: '导出Excel',
detail: `筛选: 周期${periodStart || '全部'}~${periodEnd || '全部'}, 状态${status || '全部'}`,
ipAddress,
userAgent,
});
return this.exportService.exportExcel(
{
periodStart,
periodEnd,
studentId: studentId ? +studentId : undefined,
status,
},
res!,
);
}
@Get('export/pdf/:id')
@RequirePermission('bill:export-pdf')
async exportPdf(@Param('id') id: string, @Res() res: Response, @Req() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
await this.logService.log({ userId: req?.user?.id, username: req?.user?.username, module: '账单', action: '导出PDF', targetId: +id, targetType: 'bill', ipAddress, userAgent });
await this.logService.log({
userId: req?.user?.id,
username: req?.user?.username,
module: '账单',
action: '导出PDF',
targetId: +id,
targetType: 'bill',
ipAddress,
userAgent,
});
return this.exportService.exportStudentPdf(+id, res);
}
}

View File

@@ -12,7 +12,17 @@ import { BillsExportService } from './bills-export.service';
import { BillsController } from './bills.controller';
@Module({
imports: [TypeOrmModule.forFeature([Bill, BillItem, RoomExpense, PersonalExpense, Occupancy, Room, Deposit])],
imports: [
TypeOrmModule.forFeature([
Bill,
BillItem,
RoomExpense,
PersonalExpense,
Occupancy,
Room,
Deposit,
]),
],
controllers: [BillsController],
providers: [BillsService, BillsExportService],
exports: [BillsService],

View File

@@ -37,13 +37,25 @@ export class BillsService {
});
if (existingDrafts.length > 0) {
const draftIds = existingDrafts.map((b) => b.id);
await this.itemRepo.createQueryBuilder().delete().where('billId IN (:...ids)', { ids: draftIds }).execute();
await this.billRepo.createQueryBuilder().delete().where('id IN (:...ids)', { ids: draftIds }).execute();
await this.itemRepo
.createQueryBuilder()
.delete()
.where('billId IN (:...ids)', { ids: draftIds })
.execute();
await this.billRepo
.createQueryBuilder()
.delete()
.where('id IN (:...ids)', { ids: draftIds })
.execute();
}
// 获取所有有费用的宿舍
const roomExpenses = await this.roomExpRepo.createQueryBuilder('e')
.where('e.periodStart = :periodStart AND e.periodEnd = :periodEnd', { periodStart, periodEnd })
const roomExpenses = await this.roomExpRepo
.createQueryBuilder('e')
.where('e.periodStart = :periodStart AND e.periodEnd = :periodEnd', {
periodStart,
periodEnd,
})
.getMany();
// 按宿舍分组费用
@@ -58,7 +70,8 @@ export class BillsService {
for (const [roomId, expenses] of roomExpMap) {
// 获取该宿舍在此周期内的所有入住记录
const occupancies = await this.occRepo.createQueryBuilder('o')
const occupancies = await this.occRepo
.createQueryBuilder('o')
.leftJoinAndSelect('o.student', 'student')
.where('o.roomId = :roomId', { roomId })
.andWhere('o.billingStartDate <= :periodEnd', { periodEnd })
@@ -72,11 +85,16 @@ export class BillsService {
let totalDays = 0;
for (const occ of occupancies) {
const start = new Date(Math.max(new Date(occ.billingStartDate).getTime(), pStart.getTime()));
const start = new Date(
Math.max(new Date(occ.billingStartDate).getTime(), pStart.getTime()),
);
const end = occ.billingEndDate
? new Date(Math.min(new Date(occ.billingEndDate).getTime(), pEnd.getTime()))
: pEnd;
const days = Math.max(0, Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1);
const days = Math.max(
0,
Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1,
);
studentDays.push({ studentId: occ.studentId, days });
totalDays += days;
}
@@ -107,8 +125,12 @@ export class BillsService {
}
// 获取个人附加费
const personalExps = await this.personalExpRepo.createQueryBuilder('pe')
.where('pe.expenseDate >= :periodStart AND pe.expenseDate <= :periodEnd', { periodStart, periodEnd })
const personalExps = await this.personalExpRepo
.createQueryBuilder('pe')
.where('pe.expenseDate >= :periodStart AND pe.expenseDate <= :periodEnd', {
periodStart,
periodEnd,
})
.getMany();
const personalMap = new Map<number, number>();
@@ -162,8 +184,14 @@ export class BillsService {
return { message: `成功生成 ${bills.length} 条账单`, count: bills.length, bills };
}
async findAll(query?: { periodStart?: string; periodEnd?: string; studentId?: number; status?: string }) {
const qb = this.billRepo.createQueryBuilder('b')
async findAll(query?: {
periodStart?: string;
periodEnd?: string;
studentId?: number;
status?: string;
}) {
const qb = this.billRepo
.createQueryBuilder('b')
.leftJoinAndSelect('b.student', 'student')
.orderBy('b.generatedAt', 'DESC');
if (query?.periodStart) qb.andWhere('b.periodStart = :ps', { ps: query.periodStart });
@@ -191,7 +219,8 @@ export class BillsService {
if (!bills || bills.length === 0) return bills;
const studentIds = Array.from(new Set(bills.map((b) => b.studentId)));
if (studentIds.length === 0) return bills;
const deposits = await this.depositRepo.createQueryBuilder('d')
const deposits = await this.depositRepo
.createQueryBuilder('d')
.where('d.studentId IN (:...ids)', { ids: studentIds })
.andWhere('d.status = :status', { status: 'paid' })
.getMany();
@@ -220,7 +249,12 @@ export class BillsService {
}
async batchUpdateStatus(ids: number[], status: string) {
await this.billRepo.createQueryBuilder().update().set({ status }).where('id IN (:...ids)', { ids }).execute();
await this.billRepo
.createQueryBuilder()
.update()
.set({ status })
.where('id IN (:...ids)', { ids })
.execute();
return { message: `成功更新 ${ids.length} 条账单状态` };
}
@@ -233,7 +267,11 @@ export class BillsService {
}
async batchRemove(ids: number[]) {
await this.itemRepo.createQueryBuilder().delete().where('billId IN (:...ids)', { ids }).execute();
await this.itemRepo
.createQueryBuilder()
.delete()
.where('billId IN (:...ids)', { ids })
.execute();
await this.billRepo.createQueryBuilder().delete().where('id IN (:...ids)', { ids }).execute();
return { message: `成功删除 ${ids.length} 条账单` };
}