perf: 优化订阅刷新逻辑

This commit is contained in:
richarjiang
2026-09-07 14:06:19 +08:00
parent 88cd8419c8
commit f5c7b7eaac
27 changed files with 3248 additions and 1060 deletions

View File

@@ -1,11 +1,12 @@
import { Test, TestingModule } from '@nestjs/testing'
import { NotFoundException } from '@nestjs/common'
import { BadRequestException, NotFoundException } from '@nestjs/common'
import { UserService } from '../user.service'
import { PrismaService } from '../../prisma/prisma.service'
import {
MembershipStatus,
BookingStatus,
UserRole,
CardTypeCategory,
SubscriptionMessageScene,
} from '@mp-pilates/shared'
import { ConfigService } from '@nestjs/config'
@@ -23,6 +24,7 @@ const makeUser = (overrides: Record<string, unknown> = {}) => ({
avatarUrl: 'https://example.com/avatar.png',
role: UserRole.MEMBER,
adminBookingSubscriptionCount: 0,
lastLoginAt: new Date('2024-06-01T08:00:00Z'),
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z'),
_count: { memberships: 2 },
@@ -53,6 +55,8 @@ const makeBooking = (
const mockPrisma = {
user: {
findUnique: jest.fn(),
findMany: jest.fn(),
count: jest.fn(),
update: jest.fn(),
},
subscriptionMessageConsent: {
@@ -61,6 +65,13 @@ const mockPrisma = {
},
booking: {
findMany: jest.fn(),
groupBy: jest.fn(),
},
membership: {
findFirst: jest.fn(),
update: jest.fn(),
create: jest.fn(),
updateMany: jest.fn(),
},
}
@@ -120,6 +131,7 @@ describe('UserService', () => {
avatarUrl: 'https://example.com/avatar.png',
role: UserRole.MEMBER,
activeMembershipCount: 3,
inviteShareEligible: true,
adminBookingSubscriptionCount: 0,
subscriptionMessageTemplates: {
templates: [
@@ -423,4 +435,153 @@ describe('UserService', () => {
)
})
})
describe('getMemberDetail', () => {
const cardType = {
id: 'ct-1',
name: '10次卡',
type: CardTypeCategory.TIMES,
totalTimes: 10,
durationDays: 180,
price: 150000,
originalPrice: null,
description: null,
coverUrl: null,
isActive: true,
sortOrder: 0,
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z'),
}
const membership = {
id: 'mem-1',
userId: 'user-1',
cardTypeId: 'ct-1',
remainingTimes: 6,
totalTimes: 10,
startDate: new Date('2024-01-01T00:00:00Z'),
expireDate: new Date('2099-01-01T00:00:00Z'),
status: MembershipStatus.ACTIVE,
createdAt: new Date('2024-01-01T00:00:00Z'),
updatedAt: new Date('2024-01-01T00:00:00Z'),
cardType,
}
it('returns profile, memberships, stats and upcoming bookings', async () => {
mockPrisma.user.findUnique.mockResolvedValue({
...makeUser(),
memberships: [membership],
})
mockPrisma.booking.groupBy.mockResolvedValue([
{ userId: 'user-1', status: BookingStatus.COMPLETED, _count: { id: 3 } },
{ userId: 'user-1', status: BookingStatus.CANCELLED, _count: { id: 1 } },
{ userId: 'user-1', status: BookingStatus.NO_SHOW, _count: { id: 1 } },
])
mockPrisma.booking.findMany.mockResolvedValue([
{
id: 'booking-up-1',
status: BookingStatus.CONFIRMED,
timeSlot: {
date: new Date('2099-12-31T00:00:00Z'),
startTime: '09:00',
endTime: '10:00',
},
membership: { cardType: { name: '10次卡' } },
},
])
const result = await service.getMemberDetail('user-1')
expect(result.user.userId).toBe('user-1')
expect(result.user.lastLoginAt).toBe('2024-06-01T08:00:00.000Z')
expect(result.memberships).toHaveLength(1)
expect(result.memberships[0].cardType.name).toBe('10次卡')
expect(result.stats).toEqual({
totalBookings: 5,
completedBookings: 3,
cancelledBookings: 1,
noShowBookings: 1,
})
expect(result.upcomingBookings).toEqual([
{
id: 'booking-up-1',
status: BookingStatus.CONFIRMED,
date: '2099-12-31',
startTime: '09:00',
endTime: '10:00',
cardName: '10次卡',
},
])
})
it('throws NotFoundException when member does not exist', async () => {
mockPrisma.user.findUnique.mockResolvedValue(null)
await expect(service.getMemberDetail('missing')).rejects.toThrow(NotFoundException)
})
})
describe('updateMemberProfile', () => {
it('updates nickname and phone then returns the dossier', async () => {
mockPrisma.user.findUnique
.mockResolvedValueOnce(makeUser())
.mockResolvedValueOnce({ ...makeUser({ nickname: 'Bob', phone: '13900000000' }), memberships: [] })
mockPrisma.user.update.mockResolvedValue(makeUser({ nickname: 'Bob', phone: '13900000000' }))
mockPrisma.booking.groupBy.mockResolvedValue([])
mockPrisma.booking.findMany.mockResolvedValue([])
const result = await service.updateMemberProfile('user-1', {
nickname: 'Bob',
phone: '13900000000',
})
expect(mockPrisma.user.update).toHaveBeenCalledWith({
where: { id: 'user-1' },
data: { nickname: 'Bob', phone: '13900000000' },
})
expect(result.user.nickname).toBe('Bob')
expect(result.user.phone).toBe('13900000000')
})
})
describe('deleteUserMembership', () => {
it('expires only the selected membership', async () => {
mockPrisma.membership.findFirst.mockResolvedValue({
id: 'mem-1',
userId: 'user-1',
status: MembershipStatus.ACTIVE,
})
mockPrisma.membership.update.mockResolvedValue({
id: 'mem-1',
status: MembershipStatus.EXPIRED,
})
await service.deleteUserMembership('user-1', 'mem-1')
expect(mockPrisma.membership.findFirst).toHaveBeenCalledWith({
where: { id: 'mem-1', userId: 'user-1' },
})
expect(mockPrisma.membership.update).toHaveBeenCalledWith({
where: { id: 'mem-1' },
data: { status: MembershipStatus.EXPIRED },
})
expect(mockPrisma.membership.updateMany).not.toHaveBeenCalled()
})
it('rejects when the membership is missing or belongs to another user', async () => {
mockPrisma.membership.findFirst.mockResolvedValue(null)
await expect(service.deleteUserMembership('user-1', 'mem-other')).rejects.toThrow(
NotFoundException,
)
expect(mockPrisma.membership.update).not.toHaveBeenCalled()
})
it('rejects a missing membershipId', async () => {
await expect(service.deleteUserMembership('user-1', '')).rejects.toThrow(
BadRequestException,
)
expect(mockPrisma.membership.findFirst).not.toHaveBeenCalled()
})
})
})