publishDaySlots used to DELETE the TimeSlot rows of a day that an admin just 'cleared', which made the nightly slot-generation cron resurrect the default schedule the next morning — a real production bug where classes would reappear on a teacher's rest day. Changes: - publishDaySlots now CLOSEs (never DELETEs) orphaned rows, and materializes the default template for days with no rows so an empty publish still records intent. update on existing rows preserves their current status, so re-publishing the page cannot silently reopen a CLOSED rest day. - SlotGeneratorService.generateSlots pre-fetches dates that already have any TimeSlot row and skips them, so the cron no longer overwrites admin-touched days even when admin only set status without changing times. - SlotGeneratorService.prunePastClosedSlots (new) deletes past CLOSED slots with no bookings, scheduled nightly at 02:35, so the 'no DELETE' rule above does not leak rows forever. - schedule.vue surfaces the CLOSED state with a grey '已关闭' badge and card style. Tests: 12 new specs across time-slot.service.spec.ts (publishDaySlots behaviour) and slot-generator.service.spec.ts (skip touched dates + prune conditions). Full suite: 315 / 315 passing.
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common'
|
|
import { Cron } from '@nestjs/schedule'
|
|
import { SlotGeneratorService } from '../time-slot/slot-generator.service'
|
|
|
|
@Injectable()
|
|
export class SchedulerService {
|
|
private readonly logger = new Logger(SchedulerService.name)
|
|
|
|
constructor(
|
|
private readonly slotGenerator: SlotGeneratorService,
|
|
) {}
|
|
|
|
/** 02:00 daily — generate slots 14 days ahead from week templates */
|
|
@Cron('0 2 * * *')
|
|
async handleSlotGeneration(): Promise<void> {
|
|
try {
|
|
const count = await this.slotGenerator.generateSlots(14)
|
|
this.logger.log(`[handleSlotGeneration] Created ${count} new time slots`)
|
|
} catch (err) {
|
|
this.logger.error('[handleSlotGeneration] Failed to generate slots', err)
|
|
}
|
|
}
|
|
|
|
/** 02:30 daily — close past OPEN slots */
|
|
@Cron('30 2 * * *')
|
|
async handleCleanupSlots(): Promise<void> {
|
|
try {
|
|
const count = await this.slotGenerator.cleanupExpiredSlots()
|
|
this.logger.log(`[handleCleanupSlots] Closed ${count} expired slots`)
|
|
} catch (err) {
|
|
this.logger.error('[handleCleanupSlots] Failed to clean up slots', err)
|
|
}
|
|
}
|
|
|
|
/** 02:35 daily — delete past CLOSED slots with no bookings attached */
|
|
@Cron('35 2 * * *')
|
|
async handlePrunePastClosedSlots(): Promise<void> {
|
|
try {
|
|
const count = await this.slotGenerator.prunePastClosedSlots()
|
|
this.logger.log(`[handlePrunePastClosedSlots] Pruned ${count} past closed slots`)
|
|
} catch (err) {
|
|
this.logger.error('[handlePrunePastClosedSlots] Failed to prune slots', err)
|
|
}
|
|
}
|
|
|
|
/** 03:00 daily — expire memberships past their end date or with 0 sessions */
|
|
@Cron('0 3 * * *')
|
|
async handleCheckMemberships(): Promise<void> {
|
|
try {
|
|
const count = await this.slotGenerator.checkExpiredMemberships()
|
|
this.logger.log(`[handleCheckMemberships] Updated ${count} memberships`)
|
|
} catch (err) {
|
|
this.logger.error('[handleCheckMemberships] Failed to check memberships', err)
|
|
}
|
|
}
|
|
|
|
/** 22:00 daily — mark past CONFIRMED bookings as COMPLETED */
|
|
@Cron('0 22 * * *')
|
|
async handleCompleteBookings(): Promise<void> {
|
|
try {
|
|
const count = await this.slotGenerator.completeBookings()
|
|
this.logger.log(`[handleCompleteBookings] Completed ${count} bookings`)
|
|
} catch (err) {
|
|
this.logger.error('[handleCompleteBookings] Failed to complete bookings', err)
|
|
}
|
|
}
|
|
}
|