fix(server): hide CLOSED rows from preview for legacy mini-program compatibility
The previous fix made publishDaySlots CLOSE rows instead of DELETE them. That works correctly on the server, but the live mini-program still ships the old schedule.vue which only knows about isPublished (no status field). It would re-render the CLOSED rows with the '已发布' badge, making it look like 'clear day' did nothing. Hide CLOSED rows from getSchedulePreview so the legacy client shows '当日暂无排课' for rest days — the same UX it had before this fix shipped. All-CLOSED days return []; days with a mix return only the non-CLOSED rows; days the cron hasn't touched still return the ghost template. The republished mini-program (with status-aware rendering) will see the same [] for rest days, which is consistent. Tests: 4 new specs covering all-OPEN / all-CLOSED / mixed / empty-day cases.
This commit is contained in:
@@ -231,6 +231,69 @@ describe('TimeSlotService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// getSchedulePreview
|
||||
// -------------------------------------------------------------------------
|
||||
describe('getSchedulePreview', () => {
|
||||
const date = '2026-04-07'
|
||||
|
||||
it('returns OPEN/FULL rows with isPublished: true', async () => {
|
||||
mockPrisma.timeSlot.findMany.mockResolvedValueOnce([
|
||||
makeSlot({ id: 'slot-1', startTime: '09:00', endTime: '10:00', status: TimeSlotStatus.OPEN }),
|
||||
makeSlot({ id: 'slot-2', startTime: '10:30', endTime: '11:30', status: TimeSlotStatus.FULL }),
|
||||
])
|
||||
|
||||
const result = await service.getSchedulePreview(date)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.every((s) => s.isPublished === true)).toBe(true)
|
||||
})
|
||||
|
||||
it('HIDES CLOSED rows so the legacy client does not render them as "已发布"', async () => {
|
||||
// Simulates an admin "clear day" — DB now has 13 CLOSED rows.
|
||||
mockPrisma.timeSlot.findMany.mockResolvedValueOnce(
|
||||
Array.from({ length: 13 }, (_, i) =>
|
||||
makeSlot({
|
||||
id: `slot-${i}`,
|
||||
startTime: `${String(8 + Math.floor(i / 2)).padStart(2, '0')}:${i % 2 === 0 ? '00' : '30'}`,
|
||||
endTime: `${String(8 + Math.floor(i / 2)).padStart(2, '0')}:${i % 2 === 0 ? '30' : '30'}`,
|
||||
status: TimeSlotStatus.CLOSED,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
const result = await service.getSchedulePreview(date)
|
||||
|
||||
// Critical for online compatibility: the legacy client must see []
|
||||
// so it shows "当日暂无排课", not 13 phantom "已发布" slots.
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('returns the OPEN subset and hides CLOSED when mixed', async () => {
|
||||
mockPrisma.timeSlot.findMany.mockResolvedValueOnce([
|
||||
makeSlot({ id: 'open-1', status: TimeSlotStatus.OPEN }),
|
||||
makeSlot({ id: 'closed-1', status: TimeSlotStatus.CLOSED, startTime: '10:00', endTime: '11:00' }),
|
||||
makeSlot({ id: 'open-2', status: TimeSlotStatus.OPEN, startTime: '10:30', endTime: '11:30' }),
|
||||
])
|
||||
|
||||
const result = await service.getSchedulePreview(date)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((s) => s.id).sort()).toEqual(['open-1', 'open-2'])
|
||||
expect(result.find((s) => s.id === 'closed-1')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns the ghost template when the day has no rows at all', async () => {
|
||||
mockPrisma.timeSlot.findMany.mockResolvedValueOnce([])
|
||||
|
||||
const result = await service.getSchedulePreview(date)
|
||||
|
||||
// Ghost = isPublished false, id null.
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(result.every((s) => s.isPublished === false && s.id === null)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// createManualSlot
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -148,13 +148,26 @@ export class TimeSlotService {
|
||||
|
||||
/**
|
||||
* Return a schedule preview for a given date.
|
||||
* If TimeSlot records already exist → return them (isPublished: true).
|
||||
* Otherwise → derive from the fixed default schedule (isPublished: false).
|
||||
*
|
||||
* Visibility rules (driven by the online mini-program's existing client):
|
||||
* - OPEN/FULL slots are returned with `isPublished: true`.
|
||||
* - CLOSED slots are HIDDEN from preview entirely. The pre-fix admin flow
|
||||
* used to DELETE these rows on "clear day", but a non-deleting flow
|
||||
* leaves CLOSED rows behind — the existing mini-program UI does not
|
||||
* understand the CLOSED status and would render them as "已发布",
|
||||
* making it look like the clear did not take effect. Hiding them
|
||||
* preserves the legacy "empty list = rest day" UX until the client
|
||||
* is republished with CLOSED-aware rendering.
|
||||
* - When the day is entirely CLOSED (a "rest day"), return an empty
|
||||
* array rather than the default ghost template, so the client shows
|
||||
* "当日暂无排课" instead of inviting another publish-loop cycle.
|
||||
* - When the day has no rows at all (never generated), return the
|
||||
* default template as ghost previews (`isPublished: false`).
|
||||
*/
|
||||
async getSchedulePreview(date: string): Promise<ScheduleSlotPreview[]> {
|
||||
const parsedDate = new Date(date)
|
||||
|
||||
// 1. Check for existing TimeSlot records (all statuses)
|
||||
// 1. Check for existing TimeSlot records (all statuses).
|
||||
const existingSlots = await this.prisma.timeSlot.findMany({
|
||||
where: {
|
||||
date: { gte: this.toDateOfDay(parsedDate), lte: this.toEndOfDay(parsedDate) },
|
||||
@@ -162,8 +175,14 @@ export class TimeSlotService {
|
||||
orderBy: { startTime: 'asc' },
|
||||
})
|
||||
|
||||
if (existingSlots.length > 0) {
|
||||
return existingSlots.map((slot) => ({
|
||||
// 2. Filter out CLOSED — they are admin-closed (typically "rest day")
|
||||
// and must not surface to the legacy mini-program client.
|
||||
const openSlots = existingSlots.filter(
|
||||
(slot) => slot.status !== TimeSlotStatus.CLOSED,
|
||||
)
|
||||
|
||||
if (openSlots.length > 0) {
|
||||
return openSlots.map((slot) => ({
|
||||
id: slot.id,
|
||||
date: date,
|
||||
startTime: slot.startTime,
|
||||
@@ -177,6 +196,12 @@ export class TimeSlotService {
|
||||
}))
|
||||
}
|
||||
|
||||
// 3. All rows for the day are CLOSED → admin declared a rest day.
|
||||
// Return [] so the client shows the "no slots" empty state.
|
||||
if (existingSlots.length > 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 2. No existing slots — use fixed default schedule
|
||||
const defaultSlots = getDefaultTimeSlots()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user