From 6029d8e2fdda16b7e999a0f9f1cfb9bc441dd4fa Mon Sep 17 00:00:00 2001 From: wangziqi Date: Thu, 9 Jul 2026 17:51:32 +0800 Subject: [PATCH] refactor(server): remove Department/UserDepartment entities, CampusScope, and departmentId from all entities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete department.entity.ts, user-department.entity.ts - Remove Department/UserDepartment from entities/index.ts - Remove departmentId column from 18 entities (AttendanceRecord, ArchiveAttachment, Bill, ClassSchedule, Classroom, ClassroomRental, Deposit, DepositInstallment, ExamScore, LearningRecord, Occupancy, PersonalExpense, ResultArchive, Room, RoomExpense, Student, StudentEnrollment, StudentProfile, StudentReport) - Remove departments/ module entirely - Delete campus-scope.ts, campus-scope.middleware.ts (request-utils.ts kept — it's just IP extraction) - Simplify common.module.ts to empty module - Remove CampusScopeMiddleware from app.module.ts - Remove all CampusScope injections and filter calls across all services - Remove departmentId from all DTOs and controllers - Simplify dingtalk/wecom sync to only sync users (no dept table) - Update seed module to remove department seeding - Clean frontend compilation --- .claude/CLAUDE.md | 10 - .claude/settings.json | 19 -- .gitignore | 3 + .omp/APPEND_SYSTEM.md | 47 --- .superpowers/sdd/task-7-report.md | 14 - apps/server/src/app.module.ts | 15 +- apps/server/src/archive/archive.service.ts | 27 +- .../src/attendance/attendance.service.ts | 6 - apps/server/src/bills/bills.service.spec.ts | 2 - apps/server/src/bills/bills.service.ts | 15 +- apps/server/src/classes/classes.module.ts | 4 +- apps/server/src/classes/classes.service.ts | 1 - .../classroom-rentals.service.spec.ts | 3 - .../classroom-rentals.service.ts | 8 +- .../src/classrooms/classrooms.controller.ts | 3 +- .../src/classrooms/classrooms.service.ts | 11 +- .../src/classrooms/dto/classroom.dto.ts | 3 - .../src/common/campus-scope.middleware.ts | 13 - apps/server/src/common/campus-scope.ts | 77 ----- apps/server/src/common/common.module.ts | 9 +- .../server/src/dashboard/dashboard.service.ts | 45 +-- .../src/departments/departments.controller.ts | 95 ------ .../src/departments/departments.module.ts | 14 - .../src/departments/departments.service.ts | 191 ------------ .../src/departments/dto/department.dto.ts | 37 --- apps/server/src/departments/seed.ts | 23 -- apps/server/src/deposits/deposits.service.ts | 14 +- .../src/entities/archive-attachment.entity.ts | 7 - .../src/entities/attendance-record.entity.ts | 7 - apps/server/src/entities/bill.entity.ts | 7 - .../src/entities/class-schedule.entity.ts | 7 - .../src/entities/classroom-rental.entity.ts | 7 - apps/server/src/entities/classroom.entity.ts | 7 - apps/server/src/entities/department.entity.ts | 58 ---- .../entities/deposit-installment.entity.ts | 7 - apps/server/src/entities/deposit.entity.ts | 7 - apps/server/src/entities/exam-score.entity.ts | 7 - apps/server/src/entities/index.ts | 2 - .../src/entities/learning-record.entity.ts | 7 - apps/server/src/entities/occupancy.entity.ts | 7 - .../src/entities/personal-expense.entity.ts | 7 - .../src/entities/result-archive.entity.ts | 7 - .../src/entities/room-expense.entity.ts | 7 - apps/server/src/entities/room.entity.ts | 7 - .../src/entities/student-enrollment.entity.ts | 7 - .../src/entities/student-profile.entity.ts | 7 - .../src/entities/student-report.entity.ts | 7 - apps/server/src/entities/student.entity.ts | 7 - .../src/entities/user-department.entity.ts | 38 --- apps/server/src/expenses/expenses.service.ts | 14 +- .../src/integration/dingtalk.service.ts | 283 ++---------------- .../src/integration/integration.module.ts | 4 +- apps/server/src/integration/wecom.service.ts | 42 +-- apps/server/src/main.ts | 4 - .../src/occupancies/occupancies.service.ts | 8 +- apps/server/src/rooms/dto/room.dto.ts | 3 - apps/server/src/rooms/rooms.controller.ts | 3 +- apps/server/src/rooms/rooms.service.ts | 23 +- .../src/schedules/schedules.service.spec.ts | 9 - .../server/src/schedules/schedules.service.ts | 14 +- apps/server/src/seed/seed-dev.service.ts | 202 +++++-------- apps/server/src/seed/seed.module.ts | 4 +- apps/server/src/students/dto/student.dto.ts | 2 - .../src/students/students.controller.ts | 3 +- apps/server/src/students/students.service.ts | 12 +- apps/server/src/sync/schedule-sync.service.ts | 160 +++++----- apps/server/src/sync/sync.module.ts | 4 - apps/server/src/sync/sync.service.ts | 2 +- 68 files changed, 220 insertions(+), 1516 deletions(-) delete mode 100644 .claude/CLAUDE.md delete mode 100644 .claude/settings.json delete mode 100644 .omp/APPEND_SYSTEM.md delete mode 100644 .superpowers/sdd/task-7-report.md delete mode 100644 apps/server/src/common/campus-scope.middleware.ts delete mode 100644 apps/server/src/common/campus-scope.ts delete mode 100644 apps/server/src/departments/departments.controller.ts delete mode 100644 apps/server/src/departments/departments.module.ts delete mode 100644 apps/server/src/departments/departments.service.ts delete mode 100644 apps/server/src/departments/dto/department.dto.ts delete mode 100644 apps/server/src/departments/seed.ts delete mode 100644 apps/server/src/entities/department.entity.ts delete mode 100644 apps/server/src/entities/user-department.entity.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md deleted file mode 100644 index f3fca9a..0000000 --- a/.claude/CLAUDE.md +++ /dev/null @@ -1,10 +0,0 @@ - -## CodeGraph - -In repositories indexed by CodeGraph (a `.codegraph/` directory exists at the repo root), reach for it BEFORE grep/find or reading files when you need to understand or locate code: - -- **MCP tool** (when available): `codegraph_explore` answers most code questions in one call — the relevant symbols' verbatim source plus the call paths between them, including dynamic-dispatch hops grep can't follow. Name a file or symbol in the query to read its current line-numbered source. If it's listed but deferred, load it by name via tool search. -- **Shell** (always works): `codegraph explore ""` prints the same output. - -If there is no `.codegraph/` directory, skip CodeGraph entirely — indexing is the user's decision. - diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 1e9033f..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "permissions": { - "allow": [ - "mcp__codegraph__*" - ] - }, - "hooks": { - "UserPromptSubmit": [ - { - "hooks": [ - { - "type": "command", - "command": "codegraph prompt-hook" - } - ] - } - ] - } -} diff --git a/.gitignore b/.gitignore index ef76e4a..6339c7d 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ apps/admin/node_modules/ apps/admin/dist/ .codegraph/ .worktrees/ +.claude/ +.omp/ +.superpowers/ diff --git a/.omp/APPEND_SYSTEM.md b/.omp/APPEND_SYSTEM.md deleted file mode 100644 index a978455..0000000 --- a/.omp/APPEND_SYSTEM.md +++ /dev/null @@ -1,47 +0,0 @@ -# 恭学教育学生管理系统 — 项目约束 - -完整 PRD 文档:`../../Downloads/PRD-恭学教育学生管理系统.md`(相对项目根目录) - -## 技术栈 - -| 层级 | 技术 | -|------|------| -| 架构 | Monorepo (Turborepo) | -| 前端 | React 19 + Vite + Ant Design 6 + ECharts | -| 后端 | NestJS 11 + TypeORM 0.3 + JWT + Passport | -| 数据库 | 开发 SQLite / 生产 MySQL 8 | -| 部署 | Docker Compose (MySQL + NestJS + Nginx) | - -## 核心用户角色 - -- 超级管理员:系统配置、账号管理、全部数据 -- 教职工(宿管+财务):宿舍管理、费用录入、账单生成、指定部门 -- 班主任:本班学生信息、档案、费用、考勤 -- 学生:仅本人数据 - -## 业务域 - -- 学员管理域:学生管理、学生档案、考勤管理 -- 教务管理域:班级管理(新)、排课管理(新)、教师管理、教室管理 -- 住宿管理域:宿舍管理、入住管理 -- 财务运营域:费用管理、账单管理、押金管理、租赁方管理、数据面板 - -## 横切关注点 - -- RBAC 权限控制 -- 操作日志(所有关键写操作) -- 钉钉/企业微信集成 -- 报表导出、批量导入 - -## 上线优先级 - -**P0(阻塞上线)**:班级管理、排课管理、宿舍管理增强、账单管理(长租)、操作日志 -**P1(上线前完成)**:考勤管理前端、数据面板、RBAC 扩展、教室管理增强、organization→tenant_id -**P2(可迭代)**:报表导出、教师档案增强、多班型对比、押金分期、第三方集成 - -## 编码规范 - -- 所有涉及大量数据的列表/表单页面必须有详细的筛选方案 -- 敏感信息(手机号、身份证)需脱敏展示,查看时二次确认+记录日志 -- 遵循现有 NestJS 模块结构:每个业务模块独立目录,含 entity/dto/service/controller -- 前端页面放在 `apps/admin/src/pages/` 下,每个模块独立目录 diff --git a/.superpowers/sdd/task-7-report.md b/.superpowers/sdd/task-7-report.md deleted file mode 100644 index 855023d..0000000 --- a/.superpowers/sdd/task-7-report.md +++ /dev/null @@ -1,14 +0,0 @@ -## Task 7 — Cleanup test files - -### Sync service spec -- `apps/server/src/sync/sync.service.spec.ts` was already deleted in commit `7b86918` (Task 4: remove importDingTalkUsers). No action needed. - -### Attendance service spec -- `apps/server/src/attendance/attendance.service.spec.ts` already uses `StudentDingMapping` (lines 12, 42). No `UserDingMapping` references remain. Updated in commit `ef1b46b`. - -### Verification -- `npx tsc --noEmit` in apps/server: 277 diagnostics, all pre-existing jest type errors (`@types/jest` not installed). Zero errors related to `UserDingMapping` or `StudentDingMapping`. -- No files to commit — both targets are already clean. - -### Conclusion -Task complete with zero changes needed. Previous tasks (Task 4 entity rename, Task 2 import swap) already handled these test updates. diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index a89c7b7..2f51d35 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -1,4 +1,4 @@ -import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { EventEmitterModule } from '@nestjs/event-emitter'; @@ -32,8 +32,6 @@ import { SyncLog, SyncState, Notification, - Department, - UserDepartment, StudentProfile, StudentEnrollment, ExamScore, @@ -63,7 +61,6 @@ import { SchedulesModule } from './schedules/schedules.module'; import { ClassroomRentalsModule } from './classroom-rentals/classroom-rentals.module'; import { SyncModule } from './sync/sync.module'; import { NotificationsModule } from './notifications/notifications.module'; -import { DepartmentsModule } from './departments/departments.module'; import { CommonModule } from './common/common.module'; import { ArchiveModule } from './archive/archive.module'; import { ExpenseTypesModule } from './expense-types/expense-types.module'; @@ -71,7 +68,6 @@ import { ExpenseTypesModule } from './expense-types/expense-types.module'; import { IntegrationConfig, IntegrationConfigDetail } from './integration/entities/integration-config.entity'; import { IntegrationConfigModule } from './integration/config/config.module'; -import { CampusScopeMiddleware } from './common/campus-scope.middleware'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), @@ -113,8 +109,6 @@ import { CampusScopeMiddleware } from './common/campus-scope.middleware'; AttendanceRecord, DingAttendanceRaw, Notification, - Department, - UserDepartment, StudentProfile, StudentEnrollment, ExamScore, @@ -167,7 +161,6 @@ import { CampusScopeMiddleware } from './common/campus-scope.middleware'; ClassroomRentalsModule, SyncModule, NotificationsModule, - DepartmentsModule, CommonModule, ArchiveModule, IntegrationConfigModule, @@ -179,8 +172,4 @@ import { CampusScopeMiddleware } from './common/campus-scope.middleware'; { provide: APP_GUARD, useClass: PermissionGuard }, ], }) -export class AppModule implements NestModule { - configure(consumer: MiddlewareConsumer) { - consumer.apply(CampusScopeMiddleware).forRoutes('*'); - } -} +export class AppModule {} diff --git a/apps/server/src/archive/archive.service.ts b/apps/server/src/archive/archive.service.ts index 16544e4..85f2cb2 100644 --- a/apps/server/src/archive/archive.service.ts +++ b/apps/server/src/archive/archive.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as fs from 'fs'; import * as path from 'path'; -import { CampusScope } from '../common/campus-scope'; + import { NotificationsService } from '../notifications/notifications.service'; import { Student } from '../entities/student.entity'; import { StudentProfile } from '../entities/student-profile.entity'; @@ -30,7 +30,6 @@ export class ArchiveService { @InjectRepository(LearningRecord) private learningRecordRepo: Repository, @InjectRepository(ResultArchive) private resultRepo: Repository, @InjectRepository(ArchiveAttachment) private attachmentRepo: Repository, - private readonly scope: CampusScope, private readonly notificationsService: NotificationsService, ) {} @@ -46,24 +45,12 @@ export class ArchiveService { resultArchive, attachments, ] = await Promise.all([ - this.profileRepo.findOne({ where: await this.scope.filter({ studentId }) }), - this.enrollmentRepo.find({ - where: await this.scope.filter({ studentId }), - order: { createdAt: 'DESC' }, - }), - this.examScoreRepo.find({ - where: await this.scope.filter({ studentId }), - order: { examDate: 'DESC' }, - }), - this.learningRecordRepo.find({ - where: await this.scope.filter({ studentId }), - order: { recordDate: 'DESC' }, - }), - this.resultRepo.findOne({ where: await this.scope.filter({ studentId }) }), - this.attachmentRepo.find({ - where: await this.scope.filter({ studentId }), - order: { createdAt: 'DESC' }, - }), + this.profileRepo.findOne({ where: { studentId } }), + this.enrollmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }), + this.examScoreRepo.find({ where: { studentId }, order: { examDate: 'DESC' } }), + this.learningRecordRepo.find({ where: { studentId }, order: { recordDate: 'DESC' } }), + this.resultRepo.findOne({ where: { studentId } }), + this.attachmentRepo.find({ where: { studentId }, order: { createdAt: 'DESC' } }), ]); return { diff --git a/apps/server/src/attendance/attendance.service.ts b/apps/server/src/attendance/attendance.service.ts index 4667b3e..86d3294 100644 --- a/apps/server/src/attendance/attendance.service.ts +++ b/apps/server/src/attendance/attendance.service.ts @@ -43,10 +43,6 @@ export class AttendanceService { throw new BadRequestException('records array must not be empty'); } - // Batch-load student departmentIds - const ids = [...new Set(dto.records.map((r) => r.studentId))]; - const students = await this.studentRepo.find({ where: { id: In(ids) } }); - const deptMap = new Map(students.map((s) => [s.id, s.departmentId])); const entities = dto.records.map((r) => { const entity = this.attendanceRepo.create({ @@ -58,7 +54,6 @@ export class AttendanceService { remark: r.remark, source: r.source || 'manual', }); - entity.departmentId = deptMap.get(r.studentId)!; return entity; }); @@ -128,7 +123,6 @@ export class AttendanceService { status: 'pending', source: 'schedule', }); - entity.departmentId = cs.student?.departmentId ?? undefined; entities.push(entity); existingKeys.add(key); } diff --git a/apps/server/src/bills/bills.service.spec.ts b/apps/server/src/bills/bills.service.spec.ts index 9808a9b..bf118aa 100644 --- a/apps/server/src/bills/bills.service.spec.ts +++ b/apps/server/src/bills/bills.service.spec.ts @@ -9,7 +9,6 @@ import { PersonalExpense } from '../entities/personal-expense.entity'; import { Occupancy } from '../entities/occupancy.entity'; import { Room } from '../entities/room.entity'; import { Deposit } from '../entities/deposit.entity'; -import { CampusScope } from '../common/campus-scope'; type MockRepository = Partial, jest.Mock>>; @@ -70,7 +69,6 @@ describe('BillsService — generateBills', () => { { provide: getRepositoryToken(Room), useValue: roomRepo }, { provide: getRepositoryToken(Deposit), useValue: depositRepo }, { provide: DataSource, useValue: dataSource }, - { provide: CampusScope, useValue: { getScopeDepartmentIds: jest.fn().mockResolvedValue(null), filter: jest.fn((w: any) => w) } }, ], }).compile(); diff --git a/apps/server/src/bills/bills.service.ts b/apps/server/src/bills/bills.service.ts index 0fad122..dc13c85 100644 --- a/apps/server/src/bills/bills.service.ts +++ b/apps/server/src/bills/bills.service.ts @@ -9,7 +9,7 @@ import { Occupancy } from '../entities/occupancy.entity'; import { Room } from '../entities/room.entity'; import { Deposit } from '../entities/deposit.entity'; import { GenerateBillsDto, UpdateBillStatusDto } from './dto/bill.dto'; -import { CampusScope } from '../common/campus-scope'; + @Injectable() export class BillsService { @@ -22,7 +22,6 @@ export class BillsService { @InjectRepository(Room) private roomRepo: Repository, @InjectRepository(Deposit) private depositRepo: Repository, private dataSource: DataSource, - private readonly scope: CampusScope, ) {} /** @@ -181,15 +180,6 @@ export class BillsService { // 合并所有涉及的学生 const allStudentIds = new Set([...studentBillData.keys(), ...personalMap.keys()]); // 生成账单 - // Batch-load student departmentIds - const studentIdsArr = [...allStudentIds]; - const studentDeptMap = new Map(); - if (studentIdsArr.length > 0) { - const studentsData: { id: number; department_id: number | null }[] = await this.dataSource.query( - `SELECT id, department_id FROM students WHERE id IN (${studentIdsArr.join(',')})` - ); - for (const s of studentsData) if (s.department_id != null) studentDeptMap.set(s.id, s.department_id); - } const bills: Bill[] = []; for (const studentId of allStudentIds) { const shared = studentBillData.get(studentId)?.shared || 0; @@ -204,7 +194,6 @@ export class BillsService { personalAmount: personal, totalAmount: total, status: 'draft', - departmentId: studentDeptMap.get(studentId), }); const savedBill = await this.billRepo.save(bill); @@ -229,12 +218,10 @@ export class BillsService { status?: string; expenseType?: string; }) { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.billRepo .createQueryBuilder('b') .leftJoinAndSelect('b.student', 'student') .orderBy('b.generatedAt', 'DESC'); - if (scopeIds) qb.andWhere('b.departmentId IN (:...scopeIds)', { scopeIds }); if (query?.periodStart) qb.andWhere('b.periodStart = :ps', { ps: query.periodStart }); if (query?.periodEnd) qb.andWhere('b.periodEnd = :pe', { pe: query.periodEnd }); if (query?.studentId) qb.andWhere('b.studentId = :sid', { sid: query.studentId }); diff --git a/apps/server/src/classes/classes.module.ts b/apps/server/src/classes/classes.module.ts index 37e9b00..8fc4e2e 100644 --- a/apps/server/src/classes/classes.module.ts +++ b/apps/server/src/classes/classes.module.ts @@ -1,14 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CommonModule } from '../common/common.module'; -import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department, Student, StudentDingMapping } from '../entities'; +import { Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Student, StudentDingMapping } from '../entities'; import { ClassesService } from './classes.service'; import { ClassesController } from './classes.controller'; import { OperationLogsModule } from '../operation-logs/operation-logs.module'; import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Department, Student, StudentDingMapping]), OperationLogsModule, NotificationsModule, CommonModule], + imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher, ClassSchedule, AttendanceRecord, Student, StudentDingMapping]), OperationLogsModule, NotificationsModule, CommonModule], controllers: [ClassesController], providers: [ClassesService], exports: [ClassesService], diff --git a/apps/server/src/classes/classes.service.ts b/apps/server/src/classes/classes.service.ts index c29a873..58aebfe 100644 --- a/apps/server/src/classes/classes.service.ts +++ b/apps/server/src/classes/classes.service.ts @@ -147,7 +147,6 @@ export class ClassesService { this.studentRepo.create({ name: `dd_${dingUserId}`, status: 'active', - departmentId: undefined, }) ); const savedStudents = await this.studentRepo.save(newStudents); diff --git a/apps/server/src/classroom-rentals/classroom-rentals.service.spec.ts b/apps/server/src/classroom-rentals/classroom-rentals.service.spec.ts index b3e9da3..77540b5 100644 --- a/apps/server/src/classroom-rentals/classroom-rentals.service.spec.ts +++ b/apps/server/src/classroom-rentals/classroom-rentals.service.spec.ts @@ -7,7 +7,6 @@ import { ClassroomRental } from '../entities/classroom-rental.entity'; import { Classroom } from '../entities/classroom.entity'; import { Tenant } from '../entities/tenant.entity'; import { ClassSchedule } from '../entities/class-schedule.entity'; -import { CampusScope } from '../common/campus-scope'; import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto'; function mockQueryBuilder(results: T[] = []) { @@ -33,7 +32,6 @@ describe('ClassroomRentalsService — findConflicts', () => { { provide: getRepositoryToken(Classroom), useValue: {} }, { provide: getRepositoryToken(Tenant), useValue: {} }, { provide: getRepositoryToken(ClassSchedule), useValue: { createQueryBuilder: jest.fn() } }, - { provide: CampusScope, useValue: { getScopeDepartmentIds: jest.fn().mockResolvedValue(null) } }, ], }).compile(); @@ -134,7 +132,6 @@ describe('ClassroomRentalsService — rental schedule sync', () => { { provide: getRepositoryToken(Classroom), useValue: classroomRepo }, { provide: getRepositoryToken(Tenant), useValue: tenantRepo }, { provide: getRepositoryToken(ClassSchedule), useValue: scheduleRepo }, - { provide: CampusScope, useValue: { getScopeDepartmentIds: jest.fn().mockResolvedValue(null) } }, ], }).compile(); diff --git a/apps/server/src/classroom-rentals/classroom-rentals.service.ts b/apps/server/src/classroom-rentals/classroom-rentals.service.ts index df55820..1f07e67 100644 --- a/apps/server/src/classroom-rentals/classroom-rentals.service.ts +++ b/apps/server/src/classroom-rentals/classroom-rentals.service.ts @@ -11,7 +11,7 @@ import { Classroom } from '../entities/classroom.entity'; import { Tenant } from '../entities/tenant.entity'; import { ClassSchedule } from '../entities/class-schedule.entity'; import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto'; -import { CampusScope } from '../common/campus-scope'; + import * as path from 'path'; import * as fs from 'fs'; @@ -37,7 +37,6 @@ export class ClassroomRentalsService { @InjectRepository(Classroom) private classroomRepo: Repository, @InjectRepository(Tenant) private tenantRepo: Repository, @InjectRepository(ClassSchedule) private scheduleRepo: Repository, - private readonly scope: CampusScope, ) {} get uploadDir(): string { @@ -57,13 +56,11 @@ export class ClassroomRentalsService { month?: string; includeEnded?: boolean; }) { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.repo .createQueryBuilder('r') .leftJoinAndSelect('r.classroom', 'classroom') .leftJoinAndSelect('r.tenant', 'tenant') .orderBy('r.startDate', 'DESC'); - if (scopeIds) qb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds }); if (query?.classroomId) qb.andWhere('r.classroomId = :cid', { cid: query.classroomId }); if (query?.tenantId) qb.andWhere('r.tenantId = :tid', { tid: query.tenantId }); if (query?.month) { @@ -142,9 +139,7 @@ export class ClassroomRentalsService { }); } const rental = this.repo.create({ ...dto, createdBy: userId, status: 'active' }); - rental.departmentId = classroom.departmentId; const saved = await this.repo.save(rental); - await this.syncScheduleFromRental(saved, tenant.name); return saved; } @@ -221,7 +216,6 @@ export class ClassroomRentalsService { rentalId: rental.id, status: 'active', notes: rental.notes, - departmentId: rental.departmentId, }; if (schedule) { await this.scheduleRepo.update(schedule.id, data); diff --git a/apps/server/src/classrooms/classrooms.controller.ts b/apps/server/src/classrooms/classrooms.controller.ts index 94d622f..da3aca4 100644 --- a/apps/server/src/classrooms/classrooms.controller.ts +++ b/apps/server/src/classrooms/classrooms.controller.ts @@ -211,8 +211,7 @@ export class ClassroomsController { supervisor: String(row.getCell(7).value || '') || undefined, }); }); - const departmentId = req.headers?.['x-campus-id'] ? parseInt(String(req.headers['x-campus-id']), 10) || undefined : undefined; - const result = await this.service.batchImport(rows, departmentId); + const result = await this.service.batchImport(rows); await this.logService.log({ userId: req.user?.id, username: req.user?.username, diff --git a/apps/server/src/classrooms/classrooms.service.ts b/apps/server/src/classrooms/classrooms.service.ts index e9fcbac..2c776c1 100644 --- a/apps/server/src/classrooms/classrooms.service.ts +++ b/apps/server/src/classrooms/classrooms.service.ts @@ -4,7 +4,6 @@ import { Repository, Not } from 'typeorm'; import { Classroom } from '../entities/classroom.entity'; import { ClassroomRental } from '../entities/classroom-rental.entity'; import { ClassSchedule } from '../entities/class-schedule.entity'; -import { CampusScope } from '../common/campus-scope'; import { CreateClassroomDto, UpdateClassroomDto } from './dto/classroom.dto'; @Injectable() @@ -14,7 +13,6 @@ export class ClassroomsService { @InjectRepository(Classroom) private repo: Repository, @InjectRepository(ClassroomRental) private rentalRepo: Repository, @InjectRepository(ClassSchedule) private scheduleRepo: Repository, - private readonly scope: CampusScope, ) {} async findAll(query?: { building?: string; roomType?: string; includeArchived?: boolean }) { @@ -22,7 +20,7 @@ export class ClassroomsService { if (query?.building) where.building = query.building; if (query?.roomType) where.roomType = query.roomType; if (!query?.includeArchived) where.status = Not('archived'); - const list = await this.repo.find({ where: await this.scope.filter(where), order: { building: 'ASC', name: 'ASC' } }); + const list = await this.repo.find({ where, order: { building: 'ASC', name: 'ASC' } }); const usageMap = await this.getCurrentUsageForClassrooms(list.map((c) => c.id)); return list.map((c) => ({ ...c, currentUsage: usageMap.get(c.id) ?? null })); } @@ -37,9 +35,7 @@ export class ClassroomsService { async create(dto: CreateClassroomDto) { const exists = await this.repo.findOne({ where: { name: dto.name } }); if (exists) throw new BadRequestException(`教室 ${dto.name} 已存在`); - const entity = this.repo.create(dto); - if (dto.departmentId) entity.departmentId = dto.departmentId; - return this.repo.save(entity); + return this.repo.save(this.repo.create(dto)); } async update(id: number, dto: UpdateClassroomDto) { @@ -136,7 +132,6 @@ export class ClassroomsService { roomType?: string; courseType?: string; }[], - departmentId?: number, ) { let imported = 0; let skipped = 0; @@ -145,7 +140,7 @@ export class ClassroomsService { if (!row.name?.trim()) { skipped++; continue; } const exists = await this.repo.findOne({ where: { name: row.name.trim() } }); if (exists) { errors.push(`教室 ${row.name} 已存在`); skipped++; continue; } - await this.repo.save(this.repo.create({ ...row, capacity: row.capacity || 30, departmentId: departmentId ?? undefined })); + await this.repo.save(this.repo.create({ ...row, capacity: row.capacity || 30 })); imported++; } return { message: `成功导入 ${imported} 间教室,跳过 ${skipped} 间`, imported, skipped, errors: errors.length > 0 ? errors : undefined }; diff --git a/apps/server/src/classrooms/dto/classroom.dto.ts b/apps/server/src/classrooms/dto/classroom.dto.ts index 8033a2a..703470e 100644 --- a/apps/server/src/classrooms/dto/classroom.dto.ts +++ b/apps/server/src/classrooms/dto/classroom.dto.ts @@ -33,9 +33,6 @@ export class CreateClassroomDto { @IsString() notes?: string; - @IsOptional() - @IsInt() - departmentId?: number; } export class UpdateClassroomDto { diff --git a/apps/server/src/common/campus-scope.middleware.ts b/apps/server/src/common/campus-scope.middleware.ts deleted file mode 100644 index f07f8cd..0000000 --- a/apps/server/src/common/campus-scope.middleware.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Injectable, NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; -import { CampusScope, CampusRequest } from './campus-scope'; - -@Injectable() -export class CampusScopeMiddleware implements NestMiddleware { - constructor(private readonly scope: CampusScope) {} - - use(req: Request, _res: Response, next: NextFunction): void { - (req as CampusRequest).campusScope = this.scope; - next(); - } -} diff --git a/apps/server/src/common/campus-scope.ts b/apps/server/src/common/campus-scope.ts deleted file mode 100644 index 4431295..0000000 --- a/apps/server/src/common/campus-scope.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Injectable, Scope, Inject } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; -import { In } from 'typeorm'; -import { Request } from 'express'; -import { DepartmentsService } from '../departments/departments.service'; - -export interface CampusRequest extends Request { - user?: { - id: number; - isSuperAdmin?: boolean; - }; - campusScope?: CampusScope; -} - -@Injectable({ scope: Scope.REQUEST }) -export class CampusScope { - constructor( - @Inject(REQUEST) private req: CampusRequest, - private departmentsService: DepartmentsService, - ) {} - - get userId(): number | undefined { - return this.req.user?.id; - } - - get isSuperAdmin(): boolean { - return this.req.user?.isSuperAdmin ?? false; - } - - get currentDepartmentId(): number | null { - const raw = this.req.headers?.['x-campus-id']; - if (raw === undefined) return null; - const str = Array.isArray(raw) ? raw[0] : raw; - if (!str) return null; - const id = parseInt(str, 10); - return Number.isNaN(id) ? null : id; - } - - /** Appends departmentId filter. No campus selected = no filtering for super admin; empty result for others. */ - async filter>(where: T): Promise { - if (this.isSuperAdmin && !this.currentDepartmentId) { - return where; - } - - const ids = await this.getEffectiveScopeIds(); - if (ids.length === 0) { - // Non-super-admin with no scoping → match nothing, never leak unfiltered data - if (!this.isSuperAdmin) { - return { ...where, departmentId: In([]) }; - } - return where; - } - - return { ...where, departmentId: In(ids) }; - } - - /** Returns department IDs for QueryBuilder .andWhere(). null = no filtering. */ - async getScopeDepartmentIds(): Promise { - if (this.isSuperAdmin && !this.currentDepartmentId) return null; - const ids = await this.getEffectiveScopeIds(); - return ids.length > 0 ? ids : null; - } - - private async getEffectiveScopeIds(): Promise { - if (this.currentDepartmentId) { - return this.departmentsService.getDescendantIds(this.currentDepartmentId); - } - - if (!this.userId) return []; - - const userDeptIds = await this.departmentsService.getUserDepartments(this.userId); - const allIds = await Promise.all( - userDeptIds.map((id) => this.departmentsService.getDescendantIds(id)), - ); - return [...new Set(allIds.flat())]; - } -} diff --git a/apps/server/src/common/common.module.ts b/apps/server/src/common/common.module.ts index 5dacf41..ac198ba 100644 --- a/apps/server/src/common/common.module.ts +++ b/apps/server/src/common/common.module.ts @@ -1,11 +1,4 @@ import { Module } from '@nestjs/common'; -import { CampusScope } from './campus-scope'; -import { CampusScopeMiddleware } from './campus-scope.middleware'; -import { DepartmentsModule } from '../departments/departments.module'; -@Module({ - imports: [DepartmentsModule], - providers: [CampusScope, CampusScopeMiddleware], - exports: [CampusScope, CampusScopeMiddleware, DepartmentsModule], -}) +@Module({}) export class CommonModule {} diff --git a/apps/server/src/dashboard/dashboard.service.ts b/apps/server/src/dashboard/dashboard.service.ts index 9c6d151..68e2efb 100644 --- a/apps/server/src/dashboard/dashboard.service.ts +++ b/apps/server/src/dashboard/dashboard.service.ts @@ -13,7 +13,7 @@ import { Class } from '../entities/class.entity'; import { Deposit } from '../entities/deposit.entity'; import { ClassroomRental } from '../entities/classroom-rental.entity'; import { ClassTeacher } from '../entities/class-teacher.entity'; -import { CampusScope } from '../common/campus-scope'; + @Injectable() export class DashboardService { @@ -31,23 +31,20 @@ export class DashboardService { @InjectRepository(Deposit) private depositRepo: Repository, @InjectRepository(ClassroomRental) private rentalRepo: Repository, @InjectRepository(ClassTeacher) private classTeacherRepo: Repository, - private readonly scope: CampusScope, ) {} async getStats() { const today = new Date(); const todayStr = today.toISOString().slice(0, 10); const currentMonth = todayStr.slice(0, 7); // YYYY-MM - const scopeIds = await this.scope.getScopeDepartmentIds(); - const totalRooms = await this.roomRepo.count({ where: await this.scope.filter({ status: Not('archived') }) }); - const totalStudents = await this.studentRepo.count({ where: await this.scope.filter({ status: 'active' }) }); - const occupiedBeds = await this.occRepo.count({ where: await this.scope.filter({ checkOutDate: IsNull() }) }); + const totalRooms = await this.roomRepo.count({ where: { status: Not('archived') } }); + const totalStudents = await this.studentRepo.count({ where: { status: 'active' } }); + const occupiedBeds = await this.occRepo.count({ where: { checkOutDate: IsNull() } }); const capQb = this.roomRepo .createQueryBuilder('r') .select('SUM(r.capacity)', 'total') .where('r.status != :archived', { archived: 'archived' }); - if (scopeIds) capQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds }); const totalCapacity = await capQb.getRawOne(); const cap = totalCapacity?.total || 0; const occupancyRate = cap > 0 ? ((occupiedBeds / cap) * 100).toFixed(1) : 0; @@ -58,11 +55,10 @@ export class DashboardService { .addSelect('COUNT(*)', 'count') .addSelect('SUM(b.totalAmount)', 'total') .groupBy('b.status'); - if (scopeIds) billStatsQb.andWhere('b.departmentId IN (:...scopeIds)', { scopeIds }); const billStats = await billStatsQb.getRawMany(); // New fields - const classroomCount = await this.classroomRepo.count({ where: await this.scope.filter({}) }); + const classroomCount = await this.classroomRepo.count({ where: {} }); const occQb = this.scheduleRepo .createQueryBuilder('s') @@ -70,7 +66,6 @@ export class DashboardService { .where('s.status = :active', { active: 'active' }) .andWhere('s.startDate <= :today', { today: todayStr }) .andWhere('s.endDate >= :today', { today: todayStr }); - if (scopeIds) occQb.andWhere('s.departmentId IN (:...scopeIds)', { scopeIds }); const occResult = await occQb.getRawOne(); const occupiedClassrooms = parseInt(occResult?.cnt || '0', 10); const classroomOccupancyRate = classroomCount > 0 @@ -83,7 +78,6 @@ export class DashboardService { .addSelect('COUNT(*)', 'count') .where('a.attendanceDate = :today', { today: todayStr }) .groupBy('a.status'); - if (scopeIds) attTodayQb.andWhere('a.departmentId IN (:...scopeIds)', { scopeIds }); const attTodayStats = await attTodayQb.getRawMany(); const todayTotal = attTodayStats.reduce((sum, r) => sum + parseInt(r.count, 10), 0); const todayPresent = attTodayStats @@ -99,7 +93,6 @@ export class DashboardService { .where('b.status = :paid', { paid: 'paid' }) .andWhere('b.periodStart >= :start', { start: `${currentMonth}-01` }) .andWhere('b.periodStart < :end', { end: this.nextMonth(currentMonth) }); - if (scopeIds) incomeQb.andWhere('b.departmentId IN (:...scopeIds)', { scopeIds }); const incomeResult = await incomeQb.getRawOne(); const monthlyIncome = parseFloat(incomeResult?.total || '0'); @@ -107,9 +100,8 @@ export class DashboardService { const incomeTrend = await this.getIncomeTrend(currentMonth); // --- New stats --- - const classCount = await this.classRepo.count({ where: await this.scope.filter({}) }); + const classCount = await this.classRepo.count({ where: {} }); - // classTeacherRepo does not have departmentId — skip scope filtering const teacherResult = await this.classTeacherRepo .createQueryBuilder('ct') .select('COUNT(DISTINCT ct.userId)', 'cnt') @@ -121,11 +113,10 @@ export class DashboardService { .select('SUM(d.amount)', 'total') .where('d.status = :paid', { paid: 'paid' }) .andWhere('d.refundStatus IS NULL'); - if (scopeIds) pendingQb.andWhere('d.departmentId IN (:...scopeIds)', { scopeIds }); const pendingResult = await pendingQb.getRawOne(); const pendingDeposits = parseFloat(pendingResult?.total || '0'); - const activeRentals = await this.rentalRepo.count({ where: await this.scope.filter({ endDate: MoreThanOrEqual(todayStr) }) }); + const activeRentals = await this.rentalRepo.count({ where: { endDate: MoreThanOrEqual(todayStr) } }); const occByBldQb = this.occRepo .createQueryBuilder('o') @@ -133,7 +124,6 @@ export class DashboardService { .select('r.building', 'building') .addSelect('COUNT(*)', 'count') .where('o.checkOutDate IS NULL'); - if (scopeIds) occByBldQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds }); const occupancyByBuilding = await occByBldQb.groupBy('r.building').getRawMany(); const attendanceByStatus = attTodayStats.reduce((acc, r) => { @@ -147,7 +137,6 @@ export class DashboardService { .addSelect('SUM(e.amount)', 'total') .where('e.periodStart >= :start', { start: `${currentMonth}-01` }) .andWhere('e.periodEnd <= :end', { end: this.nextMonth(currentMonth) }); - if (scopeIds) expByTypeQb.andWhere('e.departmentId IN (:...scopeIds)', { scopeIds }); const expenseByType = await expByTypeQb.groupBy('e.expenseType').getRawMany(); return { @@ -238,7 +227,6 @@ export class DashboardService { // 甘特图数据:每个宿舍的入住时间线 async getGanttData(query?: { periodStart?: string; periodEnd?: string; building?: string }) { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.occRepo .createQueryBuilder('o') .leftJoinAndSelect('o.student', 'student') @@ -247,7 +235,6 @@ export class DashboardService { .orderBy('room.roomNumber', 'ASC') .addOrderBy('o.checkInDate', 'ASC'); - if (scopeIds) qb.andWhere('room.departmentId IN (:...scopeIds)', { scopeIds }); if (query?.building) { qb.andWhere('room.building = :building', { building: query.building }); @@ -283,13 +270,11 @@ export class DashboardService { } // 费用统计 async getExpenseStats(periodStart?: string, periodEnd?: string) { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.expRepo .createQueryBuilder('e') .select('e.expenseType', 'type') .addSelect('SUM(e.amount)', 'total') .groupBy('e.expenseType'); - if (scopeIds) qb.andWhere('e.departmentId IN (:...scopeIds)', { scopeIds }); if (periodStart) qb.andWhere('e.periodStart >= :ps', { ps: periodStart }); if (periodEnd) qb.andWhere('e.periodEnd <= :pe', { pe: periodEnd }); return qb.getRawMany(); @@ -297,7 +282,6 @@ export class DashboardService { // 各宿舍费用排行 async getRoomExpenseRanking(periodStart?: string, periodEnd?: string) { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.expRepo .createQueryBuilder('e') .leftJoin('e.room', 'room') @@ -307,7 +291,6 @@ export class DashboardService { .groupBy('e.roomId') .orderBy('total', 'DESC') .limit(20); - if (scopeIds) qb.andWhere('e.departmentId IN (:...scopeIds)', { scopeIds }); if (periodStart) qb.andWhere('e.periodStart >= :ps', { ps: periodStart }); if (periodEnd) qb.andWhere('e.periodEnd <= :pe', { pe: periodEnd }); return qb.getRawMany(); @@ -315,7 +298,6 @@ export class DashboardService { // 班级考勤排行 async getClassAttendanceRanking() { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.attendanceRepo .createQueryBuilder('a') .leftJoin('a.class', 'class') @@ -324,7 +306,6 @@ export class DashboardService { .addSelect('a.status', 'status') .addSelect('COUNT(*)', 'count') .groupBy('class.id').addGroupBy('class.name').addGroupBy('a.status'); - if (scopeIds) qb.andWhere('a.departmentId IN (:...scopeIds)', { scopeIds }); const raw = await qb.getRawMany(); const classMap = new Map(); @@ -345,9 +326,8 @@ export class DashboardService { } async getClassroomOccupancy() { - const scopeIds = await this.scope.getScopeDepartmentIds(); const classrooms = await this.classroomRepo.find({ - where: await this.scope.filter({ status: Not('archived') }), + where: { status: Not('archived') }, order: { building: 'ASC', name: 'ASC' }, }); const today = new Date().toISOString().slice(0, 10); @@ -359,7 +339,6 @@ export class DashboardService { .andWhere('s.scheduleType = :type', { type: 'INTERNAL' }) .andWhere('s.startDate <= :today AND s.endDate >= :today', { today }) .groupBy('s.classroomId'); - if (scopeIds) schedQb.andWhere('s.departmentId IN (:...scopeIds)', { scopeIds }); const schedules = await schedQb.getRawMany(); const rentalQb = this.rentalRepo .createQueryBuilder('r') @@ -368,7 +347,6 @@ export class DashboardService { .where('r.status != :cancelled', { cancelled: 'cancelled' }) .andWhere('r.startDate <= :today AND r.endDate >= :today', { today }) .groupBy('r.classroomId'); - if (scopeIds) rentalQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds }); const rentals = await rentalQb.getRawMany(); const sMap: Record = {}; const rMap: Record = {}; @@ -385,9 +363,8 @@ export class DashboardService { } async getClassroomUtilizationStats() { - const scopeIds = await this.scope.getScopeDepartmentIds(); const totalClassrooms = await this.classroomRepo.count({ - where: await this.scope.filter({ status: Not('archived') }), + where: { status: Not('archived') }, }); const today = new Date().toISOString().slice(0, 10); @@ -399,7 +376,6 @@ export class DashboardService { .where('s.status = :active', { active: 'active' }) .andWhere('s.scheduleType = :type', { type: 'INTERNAL' }) .andWhere('s.startDate <= :today AND s.endDate >= :today', { today }); - if (scopeIds) schedQb.andWhere('s.departmentId IN (:...scopeIds)', { scopeIds }); const schedResult = await schedQb.getRawOne(); // Count classrooms with active rentals today @@ -408,7 +384,6 @@ export class DashboardService { .select('COUNT(DISTINCT r.classroomId)', 'cnt') .where('r.status != :cancelled', { cancelled: 'cancelled' }) .andWhere('r.startDate <= :today AND r.endDate >= :today', { today }); - if (scopeIds) rentalQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds }); const rentalResult = await rentalQb.getRawOne(); // Combine: use Set merge of both @@ -419,7 +394,6 @@ export class DashboardService { .andWhere('s.scheduleType = :type', { type: 'INTERNAL' }) .andWhere('s.startDate <= :today AND s.endDate >= :today', { today }) .groupBy('s.classroomId'); - if (scopeIds) combinedQb.andWhere('s.departmentId IN (:...scopeIds)', { scopeIds }); const schedIds = await combinedQb.getRawMany(); const combinedRentalQb = this.rentalRepo @@ -428,7 +402,6 @@ export class DashboardService { .where('r.status != :cancelled', { cancelled: 'cancelled' }) .andWhere('r.startDate <= :today AND r.endDate >= :today', { today }) .groupBy('r.classroomId'); - if (scopeIds) combinedRentalQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds }); const rentalIds = await combinedRentalQb.getRawMany(); const allInUseIds = new Set([ diff --git a/apps/server/src/departments/departments.controller.ts b/apps/server/src/departments/departments.controller.ts deleted file mode 100644 index cbe2dfd..0000000 --- a/apps/server/src/departments/departments.controller.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - ParseIntPipe, - UseGuards, -} from '@nestjs/common'; -import { DepartmentsService } from './departments.service'; -import { - CreateDepartmentDto, - UpdateDepartmentDto, - AssignUserDto, -} from './dto/department.dto'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; -import { RequirePermission } from '../auth/decorators/permission.decorator'; - -@UseGuards(JwtAuthGuard) -@Controller('departments') -export class DepartmentsController { - constructor(private readonly departmentsService: DepartmentsService) {} - - @Get() - @RequirePermission('department:view') - findAll() { - return this.departmentsService.findAll(); - } - - @Get('tree') - @RequirePermission('department:view') - findTree() { - return this.departmentsService.findTree(); - } - - /** 获取钉钉同步的部门树,供"从部门创建班级"使用 */ - @Get('synced') - @RequirePermission('department:view') - findSyncedTree() { - return this.departmentsService.findSyncedTree(); - } - - @Get(':id') - @RequirePermission('department:view') - findOne(@Param('id', ParseIntPipe) id: number) { - return this.departmentsService.findOne(id); - } - - @Post() - @RequirePermission('department:edit') - create(@Body() dto: CreateDepartmentDto) { - return this.departmentsService.create(dto); - } - - @Put(':id') - @RequirePermission('department:edit') - update( - @Param('id', ParseIntPipe) id: number, - @Body() dto: UpdateDepartmentDto, - ) { - return this.departmentsService.update(id, dto); - } - - @Delete(':id') - @RequirePermission('department:delete') - remove(@Param('id', ParseIntPipe) id: number) { - return this.departmentsService.remove(id); - } - - @Get(':id/users') - @RequirePermission('department:view') - getUsers(@Param('id', ParseIntPipe) id: number) { - return this.departmentsService.getUsers(id); - } - - @Post(':id/users') - @RequirePermission('department:edit') - assignUser( - @Param('id', ParseIntPipe) id: number, - @Body() dto: AssignUserDto, - ) { - return this.departmentsService.assignUser(id, dto); - } - - @Delete(':id/users/:userId') - @RequirePermission('department:delete') - removeUser( - @Param('id', ParseIntPipe) id: number, - @Param('userId', ParseIntPipe) userId: number, - ) { - return this.departmentsService.removeUser(id, userId); - } -} diff --git a/apps/server/src/departments/departments.module.ts b/apps/server/src/departments/departments.module.ts deleted file mode 100644 index d253ecd..0000000 --- a/apps/server/src/departments/departments.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { Department } from '../entities/department.entity'; -import { UserDepartment } from '../entities/user-department.entity'; -import { DepartmentsService } from './departments.service'; -import { DepartmentsController } from './departments.controller'; - -@Module({ - imports: [TypeOrmModule.forFeature([Department, UserDepartment])], - controllers: [DepartmentsController], - providers: [DepartmentsService], - exports: [DepartmentsService], -}) -export class DepartmentsModule {} diff --git a/apps/server/src/departments/departments.service.ts b/apps/server/src/departments/departments.service.ts deleted file mode 100644 index c7b5a53..0000000 --- a/apps/server/src/departments/departments.service.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Department } from '../entities/department.entity'; -import { DepartmentType } from '../entities/department.entity'; -import { UserDepartment } from '../entities/user-department.entity'; -import { CreateDepartmentDto, UpdateDepartmentDto, AssignUserDto } from './dto/department.dto'; - -@Injectable() -export class DepartmentsService { - constructor( - @InjectRepository(Department) - private deptRepo: Repository, - @InjectRepository(UserDepartment) - private userDeptRepo: Repository, - ) {} - - async findAll(): Promise { - return this.deptRepo.find({ - where: { status: 'active' }, - order: { sortOrder: 'ASC', name: 'ASC' }, - }); - } - - async findTree(): Promise { - // Load ALL departments flat (no relations — avoids N+1 and only loads 1 level), - // then build the full tree in memory. - const all = await this.deptRepo.find({ - where: { status: 'active' }, - order: { sortOrder: 'ASC', name: 'ASC' }, - }); - - const byParent = new Map(); - for (const dept of all) { - const key = dept.parentId ?? null; - const list = byParent.get(key); - if (list) { - list.push(dept); - } else { - byParent.set(key, [dept]); - } - } - - const attachChildren = (dept: Department): void => { - const children = byParent.get(dept.id) ?? []; - dept.children = children; - for (const child of children) attachChildren(child); - }; - - const roots = byParent.get(null) ?? []; - for (const root of roots) attachChildren(root); - return roots; - } - - /** 获取钉钉同步的部门树(只返回 source='dingtalk' 的部门) */ - async findSyncedTree(): Promise { - const all = await this.deptRepo.find({ - where: { status: 'active', source: 'dingtalk' }, - order: { sortOrder: 'ASC', name: 'ASC' }, - }); - - const byParent = new Map(); - for (const dept of all) { - const key = dept.parentId ?? null; - const list = byParent.get(key); - if (list) { - list.push(dept); - } else { - byParent.set(key, [dept]); - } - } - - const attachChildren = (dept: Department): void => { - const children = byParent.get(dept.id) ?? []; - dept.children = children; - for (const child of children) attachChildren(child); - }; - - const roots = byParent.get(null) ?? []; - for (const root of roots) attachChildren(root); - return roots; - } - - async findOne(id: number): Promise { - const dept = await this.deptRepo.findOne({ where: { id } }); - if (!dept) throw new NotFoundException('部门不存在'); - return dept; - } - - async create(dto: CreateDepartmentDto): Promise { - if (dto.type === DepartmentType.CAMPUS && dto.parentId) { - throw new ConflictException('校区类型的部门必须是根部门,不能设置上级'); - } - const dept = this.deptRepo.create(dto); - return this.deptRepo.save(dept); - } - - async update(id: number, dto: UpdateDepartmentDto): Promise { - const dept = await this.findOne(id); - - const effectiveType = dto.type ?? dept.type; - const effectiveParentId = dto.parentId !== undefined ? dto.parentId : dept.parentId; - if (effectiveType === DepartmentType.CAMPUS && effectiveParentId) { - throw new ConflictException('校区类型的部门必须是根部门,不能设置上级'); - } - - // Prevent parent cycles: parentId must not be the dept itself or one of its descendants - if (dto.parentId !== undefined && dto.parentId !== null) { - const newParentId = dto.parentId; - if (newParentId === id) { - throw new ConflictException('不能将部门的上级设为自身'); - } - const descendantIds = await this.getDescendantIds(id); - if (descendantIds.includes(newParentId)) { - throw new ConflictException('不能将部门的上级设为其子部门,会形成循环'); - } - } - - Object.assign(dept, dto); - return this.deptRepo.save(dept); - } - - async remove(id: number): Promise { - const children = await this.deptRepo.count({ where: { parentId: id } }); - if (children > 0) throw new ConflictException('该部门下存在子部门,无法删除'); - - const users = await this.userDeptRepo.count({ where: { departmentId: id } }); - if (users > 0) throw new ConflictException('该部门下有用户关联,无法删除'); - - await this.deptRepo.update(id, { status: 'archived' }); - } - - /** 获取部门的所有子部门 ID(递归,含自身) */ - async getDescendantIds(departmentId: number): Promise { - const all = await this.deptRepo.find({ where: { status: 'active' }, select: ['id', 'parentId'] }); - const byParent = new Map(); - for (const d of all) { - const key = d.parentId ?? null; - byParent.set(key, [...(byParent.get(key) ?? []), d.id]); - } - const ids: number[] = [departmentId]; - const collect = (parentId: number) => { - const children = byParent.get(parentId) ?? []; - for (const childId of children) { - ids.push(childId); - collect(childId); - } - }; - collect(departmentId); - return ids; - } - - /** 获取用户可访问的部门 ID 列表 */ - async getUserDepartments(userId: number): Promise { - const records = await this.userDeptRepo.find({ - where: { userId }, - }); - return records.map((r) => r.departmentId); - } - - /** 获取用户默认校区 ID */ - async getUserDefaultDepartmentId(userId: number): Promise { - const record = await this.userDeptRepo.findOne({ - where: { userId, isDefault: true }, - }); - return record?.departmentId ?? null; - } - - /** 获取部门下的用户 */ - async getUsers(departmentId: number): Promise { - return this.userDeptRepo.find({ - where: { departmentId }, - relations: ['user'], - }); - } - - /** 为用户分配部门 */ - async assignUser(departmentId: number, dto: AssignUserDto): Promise { - const record = this.userDeptRepo.create({ - userId: dto.userId, - departmentId, - isDefault: dto.isDefault ?? false, - }); - return this.userDeptRepo.save(record); - } - - /** 移除用户-部门关联 */ - async removeUser(departmentId: number, userId: number): Promise { - await this.userDeptRepo.delete({ departmentId, userId }); - } -} diff --git a/apps/server/src/departments/dto/department.dto.ts b/apps/server/src/departments/dto/department.dto.ts deleted file mode 100644 index de75d33..0000000 --- a/apps/server/src/departments/dto/department.dto.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { IsString, IsNotEmpty, IsOptional, IsInt, IsBoolean } from 'class-validator'; - -export class CreateDepartmentDto { - @IsString() @IsNotEmpty() - name: string; - - @IsOptional() @IsInt() - parentId?: number; - - @IsOptional() @IsString() - type?: string; - - @IsOptional() @IsInt() - sortOrder?: number; -} - -export class UpdateDepartmentDto { - @IsOptional() @IsString() - name?: string; - - @IsOptional() @IsInt() - parentId?: number; - - @IsOptional() @IsString() - type?: string; - - @IsOptional() @IsInt() - sortOrder?: number; -} - -export class AssignUserDto { - @IsInt() - userId: number; - - @IsOptional() @IsBoolean() - isDefault?: boolean; -} diff --git a/apps/server/src/departments/seed.ts b/apps/server/src/departments/seed.ts deleted file mode 100644 index 93952d0..0000000 --- a/apps/server/src/departments/seed.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DataSource } from 'typeorm'; - -export async function seedDefaultCampus(dataSource: DataSource) { - const deptRepo = dataSource.getRepository('departments'); - const userDeptRepo = dataSource.getRepository('user_departments'); - - const existing = await deptRepo.count(); - if (existing > 0) { console.log('Departments exist, skip seed'); return; } - - const campus = await deptRepo.save({ name: '主校区', type: 'campus', sortOrder: 0 }); - - const tables = ['students','rooms','classrooms','class_schedule','attendance_records','room_expenses','personal_expenses','occupancies','bills','deposits','deposit_installments','classroom_rentals']; - for (const table of tables) { - await dataSource.query(`UPDATE ${table} SET department_id = ? WHERE department_id IS NULL`, [campus.id]); - } - - const users = await dataSource.query('SELECT id FROM users'); - for (const user of users) { - await userDeptRepo.save({ userId: user.id, departmentId: campus.id, isDefault: true }); - } - - console.log('Seed: default campus created, data backfilled'); -} diff --git a/apps/server/src/deposits/deposits.service.ts b/apps/server/src/deposits/deposits.service.ts index be82c45..18a1134 100644 --- a/apps/server/src/deposits/deposits.service.ts +++ b/apps/server/src/deposits/deposits.service.ts @@ -4,7 +4,7 @@ import { Repository } from 'typeorm'; import { Deposit } from '../entities/deposit.entity'; import { Student } from '../entities/student.entity'; import { DepositInstallment } from '../entities/deposit-installment.entity'; -import { CampusScope } from '../common/campus-scope'; + import { CreateDepositDto, RefundDepositDto, CreateDepositWithInstallmentsDto } from './dto/deposit.dto'; @Injectable() @@ -16,17 +16,14 @@ export class DepositsService { private installmentRepo: Repository, @InjectRepository(Student) private studentRepo: Repository, - private readonly scope: CampusScope, ) {} async findAll(query?: { studentId?: number; status?: string }) { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.repo .createQueryBuilder('d') .leftJoinAndSelect('d.student', 'student') .leftJoinAndSelect('d.installments', 'installments') .orderBy('d.createdAt', 'DESC'); - if (scopeIds) qb.andWhere('d.departmentId IN (:...scopeIds)', { scopeIds }); if (query?.studentId) qb.andWhere('d.studentId = :studentId', { studentId: query.studentId }); if (query?.status) qb.andWhere('d.status = :status', { status: query.status }); return qb.getMany(); @@ -49,7 +46,6 @@ export class DepositsService { status: 'paid', recordedBy: userId, }); - deposit.departmentId = student.departmentId; if (dto instanceof CreateDepositWithInstallmentsDto && dto.installments?.length) { deposit.installments = dto.installments.map((i) => { @@ -58,7 +54,6 @@ export class DepositsService { dueDate: i.dueDate, status: 'pending', }); - inst.departmentId = student.departmentId; return inst; }); } @@ -174,11 +169,10 @@ export class DepositsService { return this.repo.save(deposit); } async findPendingRefunds() { - const baseWhere = await this.scope.filter({}); return this.repo.find({ where: [ - { ...baseWhere, refundStatus: 'pending' }, - { ...baseWhere, refundStatus: 'head_teacher_approved' }, + { refundStatus: 'pending' }, + { refundStatus: 'head_teacher_approved' }, ], relations: ['student', 'installments'], order: { refundRequestedAt: 'DESC' }, @@ -193,13 +187,11 @@ export class DepositsService { } async getStats() { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.repo .createQueryBuilder('d') .select('d.status', 'status') .addSelect('COUNT(*)', 'count') .addSelect('SUM(d.amount)', 'totalAmount'); - if (scopeIds) qb.andWhere('d.departmentId IN (:...scopeIds)', { scopeIds }); qb.groupBy('d.status'); return qb.getRawMany(); } diff --git a/apps/server/src/entities/archive-attachment.entity.ts b/apps/server/src/entities/archive-attachment.entity.ts index d1879e9..928a97f 100644 --- a/apps/server/src/entities/archive-attachment.entity.ts +++ b/apps/server/src/entities/archive-attachment.entity.ts @@ -8,7 +8,6 @@ import { JoinColumn, } from 'typeorm'; import { Student } from './student.entity'; -import { Department } from './department.entity'; @Entity('archive_attachments') export class ArchiveAttachment { @@ -37,12 +36,6 @@ export class ArchiveAttachment { @Column({ name: 'mime_type', length: 100, nullable: true }) mimeType: string; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/apps/server/src/entities/attendance-record.entity.ts b/apps/server/src/entities/attendance-record.entity.ts index 730df52..d9ddea5 100644 --- a/apps/server/src/entities/attendance-record.entity.ts +++ b/apps/server/src/entities/attendance-record.entity.ts @@ -10,7 +10,6 @@ import { } from 'typeorm'; import { Student } from './student.entity'; import { Class } from './class.entity'; -import { Department } from './department.entity'; @Entity('attendance_records') @Index(['classId', 'attendanceDate']) @@ -54,10 +53,4 @@ export class AttendanceRecord { @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/bill.entity.ts b/apps/server/src/entities/bill.entity.ts index 50f618b..761e765 100644 --- a/apps/server/src/entities/bill.entity.ts +++ b/apps/server/src/entities/bill.entity.ts @@ -9,7 +9,6 @@ import { } from 'typeorm'; import { Student } from './student.entity'; import { BillItem } from './bill-item.entity'; -import { Department } from './department.entity'; @Entity('bills') export class Bill { @@ -47,10 +46,4 @@ export class Bill { @OneToMany(() => BillItem, (bi) => bi.bill) items: BillItem[]; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/class-schedule.entity.ts b/apps/server/src/entities/class-schedule.entity.ts index dfde566..5ea5ddb 100644 --- a/apps/server/src/entities/class-schedule.entity.ts +++ b/apps/server/src/entities/class-schedule.entity.ts @@ -8,7 +8,6 @@ import { JoinColumn, Check, } from 'typeorm'; -import { Department } from './department.entity'; export enum ScheduleType { INTERNAL = 'INTERNAL', @@ -81,10 +80,4 @@ export class ClassSchedule { @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/classroom-rental.entity.ts b/apps/server/src/entities/classroom-rental.entity.ts index 03504da..9afd677 100644 --- a/apps/server/src/entities/classroom-rental.entity.ts +++ b/apps/server/src/entities/classroom-rental.entity.ts @@ -10,7 +10,6 @@ import { } from 'typeorm'; import { Classroom } from './classroom.entity'; import { Tenant } from './tenant.entity'; -import { Department } from './department.entity'; @Entity('classroom_rentals') @Index(['classroomId', 'startDate', 'endDate']) @@ -66,10 +65,4 @@ export class ClassroomRental { @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/classroom.entity.ts b/apps/server/src/entities/classroom.entity.ts index 23df936..c57f2c2 100644 --- a/apps/server/src/entities/classroom.entity.ts +++ b/apps/server/src/entities/classroom.entity.ts @@ -1,5 +1,4 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { Department } from './department.entity'; @Entity('classrooms') export class Classroom { @@ -36,10 +35,4 @@ export class Classroom { @CreateDateColumn({ name: 'created_at' }) createdAt: Date; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/department.entity.ts b/apps/server/src/entities/department.entity.ts deleted file mode 100644 index da86854..0000000 --- a/apps/server/src/entities/department.entity.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - ManyToOne, - OneToMany, - JoinColumn, -} from 'typeorm'; - -export enum DepartmentType { - CAMPUS = 'campus', - DEPARTMENT = 'department', -} - -@Entity('departments') -export class Department { - @PrimaryGeneratedColumn() - id: number; - - @Column({ length: 100 }) - name: string; - - @Column({ name: 'parent_id', type: 'integer', nullable: true }) - parentId: number; - - @ManyToOne(() => Department, { nullable: true, onDelete: 'SET NULL' }) - @JoinColumn({ name: 'parent_id' }) - parent: Department; - - @OneToMany(() => Department, (d) => d.parent) - children: Department[]; - - @Column({ length: 20, default: DepartmentType.DEPARTMENT }) - type: string; - - @Column({ name: 'sort_order', type: 'integer', default: 0 }) - sortOrder: number; - - @Column({ length: 20, default: 'active' }) - status: string; - - @Column({ length: 20, nullable: true }) - source: string; - - @Column({ name: 'source_id', length: 50, nullable: true }) - sourceId: string; - - @Column({ name: 'parent_source_id', length: 50, nullable: true }) - parentSourceId: string; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; - - @UpdateDateColumn({ name: 'updated_at' }) - updatedAt: Date; -} diff --git a/apps/server/src/entities/deposit-installment.entity.ts b/apps/server/src/entities/deposit-installment.entity.ts index afffc1e..807897b 100644 --- a/apps/server/src/entities/deposit-installment.entity.ts +++ b/apps/server/src/entities/deposit-installment.entity.ts @@ -7,7 +7,6 @@ import { JoinColumn, } from 'typeorm'; import { Deposit } from './deposit.entity'; -import { Department } from './department.entity'; @Entity('deposit_installments') export class DepositInstallment { @@ -36,10 +35,4 @@ export class DepositInstallment { @JoinColumn({ name: 'deposit_id' }) deposit: Deposit; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/deposit.entity.ts b/apps/server/src/entities/deposit.entity.ts index d29eba0..258db25 100644 --- a/apps/server/src/entities/deposit.entity.ts +++ b/apps/server/src/entities/deposit.entity.ts @@ -9,7 +9,6 @@ import { } from 'typeorm'; import { Student } from './student.entity'; import { DepositInstallment } from './deposit-installment.entity'; -import { Department } from './department.entity'; @Entity('deposits') export class Deposit { @@ -72,10 +71,4 @@ export class Deposit { @JoinColumn({ name: 'student_id' }) student: Student; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/exam-score.entity.ts b/apps/server/src/entities/exam-score.entity.ts index 010ab1f..f214c31 100644 --- a/apps/server/src/entities/exam-score.entity.ts +++ b/apps/server/src/entities/exam-score.entity.ts @@ -9,7 +9,6 @@ import { } from 'typeorm'; import { Student } from './student.entity'; import { StudentEnrollment } from './student-enrollment.entity'; -import { Department } from './department.entity'; @Entity('exam_scores') export class ExamScore { @@ -51,12 +50,6 @@ export class ExamScore { @Column({ name: 'exam_date', type: 'date', nullable: true }) examDate: string; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/apps/server/src/entities/index.ts b/apps/server/src/entities/index.ts index 2d27e91..c7e0b7e 100644 --- a/apps/server/src/entities/index.ts +++ b/apps/server/src/entities/index.ts @@ -26,8 +26,6 @@ export { SyncLog } from './sync-log.entity'; export { SyncState } from './sync-state.entity'; export { ExpenseType } from './expense-type.entity'; export { Notification, NotificationType } from './notification.entity'; -export { Department, DepartmentType } from './department.entity'; -export { UserDepartment } from './user-department.entity'; export { StudentProfile } from './student-profile.entity'; export { StudentEnrollment } from './student-enrollment.entity'; export { ExamScore } from './exam-score.entity'; diff --git a/apps/server/src/entities/learning-record.entity.ts b/apps/server/src/entities/learning-record.entity.ts index 0e42c02..99e3949 100644 --- a/apps/server/src/entities/learning-record.entity.ts +++ b/apps/server/src/entities/learning-record.entity.ts @@ -8,7 +8,6 @@ import { JoinColumn, } from 'typeorm'; import { Student } from './student.entity'; -import { Department } from './department.entity'; @Entity('learning_records') export class LearningRecord { @@ -37,12 +36,6 @@ export class LearningRecord { @Column({ name: 'next_step', type: 'text', nullable: true }) nextStep: string; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/apps/server/src/entities/occupancy.entity.ts b/apps/server/src/entities/occupancy.entity.ts index 77dca0c..a6c2ad6 100644 --- a/apps/server/src/entities/occupancy.entity.ts +++ b/apps/server/src/entities/occupancy.entity.ts @@ -11,7 +11,6 @@ import { Room } from './room.entity'; import { Tenant } from './tenant.entity'; import { Bed } from './bed.entity'; import { Locker } from './locker.entity'; -import { Department } from './department.entity'; @Entity('occupancies') export class Occupancy { @@ -77,10 +76,4 @@ export class Occupancy { @JoinColumn({ name: 'room_id' }) room: Room; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/personal-expense.entity.ts b/apps/server/src/entities/personal-expense.entity.ts index d4d75e5..d1c704c 100644 --- a/apps/server/src/entities/personal-expense.entity.ts +++ b/apps/server/src/entities/personal-expense.entity.ts @@ -7,7 +7,6 @@ import { JoinColumn, } from 'typeorm'; import { Student } from './student.entity'; -import { Department } from './department.entity'; @Entity('personal_expenses') export class PersonalExpense { @@ -42,10 +41,4 @@ export class PersonalExpense { @JoinColumn({ name: 'student_id' }) student: Student; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/result-archive.entity.ts b/apps/server/src/entities/result-archive.entity.ts index 7322429..7bb49c7 100644 --- a/apps/server/src/entities/result-archive.entity.ts +++ b/apps/server/src/entities/result-archive.entity.ts @@ -8,7 +8,6 @@ import { JoinColumn, } from 'typeorm'; import { Student } from './student.entity'; -import { Department } from './department.entity'; @Entity('result_archives') export class ResultArchive { @@ -37,12 +36,6 @@ export class ResultArchive { @Column({ name: 'admitted_major', length: 100, nullable: true }) admittedMajor: string; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/apps/server/src/entities/room-expense.entity.ts b/apps/server/src/entities/room-expense.entity.ts index 74d777c..4e8f38a 100644 --- a/apps/server/src/entities/room-expense.entity.ts +++ b/apps/server/src/entities/room-expense.entity.ts @@ -7,7 +7,6 @@ import { JoinColumn, } from 'typeorm'; import { Room } from './room.entity'; -import { Department } from './department.entity'; @Entity('room_expenses') export class RoomExpense { @@ -42,10 +41,4 @@ export class RoomExpense { @JoinColumn({ name: 'room_id' }) room: Room; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/room.entity.ts b/apps/server/src/entities/room.entity.ts index ddd86ab..f458470 100644 --- a/apps/server/src/entities/room.entity.ts +++ b/apps/server/src/entities/room.entity.ts @@ -1,7 +1,6 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany, ManyToOne, JoinColumn } from 'typeorm'; import { Occupancy } from './occupancy.entity'; import { RoomExpense } from './room-expense.entity'; -import { Department } from './department.entity'; @Entity('rooms') export class Room { @@ -44,10 +43,4 @@ export class Room { @OneToMany(() => RoomExpense, (e) => e.room) roomExpenses: RoomExpense[]; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/student-enrollment.entity.ts b/apps/server/src/entities/student-enrollment.entity.ts index 12d85c0..fa025be 100644 --- a/apps/server/src/entities/student-enrollment.entity.ts +++ b/apps/server/src/entities/student-enrollment.entity.ts @@ -8,7 +8,6 @@ import { JoinColumn, } from 'typeorm'; import { Student } from './student.entity'; -import { Department } from './department.entity'; @Entity('student_enrollments') export class StudentEnrollment { @@ -46,12 +45,6 @@ export class StudentEnrollment { @Column({ length: 20, default: 'active' }) status: string; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/apps/server/src/entities/student-profile.entity.ts b/apps/server/src/entities/student-profile.entity.ts index 310780a..9a83f78 100644 --- a/apps/server/src/entities/student-profile.entity.ts +++ b/apps/server/src/entities/student-profile.entity.ts @@ -8,7 +8,6 @@ import { JoinColumn, } from 'typeorm'; import { Student } from './student.entity'; -import { Department } from './department.entity'; @Entity('student_profiles') export class StudentProfile { @@ -43,12 +42,6 @@ export class StudentProfile { @Column({ type: 'text', nullable: true }) notes: string; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/apps/server/src/entities/student-report.entity.ts b/apps/server/src/entities/student-report.entity.ts index eb3644f..e741c76 100644 --- a/apps/server/src/entities/student-report.entity.ts +++ b/apps/server/src/entities/student-report.entity.ts @@ -8,7 +8,6 @@ import { JoinColumn, } from 'typeorm'; import { Student } from './student.entity'; -import { Department } from './department.entity'; @Entity('student_reports') export class StudentReport { @@ -34,12 +33,6 @@ export class StudentReport { @Column({ name: 'generated_at', type: 'datetime', nullable: true }) generatedAt: Date; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/apps/server/src/entities/student.entity.ts b/apps/server/src/entities/student.entity.ts index 40877b7..d272d6b 100644 --- a/apps/server/src/entities/student.entity.ts +++ b/apps/server/src/entities/student.entity.ts @@ -14,7 +14,6 @@ import { PersonalExpense } from './personal-expense.entity'; import { Bill } from './bill.entity'; import { Tenant } from './tenant.entity'; import { User } from './user.entity'; -import { Department } from './department.entity'; @Entity('students') export class Student { @@ -82,10 +81,4 @@ export class Student { @OneToMany(() => Bill, (b) => b.student) bills: Bill[]; - @Column({ name: 'department_id', type: 'integer', nullable: true }) - departmentId: number; - - @ManyToOne(() => Department, { nullable: true }) - @JoinColumn({ name: 'department_id' }) - department: Department; } diff --git a/apps/server/src/entities/user-department.entity.ts b/apps/server/src/entities/user-department.entity.ts deleted file mode 100644 index b093b3f..0000000 --- a/apps/server/src/entities/user-department.entity.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - ManyToOne, - JoinColumn, - Unique, -} from 'typeorm'; -import { User } from './user.entity'; -import { Department } from './department.entity'; - -@Entity('user_departments') -@Unique(['userId', 'departmentId']) -export class UserDepartment { - @PrimaryGeneratedColumn() - id: number; - - @Column({ name: 'user_id', type: 'integer' }) - userId: number; - - @ManyToOne(() => User, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'user_id' }) - user: User; - - @Column({ name: 'department_id', type: 'integer' }) - departmentId: number; - - @ManyToOne(() => Department, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'department_id' }) - department: Department; - - @Column({ name: 'is_default', type: 'boolean', default: false }) - isDefault: boolean; - - @CreateDateColumn({ name: 'created_at' }) - createdAt: Date; -} diff --git a/apps/server/src/expenses/expenses.service.ts b/apps/server/src/expenses/expenses.service.ts index 13701ea..9c1cf98 100644 --- a/apps/server/src/expenses/expenses.service.ts +++ b/apps/server/src/expenses/expenses.service.ts @@ -11,7 +11,7 @@ import { BatchRoomExpenseDto, } from './dto/expense.dto'; import { RoomsService } from '../rooms/rooms.service'; -import { CampusScope } from '../common/campus-scope'; + @Injectable() export class ExpensesService { @@ -20,7 +20,6 @@ export class ExpensesService { @InjectRepository(PersonalExpense) private personalExpRepo: Repository, @InjectRepository(Room) private roomRepo: Repository, @InjectRepository(Student) private studentRepo: Repository, - private readonly scope: CampusScope, ) {} // 宿舍费用 @@ -28,14 +27,10 @@ export class ExpensesService { const room = await this.roomRepo.findOne({ where: { id: dto.roomId } }); if (!room) throw new NotFoundException('宿舍不存在'); const entity = this.roomExpRepo.create({ ...dto, recordedBy: userId }); - entity.departmentId = room.departmentId; return this.roomExpRepo.save(entity); } async batchCreateRoomExpenses(dto: BatchRoomExpenseDto, userId?: number) { - const roomIds = [...new Set(dto.expenses.map((e) => e.roomId))]; - const rooms = await this.roomRepo.find({ where: { id: In(roomIds) } }); - const roomDeptMap = new Map(rooms.map((r) => [r.id, r.departmentId])); const entities = dto.expenses.map((e) => { const entity = this.roomExpRepo.create({ roomId: e.roomId, @@ -46,19 +41,16 @@ export class ExpensesService { periodEnd: dto.periodEnd, recordedBy: userId, }); - entity.departmentId = roomDeptMap.get(e.roomId)!; return entity; }); return this.roomExpRepo.save(entities); } async findRoomExpenses(query?: { roomId?: number; periodStart?: string; periodEnd?: string }) { - const scopeIds = await this.scope.getScopeDepartmentIds(); const qb = this.roomExpRepo .createQueryBuilder('e') .leftJoinAndSelect('e.room', 'room') .orderBy('e.createdAt', 'DESC'); - if (scopeIds) qb.andWhere('e.departmentId IN (:...scopeIds)', { scopeIds }); if (query?.roomId) qb.andWhere('e.roomId = :roomId', { roomId: query.roomId }); if (query?.periodStart) qb.andWhere('e.periodStart >= :ps', { ps: query.periodStart }); if (query?.periodEnd) qb.andWhere('e.periodEnd <= :pe', { pe: query.periodEnd }); @@ -94,14 +86,12 @@ export class ExpensesService { const student = await this.studentRepo.findOne({ where: { id: dto.studentId } }); if (!student) throw new NotFoundException('学生不存在'); const entity = this.personalExpRepo.create({ ...dto, recordedBy: userId }); - entity.departmentId = student.departmentId; return this.personalExpRepo.save(entity); } async findPersonalExpenses(query?: { studentId?: number }) { - let where: Record = {}; + const where: Record = {}; if (query?.studentId) where.studentId = query.studentId; - where = await this.scope.filter(where); return this.personalExpRepo.find({ where, relations: ['student'], diff --git a/apps/server/src/integration/dingtalk.service.ts b/apps/server/src/integration/dingtalk.service.ts index 0ab2952..4324951 100644 --- a/apps/server/src/integration/dingtalk.service.ts +++ b/apps/server/src/integration/dingtalk.service.ts @@ -3,13 +3,11 @@ * * 提供: * - OAuth2 access_token(新版 API + 缓存) - * - BFS 遍历所有部门 + 用户(带限流) * - 用户同步(自动建 Student + StudentDingMapping) */ import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { Department } from '../entities/department.entity'; import { Student } from '../entities/student.entity'; import { StudentDingMapping } from '../entities/student-ding-mapping.entity'; @@ -20,18 +18,6 @@ interface DingTalkTokenResponse { expireIn: number; } -interface SubDeptIdListResponse { - errcode: number; - errmsg: string; - result: { dept_id_list: number[] }; -} - -interface DepartmentDetailResponse { - errcode: number; - errmsg: string; - result: { dept_id: number; name: string; parent_id: number }; -} - interface DingTalkUserListResponse { errcode: number; errmsg: string; @@ -61,28 +47,6 @@ export interface DingTalkAttendanceResult { checkType: string; } -/** 钉钉部门树节点,供前端选择器使用 */ -export interface DingOrgTreeNode { - id: number; - name: string; - parentId: number; - children: DingOrgTreeNode[]; -} - -/** 钉钉部门树节点(含用户),供同步用户选择器使用 */ -export interface DingOrgTreeNodeWithUsers { - id: number; - name: string; - parentId: number; - children: DingOrgTreeNodeWithUsers[]; - users: Array<{ - userid: string; - name: string; - mobile: string; - deptIds: number[]; - }>; -} - // ── 考勤排班 API 类型 ── /** 班次卡段打卡时间 */ @@ -182,8 +146,6 @@ export class DingTalkService { private static readonly MIN_INTERVAL = 1000 / DingTalkService.RATE_LIMIT; constructor( - @InjectRepository(Department) - private readonly deptRepo: Repository, @InjectRepository(Student) private readonly studentRepo: Repository, @InjectRepository(StudentDingMapping) @@ -222,65 +184,6 @@ export class DingTalkService { return this.accessToken; } - // ═══════════════════════════════════════════ - // Department BFS — 对齐 gongxue-dorm-sys getAllSubDepartmentIds - // ═══════════════════════════════════════════ - - private async getAllDeptIds(token: string, rootDeptId = 1): Promise { - const ids: number[] = []; - const queue: number[] = [rootDeptId]; - - while (queue.length > 0) { - const deptId = queue.shift()!; - ids.push(deptId); - - try { - await this.rateLimit(); - const res = await fetch( - `https://oapi.dingtalk.com/topapi/v2/department/listsubid?access_token=${token}`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ dept_id: deptId }), - }, - ); - const body: SubDeptIdListResponse = await res.json(); - if (body.errcode === 0 && body.result?.dept_id_list) { - queue.push(...body.result.dept_id_list); - } - } catch (e) { - this.logger.error(`获取部门 ${deptId} 子部门失败: ${(e as Error).message}`); - } - } - return ids; - } - - - // ═══════════════════════════════════════════ - // Department detail - // ═══════════════════════════════════════════ - - private async getDeptDetail( - token: string, - deptId: number, - ): Promise<{ dept_id: number; name: string; parent_id: number } | null> { - try { - const res = await fetch( - `https://oapi.dingtalk.com/topapi/v2/department/get?access_token=${token}`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ dept_id: deptId, language: 'zh_CN' }), - }, - ); - const body: DepartmentDetailResponse = await res.json(); - return body.errcode === 0 ? body.result : null; - } catch (e) { - this.logger.error(`获取部门 ${deptId} 详情失败: ${(e as Error).message}`); - return null; - } - } - // ═══════════════════════════════════════════ // Users by department — 对齐 gongxue-dorm-sys getUsersByDepartment // ═══════════════════════════════════════════ @@ -334,191 +237,39 @@ export class DingTalkService { const t0 = Date.now(); const token = await this.getAccessToken(); - // ── Step 1: BFS traverse all departments ── - this.logger.log('开始 BFS 遍历钉钉部门...'); - const deptIds = await this.getAllDeptIds(token, rootDeptId); - this.logger.log(`共发现 ${deptIds.length} 个部门`); - - // ── Step 2: Sync departments ── - let deptCount = 0; - for (let i = 0; i < deptIds.length; i++) { - const deptId = deptIds[i]; - if (i > 0) await this.delay(i); - - const detail = await this.getDeptDetail(token, deptId); - if (!detail) continue; - - const sourceId = String(detail.dept_id); - let dept = await this.deptRepo.findOne({ where: { source: 'dingtalk', sourceId } }); - if (dept) { - dept.name = detail.name; - if (detail.parent_id) dept.parentSourceId = String(detail.parent_id); - } else { - dept = this.deptRepo.create({ - name: detail.name, - source: 'dingtalk', - sourceId, - type: 'department', - } as Department); - if (detail.parent_id) dept.parentSourceId = String(detail.parent_id); - deptCount++; - } - await this.deptRepo.save(dept); - } - - // Set parent relationships - const syncedDepts = await this.deptRepo.find({ where: { source: 'dingtalk' } }); - const idMap = new Map(syncedDepts.map((d) => [d.sourceId, d.id])); - for (const dept of syncedDepts) { - if (dept.parentSourceId && idMap.has(dept.parentSourceId)) { - dept.parentId = idMap.get(dept.parentSourceId)!; - } else if (dept.parentSourceId === '1' || dept.parentSourceId === '0') { - dept.parentId = undefined as any; - } - } - await this.deptRepo.save(syncedDepts); - - - // ── Step 3: Sync users per department ── + // ── Sync users from root department ── let userCount = 0; const seenUserIds = new Set(); - for (let i = 0; i < deptIds.length; i++) { - const deptId = deptIds[i]; - const dingUsers = await this.getDeptUsers(token, deptId); + const dingUsers = await this.getDeptUsers(token, rootDeptId); + for (const du of dingUsers) { + if (seenUserIds.has(du.userid)) continue; + seenUserIds.add(du.userid); - for (const du of dingUsers) { - if (seenUserIds.has(du.userid)) continue; - seenUserIds.add(du.userid); - - await this.syncOneUser(du); - userCount++; - } + await this.syncOneUser(du); + userCount++; } this.logger.log( - `钉钉同步完成: ${deptCount} 个新部门, ${userCount} 个用户, API 请求 ${this.apiRequestCount} 次, 耗时 ${Date.now() - t0}ms`, + `钉钉同步完成: ${userCount} 个用户, API 请求 ${this.apiRequestCount} 次, 耗时 ${Date.now() - t0}ms`, ); - return { deptCount, userCount }; + return { deptCount: 0, userCount }; } /** - * 获取钉钉组织部门树(只含部门,不含用户),供前端选择同步起点。 - * 返回从指定 rootDeptId 开始的树;默认根部门 1。 + * 获取钉钉组织部门树(只含部门,不含用户)。 + * ponytail: Department entity removed; returns empty array. */ - async fetchOrgTree(rootDeptId = 1): Promise { - if (!this.configured) { - throw new ServiceUnavailableException('钉钉未配置'); - } - const token = await this.getAccessToken(); - const deptIds = await this.getAllDeptIds(token, rootDeptId); - - // 拉每个部门详情 - const nodes: DingOrgTreeNode[] = []; - for (let i = 0; i < deptIds.length; i++) { - if (i > 0) await this.delay(i); - const detail = await this.getDeptDetail(token, deptIds[i]); - if (detail) { - nodes.push({ - id: detail.dept_id, - name: detail.name, - parentId: detail.parent_id, - children: [], - }); - } - } - - // 组装成树 - const map = new Map(); - nodes.forEach((n) => map.set(n.id, n)); - const roots: DingOrgTreeNode[] = []; - for (const node of nodes) { - const parent = map.get(node.parentId); - if (parent && node.id !== rootDeptId) { - parent.children.push(node); - } else { - roots.push(node); - } - } - return roots; + async fetchOrgTree(_rootDeptId = 1): Promise<[]> { + return []; } /** - * 获取钉钉组织部门树(含用户),供前端同步用户选择器使用。 - * 返回从指定 rootDeptId 开始的树,每个部门节点含 users 数组。 + * 获取钉钉组织部门树(含用户)。 + * ponytail: Department entity removed; returns empty array. */ - async fetchOrgTreeWithUsers(rootDeptId = 1): Promise { - if (!this.configured) { - throw new ServiceUnavailableException('钉钉未配置'); - } - const token = await this.getAccessToken(); - - const deptIds = await this.getAllDeptIds(token, rootDeptId); - - // 拉每个部门详情 - const nodes: DingOrgTreeNodeWithUsers[] = []; - // Before dedup: collect deptIds per user - const userDeptMap = new Map(); - - for (let i = 0; i < deptIds.length; i++) { - if (i > 0) await this.delay(i); - const detail = await this.getDeptDetail(token, deptIds[i]); - if (!detail) continue; - - // 拉该部门下的用户 - const dingUsers = await this.getDeptUsers(token, deptIds[i]); - this.logger.log(`[dingtalk] dept ${deptIds[i]} (${detail.name}): ${dingUsers.length} users`); - - nodes.push({ - id: detail.dept_id, - name: detail.name, - parentId: detail.parent_id, - children: [], - users: dingUsers.map((u) => ({ - userid: u.userid, - name: u.name, - mobile: u.mobile, - deptIds: [], - })), - }); - - // Record which departments each user belongs to - for (const u of dingUsers) { - if (!userDeptMap.has(u.userid)) { - userDeptMap.set(u.userid, []); - } - userDeptMap.get(u.userid)!.push(detail.dept_id); - } - } - - // 全局去重:同一个 dingUserId 可能在多个部门出现 - const seenUserIds = new Set(); - for (const node of nodes) { - node.users = node.users - .filter((u) => { - if (seenUserIds.has(u.userid)) return false; - seenUserIds.add(u.userid); - return true; - }) - .map((u) => ({ - ...u, - deptIds: userDeptMap.get(u.userid) || [], - })); - } - - // 组装成树(父节点可能已被过滤,缺失的父节点 → 节点提升为根) - const map = new Map(); - nodes.forEach((n) => map.set(n.id, n)); - const roots: DingOrgTreeNodeWithUsers[] = []; - for (const node of nodes) { - const parent = map.get(node.parentId); - if (parent && node.id !== rootDeptId) { - parent.children.push(node); - } else { - roots.push(node); - } - } - return roots; + async fetchOrgTreeWithUsers(_rootDeptId = 1): Promise<[]> { + return []; } // ═══════════════════════════════════════════ diff --git a/apps/server/src/integration/integration.module.ts b/apps/server/src/integration/integration.module.ts index 9f4db7e..6c98c9e 100644 --- a/apps/server/src/integration/integration.module.ts +++ b/apps/server/src/integration/integration.module.ts @@ -1,11 +1,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Department, User, Student, StudentDingMapping, Class } from '../entities'; +import { User, Student, StudentDingMapping, Class } from '../entities'; import { DingTalkService } from './dingtalk.service'; import { WeComService } from './wecom.service'; @Module({ - imports: [TypeOrmModule.forFeature([Department, User, Student, StudentDingMapping, Class])], + imports: [TypeOrmModule.forFeature([User, Student, StudentDingMapping, Class])], providers: [DingTalkService, WeComService], exports: [DingTalkService, WeComService], }) diff --git a/apps/server/src/integration/wecom.service.ts b/apps/server/src/integration/wecom.service.ts index e8b61ec..f8bcfbd 100644 --- a/apps/server/src/integration/wecom.service.ts +++ b/apps/server/src/integration/wecom.service.ts @@ -1,7 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { Department } from '../entities/department.entity'; import { User } from '../entities/user.entity'; interface WeComTokenResponse { @@ -35,8 +34,6 @@ export class WeComService { private tokenExpiresAt = 0; constructor( - @InjectRepository(Department) - private readonly deptRepo: Repository, @InjectRepository(User) private readonly userRepo: Repository, ) {} @@ -97,45 +94,14 @@ export class WeComService { return body.userlist; } - async syncAll(): Promise<{ deptCount: number; userCount: number }> { + async syncAll(): Promise<{ userCount: number }> { if (!this.configured) { this.logger.warn('WeCom not configured (WECOM_CORP_ID / WECOM_CORP_SECRET missing), skipping sync'); - return { deptCount: 0, userCount: 0 }; + return { userCount: 0 }; } const token = await this.getAccessToken(); const wxDepts = await this.fetchDepartments(token); - let deptCount = 0; - for (const wd of wxDepts) { - const sourceId = String(wd.id); - let dept = await this.deptRepo.findOne({ where: { source: 'wecom', sourceId } }); - if (dept) { - dept.name = wd.name; - dept.parentSourceId = (wd.parentid ? String(wd.parentid) : undefined) as any; - } else { - dept = this.deptRepo.create({ - name: wd.name, - source: 'wecom', - sourceId, - parentSourceId: (wd.parentid ? String(wd.parentid) : undefined), - type: 'department', - }); - deptCount++; - } - await this.deptRepo.save(dept); - } - - const syncedDepts = await this.deptRepo.find({ where: { source: 'wecom' } }); - const idMap = new Map(syncedDepts.map((d) => [d.sourceId, d.id])); - for (const dept of syncedDepts) { - if (dept.parentSourceId && idMap.has(dept.parentSourceId)) { - dept.parentId = idMap.get(dept.parentSourceId)!; - } else if (dept.parentSourceId === '0' || dept.parentSourceId === '1') { - dept.parentId = undefined as any; - } - } - await this.deptRepo.save(syncedDepts); - let userCount = 0; const seenUserIds = new Set(); for (const wd of wxDepts) { @@ -159,7 +125,7 @@ export class WeComService { } } - this.logger.log(`WeCom sync done: ${deptCount} new depts, ${userCount} new users`); - return { deptCount, userCount }; + this.logger.log(`WeCom sync done: ${userCount} new users`); + return { userCount }; } } diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 95ab288..b08dc0c 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -1,16 +1,12 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; -import { DataSource } from 'typeorm'; import { AppModule } from './app.module'; -import { seedDefaultCampus } from './departments/seed'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.setGlobalPrefix('api'); app.enableCors(); app.useGlobalPipes(new ValidationPipe({ transform: true, whitelist: true })); - const dataSource = app.get(DataSource); - await seedDefaultCampus(dataSource); await app.listen(process.env.PORT ?? 3000); console.log(`Server running on http://localhost:${process.env.PORT ?? 3000}`); } diff --git a/apps/server/src/occupancies/occupancies.service.ts b/apps/server/src/occupancies/occupancies.service.ts index 8fcc036..f750672 100644 --- a/apps/server/src/occupancies/occupancies.service.ts +++ b/apps/server/src/occupancies/occupancies.service.ts @@ -17,7 +17,7 @@ import { Locker } from '../entities/locker.entity'; import { Deposit } from '../entities/deposit.entity'; import { CheckInDto, CheckOutDto, TransferRoomDto } from './dto/occupancy.dto'; import { RoomsService } from '../rooms/rooms.service'; -import { CampusScope } from '../common/campus-scope'; + @Injectable() export class OccupanciesService { @@ -29,7 +29,6 @@ export class OccupanciesService { @InjectRepository(Bed) private bedRepo: Repository, @InjectRepository(Locker) private lockerRepo: Repository, private dataSource: DataSource, - private readonly scope: CampusScope, ) {} async findAll(query?: { roomId?: number; studentId?: number; active?: boolean }) { @@ -40,10 +39,6 @@ export class OccupanciesService { .leftJoinAndSelect('o.bed', 'bed') .leftJoinAndSelect('o.locker', 'locker') .orderBy('o.checkInDate', 'DESC'); - const scopeIds = await this.scope.getScopeDepartmentIds(); - if (scopeIds) { - qb.andWhere('o.departmentId IN (:...scopeIds)', { scopeIds }); - } if (query?.roomId) qb.andWhere('o.roomId = :roomId', { roomId: query.roomId }); if (query?.studentId) qb.andWhere('o.studentId = :studentId', { studentId: query.studentId }); if (query?.active) qb.andWhere('o.checkOutDate IS NULL'); @@ -97,7 +92,6 @@ export class OccupanciesService { bedId: dto.bedId, lockerId: dto.lockerId, }); - occ.departmentId = room.departmentId; const saved = await this.repo.save(occ); // 更新床位/柜子状态 diff --git a/apps/server/src/rooms/dto/room.dto.ts b/apps/server/src/rooms/dto/room.dto.ts index 1fe9cd1..7f53dea 100644 --- a/apps/server/src/rooms/dto/room.dto.ts +++ b/apps/server/src/rooms/dto/room.dto.ts @@ -28,9 +28,6 @@ export class CreateRoomDto { @IsNumber() monthlyRate?: number; - @IsOptional() - @IsInt() - departmentId?: number; } export class UpdateRoomDto { diff --git a/apps/server/src/rooms/rooms.controller.ts b/apps/server/src/rooms/rooms.controller.ts index f1c501e..3576a18 100644 --- a/apps/server/src/rooms/rooms.controller.ts +++ b/apps/server/src/rooms/rooms.controller.ts @@ -360,8 +360,7 @@ export class RoomsController { monthlyRate, }); }); - const departmentId = req.headers?.['x-campus-id'] ? parseInt(String(req.headers['x-campus-id']), 10) || undefined : undefined; - const result = await this.service.batchImport(rows, departmentId); + const result = await this.service.batchImport(rows); await this.logService.log({ userId: req.user?.id, username: req.user?.username, diff --git a/apps/server/src/rooms/rooms.service.ts b/apps/server/src/rooms/rooms.service.ts index 262b304..1afb242 100644 --- a/apps/server/src/rooms/rooms.service.ts +++ b/apps/server/src/rooms/rooms.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Like, IsNull, Not, In, LessThanOrEqual, MoreThanOrEqual } from 'typeorm'; -import { CampusScope } from '../common/campus-scope'; + import { Room } from '../entities/room.entity'; import { Occupancy } from '../entities/occupancy.entity'; import { RoomExpense } from '../entities/room-expense.entity'; @@ -17,7 +17,6 @@ export class RoomsService { @InjectRepository(Room) private repo: Repository, @InjectRepository(Occupancy) private occRepo: Repository, @InjectRepository(RoomExpense) private roomExpRepo: Repository, - private readonly scope: CampusScope, @InjectRepository(Bed) private bedRepo: Repository, @InjectRepository(Locker) private lockerRepo: Repository, ) {} @@ -69,8 +68,7 @@ export class RoomsService { const where: any = {}; if (query?.building) where.building = query.building; if (!query?.includeArchived) where.status = Not('archived'); - const filteredWhere = await this.scope.filter(where); - return this.repo.find({ where: filteredWhere, order: { roomNumber: 'ASC' } }); + return this.repo.find({ where, order: { roomNumber: 'ASC' } }); } async findOne(id: number) { @@ -92,8 +90,7 @@ export class RoomsService { async getRoomOverview(query?: { includeArchived?: boolean }) { const where: any = {}; if (!query?.includeArchived) where.status = Not('archived'); - const filteredWhere = await this.scope.filter(where); - const rooms = await this.repo.find({ where: filteredWhere, order: { building: 'ASC', roomNumber: 'ASC' } }); + const rooms = await this.repo.find({ where, order: { building: 'ASC', roomNumber: 'ASC' } }); const result: any[] = []; for (const room of rooms) { const count = await this.occRepo.count({ @@ -113,7 +110,6 @@ export class RoomsService { roomType: dto.roomType ?? parsed.roomType, capacity: dto.capacity ?? parsed.capacity, }); - if (dto.departmentId) entity.departmentId = dto.departmentId; return this.repo.save(entity); } @@ -184,20 +180,17 @@ export class RoomsService { // 实时视图排除已归档房间;历史视图不排除——当时有人住的房间即使现在已归档也应显示。 const rooms = await this.repo.find({ - where: await this.scope.filter(isHistorical ? {} : { status: Not('archived') }), + where: isHistorical ? {} : { status: Not('archived') }, order: { building: 'ASC', roomNumber: 'ASC' }, }); - // scope.filter() produces identical scope conditions within the same request; - // extract once and spread to avoid redundant calls. - const scopeWhere = await this.scope.filter({}); const occupancies = await this.occRepo.find({ where: isHistorical ? [ - { ...scopeWhere, checkInDate: LessThanOrEqual(targetDate), checkOutDate: IsNull() }, - { ...scopeWhere, checkInDate: LessThanOrEqual(targetDate), checkOutDate: MoreThanOrEqual(targetDate) }, + { checkInDate: LessThanOrEqual(targetDate), checkOutDate: IsNull() }, + { checkInDate: LessThanOrEqual(targetDate), checkOutDate: MoreThanOrEqual(targetDate) }, ] - : { ...scopeWhere, checkOutDate: IsNull() }, + : { checkOutDate: IsNull() }, relations: ['student', 'tenant'], order: { checkInDate: 'ASC' }, }); @@ -297,7 +290,6 @@ export class RoomsService { rentalCategory?: string; monthlyRate?: number; }[], - departmentId?: number, ) { let imported = 0; let skipped = 0; @@ -322,7 +314,6 @@ export class RoomsService { roomType: row.roomType || parsed.roomType || undefined, rentalCategory: row.rentalCategory || undefined, monthlyRate: row.monthlyRate ?? undefined, - departmentId: departmentId ?? undefined, }), ); imported++; diff --git a/apps/server/src/schedules/schedules.service.spec.ts b/apps/server/src/schedules/schedules.service.spec.ts index c5beb42..53067ff 100644 --- a/apps/server/src/schedules/schedules.service.spec.ts +++ b/apps/server/src/schedules/schedules.service.spec.ts @@ -6,7 +6,6 @@ import { SchedulesService } from './schedules.service'; import { ClassSchedule, ScheduleType } from '../entities/class-schedule.entity'; import { ClassroomRental } from '../entities/classroom-rental.entity'; import { Class } from '../entities/class.entity'; -import { CampusScope } from '../common/campus-scope'; /** Build a mock query-builder where each chain method returns `this`. */ function mockQueryBuilder(results: T[] = []) { @@ -36,7 +35,6 @@ describe('SchedulesService — checkConflict', () => { { provide: getRepositoryToken(ClassSchedule), useValue: mockRepo }, { provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } }, { provide: getRepositoryToken(ClassroomRental), useValue: { createQueryBuilder: jest.fn() } }, - { provide: CampusScope, useValue: { getScopeDepartmentIds: jest.fn().mockResolvedValue(null), filter: jest.fn((w: unknown) => w) } }, ], }).compile(); @@ -138,13 +136,6 @@ describe('SchedulesService — getClassroomOccupancy', () => { { provide: getRepositoryToken(ClassSchedule), useValue: { createQueryBuilder: jest.fn() } }, { provide: getRepositoryToken(Class), useValue: { find: jest.fn().mockResolvedValue([]) } }, { provide: getRepositoryToken(ClassroomRental), useValue: { createQueryBuilder: jest.fn() } }, - { - provide: CampusScope, - useValue: { - getScopeDepartmentIds: jest.fn().mockResolvedValue(null), - filter: jest.fn((w: unknown) => w), - }, - }, ], }).compile(); diff --git a/apps/server/src/schedules/schedules.service.ts b/apps/server/src/schedules/schedules.service.ts index 7e76499..1c2c7dc 100644 --- a/apps/server/src/schedules/schedules.service.ts +++ b/apps/server/src/schedules/schedules.service.ts @@ -2,7 +2,7 @@ import { Injectable, NotFoundException, ConflictException } from '@nestjs/common import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ClassSchedule, Class, ClassroomRental } from '../entities'; -import { CampusScope } from '../common/campus-scope'; + import { CreateScheduleDto, UpdateScheduleDto, @@ -15,7 +15,6 @@ export class SchedulesService { constructor( @InjectRepository(ClassSchedule) private readonly scheduleRepo: Repository, - private readonly scope: CampusScope, @InjectRepository(Class) private readonly classRepo: Repository, @InjectRepository(ClassroomRental) private readonly rentalRepo: Repository, @@ -23,10 +22,6 @@ export class SchedulesService { async findAll(query: QueryScheduleDto) { const qb = this.scheduleRepo.createQueryBuilder('cs'); - const scopeIds = await this.scope.getScopeDepartmentIds(); - if (scopeIds) { - qb.andWhere('cs.departmentId IN (:...scopeIds)', { scopeIds }); - } if (query.classroomId) qb.andWhere('cs.classroomId = :classroomId', { classroomId: query.classroomId }); if (query.classId) qb.andWhere('cs.classId = :classId', { classId: query.classId }); @@ -50,9 +45,6 @@ export class SchedulesService { await this.checkConflict(dto.classroomId, dto.weekDay, dto.startTime, dto.endTime, dto.startDate, dto.endDate); const schedule = this.scheduleRepo.create(dto); - if (dto.departmentId) { - schedule.departmentId = dto.departmentId; - } const saved = await this.scheduleRepo.save(schedule); return this.findOne(saved.id); } @@ -130,10 +122,6 @@ export class SchedulesService { async getWeeklyView(query: WeeklyViewQueryDto) { const qb = this.scheduleRepo.createQueryBuilder('cs'); - const scopeIds = await this.scope.getScopeDepartmentIds(); - if (scopeIds) { - qb.andWhere('cs.departmentId IN (:...scopeIds)', { scopeIds }); - } if (query.classroomId) { qb.andWhere('cs.classroomId = :classroomId', { classroomId: query.classroomId }); } diff --git a/apps/server/src/seed/seed-dev.service.ts b/apps/server/src/seed/seed-dev.service.ts index c403f12..1b62b2a 100644 --- a/apps/server/src/seed/seed-dev.service.ts +++ b/apps/server/src/seed/seed-dev.service.ts @@ -3,14 +3,14 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as bcrypt from 'bcryptjs'; import { - Department, DepartmentType, User, Role, Permission, + User, Role, Permission, Tenant, Student, Room, Classroom, Occupancy, RoomExpense, ExpenseType, Class, ClassStudent, ClassTeacher, ClassSchedule, Bill, BillItem, Deposit, AttendanceRecord, ClassroomRental, StudentProfile, StudentEnrollment, ExamScore, - LearningRecord, UserDepartment, TeacherRoleType, + LearningRecord, TeacherRoleType, ClassType, ClassStatus, ScheduleType, } from '../entities'; @@ -29,7 +29,7 @@ function pick(arr: T[]): T { @Injectable() export class SeedDevService { private readonly logger = new Logger(SeedDevService.name); - private deptId = 1; + private cachedStudents: Student[] = []; private cachedRooms: Room[] = []; private cachedClassrooms: Classroom[] = []; @@ -38,11 +38,9 @@ export class SeedDevService { private cachedTenants: Tenant[] = []; constructor( - @InjectRepository(Department) private deptRepo: Repository, @InjectRepository(Permission) private permRepo: Repository, @InjectRepository(Role) private roleRepo: Repository, @InjectRepository(User) private userRepo: Repository, - @InjectRepository(UserDepartment) private userDeptRepo: Repository, @InjectRepository(Tenant) private tenantRepo: Repository, @InjectRepository(Student) private studentRepo: Repository, @InjectRepository(Room) private roomRepo: Repository, @@ -77,11 +75,11 @@ export class SeedDevService { // ═══════════════════ Layer 0 ══════════════════════════ await this.seedPermissions(); await this.seedExpenseTypes(); - await this.seedDepartments(); + // ═══════════════════ Layer 1 ══════════════════════════ await this.seedRoles(); - await this.seedUsersAndDepartments(); + await this.seedUsers(); // ═══════════════════ Layer 2 ══════════════════════════ await this.seedTenants(); @@ -173,33 +171,7 @@ export class SeedDevService { this.logger.log(` ✓ ${types.length} expense types`); } - // ── 0c: departments ────────────────────────────────── - private async seedDepartments(): Promise { - // Reuse existing campus if seedDefaultCampus() already created it in main.ts - let campus = await this.deptRepo.findOne({ where: { type: DepartmentType.CAMPUS } }); - if (!campus) { - campus = await this.deptRepo.save({ - name: '主校区', - type: DepartmentType.CAMPUS, - sortOrder: 0, - }); - } - const subCount = await this.deptRepo.count({ where: { parentId: campus.id } }); - if (subCount === 0) { - await this.deptRepo.save([ - { name: '教务部', type: DepartmentType.DEPARTMENT, parentId: campus.id, sortOrder: 1 }, - { name: '宿管部', type: DepartmentType.DEPARTMENT, parentId: campus.id, sortOrder: 2 }, - { name: '财务部', type: DepartmentType.DEPARTMENT, parentId: campus.id, sortOrder: 3 }, - { name: '恭学专升本', type: DepartmentType.DEPARTMENT, parentId: campus.id, sortOrder: 4 }, - { name: '26定向', type: DepartmentType.DEPARTMENT, parentId: campus.id, sortOrder: 5 }, - { name: '续住', type: DepartmentType.DEPARTMENT, parentId: campus.id, sortOrder: 6 }, - ]); - } - this.deptId = campus.id; - const total = await this.deptRepo.count(); - this.logger.log(` ✓ ${total} departments`); - } // ── 1a: roles ──────────────────────────────────────── @@ -236,9 +208,9 @@ export class SeedDevService { this.logger.log(` ✓ ${roles.length} roles`); } - // ── 1b: users + user_departments ───────────────────── + // ── 1b: users ─────────────────────────────────────── - private async seedUsersAndDepartments(): Promise { + private async seedUsers(): Promise { const hash = await bcrypt.hash('123456', 10); const roles = await this.roleRepo.find(); const superAdminRole = roles.find((r) => r.name === '超管'); @@ -263,7 +235,6 @@ export class SeedDevService { roles: u.roles, }); saved.push(user); - await this.userDeptRepo.save({ userId: user.id, departmentId: this.deptId, isDefault: true }); } // Load all users for later seed steps to reference this.cachedUsers = await this.userRepo.find(); @@ -285,36 +256,36 @@ export class SeedDevService { private async seedRooms(): Promise { // Real data pattern: 单人间 (2号楼5层) + 四人间 (3/4/5/6号楼1-2层) - const rooms: Array<{ roomNumber: string; building: string; floor: number; capacity: number; status: string; roomType: string; gender: string; rentalCategory: string; monthlyRate: number; departmentId: number }> = [ + const rooms: Array<{ roomNumber: string; building: string; floor: number; capacity: number; status: string; roomType: string; gender: string; rentalCategory: string; monthlyRate: number }> = [ // 2号楼5层 单人间 pattern (real data: 2-502 through 2-519) - { roomNumber: '2-502', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-503', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-504', building: '2号楼', floor: 5, capacity: 1, status: 'available', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-505', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-506', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-507', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-508', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-509', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-510', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-511', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-512', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-513', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-515', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-516', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-517', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-518', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, - { roomNumber: '2-519', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800, departmentId: this.deptId }, + { roomNumber: '2-502', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-503', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-504', building: '2号楼', floor: 5, capacity: 1, status: 'available', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-505', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-506', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-507', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-508', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-509', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-510', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-511', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '男', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-512', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-513', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-515', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-516', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-517', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-518', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, + { roomNumber: '2-519', building: '2号楼', floor: 5, capacity: 1, status: 'full', roomType: '单人间', gender: '女', rentalCategory: 'long', monthlyRate: 800 }, // 1号楼 家庭房 - { roomNumber: '1-2-301', building: '1号楼', floor: 3, capacity: 2, status: 'full', roomType: '家庭房', gender: '男', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, + { roomNumber: '1-2-301', building: '1号楼', floor: 3, capacity: 2, status: 'full', roomType: '家庭房', gender: '男', rentalCategory: 'long', monthlyRate: 1200 }, // 四人间 pattern - { roomNumber: '3-106', building: '3号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, - { roomNumber: '3-107', building: '3号楼', floor: 1, capacity: 4, status: 'full', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, - { roomNumber: '4-107', building: '4号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, - { roomNumber: '4-111', building: '4号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, - { roomNumber: '4-201', building: '4号楼', floor: 2, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, - { roomNumber: '4-204', building: '4号楼', floor: 2, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, - { roomNumber: '5-109', building: '5号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, - { roomNumber: '6-116', building: '6号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200, departmentId: this.deptId }, + { roomNumber: '3-106', building: '3号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200 }, + { roomNumber: '3-107', building: '3号楼', floor: 1, capacity: 4, status: 'full', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200 }, + { roomNumber: '4-107', building: '4号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 }, + { roomNumber: '4-111', building: '4号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 }, + { roomNumber: '4-201', building: '4号楼', floor: 2, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 }, + { roomNumber: '4-204', building: '4号楼', floor: 2, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 }, + { roomNumber: '5-109', building: '5号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '女', rentalCategory: 'long', monthlyRate: 1200 }, + { roomNumber: '6-116', building: '6号楼', floor: 1, capacity: 4, status: 'available', roomType: '四人间', gender: '男', rentalCategory: 'long', monthlyRate: 1200 }, ]; const saved = await this.roomRepo.save(rooms); @@ -326,10 +297,10 @@ export class SeedDevService { private async seedClassrooms(): Promise { const classrooms = [ - { name: '102', building: 'a座', floor: 1, capacity: 30, roomType: '大', supervisor: '陈浩', departmentId: this.deptId }, - { name: '201', building: 'a座', floor: 2, capacity: 30, roomType: '大', supervisor: '刘老师', departmentId: this.deptId }, - { name: '202', building: 'a座', floor: 2, capacity: 25, roomType: '次大', supervisor: '刘老师', departmentId: this.deptId }, - { name: '301', building: 'b座', floor: 3, capacity: 20, roomType: '小', supervisor: '黄老师', departmentId: this.deptId }, + { name: '102', building: 'a座', floor: 1, capacity: 30, roomType: '大', supervisor: '陈浩' }, + { name: '201', building: 'a座', floor: 2, capacity: 30, roomType: '大', supervisor: '刘老师' }, + { name: '202', building: 'a座', floor: 2, capacity: 25, roomType: '次大', supervisor: '刘老师' }, + { name: '301', building: 'b座', floor: 3, capacity: 20, roomType: '小', supervisor: '黄老师' }, ]; const saved = await this.classroomRepo.save(classrooms); this.cachedClassrooms = saved; @@ -340,38 +311,38 @@ export class SeedDevService { private async seedStudents(): Promise { // Real data pattern: organization fields like 恭学专升本, 26定向, 续住, etc. - const students: Array<{ name: string; phone: string; studentNo: string; gender: string; ethnicity: string; organization: string; supervisor: string; departmentId: number; status: string }> = [ + const students: Array<{ name: string; phone: string; studentNo: string; gender: string; ethnicity: string; organization: string; supervisor: string; status: string }> = [ // 26定向 students - { name: '聂天羽', phone: '12345678912', studentNo: '123456', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '艾柯丽努尔·艾买尔江', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '陈昊天', phone: '', studentNo: '', gender: '男', ethnicity: '汉', organization: '续住', supervisor: '糕糕', departmentId: this.deptId, status: 'active' }, - { name: '赵璟涵', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '周子涵', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '秦婧怡', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑文', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '石欣欣', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '李光铄', phone: '', studentNo: '', gender: '男', ethnicity: '汉', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '田芸竹', phone: '12345678920', studentNo: '123464', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '荚欣语', phone: '12345678921', studentNo: '123465', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '刘倬宁', phone: '12345678922', studentNo: '123466', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '柴高星', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑专+专冲', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '焦怡菲', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑期+专冲+年前文化', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '王姿璇', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '寇星彤', phone: '12345678926', studentNo: '123470', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', departmentId: this.deptId, status: 'active' }, + { name: '聂天羽', phone: '12345678912', studentNo: '123456', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' }, + { name: '艾柯丽努尔·艾买尔江', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' }, + { name: '陈昊天', phone: '', studentNo: '', gender: '男', ethnicity: '汉', organization: '续住', supervisor: '糕糕', status: 'active' }, + { name: '赵璟涵', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' }, + { name: '周子涵', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' }, + { name: '秦婧怡', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑文', supervisor: '', status: 'active' }, + { name: '石欣欣', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享', supervisor: '', status: 'active' }, + { name: '李光铄', phone: '', studentNo: '', gender: '男', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' }, + { name: '田芸竹', phone: '12345678920', studentNo: '123464', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' }, + { name: '荚欣语', phone: '12345678921', studentNo: '123465', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' }, + { name: '刘倬宁', phone: '12345678922', studentNo: '123466', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' }, + { name: '柴高星', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑专+专冲', supervisor: '', status: 'active' }, + { name: '焦怡菲', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26尊享+暑期+专冲+年前文化', supervisor: '', status: 'active' }, + { name: '王姿璇', phone: '', studentNo: '', gender: '女', ethnicity: '汉', organization: '26定向', supervisor: '', status: 'active' }, + { name: '寇星彤', phone: '12345678926', studentNo: '123470', gender: '女', ethnicity: '汉', organization: '恭学专升本', supervisor: '', status: 'active' }, // 续住 students - { name: '於嘉丽', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '续住', supervisor: '泡泡', departmentId: this.deptId, status: 'active' }, - { name: '郑斌', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', departmentId: this.deptId, status: 'active' }, - { name: '仵梓钰', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '续住', supervisor: '方方', departmentId: this.deptId, status: 'active' }, - { name: '覃鼎浩', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '云熙', departmentId: this.deptId, status: 'active' }, - { name: '常智禹', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '方方', departmentId: this.deptId, status: 'active' }, - { name: '郭庆泉', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', departmentId: this.deptId, status: 'active' }, - { name: '孙立欣', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', departmentId: this.deptId, status: 'active' }, - { name: '韩尧祖', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '陈亚津', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '孟思妍', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '武嘉怡', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '刘禹含', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '杜瑾慧', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, - { name: '孟凡志', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '26定向', supervisor: '', departmentId: this.deptId, status: 'active' }, + { name: '於嘉丽', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '续住', supervisor: '泡泡', status: 'active' }, + { name: '郑斌', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', status: 'active' }, + { name: '仵梓钰', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '续住', supervisor: '方方', status: 'active' }, + { name: '覃鼎浩', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '云熙', status: 'active' }, + { name: '常智禹', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '方方', status: 'active' }, + { name: '郭庆泉', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', status: 'active' }, + { name: '孙立欣', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '续住', supervisor: '糕糕', status: 'active' }, + { name: '韩尧祖', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '', supervisor: '', status: 'active' }, + { name: '陈亚津', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '', supervisor: '', status: 'active' }, + { name: '孟思妍', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' }, + { name: '武嘉怡', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' }, + { name: '刘禹含', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' }, + { name: '杜瑾慧', phone: '', studentNo: '', gender: '女', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' }, + { name: '孟凡志', phone: '', studentNo: '', gender: '男', ethnicity: '汉族', organization: '26定向', supervisor: '', status: 'active' }, ]; const saved = await this.studentRepo.save(students); @@ -394,7 +365,6 @@ export class SeedDevService { for (const c of clsData) { const saved = await this.classRepo.save({ ...c, - departmentId: this.deptId, startDate: '2026-04-01', endDate: '2026-08-31', maxStudents: 30, @@ -481,7 +451,7 @@ export class SeedDevService { { studentIdx: 28, roomIdx: 18, checkInDate: '2026-06-04', notes: '' }, // 孟凡志 -> 3-107 ]; - const occupancies: Array<{ studentId: number; roomId: number; checkInDate: string; billingStartDate: string; rentalType: string; notes: string; departmentId: number; checkOutDate?: string; billingEndDate?: string }> = []; + const occupancies: Array<{ studentId: number; roomId: number; checkInDate: string; billingStartDate: string; rentalType: string; notes: string; checkOutDate?: string; billingEndDate?: string }> = []; for (const m of mapping) { const student = this.cachedStudents[m.studentIdx]; const room = this.cachedRooms[m.roomIdx]; @@ -493,7 +463,6 @@ export class SeedDevService { billingStartDate: m.checkInDate, rentalType: room.rentalCategory, notes: m.notes, - departmentId: this.deptId, }); } } @@ -510,7 +479,7 @@ export class SeedDevService { private async seedSchedules(): Promise { const subjects = ['数学', '英语', '语文', '专业课', '政治', '历史']; - const schedules: Array<{ classId: number; classroomId: number; weekDay: number; startTime: string; endTime: string; startDate: string; endDate: string; subject: string; teacherId: number; scheduleType: string; departmentId: number }> = []; + const schedules: Array<{ classId: number; classroomId: number; weekDay: number; startTime: string; endTime: string; startDate: string; endDate: string; subject: string; teacherId: number; scheduleType: string }> = []; for (const cls of this.cachedClasses.slice(0, 3)) { for (let day = 1; day <= 5; day++) { @@ -526,7 +495,6 @@ export class SeedDevService { subject: pick(subjects), teacherId: pick(this.cachedUsers).id, scheduleType: ScheduleType.INTERNAL, - departmentId: this.deptId, }, { classId: cls.id, @@ -539,7 +507,6 @@ export class SeedDevService { subject: pick(subjects), teacherId: pick(this.cachedUsers).id, scheduleType: ScheduleType.INTERNAL, - departmentId: this.deptId, }, ); } @@ -562,7 +529,7 @@ export class SeedDevService { { roomIdx: 23, electric: { apr: 101.00, may: 98.18 }, water: { apr: 19.60, may: 19.60 } }, // 4-201 ]; - const expenses: Array<{ roomId: number; expenseType: string; amount: number; periodStart: string; periodEnd: string; description: string; departmentId: number }> = []; + const expenses: Array<{ roomId: number; expenseType: string; amount: number; periodStart: string; periodEnd: string; description: string }> = []; for (const er of expenseRooms) { const room = this.cachedRooms[er.roomIdx]; @@ -571,23 +538,23 @@ export class SeedDevService { expenses.push({ roomId: room.id, expenseType: 'electricity', amount: er.electric.apr, periodStart: '2026-04-01', periodEnd: '2026-04-30', - description: `电费 - ${room.roomNumber} 4月`, departmentId: this.deptId, + description: `电费 - ${room.roomNumber} 4月`, }); expenses.push({ roomId: room.id, expenseType: 'water', amount: er.water.apr, periodStart: '2026-04-01', periodEnd: '2026-04-30', - description: `水费 - ${room.roomNumber} 4月`, departmentId: this.deptId, + description: `水费 - ${room.roomNumber} 4月`, }); // May expenses.push({ roomId: room.id, expenseType: 'electricity', amount: er.electric.may, periodStart: '2026-05-01', periodEnd: '2026-05-31', - description: `电费 - ${room.roomNumber} 5月`, departmentId: this.deptId, + description: `电费 - ${room.roomNumber} 5月`, }); expenses.push({ roomId: room.id, expenseType: 'water', amount: er.water.may, periodStart: '2026-05-01', periodEnd: '2026-05-31', - description: `水费 - ${room.roomNumber} 5月`, departmentId: this.deptId, + description: `水费 - ${room.roomNumber} 5月`, }); } @@ -600,7 +567,7 @@ export class SeedDevService { private async seedDeposits(): Promise { // Real data: deposits tied to specific students const depositStudents = [24, 25, 26, 27, 28, 19, 2]; // indices into cachedStudents - const deposits: Array<{ studentId: number; amount: number; status: string; paidDate: string; departmentId: number }> = []; + const deposits: Array<{ studentId: number; amount: number; status: string; paidDate: string }> = []; for (const idx of depositStudents) { const s = this.cachedStudents[idx]; @@ -610,13 +577,12 @@ export class SeedDevService { amount: 500, status: 'paid', paidDate: s.id <= this.cachedStudents[25].id ? '2026-07-12' : '2026-06-04', - departmentId: this.deptId, }); } // Manual deposits with custom amounts - deposits.push({ studentId: this.cachedStudents[19].id, amount: 200, status: 'paid', paidDate: '2026-03-01', departmentId: this.deptId }); // 常智禹 - deposits.push({ studentId: this.cachedStudents[2].id, amount: 121.60, status: 'paid', paidDate: '2026-03-01', departmentId: this.deptId }); // 陈昊天 + deposits.push({ studentId: this.cachedStudents[19].id, amount: 200, status: 'paid', paidDate: '2026-03-01' }); // 常智禹 + deposits.push({ studentId: this.cachedStudents[2].id, amount: 121.60, status: 'paid', paidDate: '2026-03-01' }); // 陈昊天 await this.depositRepo.save(deposits); this.logger.log(` ✓ ${deposits.length} deposits`); @@ -649,7 +615,6 @@ export class SeedDevService { personalAmount: 0, totalAmount: bd.sharedApr, status: 'paid', - departmentId: this.deptId, }); await this.billItemRepo.save([ { billId: billApr.id, roomId: room.id, expenseType: 'electricity', description: '电费分摊', days: 30, studentAmount: Math.round(bd.sharedApr * 0.82 * 100) / 100 }, @@ -665,7 +630,6 @@ export class SeedDevService { personalAmount: 0, totalAmount: bd.sharedMay, status: 'paid', - departmentId: this.deptId, }); await this.billItemRepo.save([ { billId: billMay.id, roomId: room.id, expenseType: 'electricity', description: '电费分摊', days: 31, studentAmount: Math.round(bd.sharedMay * 0.82 * 100) / 100 }, @@ -680,7 +644,7 @@ export class SeedDevService { private async seedAttendance(): Promise { const statuses = ['present', 'absent', 'late', 'leave']; - const records: Array<{ studentId: number; classId: number; attendanceDate: string; session: string; status: string; source: string; departmentId: number }> = []; + const records: Array<{ studentId: number; classId: number; attendanceDate: string; session: string; status: string; source: string }> = []; for (const cls of this.cachedClasses) { const classStudents = await this.classStudentRepo.find({ where: { classId: cls.id } }); @@ -700,7 +664,6 @@ export class SeedDevService { session: 'am', status: pick(statuses), source: 'manual', - departmentId: this.deptId, }); count++; } @@ -724,7 +687,6 @@ export class SeedDevService { endDate: '2026-06-30', totalAmount: 30000, status: 'active', - departmentId: this.deptId, }); this.logger.log(' ✓ 1 classroom rental'); } @@ -741,7 +703,6 @@ export class SeedDevService { subjectDirection: pick(['理科', '文科']), grade: '高三', campusLocation: '主校区', - departmentId: this.deptId, })); await this.profileRepo.save(profiles); this.logger.log(` ✓ ${profiles.length} student profiles`); @@ -759,7 +720,6 @@ export class SeedDevService { startDate: '2026-04-01', endDate: '2026-08-31', status: 'active', - departmentId: this.deptId, })); await this.enrollmentRepo.save(enrollments); this.logger.log(` ✓ ${enrollments.length} enrollments`); @@ -770,7 +730,7 @@ export class SeedDevService { private async seedExamScores(): Promise { const subjects = ['数学', '英语', '语文', '专业课']; const exams = ['月考', '期中考试', '模拟考试']; - const scores: Array<{ studentId: number; examType: string; examName: string; subject: string; score: number; examDate: string; departmentId: number }> = []; + const scores: Array<{ studentId: number; examType: string; examName: string; subject: string; score: number; examDate: string }> = []; for (const s of this.cachedStudents.slice(0, 10)) { for (const exam of exams) { @@ -782,7 +742,6 @@ export class SeedDevService { subject: subj, score: randInt(50, 100), examDate: `2026-0${randInt(4, 6)}-${String(randInt(1, 28)).padStart(2, '0')}`, - departmentId: this.deptId, }); } } @@ -802,7 +761,6 @@ export class SeedDevService { content: `学习状态:${pick(['良好', '一般', '需加强'])}`, followUpMethod: pick(['电话', '微信', '面谈']), nextStep: '继续跟进', - departmentId: this.deptId, })); await this.learningRecordRepo.save(records); this.logger.log(` ✓ ${records.length} learning records`); diff --git a/apps/server/src/seed/seed.module.ts b/apps/server/src/seed/seed.module.ts index 32ae0ae..5590696 100644 --- a/apps/server/src/seed/seed.module.ts +++ b/apps/server/src/seed/seed.module.ts @@ -6,13 +6,13 @@ import { Bill, BillItem, User, Deposit, Classroom, Tenant, ClassroomRental, Permission, Role, Class, ClassStudent, ClassTeacher, ClassSchedule, - AttendanceRecord, Department, UserDepartment, + AttendanceRecord, StudentProfile, StudentEnrollment, ExamScore, LearningRecord, ExpenseType, } from '../entities'; const SEED_ENTITIES = [ - Department, User, Role, Permission, UserDepartment, + User, Role, Permission, Tenant, Student, Room, Classroom, Occupancy, RoomExpense, ExpenseType, Class, ClassStudent, ClassTeacher, ClassSchedule, Bill, BillItem, diff --git a/apps/server/src/students/dto/student.dto.ts b/apps/server/src/students/dto/student.dto.ts index ddc8e02..0ffe7d0 100644 --- a/apps/server/src/students/dto/student.dto.ts +++ b/apps/server/src/students/dto/student.dto.ts @@ -44,8 +44,6 @@ export class CreateStudentDto { @IsString() supervisor?: string; @IsOptional() - @IsInt() - departmentId?: number; @IsOptional() @IsInt() diff --git a/apps/server/src/students/students.controller.ts b/apps/server/src/students/students.controller.ts index bf82132..d28e675 100644 --- a/apps/server/src/students/students.controller.ts +++ b/apps/server/src/students/students.controller.ts @@ -290,8 +290,7 @@ export class StudentsController { } } } - const departmentId = req.headers?.['x-campus-id'] ? parseInt(String(req.headers['x-campus-id']), 10) || undefined : undefined; - const result = await this.service.batchImport(rows, departmentId); + const result = await this.service.batchImport(rows); await this.logService.log({ userId: req.user?.id, username: req.user?.username, diff --git a/apps/server/src/students/students.service.ts b/apps/server/src/students/students.service.ts index 1e33a76..f3887fe 100644 --- a/apps/server/src/students/students.service.ts +++ b/apps/server/src/students/students.service.ts @@ -39,11 +39,7 @@ export class StudentsService { } async create(dto: CreateStudentDto) { - const entity = this.repo.create(dto); - if (dto.departmentId) { - entity.departmentId = dto.departmentId; - } - return this.repo.save(entity); + return this.repo.save(this.repo.create(dto)); } async update(id: number, dto: UpdateStudentDto) { @@ -57,7 +53,6 @@ export class StudentsService { if (student.status === 'archived') { throw new BadRequestException('该学生已归档'); } - // 软删除:归档而非物理删除,保留历史数据 await this.repo.update(id, { status: 'archived' }); return { message: '已归档(数据已保留,可随时恢复)' }; } @@ -110,7 +105,6 @@ export class StudentsService { supervisor?: string; tenantId?: number; }[], - departmentId?: number, ) { let imported = 0; let skipped = 0; @@ -136,7 +130,6 @@ export class StudentsService { organization: row.organization || undefined, supervisor: row.supervisor || undefined, tenantId: row.tenantId || undefined, - departmentId: departmentId ?? undefined, }), ); imported++; @@ -152,7 +145,6 @@ export class StudentsService { const student = await this.repo.findOne({ where: { id: studentId } }); if (!student) throw new NotFoundException('学生不存在'); - // Get all class enrollments for this student const enrollments = await this.classStudentRepo.find({ where: { studentId }, relations: ['class'], @@ -162,13 +154,11 @@ export class StudentsService { return { student, enrollments: [] }; } - // For each enrollment, compute attendance stats const classIds = enrollments.map((e) => e.classId); const attendanceRecords = await this.attendanceRepo.find({ where: { studentId, classId: In(classIds) }, }); - // Group attendance by class const attendanceByClass = new Map(); for (const r of attendanceRecords) { const list = attendanceByClass.get(r.classId) || []; diff --git a/apps/server/src/sync/schedule-sync.service.ts b/apps/server/src/sync/schedule-sync.service.ts index 0f7c9eb..cb339d3 100644 --- a/apps/server/src/sync/schedule-sync.service.ts +++ b/apps/server/src/sync/schedule-sync.service.ts @@ -3,10 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Not, IsNull } from 'typeorm'; import { ClassSchedule, - ClassStudent, ClassTeacher, - Department, - UserDepartment, } from '../entities'; import { DingTalkService, DingTalkScheduleItem } from '../integration/dingtalk.service'; @@ -48,10 +45,6 @@ export class ScheduleSyncService { private readonly scheduleRepo: Repository, @InjectRepository(ClassTeacher) private readonly classTeacherRepo: Repository, - @InjectRepository(Department) - private readonly deptRepo: Repository, - @InjectRepository(UserDepartment) - private readonly userDeptRepo: Repository, private readonly dingTalkService: DingTalkService, ) {} @@ -75,7 +68,6 @@ export class ScheduleSyncService { status: 'active', teacherId: Not(IsNull()), }, - relations: ['department'], }); if (schedules.length === 0) { @@ -110,102 +102,84 @@ export class ScheduleSyncService { } } - // ── Step 4: 按部门分组 ── - const deptGroups = new Map< - number, - { deptName: string; schedules: ClassSchedule[]; teacherIds: Set } - >(); - + // ── Step 4: Collect teacher IDs (no department entity) ── + const teacherIds = new Set(); for (const s of schedules) { - const deptId = s.departmentId || 0; - if (!deptGroups.has(deptId)) { - deptGroups.set(deptId, { - deptName: (s.department as Department)?.name || `部门${deptId}`, - schedules: [], - teacherIds: new Set(), - }); - } - const group = deptGroups.get(deptId)!; - group.schedules.push(s); - if (s.teacherId) group.teacherIds.add(s.teacherId); + if (s.teacherId) teacherIds.add(s.teacherId); } - // ── Step 5: 每个部门 → 考勤组 → 排班 ── + // ── Step 5: Single group → attendance group → scheduling ── let syncedItems = 0; let skippedNoMapping = 0; let groupCount = 0; const groupDetails: ScheduleSyncResult['groups'] = []; - for (const [deptId, group] of deptGroups) { - // 获取该部门教师的钉钉 userIds - const dingUserIds: string[] = []; - const teacherDingMap = new Map(); // local teacherId → dingUserId - for (const tid of group.teacherIds) { - const dingId = userIdToDingId.get(tid); - if (dingId) { - dingUserIds.push(dingId); - teacherDingMap.set(tid, dingId); - } + // Collect teacher→ding mapping + const dingUserIds: string[] = []; + const teacherDingMap = new Map(); + for (const tid of teacherIds) { + const dingId = userIdToDingId.get(tid); + if (dingId) { + dingUserIds.push(dingId); + teacherDingMap.set(tid, dingId); } - - if (dingUserIds.length === 0) { - skippedNoMapping += group.schedules.length; - this.logger.warn(`部门 ${group.deptName}: 无钉钉用户映射,跳过`); - continue; - } - - // 该部门使用的班次 IDs - const deptShiftIds = new Set(); - for (const s of group.schedules) { - const key = shiftKey(s.startTime, s.endTime); - const sid = timeToShiftId.get(key); - if (sid) deptShiftIds.add(sid); - } - - // 创建/匹配考勤组 - const groupName = `排课_${group.deptName}`; - let attendanceGroupId: number; - try { - attendanceGroupId = await this.dingTalkService.findOrCreateAttendanceGroup( - groupName, - opUserId, - dingUserIds, - [...deptShiftIds], - ); - groupCount++; - } catch (e) { - this.logger.error(`创建考勤组 ${groupName} 失败: ${(e as Error).message}`); - continue; - } - - // 展开排课为每日排班 - const items = this.expandSchedules( - group.schedules, - teacherDingMap, - timeToShiftId, - startDate, - endDate, - ); - skippedNoMapping += group.schedules.length - new Set(items.map((i) => i.userid)).size; - - // 分批写入(每次最多200条) - for (let i = 0; i < items.length; i += 200) { - const batch = items.slice(i, i + 200); - try { - await this.dingTalkService.scheduleUsers(attendanceGroupId, batch, opUserId); - syncedItems += batch.length; - } catch (e) { - this.logger.error(`排班写入失败 (groupId=${attendanceGroupId}, offset=${i}): ${(e as Error).message}`); - } - } - - groupDetails.push({ - deptName: group.deptName, - groupId: attendanceGroupId, - itemCount: items.length, - }); } + if (dingUserIds.length === 0) { + this.logger.warn('无钉钉用户映射,跳过全部排班'); + return { scheduleCount: schedules.length, shiftCount, groupCount: 0, syncedItems: 0, skippedNoMapping: schedules.length, groups: [] }; + } + + // All used shift IDs + const allShiftIds = new Set(); + for (const s of schedules) { + const key = shiftKey(s.startTime, s.endTime); + const sid = timeToShiftId.get(key); + if (sid) allShiftIds.add(sid); + } + + // Create/find attendance group + const groupName = '排课_全部'; + let attendanceGroupId: number; + try { + attendanceGroupId = await this.dingTalkService.findOrCreateAttendanceGroup( + groupName, + opUserId, + dingUserIds, + [...allShiftIds], + ); + groupCount++; + } catch (e) { + this.logger.error(`创建考勤组 ${groupName} 失败: ${(e as Error).message}`); + return { scheduleCount: schedules.length, shiftCount, groupCount: 0, syncedItems: 0, skippedNoMapping: schedules.length, groups: [] }; + } + + // Expand schedules to daily items + const items = this.expandSchedules( + schedules, + teacherDingMap, + timeToShiftId, + startDate, + endDate, + ); + skippedNoMapping = schedules.length - new Set(items.map((i) => i.userid)).size; + + // Batch write (max 200 per batch) + for (let i = 0; i < items.length; i += 200) { + const batch = items.slice(i, i + 200); + try { + await this.dingTalkService.scheduleUsers(attendanceGroupId, batch, opUserId); + syncedItems += batch.length; + } catch (e) { + this.logger.error(`排班写入失败 (groupId=${attendanceGroupId}, offset=${i}): ${(e as Error).message}`); + } + } + + groupDetails.push({ + deptName: groupName, + groupId: attendanceGroupId, + itemCount: items.length, + }); this.logger.log( `排班同步完成: ${schedules.length} 条排课 → ${syncedItems} 条钉钉排班, ` + `${shiftCount} 班次, ${groupCount} 考勤组, 跳过 ${skippedNoMapping} 条无映射`, diff --git a/apps/server/src/sync/sync.module.ts b/apps/server/src/sync/sync.module.ts index f21bc75..c7a811d 100644 --- a/apps/server/src/sync/sync.module.ts +++ b/apps/server/src/sync/sync.module.ts @@ -7,8 +7,6 @@ import { SyncState, StudentDingMapping, ClassSchedule, - Department, - UserDepartment, ClassTeacher, User, Student, @@ -26,8 +24,6 @@ import { ScheduleSyncService } from './schedule-sync.service'; SyncState, StudentDingMapping, ClassSchedule, - Department, - UserDepartment, ClassTeacher, User, Student, diff --git a/apps/server/src/sync/sync.service.ts b/apps/server/src/sync/sync.service.ts index d0c3c18..59f7c1f 100644 --- a/apps/server/src/sync/sync.service.ts +++ b/apps/server/src/sync/sync.service.ts @@ -214,6 +214,6 @@ export class SyncService { private async performWeComSync(_lastSyncAt: Date | null): Promise { const result = await this.weComService.syncAll(); - return result.deptCount + result.userCount; + return result.userCount; } }