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
|
// createManualSlot
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -148,13 +148,26 @@ export class TimeSlotService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a schedule preview for a given date.
|
* 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[]> {
|
async getSchedulePreview(date: string): Promise<ScheduleSlotPreview[]> {
|
||||||
const parsedDate = new Date(date)
|
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({
|
const existingSlots = await this.prisma.timeSlot.findMany({
|
||||||
where: {
|
where: {
|
||||||
date: { gte: this.toDateOfDay(parsedDate), lte: this.toEndOfDay(parsedDate) },
|
date: { gte: this.toDateOfDay(parsedDate), lte: this.toEndOfDay(parsedDate) },
|
||||||
@@ -162,8 +175,14 @@ export class TimeSlotService {
|
|||||||
orderBy: { startTime: 'asc' },
|
orderBy: { startTime: 'asc' },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (existingSlots.length > 0) {
|
// 2. Filter out CLOSED — they are admin-closed (typically "rest day")
|
||||||
return existingSlots.map((slot) => ({
|
// 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,
|
id: slot.id,
|
||||||
date: date,
|
date: date,
|
||||||
startTime: slot.startTime,
|
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
|
// 2. No existing slots — use fixed default schedule
|
||||||
const defaultSlots = getDefaultTimeSlots()
|
const defaultSlots = getDefaultTimeSlots()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user