feat: 支持分享邀请好友功能
This commit is contained in:
253
packages/server/src/invite/invite.service.ts
Normal file
253
packages/server/src/invite/invite.service.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common'
|
||||
import type { InviteReferral, InviteRewardGrant, Membership } from '@prisma/client'
|
||||
import { InviteReferralStatus, MembershipStatus, OrderStatus } from '@mp-pilates/shared'
|
||||
import type { InviteActivitySummary } from '@mp-pilates/shared'
|
||||
import { PrismaService } from '../prisma/prisma.service'
|
||||
import {
|
||||
INVITE_REWARD_REQUIRED_COUNT,
|
||||
INVITE_REWARD_TIMES,
|
||||
} from './invite.constants'
|
||||
|
||||
@Injectable()
|
||||
export class InviteService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
private isTrialCardType(type: string): boolean {
|
||||
return type === 'TRIAL'
|
||||
}
|
||||
|
||||
async bindInviterToUser(inviteeId: string, inviterId?: string | null): Promise<void> {
|
||||
if (!inviterId || inviterId === inviteeId) {
|
||||
return
|
||||
}
|
||||
|
||||
const [inviter, inviteeMembershipCount, existingReferral] = await Promise.all([
|
||||
this.prisma.user.findUnique({ where: { id: inviterId }, select: { id: true } }),
|
||||
this.prisma.membership.count({ where: { userId: inviteeId } }),
|
||||
this.prisma.inviteReferral.findUnique({ where: { inviteeId } }),
|
||||
])
|
||||
|
||||
if (!inviter || inviteeMembershipCount > 0 || existingReferral) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.prisma.inviteReferral.create({
|
||||
data: {
|
||||
inviterId,
|
||||
inviteeId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async recordTrialOrderPaid(orderId: string): Promise<void> {
|
||||
const order = await this.prisma.order.findUnique({
|
||||
where: { id: orderId },
|
||||
include: {
|
||||
cardType: true,
|
||||
user: { select: { id: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!order || order.status !== OrderStatus.PAID || !this.isTrialCardType(order.cardType.type)) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.prisma.inviteReferral.updateMany({
|
||||
where: {
|
||||
inviteeId: order.user.id,
|
||||
status: InviteReferralStatus.REGISTERED,
|
||||
trialOrderId: null,
|
||||
},
|
||||
data: {
|
||||
status: InviteReferralStatus.TRIAL_PURCHASED,
|
||||
trialOrderId: order.id,
|
||||
trialPurchasedAt: order.paidAt ?? new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async recordQualifiedTrialBooking(bookingId: string): Promise<void> {
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: {
|
||||
membership: { include: { cardType: true } },
|
||||
},
|
||||
})
|
||||
|
||||
if (!booking || booking.status !== 'COMPLETED' || !this.isTrialCardType(booking.membership.cardType.type)) {
|
||||
return
|
||||
}
|
||||
|
||||
const referral = await this.prisma.inviteReferral.findFirst({
|
||||
where: {
|
||||
inviteeId: booking.userId,
|
||||
status: {
|
||||
in: [InviteReferralStatus.REGISTERED, InviteReferralStatus.TRIAL_PURCHASED],
|
||||
},
|
||||
qualifiedBookingId: null,
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
})
|
||||
|
||||
if (!referral) {
|
||||
return
|
||||
}
|
||||
|
||||
await this.prisma.inviteReferral.update({
|
||||
where: { id: referral.id },
|
||||
data: {
|
||||
status: InviteReferralStatus.QUALIFIED,
|
||||
qualifiedBookingId: booking.id,
|
||||
qualifiedAt: booking.completedAt ?? new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
await this.grantRewardsIfEligible(referral.inviterId)
|
||||
}
|
||||
|
||||
async getInviteActivitySummary(userId: string): Promise<InviteActivitySummary> {
|
||||
const memberships = await this.prisma.membership.findMany({
|
||||
where: { userId },
|
||||
orderBy: [{ status: 'asc' }, { expireDate: 'desc' }],
|
||||
})
|
||||
const referrals = await this.prisma.inviteReferral.findMany({
|
||||
where: { inviterId: userId },
|
||||
include: {
|
||||
invitee: {
|
||||
select: {
|
||||
id: true,
|
||||
nickname: true,
|
||||
avatarUrl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
const rewardGrants = await this.prisma.inviteRewardGrant.findMany({
|
||||
where: { inviterId: userId },
|
||||
orderBy: { grantedAt: 'desc' },
|
||||
})
|
||||
|
||||
const canInvite = memberships.some((membership: Membership) => membership.status === MembershipStatus.ACTIVE)
|
||||
const qualifiedInviteCount = referrals.filter((item: InviteReferral) => item.status === InviteReferralStatus.QUALIFIED).length
|
||||
const rewardedTimes = rewardGrants.reduce((sum: number, item: InviteRewardGrant) => sum + item.rewardTimes, 0)
|
||||
const pendingRewardGrantCount = Math.max(
|
||||
0,
|
||||
qualifiedInviteCount - rewardGrants.length * INVITE_REWARD_REQUIRED_COUNT,
|
||||
)
|
||||
const currentCycleQualifiedCount = qualifiedInviteCount % INVITE_REWARD_REQUIRED_COUNT
|
||||
|
||||
return {
|
||||
inviterId: userId,
|
||||
canInvite,
|
||||
sharePath: `/pages/profile/invite?inviterId=${userId}`,
|
||||
rewardRuleInvitesRequired: INVITE_REWARD_REQUIRED_COUNT,
|
||||
rewardRuleTimes: INVITE_REWARD_TIMES,
|
||||
qualifiedInviteCount,
|
||||
rewardedTimes,
|
||||
pendingRewardGrantCount,
|
||||
pendingInviteCount: referrals.filter((item: InviteReferral) => item.status !== InviteReferralStatus.QUALIFIED).length,
|
||||
currentCycleQualifiedCount,
|
||||
nextRewardRemainingCount: currentCycleQualifiedCount === 0
|
||||
? INVITE_REWARD_REQUIRED_COUNT
|
||||
: INVITE_REWARD_REQUIRED_COUNT - currentCycleQualifiedCount,
|
||||
referrals: referrals.map((item: InviteReferral & { invitee: { nickname: string; avatarUrl: string | null } }) => ({
|
||||
id: item.id,
|
||||
inviteeId: item.inviteeId,
|
||||
inviteeNickname: item.invitee.nickname,
|
||||
inviteeAvatarUrl: item.invitee.avatarUrl,
|
||||
status: item.status as InviteReferralStatus,
|
||||
invitedAt: item.invitedAt.toISOString(),
|
||||
trialPurchasedAt: item.trialPurchasedAt?.toISOString() ?? null,
|
||||
qualifiedAt: item.qualifiedAt?.toISOString() ?? null,
|
||||
})),
|
||||
rewardGrants: rewardGrants.map((item: InviteRewardGrant) => ({
|
||||
id: item.id,
|
||||
membershipId: item.membershipId,
|
||||
qualifiedReferralCount: item.qualifiedReferralCount,
|
||||
rewardTimes: item.rewardTimes,
|
||||
grantedAt: item.grantedAt.toISOString(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
async validateInviterForTrialOrder(userId: string, inviterId?: string): Promise<void> {
|
||||
if (!inviterId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (inviterId === userId) {
|
||||
throw new BadRequestException('不能邀请自己购买体验课')
|
||||
}
|
||||
|
||||
const referral = await this.prisma.inviteReferral.findFirst({
|
||||
where: {
|
||||
inviterId,
|
||||
inviteeId: userId,
|
||||
},
|
||||
})
|
||||
|
||||
if (!referral) {
|
||||
throw new NotFoundException('邀请关系不存在或已失效')
|
||||
}
|
||||
}
|
||||
|
||||
private async grantRewardsIfEligible(inviterId: string): Promise<void> {
|
||||
const [qualifiedCount, rewardGrantCount] = await Promise.all([
|
||||
this.prisma.inviteReferral.count({
|
||||
where: {
|
||||
inviterId,
|
||||
status: InviteReferralStatus.QUALIFIED,
|
||||
},
|
||||
}),
|
||||
this.prisma.inviteRewardGrant.count({ where: { inviterId } }),
|
||||
])
|
||||
|
||||
const shouldGrantCount = Math.floor(qualifiedCount / INVITE_REWARD_REQUIRED_COUNT)
|
||||
const missingGrantCount = shouldGrantCount - rewardGrantCount
|
||||
|
||||
if (missingGrantCount <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let index = 0; index < missingGrantCount; index += 1) {
|
||||
const targetQualifiedCount = (rewardGrantCount + index + 1) * INVITE_REWARD_REQUIRED_COUNT
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const membership = await tx.membership.findFirst({
|
||||
where: {
|
||||
userId: inviterId,
|
||||
status: MembershipStatus.ACTIVE,
|
||||
},
|
||||
orderBy: [{ expireDate: 'desc' }, { createdAt: 'desc' }],
|
||||
})
|
||||
|
||||
if (!membership) {
|
||||
throw new BadRequestException('邀请人当前没有有效会员卡,无法发放奖励')
|
||||
}
|
||||
|
||||
await tx.membership.update({
|
||||
where: { id: membership.id },
|
||||
data: {
|
||||
remainingTimes: membership.remainingTimes === null
|
||||
? null
|
||||
: membership.remainingTimes + INVITE_REWARD_TIMES,
|
||||
status: MembershipStatus.ACTIVE,
|
||||
},
|
||||
})
|
||||
|
||||
await tx.inviteRewardGrant.create({
|
||||
data: {
|
||||
inviterId,
|
||||
membershipId: membership.id,
|
||||
qualifiedReferralCount: targetQualifiedCount,
|
||||
rewardTimes: INVITE_REWARD_TIMES,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user