fix: 修复身体画像约课阻断、页面响应式丢失及多项边界问题

- fix(booking): 修复未认领画像阻断约课问题,支持约课事务内自动认领绑定
- fix(app): 修复 Pinia 状态解构导致响应式失效,补齐 PortraitRadar 组件实例传参
- fix(admin): 修复经营助手复测待办点击 404,优化体验课后线索回访文案
- fix(server): 线下评估日期间隔按中国时区校验,复测按期次单项核销,优化单次评估分享卡与限流清理
- test: 补齐 body-portrait-offline 与 booking 来源画像单元测试

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-11 22:45:44 +08:00
parent ab6602e41d
commit d91282ecbd
18 changed files with 324 additions and 37 deletions

View File

@@ -160,6 +160,11 @@ function buildTxMock(overrides: Record<string, unknown> = {}) {
bookingStatusHistory: {
create: jest.fn(),
},
bodyPortraitAssessment: {
findUnique: jest.fn(),
findFirst: jest.fn(),
update: jest.fn(),
},
...overrides,
}
}
@@ -476,6 +481,58 @@ describe('BookingService', () => {
expect(result).toBeDefined()
})
it('binds anonymous originAssessmentId to current user on booking create', async () => {
const tx = buildTxMock()
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
tx.booking.findUnique.mockResolvedValue(null)
tx.membership.findUnique.mockResolvedValue(mockActiveMembership)
tx.bodyPortraitAssessment.findUnique.mockResolvedValue({ id: 'anon-assess', userId: null })
tx.bodyPortraitAssessment.update.mockResolvedValue({ id: 'anon-assess', userId: MOCK_USER_ID })
tx.booking.create.mockResolvedValue({
...mockConfirmedBooking,
originAssessmentId: 'anon-assess',
status: BookingStatus.PENDING_CONFIRMATION,
})
;(prisma.$transaction as jest.Mock).mockImplementation((fn) => fn(tx))
;(prisma.booking.findUnique as jest.Mock).mockResolvedValue({
...mockConfirmedBooking,
originAssessmentId: 'anon-assess',
status: BookingStatus.PENDING_CONFIRMATION,
timeSlot: mockOpenSlot,
membership: mockActiveMembership,
})
;(prisma.user.findMany as jest.Mock).mockResolvedValue([])
await service.createBooking(MOCK_USER_ID, { ...dto, originAssessmentId: 'anon-assess' })
expect(tx.bodyPortraitAssessment.update).toHaveBeenCalledWith({
where: { id: 'anon-assess' },
data: { userId: MOCK_USER_ID, status: 'CLAIMED', claimedAt: expect.any(Date) },
})
expect(tx.booking.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
originAssessmentId: 'anon-assess',
}),
}),
)
})
it('rejects originAssessmentId belonging to another user', async () => {
const tx = buildTxMock()
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
tx.booking.findUnique.mockResolvedValue(null)
tx.membership.findUnique.mockResolvedValue(mockActiveMembership)
tx.bodyPortraitAssessment.findUnique.mockResolvedValue({ id: 'other-assess', userId: 'someone-else' })
;(prisma.$transaction as jest.Mock).mockImplementation((fn) => fn(tx))
await expect(
service.createBooking(MOCK_USER_ID, { ...dto, originAssessmentId: 'other-assess' }),
).rejects.toThrow('画像报告不属于当前用户')
})
it('records booking status history when user creates a booking', async () => {
const nearFullSlot = { ...mockOpenSlot, bookedCount: 4, capacity: 5 }