import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as fs from 'fs'; import * as jwt from 'jsonwebtoken'; import axios from 'axios'; @Injectable() export class ApplePurchaseService { private readonly logger = new Logger(ApplePurchaseService.name); private readonly appSharedSecret: string; private readonly keyId: string; private readonly issuerId: string; private readonly bundleId: string; private readonly privateKeyPath: string; private privateKey: string; constructor(private configService: ConfigService) { this.appSharedSecret = this.configService.get('APPLE_APP_SHARED_SECRET') || ''; this.keyId = this.configService.get('APPLE_KEY_ID') || ''; this.issuerId = this.configService.get('APPLE_ISSUER_ID') || ''; this.bundleId = this.configService.get('APPLE_BUNDLE_ID') || ''; this.privateKeyPath = this.configService.get('APPLE_PRIVATE_KEY_PATH') || ''; try { // 读取私钥文件 this.privateKey = fs.readFileSync(this.privateKeyPath, 'utf8'); } catch (error) { this.logger.error(`无法读取Apple私钥文件: ${error.message}`); this.privateKey = ''; } } /** * 使用共享密钥验证收据(旧方法,适用于StoreKit 1) */ async verifyReceiptWithSharedSecret(receipt: string, isSandbox = false): Promise { try { // 苹果验证URL const productionUrl = 'https://buy.itunes.apple.com/verifyReceipt'; const sandboxUrl = 'https://sandbox.itunes.apple.com/verifyReceipt'; const verifyUrl = isSandbox ? sandboxUrl : productionUrl; this.logger.log(`验证receipt: ${receipt}`); // 发送验证请求 const response = await axios.post(verifyUrl, { 'receipt-data': receipt, 'password': this.appSharedSecret, }); // 如果状态码为21007,说明是沙盒收据,需要在沙盒环境验证 if (response.data.status === 21007 && !isSandbox) { return this.verifyReceiptWithSharedSecret(receipt, true); } this.logger.log(`Apple验证响应: ${JSON.stringify(response.data)}`); return response.data; } catch (error) { this.logger.error(`Apple验证失败: ${error.message}`); throw error; } } /** * 生成用于App Store Connect API的JWT令牌 */ private generateToken(): string { try { if (!this.privateKey) { throw new Error('私钥未加载,无法生成令牌'); } const now = Math.floor(Date.now() / 1000); const payload = { iss: this.issuerId, // 发行者ID iat: now, // 签发时间 exp: now + 3600, // 过期时间,1小时后 aud: 'appstoreconnect-v1' // 受众,固定值 }; const signOptions = { algorithm: 'ES256', // 要求使用ES256算法 header: { alg: 'ES256', kid: this.keyId, // 密钥ID typ: 'JWT' } }; return jwt.sign(payload, this.privateKey, signOptions as jwt.SignOptions); } catch (error) { this.logger.error(`生成JWT令牌失败: ${error.message}`); throw error; } } /** * 使用StoreKit 2 API验证交易(新方法) */ async verifyTransactionWithJWT(transactionId: string): Promise { try { const token = this.generateToken(); // App Store Server API请求 const url = `https://api.storekit.itunes.apple.com/inApps/v1/transactions/${transactionId}`; const response = await axios.get(url, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); this.logger.debug(`交易验证响应: ${JSON.stringify(response.data)}`); return response.data; } catch (error) { this.logger.error(`交易验证失败: ${error.message}`); throw error; } } /** * 查询订阅状态 */ async getSubscriptionStatus(originalTransactionId: string): Promise { try { const token = this.generateToken(); // 查询订阅状态API const url = `https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/${originalTransactionId}`; const response = await axios.get(url, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); this.logger.debug(`订阅状态响应: ${JSON.stringify(response.data)}`); return response.data; } catch (error) { this.logger.error(`获取订阅状态失败: ${error.message}`); throw error; } } /** * 验证收据 * 综合使用旧API和新API进行验证 */ async verifyPurchase(receipt: string, transactionId: string): Promise { try { // 首先使用共享密钥验证收据 const receiptVerification = await this.verifyReceiptWithSharedSecret(receipt, false); if (receiptVerification.status !== 0) { this.logger.warn(`收据验证失败,状态码: ${receiptVerification.status}`); return false; } // 如果提供了transactionId,则使用JWT验证交易 if (transactionId && this.privateKey) { try { const transactionVerification = await this.verifyTransactionWithJWT(transactionId); // 验证bundleId if (transactionVerification.bundleId !== this.bundleId) { this.logger.warn(`交易绑定的bundleId不匹配: ${transactionVerification.bundleId} !== ${this.bundleId}`); return false; } // 验证交易状态 if (transactionVerification.signedTransactionInfo?.transactionState !== 1) { this.logger.warn(`交易状态无效: ${transactionVerification.signedTransactionInfo?.transactionState}`); return false; } return true; } catch (error) { this.logger.error(`JWT交易验证失败: ${error.message}`); // 如果JWT验证失败,但收据验证成功,仍然返回true return true; } } return true; } catch (error) { this.logger.error(`验证购买失败: ${error.message}`); return false; } } /** * 处理App Store服务器通知 * 用于处理苹果服务器发送的通知事件 */ async processServerNotification(signedPayload: string): Promise { try { this.logger.log(`接收到App Store服务器通知: ${signedPayload.substring(0, 100)}...`); // 解码JWS格式的负载 const notificationPayload = this.decodeJWS<{ // SUBSCRIBED notificationType: string; subtype: string; notificationUUID: string; data: { appAppleId: number; bundleId: string; environment: string; bundleVersion: string; signedTransactionInfo: string; signedRenewalInfo: string; status: number; }; // 时间戳 signedDate: number }>(signedPayload); if (!notificationPayload) { throw new Error('无法解码通知负载'); } this.logger.log(`解码后的通知负载: ${JSON.stringify(notificationPayload, null, 2)}`); // 验证通知的合法性 if (!this.validateNotification(notificationPayload)) { throw new Error('通知验证失败'); } // 解码交易信息 let transactionInfo = null; if (notificationPayload.data?.signedTransactionInfo) { transactionInfo = this.decodeJWS(notificationPayload.data.signedTransactionInfo); } // 解码续订信息 let renewalInfo = null; if (notificationPayload.data?.signedRenewalInfo) { renewalInfo = this.decodeJWS(notificationPayload.data.signedRenewalInfo); } return { notificationPayload, transactionInfo, renewalInfo, }; } catch (error) { this.logger.error(`处理App Store服务器通知失败: ${error.message}`); throw error; } } /** * 解码JWS格式的数据 */ private decodeJWS(jwsString: string): T | null { try { // JWS格式: header.payload.signature const parts = jwsString.split('.'); if (parts.length !== 3) { throw new Error('无效的JWS格式'); } // 解码payload部分(Base64URL编码) const payload = parts[1]; const decodedPayload = Buffer.from(payload, 'base64url').toString('utf8'); return JSON.parse(decodedPayload); } catch (error) { this.logger.error(`解码JWS失败: ${error.message}`); return null; } } /** * 验证通知的合法性 */ private validateNotification(notificationPayload: any): boolean { try { // 验证必要字段 if (!notificationPayload.notificationType) { this.logger.warn('通知缺少notificationType字段'); return false; } if (!notificationPayload.data?.bundleId) { this.logger.warn('通知缺少bundleId字段'); return false; } // 验证bundleId是否匹配 if (notificationPayload.data.bundleId !== this.bundleId) { this.logger.warn(`bundleId不匹配: ${notificationPayload.data.bundleId} !== ${this.bundleId}`); return false; } return true; } catch (error) { this.logger.error(`验证通知失败: ${error.message}`); return false; } } /** * 根据通知类型处理不同的业务逻辑 */ async handleNotificationByType(notificationType: string, transactionInfo: any, renewalInfo: any): Promise { this.logger.log(`处理通知类型: ${notificationType}`); switch (notificationType) { case 'SUBSCRIBED': return this.handleSubscribed(transactionInfo, renewalInfo); case 'DID_RENEW': return this.handleDidRenew(transactionInfo, renewalInfo); case 'EXPIRED': return this.handleExpired(transactionInfo, renewalInfo); case 'DID_FAIL_TO_RENEW': return this.handleDidFailToRenew(transactionInfo, renewalInfo); case 'DID_CHANGE_RENEWAL_STATUS': return this.handleDidChangeRenewalStatus(transactionInfo, renewalInfo); case 'REFUND': return this.handleRefund(transactionInfo, renewalInfo); case 'REVOKE': return this.handleRevoke(transactionInfo, renewalInfo); case 'TEST': return this.handleTest(transactionInfo, renewalInfo); default: this.logger.warn(`未处理的通知类型: ${notificationType}`); return { handled: false, notificationType }; } } /** * 处理订阅通知 */ private async handleSubscribed(transactionInfo: any, renewalInfo: any): Promise { this.logger.log('处理订阅通知', transactionInfo, renewalInfo); // 这里可以实现具体的订阅处理逻辑 // 例如:更新用户的订阅状态、发送欢迎邮件等 return { handled: true, action: 'subscribed' }; } /** * 处理续订通知 */ private async handleDidRenew(transactionInfo: any, renewalInfo: any): Promise { this.logger.log('处理续订通知'); // 这里可以实现具体的续订处理逻辑 // 例如:延长用户的订阅期限 return { handled: true, action: 'renewed' }; } /** * 处理过期通知 */ private async handleExpired(transactionInfo: any, renewalInfo: any): Promise { this.logger.log('处理过期通知'); // 这里可以实现具体的过期处理逻辑 // 例如:停止用户的订阅服务 return { handled: true, action: 'expired' }; } /** * 处理续订失败通知 */ private async handleDidFailToRenew(transactionInfo: any, renewalInfo: any): Promise { this.logger.log('处理续订失败通知'); // 这里可以实现具体的续订失败处理逻辑 // 例如:发送付款提醒邮件 return { handled: true, action: 'renewal_failed' }; } /** * 处理续订状态变更通知 */ private async handleDidChangeRenewalStatus(transactionInfo: any, renewalInfo: any): Promise { this.logger.log('处理续订状态变更通知'); // 这里可以实现具体的续订状态变更处理逻辑 return { handled: true, action: 'renewal_status_changed' }; } /** * 处理退款通知 */ private async handleRefund(transactionInfo: any, renewalInfo: any): Promise { this.logger.log('处理退款通知'); // 这里可以实现具体的退款处理逻辑 // 例如:撤销用户的订阅权限 return { handled: true, action: 'refunded' }; } /** * 处理撤销通知 */ private async handleRevoke(transactionInfo: any, renewalInfo: any): Promise { this.logger.log('处理撤销通知'); // 这里可以实现具体的撤销处理逻辑 return { handled: true, action: 'revoked' }; } /** * 处理测试通知 */ private async handleTest(transactionInfo: any, renewalInfo: any): Promise { this.logger.log('处理测试通知'); return { handled: true, action: 'test' }; } async handleServerNotification(payload: any): Promise { try { this.logger.log(`收到Apple服务器通知: ${JSON.stringify(payload)}`); // 验证通知签名(生产环境应该实现) // ... const notificationType = payload.notificationType; const subtype = payload.subtype; const data = payload.data; // 处理不同类型的通知 switch (notificationType) { case 'CONSUMPTION_REQUEST': // 消费请求,用于非消耗型项目 break; case 'DID_CHANGE_RENEWAL_PREF': // 订阅续订偏好改变 break; case 'DID_CHANGE_RENEWAL_STATUS': // 订阅续订状态改变 break; case 'DID_FAIL_TO_RENEW': // 订阅续订失败 break; case 'DID_RENEW': // 订阅已续订 break; case 'EXPIRED': // 订阅已过期 break; case 'GRACE_PERIOD_EXPIRED': // 宽限期已过期 break; case 'OFFER_REDEEMED': // 优惠已兑换 break; case 'PRICE_INCREASE': // 价格上涨 break; case 'REFUND': // 退款 break; case 'REVOKE': // 撤销 break; default: this.logger.warn(`未知的通知类型: ${notificationType}`); } return { success: true }; } catch (error) { this.logger.error(`处理服务器通知失败: ${error.message}`); throw error; } } //从apple获取用户的订阅历史,参考 storekit2 的示例 async getAppleSubscriptionHistory(userId: string): Promise { const token = this.generateToken(); const url = `https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/${userId}/history`; const response = await axios.get(url, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); return response.data; } }