forked from wangziqi/gongxue-base
feat: replace tenants with organization management
This commit is contained in:
@@ -36,13 +36,13 @@ export class ClassroomRentalsController {
|
||||
@RequirePermission('rental:view')
|
||||
findAll(
|
||||
@Query('classroomId') classroomId?: string,
|
||||
@Query('tenantId') tenantId?: string,
|
||||
@Query('lesseeOrganizationId') lesseeOrganizationId?: string,
|
||||
@Query('month') month?: string,
|
||||
@Query('includeEnded') includeEnded?: string,
|
||||
) {
|
||||
return this.service.findAll({
|
||||
classroomId: classroomId ? +classroomId : undefined,
|
||||
tenantId: tenantId ? +tenantId : undefined,
|
||||
lesseeOrganizationId: lesseeOrganizationId ? +lesseeOrganizationId : undefined,
|
||||
month,
|
||||
includeEnded: includeEnded === 'true',
|
||||
});
|
||||
@@ -79,10 +79,18 @@ export class ClassroomRentalsController {
|
||||
throw new BadRequestException('月份必须在 1-12 之间');
|
||||
}
|
||||
const parsedExcludeId = excludeId === undefined ? undefined : Number(excludeId);
|
||||
if (parsedExcludeId !== undefined && (!Number.isInteger(parsedExcludeId) || parsedExcludeId <= 0)) {
|
||||
if (
|
||||
parsedExcludeId !== undefined &&
|
||||
(!Number.isInteger(parsedExcludeId) || parsedExcludeId <= 0)
|
||||
) {
|
||||
throw new BadRequestException('排除的租赁订单不合法');
|
||||
}
|
||||
return this.service.getUnavailableDates(parsedClassroomId, parsedYear, parsedMonth, parsedExcludeId);
|
||||
return this.service.getUnavailableDates(
|
||||
parsedClassroomId,
|
||||
parsedYear,
|
||||
parsedMonth,
|
||||
parsedExcludeId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -103,7 +111,7 @@ export class ClassroomRentalsController {
|
||||
action: '新增租赁',
|
||||
targetId: result.id,
|
||||
targetType: 'classroom-rental',
|
||||
detail: `教室${dto.classroomId} 租赁方${dto.tenantId} ${dto.startDate}~${dto.endDate}`,
|
||||
detail: `教室${dto.classroomId} 承租机构${dto.lesseeOrganizationId} ${dto.startDate}~${dto.endDate}`,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
});
|
||||
|
||||
@@ -2,14 +2,17 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { Classroom } from '../entities/classroom.entity';
|
||||
import { Tenant } from '../entities/tenant.entity';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||
import { ClassroomRentalsService } from './classroom-rentals.service';
|
||||
import { ClassroomRentalsController } from './classroom-rentals.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ClassroomRental, Classroom, Tenant, ClassSchedule]), OperationLogsModule],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ClassroomRental, Classroom, Organization, ClassSchedule]),
|
||||
OperationLogsModule,
|
||||
],
|
||||
controllers: [ClassroomRentalsController],
|
||||
providers: [ClassroomRentalsService],
|
||||
exports: [ClassroomRentalsService],
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Not, Repository } from 'typeorm';
|
||||
import { ClassroomRentalsService } from './classroom-rentals.service';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { Classroom } from '../entities/classroom.entity';
|
||||
import { Tenant } from '../entities/tenant.entity';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||
import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto';
|
||||
|
||||
@@ -28,9 +28,12 @@ describe('ClassroomRentalsService — findConflicts', () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ClassroomRentalsService,
|
||||
{ provide: getRepositoryToken(ClassroomRental), useValue: { createQueryBuilder: jest.fn() } },
|
||||
{
|
||||
provide: getRepositoryToken(ClassroomRental),
|
||||
useValue: { createQueryBuilder: jest.fn() },
|
||||
},
|
||||
{ provide: getRepositoryToken(Classroom), useValue: {} },
|
||||
{ provide: getRepositoryToken(Tenant), useValue: {} },
|
||||
{ provide: getRepositoryToken(Organization), useValue: {} },
|
||||
{ provide: getRepositoryToken(ClassSchedule), useValue: { createQueryBuilder: jest.fn() } },
|
||||
],
|
||||
}).compile();
|
||||
@@ -41,7 +44,12 @@ describe('ClassroomRentalsService — findConflicts', () => {
|
||||
});
|
||||
|
||||
it('returns rental conflicts when no schedule conflicts exist', async () => {
|
||||
const rental = { id: 1, startDate: '2026-03-01', endDate: '2026-03-31', tenant: { name: 'A机构' } } as ClassroomRental;
|
||||
const rental = {
|
||||
id: 1,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
organization: { name: 'A机构' },
|
||||
} as ClassroomRental;
|
||||
const rentalQb = mockQueryBuilder<ClassroomRental>([rental]);
|
||||
const scheduleQb = mockQueryBuilder<ClassSchedule>([]);
|
||||
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
||||
@@ -56,18 +64,32 @@ describe('ClassroomRentalsService — findConflicts', () => {
|
||||
it('throws ConflictException when an active schedule overlaps the same classroom and date range', async () => {
|
||||
const rentalQb = mockQueryBuilder<ClassroomRental>([]);
|
||||
const scheduleQb = mockQueryBuilder<ClassSchedule>([
|
||||
{ id: 5, subject: '数学', weekDay: 1, startDate: '2026-03-01', endDate: '2026-06-30' } as ClassSchedule,
|
||||
{
|
||||
id: 5,
|
||||
subject: '数学',
|
||||
weekDay: 1,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-06-30',
|
||||
} as ClassSchedule,
|
||||
]);
|
||||
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
||||
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(scheduleQb);
|
||||
|
||||
await expect(service.findConflicts(1, '2026-03-15', '2026-04-15')).rejects.toThrow(ConflictException);
|
||||
await expect(service.findConflicts(1, '2026-03-15', '2026-04-15')).rejects.toThrow(
|
||||
ConflictException,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not treat a weekly schedule as a conflict when its weekday does not occur in the rental range', async () => {
|
||||
const rentalQb = mockQueryBuilder<ClassroomRental>([]);
|
||||
const scheduleQb = mockQueryBuilder<ClassSchedule>([
|
||||
{ id: 5, subject: '数学', weekDay: 1, startDate: '2026-07-01', endDate: '2026-07-31' } as ClassSchedule,
|
||||
{
|
||||
id: 5,
|
||||
subject: '数学',
|
||||
weekDay: 1,
|
||||
startDate: '2026-07-01',
|
||||
endDate: '2026-07-31',
|
||||
} as ClassSchedule,
|
||||
]);
|
||||
(rentalRepo.createQueryBuilder as jest.Mock).mockReturnValue(rentalQb);
|
||||
(scheduleRepo.createQueryBuilder as jest.Mock).mockReturnValue(scheduleQb);
|
||||
@@ -111,7 +133,7 @@ describe('ClassroomRentalsService — unavailable dates', () => {
|
||||
ClassroomRentalsService,
|
||||
{ provide: getRepositoryToken(ClassroomRental), useValue: { find: jest.fn() } },
|
||||
{ provide: getRepositoryToken(Classroom), useValue: {} },
|
||||
{ provide: getRepositoryToken(Tenant), useValue: {} },
|
||||
{ provide: getRepositoryToken(Organization), useValue: {} },
|
||||
{ provide: getRepositoryToken(ClassSchedule), useValue: { find: jest.fn() } },
|
||||
],
|
||||
}).compile();
|
||||
@@ -151,12 +173,18 @@ describe('ClassroomRentalsService — unavailable dates', () => {
|
||||
describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
let service: ClassroomRentalsService;
|
||||
let rentalRepo: jest.Mocked<
|
||||
Pick<Repository<ClassroomRental>, 'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'>
|
||||
Pick<
|
||||
Repository<ClassroomRental>,
|
||||
'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'
|
||||
>
|
||||
>;
|
||||
let classroomRepo: jest.Mocked<Pick<Repository<Classroom>, 'findOne'>>;
|
||||
let tenantRepo: jest.Mocked<Pick<Repository<Tenant>, 'findOne'>>;
|
||||
let organizationRepo: jest.Mocked<Pick<Repository<Organization>, 'findOne'>>;
|
||||
let scheduleRepo: jest.Mocked<
|
||||
Pick<Repository<ClassSchedule>, 'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'>
|
||||
Pick<
|
||||
Repository<ClassSchedule>,
|
||||
'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'
|
||||
>
|
||||
>;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -168,12 +196,17 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
delete: jest.fn(),
|
||||
createQueryBuilder: jest.fn(),
|
||||
} as jest.Mocked<
|
||||
Pick<Repository<ClassroomRental>, 'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'>
|
||||
Pick<
|
||||
Repository<ClassroomRental>,
|
||||
'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'
|
||||
>
|
||||
>;
|
||||
|
||||
classroomRepo = { findOne: jest.fn() } as jest.Mocked<Pick<Repository<Classroom>, 'findOne'>>;
|
||||
|
||||
tenantRepo = { findOne: jest.fn() } as jest.Mocked<Pick<Repository<Tenant>, 'findOne'>>;
|
||||
organizationRepo = { findOne: jest.fn() } as jest.Mocked<
|
||||
Pick<Repository<Organization>, 'findOne'>
|
||||
>;
|
||||
|
||||
scheduleRepo = {
|
||||
findOne: jest.fn(),
|
||||
@@ -183,7 +216,10 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
delete: jest.fn(),
|
||||
createQueryBuilder: jest.fn(),
|
||||
} as jest.Mocked<
|
||||
Pick<Repository<ClassSchedule>, 'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'>
|
||||
Pick<
|
||||
Repository<ClassSchedule>,
|
||||
'findOne' | 'save' | 'create' | 'update' | 'delete' | 'createQueryBuilder'
|
||||
>
|
||||
>;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
@@ -191,7 +227,7 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
ClassroomRentalsService,
|
||||
{ provide: getRepositoryToken(ClassroomRental), useValue: rentalRepo },
|
||||
{ provide: getRepositoryToken(Classroom), useValue: classroomRepo },
|
||||
{ provide: getRepositoryToken(Tenant), useValue: tenantRepo },
|
||||
{ provide: getRepositoryToken(Organization), useValue: organizationRepo },
|
||||
{ provide: getRepositoryToken(ClassSchedule), useValue: scheduleRepo },
|
||||
],
|
||||
}).compile();
|
||||
@@ -203,22 +239,43 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
it('saves the rental and creates a RENTAL class_schedule row', async () => {
|
||||
const dto: CreateRentalDto = {
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
lesseeOrganizationId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
};
|
||||
const classroom = { id: 1, departmentId: 10 } as Classroom;
|
||||
const tenant = { id: 2, name: 'Tenant A' } as Tenant;
|
||||
const hostOrganization = {
|
||||
id: 1,
|
||||
name: 'Host',
|
||||
isHost: true,
|
||||
status: 'active',
|
||||
} as Organization;
|
||||
const organization = {
|
||||
id: 2,
|
||||
name: 'Organization A',
|
||||
isHost: false,
|
||||
status: 'active',
|
||||
} as Organization;
|
||||
|
||||
classroomRepo.findOne.mockResolvedValue(classroom);
|
||||
tenantRepo.findOne.mockResolvedValue(tenant);
|
||||
rentalRepo.create.mockImplementation((entity) => ({ ...(entity as object) } as ClassroomRental));
|
||||
rentalRepo.save.mockImplementation((entity) => Promise.resolve({ ...(entity as object), id: 1 } as ClassroomRental));
|
||||
organizationRepo.findOne
|
||||
.mockResolvedValueOnce(hostOrganization)
|
||||
.mockResolvedValueOnce(organization);
|
||||
rentalRepo.create.mockImplementation(
|
||||
(entity) => ({ ...(entity as object) }) as ClassroomRental,
|
||||
);
|
||||
rentalRepo.save.mockImplementation((entity) =>
|
||||
Promise.resolve({ ...(entity as object), id: 1 } as ClassroomRental),
|
||||
);
|
||||
rentalRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassroomRental>([]));
|
||||
scheduleRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassSchedule>([]));
|
||||
scheduleRepo.findOne.mockResolvedValue(null);
|
||||
scheduleRepo.create.mockImplementation((entity) => ({ ...(entity as object) } as ClassSchedule));
|
||||
scheduleRepo.save.mockImplementation((entity) => Promise.resolve({ ...(entity as object), id: 100 } as ClassSchedule));
|
||||
scheduleRepo.create.mockImplementation(
|
||||
(entity) => ({ ...(entity as object) }) as ClassSchedule,
|
||||
);
|
||||
scheduleRepo.save.mockImplementation((entity) =>
|
||||
Promise.resolve({ ...(entity as object), id: 100 } as ClassSchedule),
|
||||
);
|
||||
|
||||
const result = await service.create(dto);
|
||||
|
||||
@@ -226,11 +283,11 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
expect(rentalRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
lessorOrganizationId: 1,
|
||||
lesseeOrganizationId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
status: 'active',
|
||||
departmentId: 10,
|
||||
}),
|
||||
);
|
||||
expect(scheduleRepo.create).toHaveBeenCalledWith(
|
||||
@@ -241,12 +298,11 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
endTime: '23:59',
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
subject: 'Tenant A 租赁',
|
||||
subject: 'Organization A 租赁',
|
||||
teacherId: null,
|
||||
scheduleType: 'RENTAL',
|
||||
rentalId: 1,
|
||||
status: 'active',
|
||||
departmentId: 10,
|
||||
}),
|
||||
);
|
||||
expect(scheduleRepo.save).toHaveBeenCalled();
|
||||
@@ -258,13 +314,12 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
const existingRental = {
|
||||
id: 1,
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
lesseeOrganizationId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
status: 'active',
|
||||
notes: '',
|
||||
departmentId: 10,
|
||||
tenant: { id: 2, name: 'Tenant A' } as Tenant,
|
||||
lesseeOrganization: { id: 2, name: 'Organization A' } as Organization,
|
||||
classroom: { id: 1 } as Classroom,
|
||||
} as ClassroomRental;
|
||||
const updatedRental = {
|
||||
@@ -272,7 +327,12 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2026-04-30',
|
||||
} as ClassroomRental;
|
||||
const existingSchedule = { id: 50, rentalId: 1, scheduleType: 'RENTAL', classroomId: 1 } as ClassSchedule;
|
||||
const existingSchedule = {
|
||||
id: 50,
|
||||
rentalId: 1,
|
||||
scheduleType: 'RENTAL',
|
||||
classroomId: 1,
|
||||
} as ClassSchedule;
|
||||
|
||||
rentalRepo.findOne.mockResolvedValueOnce(existingRental).mockResolvedValueOnce(updatedRental);
|
||||
rentalRepo.createQueryBuilder.mockReturnValue(mockQueryBuilder<ClassroomRental>([]));
|
||||
@@ -295,7 +355,7 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
startDate: '2026-04-01',
|
||||
endDate: '2026-04-30',
|
||||
status: 'active',
|
||||
subject: 'Tenant A 租赁',
|
||||
subject: 'Organization A 租赁',
|
||||
}),
|
||||
);
|
||||
expect(scheduleRepo.create).not.toHaveBeenCalled();
|
||||
@@ -306,12 +366,11 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
const rental = {
|
||||
id: 1,
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
lesseeOrganizationId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
status: 'active',
|
||||
departmentId: 10,
|
||||
tenant: { id: 2, name: 'Tenant A' } as Tenant,
|
||||
lesseeOrganization: { id: 2, name: 'Organization A' } as Organization,
|
||||
} as ClassroomRental;
|
||||
const cancelledRental = { ...rental, status: 'cancelled' } as ClassroomRental;
|
||||
|
||||
@@ -332,12 +391,11 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
const rental = {
|
||||
id: 1,
|
||||
classroomId: 1,
|
||||
tenantId: 2,
|
||||
lesseeOrganizationId: 2,
|
||||
startDate: '2026-03-01',
|
||||
endDate: '2026-03-31',
|
||||
status: 'active',
|
||||
departmentId: 10,
|
||||
tenant: { id: 2, name: 'Tenant A' } as Tenant,
|
||||
lesseeOrganization: { id: 2, name: 'Organization A' } as Organization,
|
||||
} as ClassroomRental;
|
||||
|
||||
rentalRepo.findOne.mockResolvedValue(rental);
|
||||
@@ -349,3 +407,55 @@ describe('ClassroomRentalsService — rental schedule sync', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ClassroomRentalsService — organization roles', () => {
|
||||
it('stores explicit lessor and lessee organizations for a rental', async () => {
|
||||
const rentalRepo = {
|
||||
findOne: jest.fn(),
|
||||
save: jest.fn(async (value) => ({ ...value, id: 1 })),
|
||||
create: jest.fn((value) => value),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder<ClassroomRental>([])),
|
||||
} as any;
|
||||
const classroomRepo = {
|
||||
findOne: jest.fn().mockResolvedValue({ id: 1, departmentId: 10 }),
|
||||
} as any;
|
||||
const organizationRepo = {
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ id: 1, name: '本机构', isHost: true, status: 'active' })
|
||||
.mockResolvedValueOnce({ id: 2, name: '合作机构', isHost: false, status: 'active' }),
|
||||
} as any;
|
||||
const scheduleRepo = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
save: jest.fn(async (value) => ({ ...value, id: 100 })),
|
||||
create: jest.fn((value) => value),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder<ClassSchedule>([])),
|
||||
} as any;
|
||||
|
||||
const service = new ClassroomRentalsService(
|
||||
rentalRepo,
|
||||
classroomRepo,
|
||||
organizationRepo,
|
||||
scheduleRepo,
|
||||
);
|
||||
|
||||
await service.create({
|
||||
classroomId: 1,
|
||||
lessorOrganizationId: 1,
|
||||
lesseeOrganizationId: 2,
|
||||
startDate: '2026-08-01',
|
||||
endDate: '2026-08-31',
|
||||
} as any);
|
||||
|
||||
expect(rentalRepo.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
lessorOrganizationId: 1,
|
||||
lesseeOrganizationId: 2,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,14 +8,14 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Not, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { Classroom } from '../entities/classroom.entity';
|
||||
import { Tenant } from '../entities/tenant.entity';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { ClassSchedule } from '../entities/class-schedule.entity';
|
||||
import { CreateRentalDto, UpdateRentalDto } from './dto/rental.dto';
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
// 预设色板(与 tenants.service 保持一致,作为颜色兜底)
|
||||
// 预设色板(与 organizations.service 保持一致,作为颜色兜底)
|
||||
const COLOR_PALETTE = [
|
||||
'#ff7875',
|
||||
'#ffa940',
|
||||
@@ -31,11 +31,10 @@ const COLOR_PALETTE = [
|
||||
|
||||
@Injectable()
|
||||
export class ClassroomRentalsService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ClassroomRental) private repo: Repository<ClassroomRental>,
|
||||
@InjectRepository(Classroom) private classroomRepo: Repository<Classroom>,
|
||||
@InjectRepository(Tenant) private tenantRepo: Repository<Tenant>,
|
||||
@InjectRepository(Organization) private organizationRepo: Repository<Organization>,
|
||||
@InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
|
||||
) {}
|
||||
|
||||
@@ -52,17 +51,18 @@ export class ClassroomRentalsService {
|
||||
|
||||
async findAll(query?: {
|
||||
classroomId?: number;
|
||||
tenantId?: number;
|
||||
lesseeOrganizationId?: number;
|
||||
month?: string;
|
||||
includeEnded?: boolean;
|
||||
}) {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('r')
|
||||
.leftJoinAndSelect('r.classroom', 'classroom')
|
||||
.leftJoinAndSelect('r.tenant', 'tenant')
|
||||
.leftJoinAndSelect('r.lesseeOrganization', 'lesseeOrganization')
|
||||
.orderBy('r.startDate', 'DESC');
|
||||
if (query?.classroomId) qb.andWhere('r.classroomId = :cid', { cid: query.classroomId });
|
||||
if (query?.tenantId) qb.andWhere('r.tenantId = :tid', { tid: query.tenantId });
|
||||
if (query?.lesseeOrganizationId)
|
||||
qb.andWhere('r.lesseeOrganizationId = :oid', { oid: query.lesseeOrganizationId });
|
||||
if (query?.month) {
|
||||
const [y, m] = query.month.split('-').map(Number);
|
||||
const first = `${y}-${String(m).padStart(2, '0')}-01`;
|
||||
@@ -75,7 +75,10 @@ export class ClassroomRentalsService {
|
||||
}
|
||||
|
||||
async findOne(id: number) {
|
||||
const rental = await this.repo.findOne({ where: { id }, relations: ['classroom', 'tenant'] });
|
||||
const rental = await this.repo.findOne({
|
||||
where: { id },
|
||||
relations: ['classroom', 'lessorOrganization', 'lesseeOrganization'],
|
||||
});
|
||||
if (!rental) throw new NotFoundException('租赁订单不存在');
|
||||
return rental;
|
||||
}
|
||||
@@ -128,7 +131,7 @@ export class ClassroomRentalsService {
|
||||
async findConflicts(classroomId: number, startDate: string, endDate: string, excludeId?: number) {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('r')
|
||||
.leftJoinAndSelect('r.tenant', 'tenant')
|
||||
.leftJoinAndSelect('r.lesseeOrganization', 'lesseeOrganization')
|
||||
.where('r.classroomId = :cid', { cid: classroomId })
|
||||
.andWhere('r.status != :cancelled', { cancelled: 'cancelled' })
|
||||
.andWhere('r.startDate <= :end', { end: endDate })
|
||||
@@ -156,7 +159,7 @@ export class ClassroomRentalsService {
|
||||
id: s.id,
|
||||
startDate: s.startDate,
|
||||
endDate: s.endDate,
|
||||
tenantName: `[内部排课] ${s.subject}`,
|
||||
organizationName: `[内部排课] ${s.subject}`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
@@ -164,7 +167,11 @@ export class ClassroomRentalsService {
|
||||
return rentals;
|
||||
}
|
||||
|
||||
private hasScheduleOccurrence(schedule: ClassSchedule, startDate: string, endDate: string): boolean {
|
||||
private hasScheduleOccurrence(
|
||||
schedule: ClassSchedule,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
): boolean {
|
||||
const overlapStart = schedule.startDate > startDate ? schedule.startDate : startDate;
|
||||
const overlapEnd = schedule.endDate < endDate ? schedule.endDate : endDate;
|
||||
if (overlapStart > overlapEnd) return false;
|
||||
@@ -191,7 +198,12 @@ export class ClassroomRentalsService {
|
||||
}
|
||||
}
|
||||
|
||||
private addScheduleOccurrences(dates: Set<string>, schedule: ClassSchedule, startDate: string, endDate: string) {
|
||||
private addScheduleOccurrences(
|
||||
dates: Set<string>,
|
||||
schedule: ClassSchedule,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
) {
|
||||
const overlapStart = schedule.startDate > startDate ? schedule.startDate : startDate;
|
||||
const overlapEnd = schedule.endDate < endDate ? schedule.endDate : endDate;
|
||||
if (overlapStart > overlapEnd) return;
|
||||
@@ -210,8 +222,19 @@ export class ClassroomRentalsService {
|
||||
if (dto.startDate > dto.endDate) throw new BadRequestException('起始日期不能晚于结束日期');
|
||||
const classroom = await this.classroomRepo.findOne({ where: { id: dto.classroomId } });
|
||||
if (!classroom) throw new NotFoundException('教室不存在');
|
||||
const tenant = await this.tenantRepo.findOne({ where: { id: dto.tenantId } });
|
||||
if (!tenant) throw new NotFoundException('租赁方不存在');
|
||||
const lessorOrganization = dto.lessorOrganizationId
|
||||
? await this.organizationRepo.findOne({
|
||||
where: { id: dto.lessorOrganizationId, status: 'active' },
|
||||
})
|
||||
: await this.organizationRepo.findOne({ where: { isHost: true, status: 'active' } });
|
||||
if (!lessorOrganization) throw new NotFoundException('出租机构不存在或未启用');
|
||||
const lesseeOrganization = await this.organizationRepo.findOne({
|
||||
where: { id: dto.lesseeOrganizationId, status: 'active' },
|
||||
});
|
||||
if (!lesseeOrganization) throw new NotFoundException('承租机构不存在或未启用');
|
||||
if (lessorOrganization.id === lesseeOrganization.id) {
|
||||
throw new BadRequestException('出租机构和承租机构不能相同');
|
||||
}
|
||||
|
||||
const conflicts = await this.findConflicts(dto.classroomId, dto.startDate, dto.endDate);
|
||||
if (conflicts.length > 0) {
|
||||
@@ -221,12 +244,19 @@ export class ClassroomRentalsService {
|
||||
id: c.id,
|
||||
startDate: c.startDate,
|
||||
endDate: c.endDate,
|
||||
tenantName: c.tenant?.name,
|
||||
organizationName: c.lesseeOrganization?.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
const rental = this.repo.create({ ...dto, createdBy: userId, status: 'active' });
|
||||
const rental = this.repo.create({
|
||||
...dto,
|
||||
lessorOrganizationId: lessorOrganization.id,
|
||||
lesseeOrganizationId: lesseeOrganization.id,
|
||||
createdBy: userId,
|
||||
status: 'active',
|
||||
});
|
||||
const saved = await this.repo.save(rental);
|
||||
await this.syncScheduleFromRental(saved, lesseeOrganization.name);
|
||||
return saved;
|
||||
}
|
||||
|
||||
@@ -246,11 +276,28 @@ export class ClassroomRentalsService {
|
||||
id: c.id,
|
||||
startDate: c.startDate,
|
||||
endDate: c.endDate,
|
||||
tenantName: c.tenant?.name,
|
||||
organizationName: c.lesseeOrganization?.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
const newLessorId = dto.lessorOrganizationId ?? rental.lessorOrganizationId;
|
||||
const newLesseeId = dto.lesseeOrganizationId ?? rental.lesseeOrganizationId;
|
||||
if (newLessorId === newLesseeId) {
|
||||
throw new BadRequestException('出租机构和承租机构不能相同');
|
||||
}
|
||||
if (dto.lessorOrganizationId) {
|
||||
const lessor = await this.organizationRepo.findOne({
|
||||
where: { id: dto.lessorOrganizationId, status: 'active' },
|
||||
});
|
||||
if (!lessor) throw new NotFoundException('出租机构不存在或未启用');
|
||||
}
|
||||
if (dto.lesseeOrganizationId) {
|
||||
const lessee = await this.organizationRepo.findOne({
|
||||
where: { id: dto.lesseeOrganizationId, status: 'active' },
|
||||
});
|
||||
if (!lessee) throw new NotFoundException('承租机构不存在或未启用');
|
||||
}
|
||||
await this.repo.update(id, dto);
|
||||
const updated = await this.findOne(id);
|
||||
if (dto.status === 'cancelled') {
|
||||
@@ -283,8 +330,8 @@ export class ClassroomRentalsService {
|
||||
/**
|
||||
* 同步租赁订单到 class_schedules(schedule_type = 'RENTAL')
|
||||
*/
|
||||
private async syncScheduleFromRental(rental: ClassroomRental, tenantName?: string) {
|
||||
const name = tenantName || rental.tenant?.name || '租赁方';
|
||||
private async syncScheduleFromRental(rental: ClassroomRental, organizationName?: string) {
|
||||
const name = organizationName || rental.lesseeOrganization?.name || '承租机构';
|
||||
const weekDay = this.dateToWeekDay(rental.startDate);
|
||||
let schedule = await this.scheduleRepo.findOne({
|
||||
where: { rentalId: rental.id, scheduleType: 'RENTAL' },
|
||||
@@ -395,13 +442,13 @@ export class ClassroomRentalsService {
|
||||
});
|
||||
const rentals = await this.repo
|
||||
.createQueryBuilder('r')
|
||||
.leftJoinAndSelect('r.tenant', 'tenant')
|
||||
.leftJoinAndSelect('r.lesseeOrganization', 'lesseeOrganization')
|
||||
.leftJoinAndSelect('r.classroom', 'classroom')
|
||||
.where('r.status != :cancelled', { cancelled: 'cancelled' })
|
||||
.andWhere('r.startDate <= :last AND r.endDate >= :first', { first, last })
|
||||
.getMany();
|
||||
|
||||
const tenantMap = new Map<number, any>();
|
||||
const organizationMap = new Map<number, any>();
|
||||
const matrix: Record<number, Record<number, any>> = {};
|
||||
const summary: Record<
|
||||
number,
|
||||
@@ -420,11 +467,13 @@ export class ClassroomRentalsService {
|
||||
const monthEnd = new Date(last);
|
||||
const effStart = start < monthStart ? monthStart : start;
|
||||
const effEnd = end > monthEnd ? monthEnd : end;
|
||||
if (rental.tenant && !tenantMap.has(rental.tenant.id)) {
|
||||
tenantMap.set(rental.tenant.id, {
|
||||
id: rental.tenant.id,
|
||||
name: rental.tenant.name,
|
||||
color: rental.tenant.color || COLOR_PALETTE[rental.tenant.id % COLOR_PALETTE.length],
|
||||
if (rental.lesseeOrganization && !organizationMap.has(rental.lesseeOrganization.id)) {
|
||||
organizationMap.set(rental.lesseeOrganization.id, {
|
||||
id: rental.lesseeOrganization.id,
|
||||
name: rental.lesseeOrganization.name,
|
||||
color:
|
||||
rental.lesseeOrganization.color ||
|
||||
COLOR_PALETTE[rental.lesseeOrganization.id % COLOR_PALETTE.length],
|
||||
});
|
||||
}
|
||||
for (let d = new Date(effStart); d <= effEnd; d.setDate(d.getDate() + 1)) {
|
||||
@@ -433,10 +482,11 @@ export class ClassroomRentalsService {
|
||||
matrix[rental.classroomId][day] = {
|
||||
scheduleType: 'RENTAL',
|
||||
rentalId: rental.id,
|
||||
tenantId: rental.tenantId,
|
||||
tenantName: rental.tenant?.name || '未知',
|
||||
organizationId: rental.lesseeOrganizationId,
|
||||
organizationName: rental.lesseeOrganization?.name || '未知',
|
||||
color:
|
||||
rental.tenant?.color || COLOR_PALETTE[(rental.tenantId || 0) % COLOR_PALETTE.length],
|
||||
rental.lesseeOrganization?.color ||
|
||||
COLOR_PALETTE[(rental.lesseeOrganizationId || 0) % COLOR_PALETTE.length],
|
||||
hasContract: !!rental.contractPath,
|
||||
};
|
||||
}
|
||||
@@ -454,8 +504,12 @@ export class ClassroomRentalsService {
|
||||
|
||||
for (const sched of schedules) {
|
||||
if (!sched.classroomId) continue;
|
||||
const schedStart = new Date(Math.max(new Date(sched.startDate).getTime(), new Date(first).getTime()));
|
||||
const schedEnd = new Date(Math.min(new Date(sched.endDate).getTime(), new Date(last).getTime()));
|
||||
const schedStart = new Date(
|
||||
Math.max(new Date(sched.startDate).getTime(), new Date(first).getTime()),
|
||||
);
|
||||
const schedEnd = new Date(
|
||||
Math.min(new Date(sched.endDate).getTime(), new Date(last).getTime()),
|
||||
);
|
||||
for (let d = new Date(schedStart); d <= schedEnd; d.setDate(d.getDate() + 1)) {
|
||||
const dow = d.getDay() === 0 ? 7 : d.getDay();
|
||||
if (dow !== sched.weekDay) continue;
|
||||
@@ -494,7 +548,7 @@ export class ClassroomRentalsService {
|
||||
capacity: c.capacity,
|
||||
supervisor: c.supervisor,
|
||||
})),
|
||||
tenants: Array.from(tenantMap.values()),
|
||||
organizations: Array.from(organizationMap.values()),
|
||||
matrix,
|
||||
summary,
|
||||
};
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsEnum,
|
||||
IsDateString,
|
||||
} from 'class-validator';
|
||||
import { IsOptional, IsString, IsInt, IsNumber, IsEnum, IsDateString } from 'class-validator';
|
||||
|
||||
export class CreateRentalDto {
|
||||
@IsInt()
|
||||
classroomId: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
tenantId: number;
|
||||
lessorOrganizationId?: number;
|
||||
|
||||
@IsInt()
|
||||
lesseeOrganizationId: number;
|
||||
|
||||
@IsDateString()
|
||||
startDate: string;
|
||||
@@ -41,7 +37,11 @@ export class UpdateRentalDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
tenantId?: number;
|
||||
lessorOrganizationId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
lesseeOrganizationId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
|
||||
Reference in New Issue
Block a user