feat: 支持课后评价与成长档案

完成后即可评价并订阅提醒,馆主可记录体测、笔记与成长照片,首页展示匿名星级均分。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
richarjiang
2026-09-09 15:55:48 +08:00
parent c3e46f7ffa
commit 139882d7a1
50 changed files with 1307 additions and 15 deletions

View File

@@ -0,0 +1,29 @@
import { ReviewReminderService } from '../review-reminder.service'
import { PrismaService } from '../../prisma/prisma.service'
import { SubscriptionMessageService } from '../../user/subscription-message.service'
import { ConfigService } from '@nestjs/config'
import { Logger } from '@nestjs/common'
describe('Review reminder queue', () => {
const db = { booking: { findMany: jest.fn(), updateMany: jest.fn(), update: jest.fn() } }
const messages = { sendReviewReminder: jest.fn() }, config = { get: jest.fn() }
let service: ReviewReminderService
beforeEach(() => {
jest.resetAllMocks()
config.get.mockReturnValue('tmpl')
service = new ReviewReminderService(db as unknown as PrismaService, messages as unknown as SubscriptionMessageService, config as unknown as ConfigService)
jest.spyOn(Logger.prototype, 'error').mockImplementation(() => {})
db.booking.findMany.mockResolvedValue([{ id: 'b', userId: 'u', user: { openid: 'o' } }])
})
afterEach(() => jest.restoreAllMocks())
it('does not consume queue when template is unconfigured', async () => { config.get.mockReturnValue(''); await service.run(); expect(db.booking.findMany).not.toHaveBeenCalled() })
it('queries only due, unreviewed completed lessons and atomically claims a booking', async () => {
db.booking.updateMany.mockResolvedValue({ count: 1 }); messages.sendReviewReminder.mockResolvedValue(true)
await service.run()
const where = db.booking.findMany.mock.calls[0][0].where
expect(where.status).toBe('COMPLETED'); expect(where.review).toBeNull(); expect(where.reviewReminderDueAt.lte).toBeInstanceOf(Date)
expect(db.booking.updateMany.mock.calls[0][0].where).toEqual({ id: 'b', status: 'COMPLETED', review: null, reviewReminderClaimedAt: null })
expect(db.booking.update).toHaveBeenCalledWith({ where: { id: 'b' }, data: { reviewReminderSentAt: expect.any(Date) } })
})
it('does not send if another worker claimed or member already reviewed', async () => { db.booking.updateMany.mockResolvedValue({ count: 0 }); await service.run(); expect(messages.sendReviewReminder).not.toHaveBeenCalled() })
it('does not mark a failed or unknown send as delivered or automatically release it', async () => { db.booking.updateMany.mockResolvedValue({ count: 1 }); messages.sendReviewReminder.mockRejectedValue(new Error('timeout')); await service.run(); expect(db.booking.update).not.toHaveBeenCalled(); expect(db.booking.updateMany).toHaveBeenCalledTimes(1) })
})

View File

@@ -1,3 +1,4 @@
import { FlashSaleService } from '../../flash-sale/flash-sale.service'
import { Test, TestingModule } from '@nestjs/testing'
import { Logger } from '@nestjs/common'
import { SchedulerService } from '../scheduler.service'
@@ -33,6 +34,7 @@ describe('SchedulerService', () => {
providers: [
SchedulerService,
{ provide: SlotGeneratorService, useValue: mockSlotGenerator },
{ provide: FlashSaleService, useValue: { expireUnpaidReservations: jest.fn() } },
],
}).compile()