fix(server): 修复排课发布唯一约束冲突并更正默认时段模板

发布时按日期+起止时间匹配已有时段,避免幽灵预览重复 create;默认课表改为上午整点与下午 13:30 起每小时至 21:30。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-14 14:26:43 +08:00
parent d91282ecbd
commit b72357030d
6 changed files with 170 additions and 27 deletions

View File

@@ -7,6 +7,7 @@ import {
TimeSlotSource,
BookingStatus,
DEFAULT_SLOT_CAPACITY,
getDefaultTimeSlots,
} from '@mp-pilates/shared'
// ---------------------------------------------------------------------------
@@ -508,6 +509,102 @@ describe('TimeSlotService', () => {
expect(createData.capacity).toBe(4)
})
it('updates materialized template rows when ghost preview is published without ids', async () => {
const defaultSlots = getDefaultTimeSlots()
const materialized = defaultSlots.map((slot, index) =>
makeSlot({
id: `slot-${index}`,
startTime: slot.startTime,
endTime: slot.endTime,
source: TimeSlotSource.TEMPLATE,
}),
)
mockPrisma.timeSlot.findMany
.mockResolvedValueOnce([])
.mockResolvedValueOnce(materialized)
.mockResolvedValueOnce(materialized)
mockPrisma.timeSlot.createMany.mockResolvedValueOnce({ count: defaultSlots.length })
mockPrisma.timeSlot.update.mockImplementation(async (args: { where: { id: string } }) => {
const row = materialized.find((s) => s.id === args.where.id)!
return { ...row }
})
await service.publishDaySlots({
date,
slots: defaultSlots.map((slot) => ({
startTime: slot.startTime,
endTime: slot.endTime,
capacity: DEFAULT_SLOT_CAPACITY,
})),
})
expect(mockPrisma.timeSlot.createMany).toHaveBeenCalledTimes(1)
expect(mockPrisma.timeSlot.create).not.toHaveBeenCalled()
expect(mockPrisma.timeSlot.update).toHaveBeenCalledTimes(defaultSlots.length)
})
it('reopens an existing row by time when admin re-adds a slot without existingSlotId', async () => {
const existing = [
makeSlot({
id: 'slot-open',
startTime: '09:30',
endTime: '10:30',
status: TimeSlotStatus.OPEN,
}),
]
mockPrisma.timeSlot.findMany
.mockResolvedValueOnce(existing)
.mockResolvedValueOnce(existing)
mockPrisma.timeSlot.update.mockResolvedValueOnce({
...existing[0],
status: TimeSlotStatus.OPEN,
source: TimeSlotSource.MANUAL,
capacity: 3,
})
await service.publishDaySlots({
date,
slots: [{ startTime: '09:30', endTime: '10:30', capacity: 3 }],
})
expect(mockPrisma.timeSlot.create).not.toHaveBeenCalled()
const updateData = (mockPrisma.timeSlot.update.mock.calls[0][0] as {
data: { status: string; source: string; capacity: number }
}).data
expect(updateData.status).toBe(TimeSlotStatus.OPEN)
expect(updateData.source).toBe(TimeSlotSource.MANUAL)
expect(updateData.capacity).toBe(3)
})
it('reopens a CLOSED row when admin publishes the same time window as a new slot', async () => {
const existing = [
makeSlot({
id: 'slot-closed',
startTime: '11:00',
endTime: '12:00',
status: TimeSlotStatus.CLOSED,
}),
]
mockPrisma.timeSlot.findMany
.mockResolvedValueOnce(existing)
.mockResolvedValueOnce(existing)
mockPrisma.timeSlot.update.mockResolvedValueOnce({
...existing[0],
status: TimeSlotStatus.OPEN,
})
await service.publishDaySlots({
date,
slots: [{ startTime: '11:00', endTime: '12:00', capacity: 2 }],
})
expect(mockPrisma.timeSlot.create).not.toHaveBeenCalled()
const updateData = (mockPrisma.timeSlot.update.mock.calls[0][0] as {
data: { status: string }
}).data
expect(updateData.status).toBe(TimeSlotStatus.OPEN)
})
it('returns the final state of the day ordered by startTime', async () => {
const finalState = [
makeSlot({ id: 'a', startTime: '09:00', endTime: '10:00' }),