feat: improve attendance scheduling and API validation

This commit is contained in:
2026-07-14 23:12:14 +08:00
parent c75a08affe
commit e45da7f998
33 changed files with 869 additions and 297 deletions

View File

@@ -1,4 +1,6 @@
import { IsIn, IsInt, IsString, IsNumber, IsOptional, Matches, Min } from 'class-validator';
import { IsDateString, IsIn, IsInt, IsString, IsNumber, IsOptional, Matches, Min } from 'class-validator';
import { PartialType } from '@nestjs/mapped-types';
import { Type } from 'class-transformer';
export class CreateRoomExpenseDto {
@IsInt()
@@ -7,13 +9,14 @@ export class CreateRoomExpenseDto {
@IsString()
expenseType: string;
@IsNumber()
@IsNumber({ maxDecimalPlaces: 2 })
@Min(0.01)
amount: number;
@IsString()
@IsDateString()
periodStart: string;
@IsString()
@IsDateString()
periodEnd: string;
@IsOptional()
@@ -32,10 +35,11 @@ export class CreatePersonalExpenseDto {
@IsString()
expenseType: string;
@IsNumber()
@IsNumber({ maxDecimalPlaces: 2 })
@Min(0.01)
amount: number;
@IsString()
@IsDateString()
expenseDate: string;
@IsOptional()
@@ -43,6 +47,33 @@ export class CreatePersonalExpenseDto {
description?: string;
}
export class UpdateRoomExpenseDto extends PartialType(CreateRoomExpenseDto) {}
export class UpdatePersonalExpenseDto extends PartialType(CreatePersonalExpenseDto) {}
export class QueryRoomExpenseDto {
@IsOptional()
@Type(() => Number)
@IsInt()
roomId?: number;
@IsOptional()
@IsDateString()
periodStart?: string;
@IsOptional()
@IsDateString()
periodEnd?: string;
}
export class QueryPersonalExpenseDto {
@IsOptional()
@Type(() => Number)
@IsInt()
studentId?: number;
}
export class BatchRoomExpenseDto {
@IsString()
periodStart: string;

View File

@@ -12,6 +12,7 @@ import {
Res,
UseInterceptors,
UploadedFile,
ParseIntPipe,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
@@ -21,6 +22,10 @@ import {
CreatePersonalExpenseDto,
BatchRoomExpenseDto,
CreateStudentUtilityBillDto,
QueryPersonalExpenseDto,
QueryRoomExpenseDto,
UpdatePersonalExpenseDto,
UpdateRoomExpenseDto,
} from './dto/expense.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { OperationLogsService } from '../operation-logs/operation-logs.service';
@@ -136,29 +141,21 @@ export class ExpensesController {
@Get('room')
@RequirePermission('expense:view')
findRoomExpenses(
@Query('roomId') roomId?: string,
@Query('periodStart') periodStart?: string,
@Query('periodEnd') periodEnd?: string,
) {
return this.service.findRoomExpenses({
roomId: roomId ? +roomId : undefined,
periodStart,
periodEnd,
});
findRoomExpenses(@Query() query: QueryRoomExpenseDto) {
return this.service.findRoomExpenses(query);
}
@Delete('room/:id')
@RequirePermission('expense:delete')
async deleteRoomExpense(@Param('id') id: string, @Request() req: any) {
async deleteRoomExpense(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.deleteRoomExpense(+id);
const result = await this.service.deleteRoomExpense(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '费用管理',
action: '删除费用',
targetId: +id,
targetId: id,
targetType: 'room_expense',
ipAddress,
userAgent,
@@ -186,18 +183,18 @@ export class ExpensesController {
@Put('room/:id')
@RequirePermission('expense:edit')
async updateRoomExpense(
@Param('id') id: string,
@Body() dto: CreateRoomExpenseDto,
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdateRoomExpenseDto,
@Request() req: any,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.updateRoomExpense(+id, dto);
const result = await this.service.updateRoomExpense(id, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '费用管理',
action: '编辑费用',
targetId: +id,
targetId: id,
targetType: 'room_expense',
detail: `¥${dto.amount} ${dto.expenseType}`,
ipAddress,
@@ -225,21 +222,21 @@ export class ExpensesController {
@Get('personal')
@RequirePermission('expense:view')
findPersonalExpenses(@Query('studentId') studentId?: string) {
return this.service.findPersonalExpenses({ studentId: studentId ? +studentId : undefined });
findPersonalExpenses(@Query() query: QueryPersonalExpenseDto) {
return this.service.findPersonalExpenses(query);
}
@Delete('personal/:id')
@RequirePermission('expense:delete')
async deletePersonalExpense(@Param('id') id: string, @Request() req: any) {
async deletePersonalExpense(@Param('id', ParseIntPipe) id: number, @Request() req: any) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.deletePersonalExpense(+id);
const result = await this.service.deletePersonalExpense(id);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '费用管理',
action: '删除费用',
targetId: +id,
targetId: id,
ipAddress,
userAgent,
});
@@ -266,18 +263,18 @@ export class ExpensesController {
@Put('personal/:id')
@RequirePermission('expense:edit')
async updatePersonalExpense(
@Param('id') id: string,
@Body() dto: CreatePersonalExpenseDto,
@Param('id', ParseIntPipe) id: number,
@Body() dto: UpdatePersonalExpenseDto,
@Request() req: any,
) {
const { ipAddress, userAgent } = extractRequestInfo(req);
const result = await this.service.updatePersonalExpense(+id, dto);
const result = await this.service.updatePersonalExpense(id, dto);
await this.logService.log({
userId: req.user?.id,
username: req.user?.username,
module: '费用管理',
action: '编辑费用',
targetId: +id,
targetId: id,
detail: `¥${dto.amount} ${dto.expenseType}`,
ipAddress,
userAgent,