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.