feat: replace tenants with organization management
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { IsString, IsOptional, IsEnum, IsNumber, IsInt } from 'class-validator';
|
||||
import { IsString, IsOptional, IsEnum, IsInt } from 'class-validator';
|
||||
|
||||
export class CreateStudentDto {
|
||||
@IsString()
|
||||
@@ -32,19 +32,12 @@ export class CreateStudentDto {
|
||||
@IsString()
|
||||
emergencyPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
organization?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
tenantId?: number;
|
||||
@IsInt()
|
||||
organizationId: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
supervisor?: string;
|
||||
@IsOptional()
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
classId?: number;
|
||||
@@ -84,12 +77,8 @@ export class UpdateStudentDto {
|
||||
emergencyPhone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
organization?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
tenantId?: number;
|
||||
@IsInt()
|
||||
organizationId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Tenant } from '../entities/tenant.entity';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { ClassTeacher } from '../entities/class-teacher.entity';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import type { Response } from 'express';
|
||||
@@ -34,7 +34,7 @@ export class StudentsController {
|
||||
constructor(
|
||||
private service: StudentsService,
|
||||
private logService: OperationLogsService,
|
||||
@InjectRepository(Tenant) private tenantRepo: Repository<Tenant>,
|
||||
@InjectRepository(Organization) private organizationRepo: Repository<Organization>,
|
||||
) {}
|
||||
|
||||
private canManageAllStudents(user: { isSuperAdmin?: boolean; permissions?: string[] }): boolean {
|
||||
@@ -51,7 +51,7 @@ export class StudentsController {
|
||||
@Query('name') name: string | undefined,
|
||||
@Query('status') status: string | undefined,
|
||||
@Query('includeArchived') includeArchived: string | undefined,
|
||||
@Query('tenantId') tenantId: string | undefined,
|
||||
@Query('organizationId') organizationId: string | undefined,
|
||||
@Request() req: { user: { id: number; isSuperAdmin?: boolean; permissions?: string[] } },
|
||||
) {
|
||||
const classIds = await this.service.getAccessibleClassIds(
|
||||
@@ -63,7 +63,7 @@ export class StudentsController {
|
||||
name,
|
||||
status,
|
||||
includeArchived: includeArchived === 'true',
|
||||
tenantId: tenantId ? +tenantId : undefined,
|
||||
organizationId: organizationId ? +organizationId : undefined,
|
||||
},
|
||||
classIds,
|
||||
);
|
||||
@@ -94,7 +94,7 @@ export class StudentsController {
|
||||
{ header: '民族', key: 'ethnicity', width: 10 },
|
||||
{ header: '紧急联系人', key: 'emergencyContact', width: 15 },
|
||||
{ header: '紧急联系人电话', key: 'emergencyPhone', width: 18 },
|
||||
{ header: '所属机构', key: 'tenant', width: 18 },
|
||||
{ header: '所属机构', key: 'organization', width: 18 },
|
||||
{ header: '负责人/班主任', key: 'supervisor', width: 15 },
|
||||
{ header: '状态', key: 'status', width: 10 },
|
||||
];
|
||||
@@ -115,7 +115,7 @@ export class StudentsController {
|
||||
ethnicity: s.ethnicity || '',
|
||||
emergencyContact: s.emergencyContact || '',
|
||||
emergencyPhone: s.emergencyPhone || '',
|
||||
tenant: s.tenant?.name || '',
|
||||
organization: s.organization?.name || '',
|
||||
supervisor: s.supervisor || '',
|
||||
status: statusMap[s.status] || s.status,
|
||||
});
|
||||
@@ -152,7 +152,7 @@ export class StudentsController {
|
||||
{ header: '民族', key: 'ethnicity', width: 10 },
|
||||
{ header: '紧急联系人', key: 'emergencyContact', width: 15 },
|
||||
{ header: '紧急联系人电话', key: 'emergencyPhone', width: 18 },
|
||||
{ header: '所属机构(租赁方名称)', key: 'tenant', width: 18 },
|
||||
{ header: '所属机构名称', key: 'organization', width: 18 },
|
||||
{ header: '负责人/班主任', key: 'supervisor', width: 15 },
|
||||
];
|
||||
ws.getRow(1).font = { bold: true };
|
||||
@@ -165,7 +165,7 @@ export class StudentsController {
|
||||
ethnicity: '汉族',
|
||||
emergencyContact: '张父',
|
||||
emergencyPhone: '13900000000',
|
||||
tenant: 'XX教育公司',
|
||||
organization: 'XX教育公司',
|
||||
supervisor: '',
|
||||
});
|
||||
res.setHeader(
|
||||
@@ -290,9 +290,9 @@ export class StudentsController {
|
||||
ethnicity?: string;
|
||||
emergencyContact?: string;
|
||||
emergencyPhone?: string;
|
||||
tenant?: string;
|
||||
organization?: string;
|
||||
supervisor?: string;
|
||||
tenantId?: number;
|
||||
organizationId?: number;
|
||||
}[] = [];
|
||||
ws.eachRow((row, idx) => {
|
||||
if (idx === 1) return;
|
||||
@@ -304,16 +304,18 @@ export class StudentsController {
|
||||
ethnicity: String(row.getCell(5).value || '').trim() || undefined,
|
||||
emergencyContact: String(row.getCell(6).value || '').trim() || undefined,
|
||||
emergencyPhone: String(row.getCell(7).value || '').trim() || undefined,
|
||||
tenant: String(row.getCell(8).value || '').trim() || undefined,
|
||||
organization: String(row.getCell(8).value || '').trim() || undefined,
|
||||
supervisor: String(row.getCell(9).value || '').trim() || undefined,
|
||||
});
|
||||
});
|
||||
// Resolve tenant names to IDs
|
||||
// Resolve organization names to IDs
|
||||
for (const row of rows) {
|
||||
if (row.tenant) {
|
||||
const tenant = await this.tenantRepo.findOne({ where: { name: row.tenant } });
|
||||
if (tenant) {
|
||||
row.tenantId = tenant.id;
|
||||
if (row.organization) {
|
||||
const organization = await this.organizationRepo.findOne({
|
||||
where: { name: row.organization },
|
||||
});
|
||||
if (organization) {
|
||||
row.organizationId = organization.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,7 +350,7 @@ export class StudentsController {
|
||||
emergencyPhone?: string;
|
||||
organization?: string;
|
||||
supervisor?: string;
|
||||
tenantId?: number;
|
||||
organizationId?: number;
|
||||
}[] = [];
|
||||
ws.eachRow((row, idx) => {
|
||||
if (idx === 1) return;
|
||||
@@ -364,11 +366,13 @@ export class StudentsController {
|
||||
supervisor: String(row.getCell(9).value || '').trim() || undefined,
|
||||
});
|
||||
});
|
||||
// Resolve tenant names to IDs
|
||||
// Resolve organization names to IDs
|
||||
for (const row of rows) {
|
||||
if (row.organization) {
|
||||
const tenant = await this.tenantRepo.findOne({ where: { name: row.organization } });
|
||||
if (tenant) row.tenantId = tenant.id;
|
||||
const organization = await this.organizationRepo.findOne({
|
||||
where: { name: row.organization },
|
||||
});
|
||||
if (organization) row.organizationId = organization.id;
|
||||
}
|
||||
}
|
||||
const result = await this.service.matchImport(rows);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Student } from '../entities/student.entity';
|
||||
import { Class } from '../entities/class.entity';
|
||||
import { Tenant } from '../entities/tenant.entity';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { ClassStudent } from '../entities/class-student.entity';
|
||||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||||
import { ClassTeacher } from '../entities/class-teacher.entity';
|
||||
@@ -16,7 +16,7 @@ import { StudentsController } from './students.controller';
|
||||
Class,
|
||||
ClassStudent,
|
||||
AttendanceRecord,
|
||||
Tenant,
|
||||
Organization,
|
||||
ClassTeacher,
|
||||
]),
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('StudentsService — teacher class scope', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
await service.findAll({}, [3, 5]);
|
||||
@@ -22,7 +23,7 @@ describe('StudentsService — teacher class scope', () => {
|
||||
expect(repo.find).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ id: expect.any(Object) }),
|
||||
relations: ['tenant'],
|
||||
relations: ['organization'],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -35,6 +36,7 @@ describe('StudentsService — teacher class scope', () => {
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
);
|
||||
|
||||
await expect(service.findAll({}, [])).resolves.toEqual([]);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Class } from '../entities/class.entity';
|
||||
import { ClassStudent } from '../entities/class-student.entity';
|
||||
import { ClassTeacher } from '../entities/class-teacher.entity';
|
||||
import { AttendanceRecord } from '../entities/attendance-record.entity';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { CreateStudentDto, UpdateStudentDto } from './dto/student.dto';
|
||||
|
||||
@Injectable()
|
||||
@@ -16,6 +17,7 @@ export class StudentsService {
|
||||
@InjectRepository(Class) private classRepo: Repository<Class>,
|
||||
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
|
||||
@InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
|
||||
@InjectRepository(Organization) private organizationRepo: Repository<Organization>,
|
||||
) {}
|
||||
|
||||
async getAccessibleClassIds(userId: number, canManageAll = false): Promise<number[] | undefined> {
|
||||
@@ -29,13 +31,13 @@ export class StudentsService {
|
||||
name?: string;
|
||||
status?: string;
|
||||
includeArchived?: boolean;
|
||||
tenantId?: number | string;
|
||||
organizationId?: number | string;
|
||||
},
|
||||
accessibleClassIds?: number[],
|
||||
) {
|
||||
const where: FindOptionsWhere<Student> = {};
|
||||
if (query?.name) where.name = Like(`%${query.name}%`);
|
||||
if (query?.tenantId) where.tenantId = Number(query.tenantId);
|
||||
if (query?.organizationId) where.organizationId = Number(query.organizationId);
|
||||
if (query?.status) {
|
||||
where.status = query.status;
|
||||
} else if (!query?.includeArchived) {
|
||||
@@ -50,7 +52,7 @@ export class StudentsService {
|
||||
if (studentIds.length === 0) return [];
|
||||
where.id = In(studentIds);
|
||||
}
|
||||
return this.repo.find({ where, order: { createdAt: 'DESC' }, relations: ['tenant'] });
|
||||
return this.repo.find({ where, order: { createdAt: 'DESC' }, relations: ['organization'] });
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
@@ -63,11 +65,13 @@ export class StudentsService {
|
||||
}
|
||||
|
||||
async create(dto: CreateStudentDto) {
|
||||
await this.assertActiveOrganization(dto.organizationId);
|
||||
return this.repo.save(this.repo.create(dto));
|
||||
}
|
||||
|
||||
async update(id: number, dto: UpdateStudentDto) {
|
||||
await this.findOne(id);
|
||||
if (dto.organizationId) await this.assertActiveOrganization(dto.organizationId);
|
||||
await this.repo.update(id, dto);
|
||||
return this.repo.findOne({ where: { id } });
|
||||
}
|
||||
@@ -127,7 +131,7 @@ export class StudentsService {
|
||||
emergencyPhone?: string;
|
||||
organization?: string;
|
||||
supervisor?: string;
|
||||
tenantId?: number;
|
||||
organizationId?: number;
|
||||
}[],
|
||||
) {
|
||||
let imported = 0;
|
||||
@@ -151,9 +155,8 @@ export class StudentsService {
|
||||
ethnicity: row.ethnicity || undefined,
|
||||
emergencyContact: row.emergencyContact || undefined,
|
||||
emergencyPhone: row.emergencyPhone || undefined,
|
||||
organization: row.organization || undefined,
|
||||
supervisor: row.supervisor || undefined,
|
||||
tenantId: row.tenantId || undefined,
|
||||
organizationId: row.organizationId || (await this.getHostOrganizationId()),
|
||||
}),
|
||||
);
|
||||
imported++;
|
||||
@@ -176,7 +179,7 @@ export class StudentsService {
|
||||
emergencyPhone?: string;
|
||||
organization?: string;
|
||||
supervisor?: string;
|
||||
tenantId?: number;
|
||||
organizationId?: number;
|
||||
}[],
|
||||
) {
|
||||
let matched = 0;
|
||||
@@ -208,9 +211,8 @@ export class StudentsService {
|
||||
| 'ethnicity'
|
||||
| 'emergencyContact'
|
||||
| 'emergencyPhone'
|
||||
| 'organization'
|
||||
| 'supervisor'
|
||||
| 'tenantId'
|
||||
| 'organizationId'
|
||||
>
|
||||
> = {};
|
||||
if (row.name?.trim()) updates.name = row.name.trim();
|
||||
@@ -220,9 +222,8 @@ export class StudentsService {
|
||||
if (row.ethnicity) updates.ethnicity = row.ethnicity;
|
||||
if (row.emergencyContact) updates.emergencyContact = row.emergencyContact;
|
||||
if (row.emergencyPhone) updates.emergencyPhone = row.emergencyPhone;
|
||||
if (row.organization) updates.organization = row.organization;
|
||||
if (row.supervisor) updates.supervisor = row.supervisor;
|
||||
if (row.tenantId) updates.tenantId = row.tenantId;
|
||||
if (row.organizationId) updates.organizationId = row.organizationId;
|
||||
await this.repo.update(student.id, updates as Partial<Student>);
|
||||
matched++;
|
||||
}
|
||||
@@ -233,6 +234,19 @@ export class StudentsService {
|
||||
};
|
||||
}
|
||||
|
||||
private async assertActiveOrganization(id: number) {
|
||||
const organization = await this.organizationRepo.findOne({ where: { id, status: 'active' } });
|
||||
if (!organization) throw new BadRequestException('所属机构不存在或已归档');
|
||||
}
|
||||
|
||||
private async getHostOrganizationId() {
|
||||
const organization = await this.organizationRepo.findOne({
|
||||
where: { isHost: true, status: 'active' },
|
||||
});
|
||||
if (!organization) throw new BadRequestException('尚未配置本机构');
|
||||
return organization.id;
|
||||
}
|
||||
|
||||
async compareClasses(studentId: number) {
|
||||
const student = await this.repo.findOne({ where: { id: studentId } });
|
||||
if (!student) throw new NotFoundException('学生不存在');
|
||||
|
||||
Reference in New Issue
Block a user