import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../../infrastructure/database/prisma.service'; /** * M-MEMBER-01-04: EffectiveMembershipService * * 计算所有来源(Apple/Admin/Trial/Compensation)的 Grant, * 合并为单一 effectivePlan。 * * 首版规则:任意有效 premium grant → effectivePlan = premium,否则 free。 * Apple expired/revoked 只影响对应 Apple grant,不触及 Admin/Trial/Compensation。 */ export interface MembershipGrant { id: string; source: string; // "apple" | "admin" | "trial" | "compensation" status: string; planCode: string; // "free" | "premium" planName: string; startedAt: string; expiresAt: string | null; gracePeriodExpiresAt: string | null; autoRenewStatus: string | null; environment: string | null; } export interface EffectiveMembership { effectivePlan: 'free' | 'premium'; grants: MembershipGrant[]; appAccountToken: string | null; } @Injectable() export class EffectiveMembershipService { private readonly logger = new Logger(EffectiveMembershipService.name); // status 值中哪些视为"当前有效"(保留权益) private readonly ACTIVE_STATUSES = new Set(['active', 'grace_period', 'billing_retry']); constructor(private readonly prisma: PrismaService) {} /** * 计算当前用户的有效会员。 * * 规则: * status IN ('active', 'grace_period', 'billing_retry') * AND (expiresAt IS NULL OR expiresAt > NOW()) * → 该 Grant 视为有效 * * 任意有效 premium grant 存在 → effectivePlan = premium * 否则 → effectivePlan = free * * billing_retry:仅在 expiresAt > NOW() 时有效(Apple 在重试期间保留权益, * 但如果已超过原到期日且无 Grace Period,则不应视为有效)。 */ async compute(userId: string): Promise { const now = new Date(); // 查询所有 UserMembership 记录(非 soft-deleted),include plan const records = await this.prisma.userMembership.findMany({ where: { userId }, include: { plan: true }, orderBy: { createdAt: 'desc' }, }); // 过滤有效 Grant const grants: MembershipGrant[] = []; for (const r of records) { if (!this.ACTIVE_STATUSES.has(r.status)) continue; // Admin 永久会员:expiresAt = null → 始终有效 // grace_period:用 gracePeriodExpiresAt 代替 expiresAt 判断 if (r.status === 'grace_period') { if (r.gracePeriodExpiresAt && r.gracePeriodExpiresAt <= now) continue; } else { const isExpired = r.expiresAt !== null && r.expiresAt <= now; if (isExpired) continue; } // billing_retry 额外检查:仅在有有效 Grace Period 时保留权益 if (r.status === 'billing_retry') { if (r.gracePeriodExpiresAt && r.gracePeriodExpiresAt <= now) continue; } grants.push({ id: r.id, source: r.source, status: r.status, planCode: r.plan.code, planName: r.plan.name, startedAt: r.startedAt.toISOString(), expiresAt: r.expiresAt?.toISOString() ?? null, gracePeriodExpiresAt: r.gracePeriodExpiresAt?.toISOString() ?? null, autoRenewStatus: r.autoRenewStatus, environment: r.environment, }); } // 判定 effectivePlan const hasPremium = grants.some(g => g.planCode === 'premium'); const effectivePlan = hasPremium ? 'premium' : 'free'; // 获取 appAccountToken const user = await this.prisma.user.findUnique({ where: { id: userId }, select: { appAccountToken: true }, }); return { effectivePlan, grants, appAccountToken: user?.appAccountToken ?? null, }; } /** * 获取当前有效 Plan 的权益摘要。 * 从 MembershipPlan 字段读取——本阶段不接入 EffectiveQuota(由 Issue 07 实现)。 */ async getEntitlements(userId: string) { const membership = await this.compute(userId); const planCode = membership.effectivePlan; const plan = await this.prisma.membershipPlan.findUnique({ where: { code: planCode }, }); if (!plan) { return { planCode, entitlements: null }; } return { planCode, entitlements: { maxKnowledgeBases: plan.maxKnowledgeBases, maxStorageBytes: Number(plan.maxStorageBytes), maxFileSizeBytes: Number(plan.maxFileSizeBytes), monthlyOcrPages: plan.monthlyOcrPages, monthlyVisionPages: plan.monthlyVisionPages, monthlyChatCount: plan.monthlyChatCount, monthlyAiAnalysisCount: plan.monthlyAiAnalysisCount, monthlyRecallCount: plan.monthlyRecallCount, monthlyCardGenCount: plan.monthlyCardGenCount, monthlyQuizCount: plan.monthlyQuizCount, }, }; } }