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

@@ -1,4 +1,19 @@
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request, Res, UseInterceptors, UploadedFile, BadRequestException } from '@nestjs/common';
import {
Controller,
Get,
Post,
Put,
Delete,
Body,
Param,
Query,
UseGuards,
Request,
Res,
UseInterceptors,
UploadedFile,
BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
import * as fs from 'fs';
@@ -12,7 +27,10 @@ import { RequirePermission } from '../auth/decorators/permission.decorator';
@UseGuards(JwtAuthGuard)
@Controller('classroom-rentals')
export class ClassroomRentalsController {
constructor(private service: ClassroomRentalsService, private logService: OperationLogsService) {}
constructor(
private service: ClassroomRentalsService,
private logService: OperationLogsService,
) {}
@Get()
@RequirePermission('rental:view')
@@ -52,10 +70,15 @@ export class ClassroomRentalsController {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.create(dto, req.user?.id);
await this.logService.log({
userId: req.user?.id, username: req.user?.username,
module: '教室租赁', action: '新增租赁', targetId: result.id, targetType: 'classroom-rental',
userId: req.user?.id,
username: req.user?.username,
module: '教室租赁',
action: '新增租赁',
targetId: result.id,
targetType: 'classroom-rental',
detail: `教室${dto.classroomId} 租赁方${dto.tenantId} ${dto.startDate}~${dto.endDate}`,
ipAddress, userAgent,
ipAddress,
userAgent,
});
return result;
}
@@ -66,9 +89,15 @@ export class ClassroomRentalsController {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.update(+id, dto);
await this.logService.log({
userId: req.user?.id, username: req.user?.username,
module: '教室租赁', action: '编辑租赁', targetId: +id, targetType: 'classroom-rental',
detail: JSON.stringify(dto), ipAddress, userAgent,
userId: req.user?.id,
username: req.user?.username,
module: '教室租赁',
action: '编辑租赁',
targetId: +id,
targetType: 'classroom-rental',
detail: JSON.stringify(dto),
ipAddress,
userAgent,
});
return result;
}
@@ -79,9 +108,14 @@ export class ClassroomRentalsController {
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: 'classroom-rental',
ipAddress, userAgent,
userId: req.user?.id,
username: req.user?.username,
module: '教室租赁',
action: '删除租赁',
targetId: +id,
targetType: 'classroom-rental',
ipAddress,
userAgent,
});
return result;
}
@@ -89,23 +123,35 @@ export class ClassroomRentalsController {
// 合同上传multer 限制 10MB + 仅 PDF
@Post(':id/contract')
@RequirePermission('rental:edit')
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: 10 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
if (file.mimetype !== 'application/pdf') {
return cb(new BadRequestException('仅支持 PDF 文件'), false);
}
cb(null, true);
},
}))
async uploadContract(@Param('id') id: string, @UploadedFile() file: Express.Multer.File, @Request() req: any) {
@UseInterceptors(
FileInterceptor('file', {
limits: { fileSize: 10 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
if (file.mimetype !== 'application/pdf') {
return cb(new BadRequestException('仅支持 PDF 文件'), false);
}
cb(null, true);
},
}),
)
async uploadContract(
@Param('id') id: string,
@UploadedFile() file: Express.Multer.File,
@Request() req: any,
) {
if (!file) throw new BadRequestException('请上传合同文件');
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.attachContract(+id, file);
await this.logService.log({
userId: req.user?.id, username: req.user?.username,
module: '教室租赁', action: '上传合同', targetId: +id, targetType: 'classroom-rental',
detail: file.originalname, ipAddress, userAgent,
userId: req.user?.id,
username: req.user?.username,
module: '教室租赁',
action: '上传合同',
targetId: +id,
targetType: 'classroom-rental',
detail: file.originalname,
ipAddress,
userAgent,
});
return result;
}
@@ -115,7 +161,10 @@ export class ClassroomRentalsController {
async downloadContract(@Param('id') id: string, @Res() res: Response) {
const { fullPath, originalName } = await this.service.getContractPath(+id);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(originalName)}"`);
res.setHeader(
'Content-Disposition',
`attachment; filename="${encodeURIComponent(originalName)}"`,
);
const stream = fs.createReadStream(fullPath);
stream.pipe(res);
}
@@ -126,9 +175,14 @@ export class ClassroomRentalsController {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.removeContract(+id);
await this.logService.log({
userId: req.user?.id, username: req.user?.username,
module: '教室租赁', action: '删除合同', targetId: +id, targetType: 'classroom-rental',
ipAddress, userAgent,
userId: req.user?.id,
username: req.user?.username,
module: '教室租赁',
action: '删除合同',
targetId: +id,
targetType: 'classroom-rental',
ipAddress,
userAgent,
});
return result;
}

View File

@@ -1,4 +1,9 @@
import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common';
import {
Injectable,
NotFoundException,
BadRequestException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Not } from 'typeorm';
import { ClassroomRental } from '../entities/classroom-rental.entity';
@@ -10,8 +15,16 @@ import * as fs from 'fs';
// 预设色板(与 tenants.service 保持一致,作为颜色兜底)
const COLOR_PALETTE = [
'#ff7875', '#ffa940', '#ffc53d', '#73d13d', '#36cfc9',
'#40a9ff', '#597ef7', '#9254de', '#f759ab', '#8c8c8c',
'#ff7875',
'#ffa940',
'#ffc53d',
'#73d13d',
'#36cfc9',
'#40a9ff',
'#597ef7',
'#9254de',
'#f759ab',
'#8c8c8c',
];
@Injectable()
@@ -33,8 +46,14 @@ export class ClassroomRentalsService {
}
}
async findAll(query?: { classroomId?: number; tenantId?: number; month?: string; includeEnded?: boolean }) {
const qb = this.repo.createQueryBuilder('r')
async findAll(query?: {
classroomId?: number;
tenantId?: number;
month?: string;
includeEnded?: boolean;
}) {
const qb = this.repo
.createQueryBuilder('r')
.leftJoinAndSelect('r.classroom', 'classroom')
.leftJoinAndSelect('r.tenant', 'tenant')
.orderBy('r.startDate', 'DESC');
@@ -63,7 +82,8 @@ export class ClassroomRentalsService {
* 重叠判定start1 <= end2 AND start2 <= end1
*/
async findConflicts(classroomId: number, startDate: string, endDate: string, excludeId?: number) {
const qb = this.repo.createQueryBuilder('r')
const qb = this.repo
.createQueryBuilder('r')
.leftJoinAndSelect('r.tenant', 'tenant')
.where('r.classroomId = :cid', { cid: classroomId })
.andWhere('r.status != :cancelled', { cancelled: 'cancelled' })
@@ -84,7 +104,12 @@ export class ClassroomRentalsService {
if (conflicts.length > 0) {
throw new ConflictException({
message: '该教室在此时间段已有租赁',
conflicts: conflicts.map(c => ({ id: c.id, startDate: c.startDate, endDate: c.endDate, tenantName: c.tenant?.name })),
conflicts: conflicts.map((c) => ({
id: c.id,
startDate: c.startDate,
endDate: c.endDate,
tenantName: c.tenant?.name,
})),
});
}
return this.repo.save(this.repo.create({ ...dto, createdBy: userId, status: 'active' }));
@@ -102,7 +127,12 @@ export class ClassroomRentalsService {
if (conflicts.length > 0) {
throw new ConflictException({
message: '修改后时间段与已有租赁冲突',
conflicts: conflicts.map(c => ({ id: c.id, startDate: c.startDate, endDate: c.endDate, tenantName: c.tenant?.name })),
conflicts: conflicts.map((c) => ({
id: c.id,
startDate: c.startDate,
endDate: c.endDate,
tenantName: c.tenant?.name,
})),
});
}
}
@@ -116,7 +146,11 @@ export class ClassroomRentalsService {
if (rental.contractPath) {
const full = path.join(this.uploadDir, rental.contractPath);
if (fs.existsSync(full)) {
try { fs.unlinkSync(full); } catch { /* ignore */ }
try {
fs.unlinkSync(full);
} catch {
/* ignore */
}
}
}
await this.repo.delete(id);
@@ -133,7 +167,9 @@ export class ClassroomRentalsService {
const ext = path.extname(file.originalname).toLowerCase();
if (ext !== '.pdf') throw new BadRequestException('文件扩展名必须为 .pdf');
// UUID 文件名
const uuid = (globalThis as any).crypto?.randomUUID?.() || require('crypto').randomBytes(16).toString('hex');
const uuid =
(globalThis as any).crypto?.randomUUID?.() ||
require('crypto').randomBytes(16).toString('hex');
const filename = `${uuid}.pdf`;
const fullPath = path.join(this.uploadDir, filename);
// 路径遍历防护
@@ -142,7 +178,11 @@ export class ClassroomRentalsService {
if (rental.contractPath) {
const oldPath = path.join(this.uploadDir, rental.contractPath);
if (fs.existsSync(oldPath)) {
try { fs.unlinkSync(oldPath); } catch { /* ignore */ }
try {
fs.unlinkSync(oldPath);
} catch {
/* ignore */
}
}
}
fs.writeFileSync(fullPath, file.buffer);
@@ -158,7 +198,11 @@ export class ClassroomRentalsService {
if (!rental.contractPath) throw new BadRequestException('该租赁未上传合同');
const fullPath = path.join(this.uploadDir, rental.contractPath);
if (fs.existsSync(fullPath)) {
try { fs.unlinkSync(fullPath); } catch { /* ignore */ }
try {
fs.unlinkSync(fullPath);
} catch {
/* ignore */
}
}
await this.repo.update(id, { contractPath: null as any, contractOriginalName: null as any });
return { message: '合同已删除' };
@@ -188,7 +232,8 @@ export class ClassroomRentalsService {
where: { status: Not('archived') },
order: { building: 'ASC', name: 'ASC' },
});
const rentals = await this.repo.createQueryBuilder('r')
const rentals = await this.repo
.createQueryBuilder('r')
.leftJoinAndSelect('r.tenant', 'tenant')
.leftJoinAndSelect('r.classroom', 'classroom')
.where('r.status != :cancelled', { cancelled: 'cancelled' })
@@ -197,7 +242,10 @@ export class ClassroomRentalsService {
const tenantMap = new Map<number, any>();
const matrix: Record<number, Record<number, any>> = {};
const summary: Record<number, { totalDays: number; rentedDays: number; idleDays: number; occupancyRate: number }> = {};
const summary: Record<
number,
{ totalDays: number; rentedDays: number; idleDays: number; occupancyRate: number }
> = {};
for (const cls of classrooms) {
matrix[cls.id] = {};
@@ -225,7 +273,8 @@ export class ClassroomRentalsService {
rentalId: rental.id,
tenantId: rental.tenantId,
tenantName: rental.tenant?.name || '未知',
color: rental.tenant?.color || COLOR_PALETTE[(rental.tenantId || 0) % COLOR_PALETTE.length],
color:
rental.tenant?.color || COLOR_PALETTE[(rental.tenantId || 0) % COLOR_PALETTE.length],
hasContract: !!rental.contractPath,
};
}
@@ -243,7 +292,15 @@ export class ClassroomRentalsService {
year,
month,
days: lastDay,
classrooms: classrooms.map(c => ({ id: c.id, name: c.name, building: c.building, floor: c.floor, roomType: c.roomType, capacity: c.capacity, supervisor: c.supervisor })),
classrooms: classrooms.map((c) => ({
id: c.id,
name: c.name,
building: c.building,
floor: c.floor,
roomType: c.roomType,
capacity: c.capacity,
supervisor: c.supervisor,
})),
tenants: Array.from(tenantMap.values()),
matrix,
summary,

View File

@@ -1,4 +1,12 @@
import { IsOptional, IsString, IsNotEmpty, IsInt, IsNumber, IsEnum, IsDateString } from 'class-validator';
import {
IsOptional,
IsString,
IsNotEmpty,
IsInt,
IsNumber,
IsEnum,
IsDateString,
} from 'class-validator';
export class CreateRentalDto {
@IsInt()