forked from wangziqi/gongxue-base
feat: replace tenants with organization management
This commit is contained in:
@@ -20,14 +20,14 @@ export class CheckInDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
rentalType?: string;
|
||||
stayType?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
tenantId?: number;
|
||||
responsibleOrganizationId?: number;
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
bedId?: number; // 后续改 required
|
||||
bedId?: number; // 后续改 required
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
|
||||
@@ -96,7 +96,9 @@ export class OccupanciesController {
|
||||
content: `您已入住房间 #${dto.roomId}`,
|
||||
});
|
||||
}
|
||||
} catch (_) { /* don't block response */ }
|
||||
} catch (_) {
|
||||
/* don't block response */
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -126,7 +128,9 @@ export class OccupanciesController {
|
||||
content: `您已退宿房间 #${result.roomId}`,
|
||||
});
|
||||
}
|
||||
} catch (_) { /* don't block response */ }
|
||||
} catch (_) {
|
||||
/* don't block response */
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -215,7 +219,7 @@ export class OccupanciesController {
|
||||
gender: r.student?.gender || '',
|
||||
phone: r.student?.phone || '',
|
||||
idNumber: r.student?.idNumber || '',
|
||||
organization: r.student?.organization || '',
|
||||
organization: r.student?.organization?.name || '',
|
||||
supervisor: r.student?.supervisor || '',
|
||||
checkInDate: r.checkInDate || '',
|
||||
checkOutDate: r.checkOutDate || '',
|
||||
|
||||
@@ -6,13 +6,18 @@ import { Student } from '../entities/student.entity';
|
||||
import { Deposit } from '../entities/deposit.entity';
|
||||
import { Bed } from '../entities/bed.entity';
|
||||
import { Locker } from '../entities/locker.entity';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { OccupanciesService } from './occupancies.service';
|
||||
import { OccupanciesController } from './occupancies.controller';
|
||||
import { OperationLogsModule } from '../operation-logs/operation-logs.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Occupancy, Room, Student, Deposit, Bed, Locker]), OperationLogsModule, NotificationsModule],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Occupancy, Room, Student, Deposit, Bed, Locker, Organization]),
|
||||
OperationLogsModule,
|
||||
NotificationsModule,
|
||||
],
|
||||
controllers: [OccupanciesController],
|
||||
providers: [OccupanciesService],
|
||||
exports: [OccupanciesService],
|
||||
|
||||
47
apps/server/src/occupancies/occupancies.service.spec.ts
Normal file
47
apps/server/src/occupancies/occupancies.service.spec.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { OccupanciesService } from './occupancies.service';
|
||||
import { Occupancy } from '../entities/occupancy.entity';
|
||||
import { Room } from '../entities/room.entity';
|
||||
import { Student } from '../entities/student.entity';
|
||||
import { Deposit } from '../entities/deposit.entity';
|
||||
import { Bed } from '../entities/bed.entity';
|
||||
import { Locker } from '../entities/locker.entity';
|
||||
|
||||
describe('OccupanciesService — responsible organization', () => {
|
||||
it('defaults the responsible organization to the student organization', async () => {
|
||||
const occupancyRepo = {
|
||||
findOne: jest.fn().mockResolvedValue(null),
|
||||
count: jest.fn().mockResolvedValue(0),
|
||||
create: jest.fn((value) => value),
|
||||
save: jest.fn(async (value) => ({ ...value, id: 10 })),
|
||||
} as any as Repository<Occupancy>;
|
||||
const roomRepo = {
|
||||
findOne: jest.fn().mockResolvedValue({ id: 2, capacity: 4, gender: null }),
|
||||
update: jest.fn(),
|
||||
} as any as Repository<Room>;
|
||||
const studentRepo = {
|
||||
findOne: jest.fn().mockResolvedValue({ id: 3, gender: '男', organizationId: 7 }),
|
||||
} as any as Repository<Student>;
|
||||
|
||||
const service = new OccupanciesService(
|
||||
occupancyRepo,
|
||||
roomRepo,
|
||||
studentRepo,
|
||||
{} as Repository<Deposit>,
|
||||
{} as Repository<Bed>,
|
||||
{} as Repository<Locker>,
|
||||
{} as Repository<any>,
|
||||
{} as DataSource,
|
||||
);
|
||||
|
||||
await service.checkIn({
|
||||
studentId: 3,
|
||||
roomId: 2,
|
||||
checkInDate: '2026-07-10',
|
||||
});
|
||||
|
||||
expect(occupancyRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ responsibleOrganizationId: 7 }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -15,10 +15,11 @@ import { Student } from '../entities/student.entity';
|
||||
import { Bed } from '../entities/bed.entity';
|
||||
import { Locker } from '../entities/locker.entity';
|
||||
import { Deposit } from '../entities/deposit.entity';
|
||||
import { Organization } from '../entities/organization.entity';
|
||||
import { uuidV7 } from '../common/uuid-v7';
|
||||
import { CheckInDto, CheckOutDto, TransferRoomDto } from './dto/occupancy.dto';
|
||||
import { RoomsService } from '../rooms/rooms.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class OccupanciesService {
|
||||
constructor(
|
||||
@@ -28,6 +29,7 @@ export class OccupanciesService {
|
||||
@InjectRepository(Deposit) private depositRepo: Repository<Deposit>,
|
||||
@InjectRepository(Bed) private bedRepo: Repository<Bed>,
|
||||
@InjectRepository(Locker) private lockerRepo: Repository<Locker>,
|
||||
@InjectRepository(Organization) private organizationRepo: Repository<Organization>,
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
@@ -38,6 +40,7 @@ export class OccupanciesService {
|
||||
.leftJoinAndSelect('o.room', 'room')
|
||||
.leftJoinAndSelect('o.bed', 'bed')
|
||||
.leftJoinAndSelect('o.locker', 'locker')
|
||||
.leftJoinAndSelect('o.responsibleOrganization', 'responsibleOrganization')
|
||||
.orderBy('o.checkInDate', 'DESC');
|
||||
if (query?.roomId) qb.andWhere('o.roomId = :roomId', { roomId: query.roomId });
|
||||
if (query?.studentId) qb.andWhere('o.studentId = :studentId', { studentId: query.studentId });
|
||||
@@ -76,7 +79,9 @@ export class OccupanciesService {
|
||||
|
||||
// 柜子校验
|
||||
if (dto.lockerId) {
|
||||
const locker = await this.lockerRepo.findOne({ where: { id: dto.lockerId, roomId: dto.roomId } });
|
||||
const locker = await this.lockerRepo.findOne({
|
||||
where: { id: dto.lockerId, roomId: dto.roomId },
|
||||
});
|
||||
if (!locker) throw new BadRequestException('柜子不存在或不属于该宿舍');
|
||||
if (locker.status !== 'available') throw new BadRequestException('该柜子已被占用或维修中');
|
||||
}
|
||||
@@ -86,8 +91,8 @@ export class OccupanciesService {
|
||||
roomId: dto.roomId,
|
||||
checkInDate: dto.checkInDate,
|
||||
billingStartDate: dto.billingStartDate || dto.checkInDate,
|
||||
rentalType: dto.rentalType,
|
||||
tenantId: dto.tenantId,
|
||||
stayType: dto.stayType,
|
||||
responsibleOrganizationId: dto.responsibleOrganizationId ?? student.organizationId,
|
||||
notes: dto.notes,
|
||||
bedId: dto.bedId,
|
||||
lockerId: dto.lockerId,
|
||||
@@ -192,12 +197,16 @@ export class OccupanciesService {
|
||||
|
||||
// 新床位校验
|
||||
if (dto.newBedId) {
|
||||
const newBed = await runner.manager.findOne(Bed, { where: { id: dto.newBedId, roomId: dto.newRoomId } });
|
||||
const newBed = await runner.manager.findOne(Bed, {
|
||||
where: { id: dto.newBedId, roomId: dto.newRoomId },
|
||||
});
|
||||
if (!newBed) throw new BadRequestException('目标床位不存在或不属于目标宿舍');
|
||||
if (newBed.status !== 'available') throw new BadRequestException('目标床位已被占用');
|
||||
}
|
||||
if (dto.newLockerId) {
|
||||
const newLocker = await runner.manager.findOne(Locker, { where: { id: dto.newLockerId, roomId: dto.newRoomId } });
|
||||
const newLocker = await runner.manager.findOne(Locker, {
|
||||
where: { id: dto.newLockerId, roomId: dto.newRoomId },
|
||||
});
|
||||
if (!newLocker) throw new BadRequestException('目标柜子不存在或不属于目标宿舍');
|
||||
if (newLocker.status !== 'available') throw new BadRequestException('目标柜子已被占用');
|
||||
}
|
||||
@@ -214,8 +223,8 @@ export class OccupanciesService {
|
||||
roomId: dto.newRoomId,
|
||||
checkInDate: dto.transferDate,
|
||||
billingStartDate: dto.newBillingStartDate || defaultBillingStart,
|
||||
rentalType: oldOcc.rentalType,
|
||||
tenantId: oldOcc.tenantId,
|
||||
stayType: oldOcc.stayType,
|
||||
responsibleOrganizationId: oldOcc.responsibleOrganizationId,
|
||||
notes: `从${oldOcc.roomId}号房换入`,
|
||||
bedId: dto.newBedId,
|
||||
lockerId: dto.newLockerId,
|
||||
@@ -339,7 +348,8 @@ export class OccupanciesService {
|
||||
}
|
||||
// 释放床位/柜子
|
||||
if (occ.bedId) await runner.manager.update(Bed, occ.bedId, { status: 'available' });
|
||||
if (occ.lockerId) await runner.manager.update(Locker, occ.lockerId, { status: 'available' });
|
||||
if (occ.lockerId)
|
||||
await runner.manager.update(Locker, occ.lockerId, { status: 'available' });
|
||||
success++;
|
||||
}
|
||||
await runner.commitTransaction();
|
||||
@@ -395,7 +405,23 @@ export class OccupanciesService {
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 查找或创建学生
|
||||
// 1. 解析所属机构;未填写时默认本机构
|
||||
let organization = row.organization?.trim()
|
||||
? await this.organizationRepo.findOne({ where: { name: row.organization.trim() } })
|
||||
: await this.organizationRepo.findOne({ where: { isHost: true, status: 'active' } });
|
||||
if (!organization && row.organization?.trim()) {
|
||||
organization = await this.organizationRepo.save(
|
||||
this.organizationRepo.create({
|
||||
publicId: uuidV7(),
|
||||
code: `ORG_${Date.now()}_${i}`,
|
||||
name: row.organization.trim(),
|
||||
isHost: false,
|
||||
status: 'active',
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (!organization) throw new BadRequestException('尚未配置本机构');
|
||||
|
||||
let student = await this.studentRepo.findOne({ where: { name: row.name.trim() } });
|
||||
if (!student) {
|
||||
student = await this.studentRepo.save(
|
||||
@@ -407,7 +433,7 @@ export class OccupanciesService {
|
||||
ethnicity: row.ethnicity?.trim() || undefined,
|
||||
emergencyContact: row.emergencyContact?.trim() || undefined,
|
||||
emergencyPhone: row.emergencyPhone?.trim() || undefined,
|
||||
organization: row.organization?.trim() || undefined,
|
||||
organizationId: organization.id,
|
||||
supervisor: row.supervisor?.trim() || undefined,
|
||||
}),
|
||||
);
|
||||
@@ -422,8 +448,7 @@ export class OccupanciesService {
|
||||
updates.emergencyContact = row.emergencyContact.trim();
|
||||
if (!student.emergencyPhone && row.emergencyPhone?.trim())
|
||||
updates.emergencyPhone = row.emergencyPhone.trim();
|
||||
if (!student.organization && row.organization?.trim())
|
||||
updates.organization = row.organization.trim();
|
||||
if (!student.organizationId) updates.organizationId = organization.id;
|
||||
if (!student.supervisor && row.supervisor?.trim())
|
||||
updates.supervisor = row.supervisor.trim();
|
||||
if (Object.keys(updates).length > 0) {
|
||||
@@ -486,6 +511,7 @@ export class OccupanciesService {
|
||||
roomId: room.id,
|
||||
checkInDate,
|
||||
billingStartDate: checkInDate,
|
||||
responsibleOrganizationId: student.organizationId || organization.id,
|
||||
};
|
||||
// 如果有退宿日期,直接记录
|
||||
if (row.checkOutDate?.trim()) {
|
||||
|
||||
Reference in New Issue
Block a user