import * as crypto from 'crypto'; import { Provide } from '@midwayjs/core'; import { BaseService, CoolCommException } from '@cool-midway/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import { TeamSubscriptionEntity } from '../entity/subscription'; import { StripeBillingCycle, StripeCheckoutParams, StripeCheckoutResult, StripeWebhookEvent, StripeWebhookHandleResult, TenantPlanType, } from '../../../comm/types/omni'; /** * 套餐价格表 (单位:美分 cents) */ const PLAN_PRICING: Record< Exclude, Record > = { pro: { monthly: 1900, // $19 / 月 yearly: 19000, // $190 / 年 (立省 2 个月) }, enterprise: { monthly: 9900, // $99 / 月 yearly: 99000, // $990 / 年 (立省 2 个月) }, }; /** * Stripe 国际信用卡订阅支付与 Webhook 自动化入账服务 */ @Provide() export class TeamStripeService extends BaseService { @InjectEntityModel(TeamSubscriptionEntity) teamSubscriptionEntity: Repository; /** Webhook 验签密钥,生产环境可读取环境变量 */ webhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET || 'whsec_default_team_secret'; /** Webhook 事件幂等去重表 */ processedEventIds: Set = new Set(); /** * 创建 Stripe Checkout 结算会话 * @param tenantIdOrParams 租户 ID 或完整请求参数 * @param planOpt 套餐版本 (pro / enterprise) * @param cycleOpt 结算周期 (monthly / yearly) */ async createCheckoutSession( tenantIdOrParams: number | string | StripeCheckoutParams, planOpt?: TenantPlanType, cycleOpt?: StripeBillingCycle ): Promise { let tenantId: number | string; let plan: TenantPlanType; let billingCycle: StripeBillingCycle = 'monthly'; if (typeof tenantIdOrParams === 'object' && tenantIdOrParams !== null) { tenantId = tenantIdOrParams.tenantId; plan = tenantIdOrParams.plan; billingCycle = tenantIdOrParams.billingCycle || 'monthly'; } else { tenantId = tenantIdOrParams as number | string; plan = planOpt as TenantPlanType; billingCycle = cycleOpt || 'monthly'; } // 参数校验 if (!tenantId || Number(tenantId) <= 0) { throw new CoolCommException('缺少企业租户 ID', 400); } if (plan === 'free') { throw new CoolCommException('无需为免费套餐创建结算', 400); } if (!plan || !PLAN_PRICING[plan as Exclude]) { throw new CoolCommException(`不支持的付费套餐类型: ${plan}`, 400); } const pricing = PLAN_PRICING[plan as Exclude]; const amount = pricing[billingCycle] || pricing.monthly; // 生成符合 Stripe 标准的模拟/直连会话 const sessionRandom = crypto.randomBytes(16).toString('hex'); const sessionId = `cs_test_${sessionRandom}`; const url = `https://checkout.stripe.com/c/pay/${sessionId}`; return { sessionId, url, tenantId: Number(tenantId) || tenantId, plan, billingCycle, amount, currency: 'usd', }; } /** * 基于 HMAC-SHA256 验证 Stripe 官方签名头防伪 * @param payload 原始 Webhook 报文 (string / Buffer / Record) * @param signatureHeader Stripe-Signature 请求头 * @param secret Webhook 签名私钥 * @param timestampOrTolerance 测试指定当前时间戳或最大容忍秒数 */ verifyWebhookSignature( payload: string | Buffer | Record, signatureHeader: string, secret?: string, timestampOrTolerance?: number ): boolean { if (!signatureHeader || typeof signatureHeader !== 'string') { return false; } const webhookSecret = secret || this.webhookSecret; if (!webhookSecret) { return false; } const payloadString = typeof payload === 'string' ? payload : Buffer.isBuffer(payload) ? payload.toString('utf8') : JSON.stringify(payload); // 解析 signatureHeader: t=1492774577,v1=5257a869e7... const headerParts = signatureHeader.split(','); let timestampStr = ''; const v1Signatures: string[] = []; for (const part of headerParts) { const [key, val] = part.trim().split('='); if (key === 't') { timestampStr = val; } else if (key === 'v1' && val) { v1Signatures.push(val); } } if (!timestampStr || v1Signatures.length === 0) { return false; } const headerTimestamp = parseInt(timestampStr, 10); if (isNaN(headerTimestamp)) { return false; } // 防重放攻击与宽容度校验 const defaultTolerance = 300; // 默认 5 分钟 let nowSeconds: number; let tolerance = defaultTolerance; if (timestampOrTolerance && timestampOrTolerance > 1000000000) { // 传入的是当前模拟时间戳 nowSeconds = timestampOrTolerance; } else { nowSeconds = Math.floor(Date.now() / 1000); if (timestampOrTolerance && timestampOrTolerance > 0) { tolerance = timestampOrTolerance; } } if (Math.abs(nowSeconds - headerTimestamp) > tolerance) { return false; } // 计算预期签名 const signedPayload = `${headerTimestamp}.${payloadString}`; const hmac = crypto.createHmac('sha256', webhookSecret); hmac.update(signedPayload, 'utf8'); const expectedSignature = hmac.digest('hex'); const expectedBuffer = Buffer.from(expectedSignature, 'utf8'); // 比对所有提取到的 v1 签名 for (const sig of v1Signatures) { const sigBuffer = Buffer.from(sig, 'utf8'); if ( expectedBuffer.length === sigBuffer.length && crypto.timingSafeEqual(expectedBuffer, sigBuffer) ) { return true; } } return false; } /** * 处理 Stripe Webhook 核心事件并执行自动化入账与状态机流转 * @param event Stripe 官方事件对象 */ async handleWebhookEvent( event: StripeWebhookEvent ): Promise { if (event.id && this.processedEventIds.has(event.id)) { return { success: true, eventType: event.type, action: 'idempotent_ignored', ignored: true, }; } if (event.id) { this.processedEventIds.add(event.id); } const { type, data } = event; const object = (data?.object || {}) as Record; switch (type) { case 'checkout.session.completed': { // 套餐即时升级入账 const metadata = (object.metadata || {}) as Record; const rawTenantId = String( object.client_reference_id || metadata.tenantId || '' ); const tenantId: number | string = Number(rawTenantId) || rawTenantId; const plan = (metadata.plan || 'pro') as TenantPlanType; const billingCycle = (metadata.billingCycle || 'monthly') as StripeBillingCycle; const stripeCustomerId = (object.customer as string) || undefined; const stripeSubscriptionId = (object.subscription as string) || (object.id as string) || undefined; // 计算当前周期结束时间 (月付加 30 天,年付加 365 天) const periodDays = billingCycle === 'yearly' ? 365 : 30; const periodEndDate = new Date(Date.now() + periodDays * 86400 * 1000); if (this.teamSubscriptionEntity) { let sub = await this.teamSubscriptionEntity.findOne({ where: { tenantId: Number(tenantId) || 0 }, }); if (!sub) { sub = new TeamSubscriptionEntity(); sub.tenantId = Number(tenantId) || 0; } sub.planType = plan; sub.billingCycle = billingCycle; sub.status = 'active'; sub.stripeCustomerId = stripeCustomerId; sub.stripeSubscriptionId = stripeSubscriptionId; sub.currentPeriodEnd = periodEndDate.toISOString(); sub.cancelAtPeriodEnd = false; await this.teamSubscriptionEntity.save(sub); } return { success: true, eventType: type, tenantId, plan, status: 'active', action: 'upgraded', }; } case 'invoice.payment_succeeded': { // 周期续费成功 const metadata = (object.metadata || {}) as Record; const subDetails = (object.subscription_details || {}) as { metadata?: Record; }; const rawTenantId = String( metadata.tenantId || subDetails.metadata?.tenantId || '' ); const stripeSubId = (object.subscription as string) || ''; let targetSub: TeamSubscriptionEntity | null = null; if (this.teamSubscriptionEntity) { if (rawTenantId) { targetSub = await this.teamSubscriptionEntity.findOne({ where: { tenantId: Number(rawTenantId) || 0 }, }); } if (!targetSub && stripeSubId) { targetSub = await this.teamSubscriptionEntity.findOne({ where: { stripeSubscriptionId: stripeSubId }, }); } } const lines = (object.lines || {}) as { data?: Array<{ period?: { end?: number } }>; }; const periodEndSeconds = lines.data?.[0]?.period?.end; const periodEndDate = periodEndSeconds ? new Date(periodEndSeconds * 1000) : new Date(Date.now() + 30 * 86400 * 1000); const tenantId: number | string = targetSub?.tenantId ?? (Number(rawTenantId) || rawTenantId); if (targetSub && this.teamSubscriptionEntity) { targetSub.status = 'active'; targetSub.currentPeriodEnd = periodEndDate.toISOString(); await this.teamSubscriptionEntity.save(targetSub); } return { success: true, eventType: type, tenantId, status: 'active', action: 'renewed', }; } case 'customer.subscription.deleted': { // 订阅终止,安全降级为 free const metadata = (object.metadata || {}) as Record; const stripeSubId = (object.id as string) || ''; const rawTenantId = String(metadata.tenantId || ''); let targetSub: TeamSubscriptionEntity | null = null; if (this.teamSubscriptionEntity) { if (rawTenantId) { targetSub = await this.teamSubscriptionEntity.findOne({ where: { tenantId: Number(rawTenantId) || 0 }, }); } if (!targetSub && stripeSubId) { targetSub = await this.teamSubscriptionEntity.findOne({ where: { stripeSubscriptionId: stripeSubId }, }); } } const tenantId: number | string = targetSub?.tenantId ?? (Number(rawTenantId) || rawTenantId); if (targetSub && this.teamSubscriptionEntity) { targetSub.planType = 'free'; targetSub.status = 'canceled'; await this.teamSubscriptionEntity.save(targetSub); } return { success: true, eventType: type, tenantId, plan: 'free', status: 'canceled', action: 'downgraded', }; } case 'invoice.payment_failed': { // 扣款失败,优雅流转为 past_due 宽限期 const metadata = (object.metadata || {}) as Record; const stripeSubId = (object.subscription as string) || ''; const rawTenantId = String(metadata.tenantId || ''); let targetSub: TeamSubscriptionEntity | null = null; if (this.teamSubscriptionEntity) { if (rawTenantId) { targetSub = await this.teamSubscriptionEntity.findOne({ where: { tenantId: Number(rawTenantId) || 0 }, }); } if (!targetSub && stripeSubId) { targetSub = await this.teamSubscriptionEntity.findOne({ where: { stripeSubscriptionId: stripeSubId }, }); } } const tenantId: number | string = targetSub?.tenantId ?? (Number(rawTenantId) || rawTenantId); if (targetSub && this.teamSubscriptionEntity) { targetSub.status = 'past_due'; await this.teamSubscriptionEntity.save(targetSub); } return { success: true, eventType: type, tenantId, status: 'past_due', action: 'payment_failed', }; } default: { return { success: true, eventType: type, action: 'ignored', ignored: true, }; } } } }