perf: 支持约课以及消息推送能力

This commit is contained in:
richarjiang
2026-04-12 21:44:44 +08:00
parent 9639f44698
commit c60821c5ff
28 changed files with 963 additions and 86 deletions

View File

@@ -1,14 +1,47 @@
import { Injectable, NotFoundException } from '@nestjs/common'
import { MembershipStatus, BookingStatus, UserRole, CardTypeCategory } from '@mp-pilates/shared'
import type { PaginatedData, UserProfileResponse, UserStatsResponse } from '@mp-pilates/shared'
import {
MembershipStatus,
BookingStatus,
UserRole,
CardTypeCategory,
SubscriptionMessageScene,
} from '@mp-pilates/shared'
import type {
PaginatedData,
UserProfileResponse,
UserStatsResponse,
SubscriptionMessageConsentSummary,
SubscriptionMessageRequestItem,
SubscriptionMessageRequestResult,
SubscriptionMessageTemplateConfig,
} from '@mp-pilates/shared'
import { ConfigService } from '@nestjs/config'
import { PrismaService } from '../prisma/prisma.service'
import { UpdateUserMembershipDto } from './dto/update-user-membership.dto'
const VALID_CARD_TYPES = new Set<string>(Object.values(CardTypeCategory))
type SubscriptionMessageConsentDelegate = PrismaService['subscriptionMessageConsent']
type SubscriptionMessageConsentRecord = Awaited<ReturnType<SubscriptionMessageConsentDelegate['findMany']>>[number]
@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly configService: ConfigService,
) {}
private buildSubscriptionTemplateConfig(): SubscriptionMessageTemplateConfig {
return {
templates: [
{
templateId: this.configService.get<string>('WX_SUBSCRIBE_TEMPLATE_BOOKING_CONFIRMED', ''),
scene: SubscriptionMessageScene.BOOKING_CREATED,
description: '购卡或预约时请求一次订阅,用于后续预约确认通知推送',
},
].filter((item) => item.templateId),
}
}
async getProfile(userId: string): Promise<UserProfileResponse> {
const user = await this.prisma.user.findUnique({
@@ -35,6 +68,7 @@ export class UserService {
avatarUrl: user.avatarUrl,
role: user.role as UserRole,
activeMembershipCount: user._count.memberships,
subscriptionMessageTemplates: this.buildSubscriptionTemplateConfig(),
createdAt: user.createdAt.toISOString(),
}
}
@@ -67,10 +101,86 @@ export class UserService {
avatarUrl: updated.avatarUrl,
role: updated.role as UserRole,
activeMembershipCount: updated._count.memberships,
subscriptionMessageTemplates: this.buildSubscriptionTemplateConfig(),
createdAt: updated.createdAt.toISOString(),
}
}
getSubscriptionMessageTemplates(): SubscriptionMessageTemplateConfig {
return this.buildSubscriptionTemplateConfig()
}
async reportSubscriptionMessageRequests(
userId: string,
requests: readonly SubscriptionMessageRequestItem[],
): Promise<SubscriptionMessageConsentSummary[]> {
if (requests.length === 0) {
return []
}
await Promise.all(
requests.map((item) => this.prisma.subscriptionMessageConsent.upsert({
where: {
userId_templateId_scene: {
userId,
templateId: item.templateId,
scene: item.scene,
},
},
create: {
userId,
templateId: item.templateId,
scene: item.scene,
totalRequestCount: 1,
acceptCount: item.result === 'accept' ? 1 : 0,
rejectCount: item.result === 'reject' ? 1 : 0,
banCount: item.result === 'ban' ? 1 : 0,
filterCount: item.result === 'filter' ? 1 : 0,
sentCount: 0,
lastResult: item.result,
lastRequestedAt: new Date(),
},
update: {
totalRequestCount: { increment: 1 },
acceptCount: { increment: item.result === 'accept' ? 1 : 0 },
rejectCount: { increment: item.result === 'reject' ? 1 : 0 },
banCount: { increment: item.result === 'ban' ? 1 : 0 },
filterCount: { increment: item.result === 'filter' ? 1 : 0 },
lastResult: item.result,
lastRequestedAt: new Date(),
},
})),
)
const summaries = await this.prisma.subscriptionMessageConsent.findMany({
where: {
userId,
OR: requests.map((item) => ({
templateId: item.templateId,
scene: item.scene,
})),
},
orderBy: { updatedAt: 'desc' },
})
return summaries.map((item: SubscriptionMessageConsentRecord) => ({
userId: item.userId,
templateId: item.templateId,
scene: item.scene as SubscriptionMessageScene,
totalRequestCount: item.totalRequestCount,
acceptCount: item.acceptCount,
rejectCount: item.rejectCount,
banCount: item.banCount,
filterCount: item.filterCount,
sentCount: item.sentCount,
lastResult: item.lastResult as SubscriptionMessageRequestResult,
lastRequestedAt: item.lastRequestedAt.toISOString(),
lastSentAt: item.lastSentAt?.toISOString() ?? null,
createdAt: item.createdAt.toISOString(),
updatedAt: item.updatedAt.toISOString(),
}))
}
async getStats(userId: string): Promise<UserStatsResponse> {
const completedBookings = await this.prisma.booking.findMany({
where: {