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,4 +1,4 @@
import { Injectable, NotFoundException } from '@nestjs/common'
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'
import {
MembershipStatus,
BookingStatus,
@@ -15,6 +15,10 @@ import type {
SubscriptionMessageRequestResult,
SubscriptionMessageTemplate,
SubscriptionMessageTemplateConfig,
AdminMemberSummary,
AdminMemberDetail,
MembershipWithCardType,
UpdateAdminMemberProfileDto,
} from '@mp-pilates/shared'
import { ConfigService } from '@nestjs/config'
import { PrismaService } from '../prisma/prisma.service'
@@ -23,6 +27,63 @@ import { UpdateUserMembershipDto } from './dto/update-user-membership.dto'
const VALID_CARD_TYPES = new Set<string>(Object.values(CardTypeCategory))
const ADMIN_BOOKING_SUBSCRIPTION_INCREMENT = 1
function serializeMembership(membership: {
id: string
userId: string
cardTypeId: string
remainingTimes: number | null
totalTimes: number | null
startDate: Date
expireDate: Date
status: string
createdAt: Date
updatedAt: Date
cardType: {
id: string
name: string
type: string
totalTimes: number | null
durationDays: number
price: number | { toString(): string }
originalPrice: number | { toString(): string } | null
description: string | null
coverUrl: string | null
isActive: boolean
sortOrder: number
createdAt: Date
updatedAt: Date
}
}): MembershipWithCardType {
const { cardType, ...rest } = membership
return {
id: rest.id,
userId: rest.userId,
cardTypeId: rest.cardTypeId,
remainingTimes: rest.remainingTimes,
totalTimes: rest.totalTimes,
startDate: rest.startDate.toISOString(),
expireDate: rest.expireDate.toISOString(),
status: rest.status as MembershipStatus,
createdAt: rest.createdAt.toISOString(),
updatedAt: rest.updatedAt.toISOString(),
cardType: {
id: cardType.id,
name: cardType.name,
type: cardType.type as CardTypeCategory,
totalTimes: cardType.totalTimes,
durationDays: cardType.durationDays,
price: Number(cardType.price),
originalPrice: cardType.originalPrice == null ? null : Number(cardType.originalPrice),
description: cardType.description,
coverUrl: cardType.coverUrl,
isActive: cardType.isActive,
sortOrder: cardType.sortOrder,
createdAt: cardType.createdAt.toISOString(),
updatedAt: cardType.updatedAt.toISOString(),
},
}
}
type SubscriptionMessageConsentDelegate = PrismaService['subscriptionMessageConsent']
type SubscriptionMessageConsentRecord = Awaited<ReturnType<SubscriptionMessageConsentDelegate['findMany']>>[number]
@@ -298,16 +359,7 @@ export class UserService {
limit: number,
search?: string,
cardType?: string,
): Promise<PaginatedData<{
userId: string
openid: string
nickname: string
phone: string | null
avatarUrl: string | null
totalBookings: number
completedBookings: number
cancelledBookings: number
}>> {
): Promise<PaginatedData<AdminMemberSummary>> {
const where: {
OR?: Array<{ [key: string]: unknown }>
memberships?: {
@@ -348,6 +400,14 @@ export class UserService {
nickname: true,
phone: true,
avatarUrl: true,
createdAt: true,
lastLoginAt: true,
memberships: {
where: { status: MembershipStatus.ACTIVE },
include: { cardType: { select: { name: true, type: true } } },
orderBy: { expireDate: 'desc' },
take: 1,
},
_count: {
select: {
bookings: true,
@@ -383,12 +443,18 @@ export class UserService {
const items = users.map((u) => {
const s = statsMap.get(u.id) ?? { total: 0, completed: 0, cancelled: 0 }
const active = u.memberships[0]
return {
userId: u.id,
openid: u.openid,
nickname: u.nickname,
phone: u.phone,
avatarUrl: u.avatarUrl,
createdAt: u.createdAt.toISOString(),
lastLoginAt: u.lastLoginAt?.toISOString() ?? null,
activeCard: active
? { name: active.cardType.name, type: active.cardType.type as CardTypeCategory }
: null,
totalBookings: s.total,
completedBookings: s.completed,
cancelledBookings: s.cancelled,
@@ -398,6 +464,109 @@ export class UserService {
return { items, total, page, limit }
}
async getMemberDetail(userId: string): Promise<AdminMemberDetail> {
const user = await this.prisma.user.findUnique({
where: { id: userId },
include: {
memberships: {
include: { cardType: true },
orderBy: [{ createdAt: 'desc' }],
},
},
})
if (!user) {
throw new NotFoundException('User not found')
}
const today = new Date()
today.setUTCHours(0, 0, 0, 0)
const [bookingStats, upcoming] = await Promise.all([
this.prisma.booking.groupBy({
by: ['status'],
where: { userId },
_count: { id: true },
}),
this.prisma.booking.findMany({
where: {
userId,
status: {
in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED],
},
timeSlot: { date: { gte: today } },
},
include: {
timeSlot: true,
membership: { include: { cardType: { select: { name: true } } } },
},
orderBy: [
{ timeSlot: { date: 'asc' } },
{ timeSlot: { startTime: 'asc' } },
],
}),
])
const stats = { total: 0, completed: 0, cancelled: 0, noShow: 0 }
for (const row of bookingStats) {
stats.total += row._count.id
if (row.status === BookingStatus.COMPLETED) stats.completed += row._count.id
if (row.status === BookingStatus.CANCELLED) stats.cancelled += row._count.id
if (row.status === BookingStatus.NO_SHOW) stats.noShow += row._count.id
}
return {
user: {
userId: user.id,
openid: user.openid,
nickname: user.nickname,
phone: user.phone,
avatarUrl: user.avatarUrl,
createdAt: user.createdAt.toISOString(),
lastLoginAt: user.lastLoginAt?.toISOString() ?? null,
},
memberships: user.memberships.map((membership) => serializeMembership(membership)),
stats: {
totalBookings: stats.total,
completedBookings: stats.completed,
cancelledBookings: stats.cancelled,
noShowBookings: stats.noShow,
},
upcomingBookings: upcoming.map((booking) => ({
id: booking.id,
status: booking.status as BookingStatus,
date: booking.timeSlot.date.toISOString().slice(0, 10),
startTime: booking.timeSlot.startTime,
endTime: booking.timeSlot.endTime,
cardName: booking.membership.cardType.name,
})),
}
}
async updateMemberProfile(
userId: string,
dto: UpdateAdminMemberProfileDto,
): Promise<AdminMemberDetail> {
const existing = await this.prisma.user.findUnique({ where: { id: userId } })
if (!existing) {
throw new NotFoundException('User not found')
}
const phone = dto.phone === undefined
? undefined
: (dto.phone?.trim() ? dto.phone.trim() : null)
await this.prisma.user.update({
where: { id: userId },
data: {
...(dto.nickname !== undefined && { nickname: dto.nickname.trim() }),
...(phone !== undefined && { phone }),
},
})
return this.getMemberDetail(userId)
}
// ─── Membership management ────────────────────────────────────────────────
async getUserMembership(userId: string) {
@@ -429,7 +598,15 @@ export class UserService {
status,
}
const existing = await this.prisma.membership.findFirst({ where: { userId } })
const existing = dto.membershipId
? await this.prisma.membership.findFirst({
where: { id: dto.membershipId, userId },
})
: await this.prisma.membership.findFirst({ where: { userId } })
if (dto.membershipId && !existing) {
throw new NotFoundException('Membership not found')
}
if (existing) {
return this.prisma.membership.update({
@@ -445,9 +622,20 @@ export class UserService {
})
}
async deleteUserMembership(userId: string): Promise<void> {
await this.prisma.membership.updateMany({
where: { userId },
async deleteUserMembership(userId: string, membershipId: string): Promise<void> {
if (!membershipId) {
throw new BadRequestException('membershipId is required')
}
const existing = await this.prisma.membership.findFirst({
where: { id: membershipId, userId },
})
if (!existing) {
throw new NotFoundException('Membership not found')
}
await this.prisma.membership.update({
where: { id: existing.id },
data: { status: MembershipStatus.EXPIRED },
})
}