feat: 初始化项目

This commit is contained in:
richarjiang
2025-08-13 15:17:33 +08:00
commit 4f9d648a50
72 changed files with 29051 additions and 0 deletions

View File

@@ -0,0 +1,494 @@
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<string>('APPLE_APP_SHARED_SECRET') || '';
this.keyId = this.configService.get<string>('APPLE_KEY_ID') || '';
this.issuerId = this.configService.get<string>('APPLE_ISSUER_ID') || '';
this.bundleId = this.configService.get<string>('APPLE_BUNDLE_ID') || '';
this.privateKeyPath = this.configService.get<string>('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<any> {
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<any> {
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<any> {
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<boolean> {
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<any> {
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<T>(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<any> {
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<any> {
this.logger.log('处理订阅通知', transactionInfo, renewalInfo);
// 这里可以实现具体的订阅处理逻辑
// 例如:更新用户的订阅状态、发送欢迎邮件等
return { handled: true, action: 'subscribed' };
}
/**
* 处理续订通知
*/
private async handleDidRenew(transactionInfo: any, renewalInfo: any): Promise<any> {
this.logger.log('处理续订通知');
// 这里可以实现具体的续订处理逻辑
// 例如:延长用户的订阅期限
return { handled: true, action: 'renewed' };
}
/**
* 处理过期通知
*/
private async handleExpired(transactionInfo: any, renewalInfo: any): Promise<any> {
this.logger.log('处理过期通知');
// 这里可以实现具体的过期处理逻辑
// 例如:停止用户的订阅服务
return { handled: true, action: 'expired' };
}
/**
* 处理续订失败通知
*/
private async handleDidFailToRenew(transactionInfo: any, renewalInfo: any): Promise<any> {
this.logger.log('处理续订失败通知');
// 这里可以实现具体的续订失败处理逻辑
// 例如:发送付款提醒邮件
return { handled: true, action: 'renewal_failed' };
}
/**
* 处理续订状态变更通知
*/
private async handleDidChangeRenewalStatus(transactionInfo: any, renewalInfo: any): Promise<any> {
this.logger.log('处理续订状态变更通知');
// 这里可以实现具体的续订状态变更处理逻辑
return { handled: true, action: 'renewal_status_changed' };
}
/**
* 处理退款通知
*/
private async handleRefund(transactionInfo: any, renewalInfo: any): Promise<any> {
this.logger.log('处理退款通知');
// 这里可以实现具体的退款处理逻辑
// 例如:撤销用户的订阅权限
return { handled: true, action: 'refunded' };
}
/**
* 处理撤销通知
*/
private async handleRevoke(transactionInfo: any, renewalInfo: any): Promise<any> {
this.logger.log('处理撤销通知');
// 这里可以实现具体的撤销处理逻辑
return { handled: true, action: 'revoked' };
}
/**
* 处理测试通知
*/
private async handleTest(transactionInfo: any, renewalInfo: any): Promise<any> {
this.logger.log('处理测试通知');
return { handled: true, action: 'test' };
}
async handleServerNotification(payload: any): Promise<any> {
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<any> {
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;
}
}