feat(task1): restructure directories for turborepo monorepo
- Move backend/ to apps/server/ via git mv - Move frontend/ to apps/admin/ via git mv - Create packages/typescript-config/ with base, nestjs, and react-vite presets
This commit is contained in:
132
apps/server/src/classrooms/classrooms.controller.ts
Normal file
132
apps/server/src/classrooms/classrooms.controller.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request, Res, UseInterceptors, UploadedFile } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import type { Response } from 'express';
|
||||
import { ClassroomsService } from './classrooms.service';
|
||||
import { CreateClassroomDto, UpdateClassroomDto } from './dto/classroom.dto';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
||||
import { extractRequestInfo } from '../common/request-utils';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
import * as ExcelJS from 'exceljs';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('classrooms')
|
||||
export class ClassroomsController {
|
||||
constructor(private service: ClassroomsService, private logService: OperationLogsService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermission('classroom:view')
|
||||
findAll(@Query('building') building?: string, @Query('roomType') roomType?: string, @Query('includeArchived') includeArchived?: string) {
|
||||
return this.service.findAll({
|
||||
building,
|
||||
roomType,
|
||||
includeArchived: includeArchived === 'true',
|
||||
});
|
||||
}
|
||||
|
||||
@Get('template')
|
||||
@RequirePermission('classroom:view')
|
||||
async downloadTemplate(@Res() res: Response) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const ws = workbook.addWorksheet('教室导入模板');
|
||||
ws.columns = [
|
||||
{ header: '教室名', key: 'name', width: 15 },
|
||||
{ header: '楼栋', key: 'building', width: 12 },
|
||||
{ header: '楼层', key: 'floor', width: 8 },
|
||||
{ header: '类型', key: 'roomType', width: 10 },
|
||||
{ header: '容量', key: 'capacity', width: 10 },
|
||||
{ header: '课程类型', key: 'courseType', width: 16 },
|
||||
{ header: '负责人', key: 'supervisor', width: 12 },
|
||||
];
|
||||
ws.getRow(1).font = { bold: true };
|
||||
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
|
||||
ws.addRow({ name: 'A201', building: 'A座', floor: 2, roomType: '大', capacity: 60, courseType: '尊享培优班', supervisor: '张老师' });
|
||||
ws.addRow({ name: 'B301', building: 'B座', floor: 3, roomType: '次大', capacity: 40, courseType: '专业课集训班', supervisor: '李老师' });
|
||||
ws.addRow({ name: 'B405', building: 'B座', floor: 4, roomType: '小', capacity: 20, courseType: '', supervisor: '' });
|
||||
|
||||
// 说明sheet
|
||||
const ws2 = workbook.addWorksheet('使用说明');
|
||||
ws2.columns = [{ header: '说明', key: 'note', width: 80 }];
|
||||
ws2.getRow(1).font = { bold: true };
|
||||
[
|
||||
'1. 教室名必填,建议采用「楼栋+房号」如 A201、B301',
|
||||
'2. 类型可填 大 / 次大 / 小,为空默认「大」',
|
||||
'3. 同名教室会自动跳过(不覆盖)',
|
||||
'4. 课程类型可填尊享培优班、专业课集训班等产品班级',
|
||||
'5. 负责人为班主任/对接人',
|
||||
].forEach((note) => ws2.addRow({ note }));
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=classroom_template.xlsx');
|
||||
await workbook.xlsx.write(res);
|
||||
res.end();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission('classroom:view')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.service.findOne(+id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermission('classroom:create')
|
||||
async create(@Body() dto: CreateClassroomDto, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.create(dto);
|
||||
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室', action: '新增教室', targetId: result.id, targetType: 'classroom', detail: dto.name, ipAddress, userAgent });
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@RequirePermission('classroom:edit')
|
||||
async update(@Param('id') id: string, @Body() dto: UpdateClassroomDto, @Request() req: any) {
|
||||
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', detail: JSON.stringify(dto), ipAddress, userAgent });
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@RequirePermission('classroom:delete')
|
||||
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: 'classroom', ipAddress, userAgent });
|
||||
return result;
|
||||
}
|
||||
|
||||
@Put(':id/restore')
|
||||
@RequirePermission('classroom:edit')
|
||||
async restore(@Param('id') id: string, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const result = await this.service.restore(+id);
|
||||
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室', action: '恢复教室', targetId: +id, targetType: 'classroom', ipAddress, userAgent });
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post('import')
|
||||
@RequirePermission('classroom:create')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async importExcel(@UploadedFile() file: Express.Multer.File, @Request() req: any) {
|
||||
const { ipAddress, userAgent } = extractRequestInfo(req);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file.buffer as any);
|
||||
const ws = workbook.worksheets[0];
|
||||
const rows: any[] = [];
|
||||
ws.eachRow((row, idx) => {
|
||||
if (idx === 1) return;
|
||||
rows.push({
|
||||
name: String(row.getCell(1).value || ''),
|
||||
building: String(row.getCell(2).value || '') || undefined,
|
||||
floor: Number(row.getCell(3).value) || undefined,
|
||||
roomType: String(row.getCell(4).value || '') || undefined,
|
||||
capacity: Number(row.getCell(5).value) || undefined,
|
||||
courseType: String(row.getCell(6).value || '') || undefined,
|
||||
supervisor: String(row.getCell(7).value || '') || undefined,
|
||||
});
|
||||
});
|
||||
const result = await this.service.batchImport(rows);
|
||||
await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '教室', action: '批量导入', detail: result.message, ipAddress, userAgent });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
15
apps/server/src/classrooms/classrooms.module.ts
Normal file
15
apps/server/src/classrooms/classrooms.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Classroom } from '../entities/classroom.entity';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { ClassroomsService } from './classrooms.service';
|
||||
import { ClassroomsController } from './classrooms.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Classroom, ClassroomRental]), OperationLogsModule],
|
||||
controllers: [ClassroomsController],
|
||||
providers: [ClassroomsService],
|
||||
exports: [ClassroomsService],
|
||||
})
|
||||
export class ClassroomsModule {}
|
||||
78
apps/server/src/classrooms/classrooms.service.ts
Normal file
78
apps/server/src/classrooms/classrooms.service.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Not } from 'typeorm';
|
||||
import { Classroom } from '../entities/classroom.entity';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { CreateClassroomDto, UpdateClassroomDto } from './dto/classroom.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ClassroomsService {
|
||||
constructor(
|
||||
@InjectRepository(Classroom) private repo: Repository<Classroom>,
|
||||
@InjectRepository(ClassroomRental) private rentalRepo: Repository<ClassroomRental>,
|
||||
) {}
|
||||
|
||||
async findAll(query?: { building?: string; roomType?: string; includeArchived?: boolean }) {
|
||||
const where: any = {};
|
||||
if (query?.building) where.building = query.building;
|
||||
if (query?.roomType) where.roomType = query.roomType;
|
||||
if (!query?.includeArchived) where.status = Not('archived');
|
||||
return this.repo.find({ where, order: { building: 'ASC', name: 'ASC' } });
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const cls = await this.repo.findOne({ where: { id } });
|
||||
if (!cls) throw new NotFoundException('教室不存在');
|
||||
return cls;
|
||||
}
|
||||
|
||||
async create(dto: CreateClassroomDto) {
|
||||
const exists = await this.repo.findOne({ where: { name: dto.name } });
|
||||
if (exists) throw new BadRequestException(`教室 ${dto.name} 已存在`);
|
||||
return this.repo.save(this.repo.create(dto));
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateClassroomDto) {
|
||||
await this.findOne(id);
|
||||
await this.repo.update(id, dto);
|
||||
return this.repo.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
await this.findOne(id);
|
||||
// 若存在未结束的租赁订单,不允许归档
|
||||
const active = await this.rentalRepo.count({ where: { classroomId: id, status: 'active' } });
|
||||
if (active > 0) throw new BadRequestException('该教室存在进行中的租赁订单,无法归档');
|
||||
await this.repo.update(id, { status: 'archived' });
|
||||
return { message: '已归档' };
|
||||
}
|
||||
|
||||
async restore(id: number) {
|
||||
const cls = await this.findOne(id);
|
||||
if (cls.status !== 'archived') throw new BadRequestException('该教室未被归档');
|
||||
await this.repo.update(id, { status: 'available' });
|
||||
return { message: '已恢复' };
|
||||
}
|
||||
|
||||
async batchImport(rows: { name: string; building?: string; floor?: number; capacity?: number; roomType?: string; courseType?: string; supervisor?: string }[]) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
for (const row of rows) {
|
||||
if (!row.name || !row.name.trim()) { skipped++; continue; }
|
||||
const name = row.name.trim();
|
||||
const exists = await this.repo.findOne({ where: { name } });
|
||||
if (exists) { skipped++; continue; }
|
||||
await this.repo.save(this.repo.create({
|
||||
name,
|
||||
building: row.building?.trim() || undefined,
|
||||
floor: row.floor || undefined,
|
||||
capacity: row.capacity || 30,
|
||||
roomType: row.roomType?.trim() || '大',
|
||||
courseType: row.courseType?.trim() || undefined,
|
||||
supervisor: row.supervisor?.trim() || undefined,
|
||||
}));
|
||||
imported++;
|
||||
}
|
||||
return { message: `成功导入 ${imported} 间教室,跳过 ${skipped} 条(重复或空行)`, imported, skipped };
|
||||
}
|
||||
}
|
||||
73
apps/server/src/classrooms/dto/classroom.dto.ts
Normal file
73
apps/server/src/classrooms/dto/classroom.dto.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { IsOptional, IsString, IsNotEmpty, IsInt, IsEnum } from 'class-validator';
|
||||
|
||||
export class CreateClassroomDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
building?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
floor?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
capacity?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
roomType?: string; // 大 / 次大 / 小
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
courseType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
supervisor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class UpdateClassroomDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
building?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
floor?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
capacity?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
roomType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
courseType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
supervisor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(['available', 'archived'])
|
||||
status?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user