feat: 初始化项目
This commit is contained in:
241
src/users/services/apple-auth.service.ts
Normal file
241
src/users/services/apple-auth.service.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { Injectable, Logger, UnauthorizedException, BadRequestException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import * as jwksClient from 'jwks-rsa';
|
||||
import axios from 'axios';
|
||||
|
||||
export interface AppleTokenPayload {
|
||||
iss: string; // 发行者,应该是 https://appleid.apple.com
|
||||
aud: string; // 受众,应该是你的 bundle ID
|
||||
exp: number; // 过期时间
|
||||
iat: number; // 签发时间
|
||||
sub: string; // 用户的唯一标识符
|
||||
email?: string; // 用户邮箱(可选)
|
||||
email_verified?: boolean; // 邮箱是否验证(可选)
|
||||
is_private_email?: boolean; // 是否是私有邮箱(可选)
|
||||
real_user_status?: number; // 真实用户状态
|
||||
transfer_sub?: string; // 转移的用户标识符(可选)
|
||||
}
|
||||
|
||||
export interface AccessTokenPayload {
|
||||
sub: string; // 用户ID
|
||||
email?: string; // 用户邮箱
|
||||
iat: number; // 签发时间
|
||||
exp: number; // 过期时间
|
||||
type: 'access'; // token类型
|
||||
}
|
||||
|
||||
export interface RefreshTokenPayload {
|
||||
sub: string; // 用户ID
|
||||
iat: number; // 签发时间
|
||||
exp: number; // 过期时间
|
||||
type: 'refresh'; // token类型
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AppleAuthService {
|
||||
private readonly logger = new Logger(AppleAuthService.name);
|
||||
private readonly bundleId: string;
|
||||
private readonly jwksClient: jwksClient.JwksClient;
|
||||
private readonly accessTokenSecret: string;
|
||||
private readonly refreshTokenSecret: string;
|
||||
private readonly accessTokenExpiresIn = 30 * 24 * 60 * 60; // 30天(秒)
|
||||
private readonly refreshTokenExpiresIn = 90 * 24 * 60 * 60; // 90天(秒)
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly jwtService: JwtService,
|
||||
) {
|
||||
this.bundleId = this.configService.get<string>('APPLE_BUNDLE_ID') || '';
|
||||
this.accessTokenSecret = this.configService.get<string>('JWT_ACCESS_SECRET') || 'your-access-token-secret-key';
|
||||
this.refreshTokenSecret = this.configService.get<string>('JWT_REFRESH_SECRET') || 'your-refresh-token-secret-key';
|
||||
|
||||
// 初始化 JWKS 客户端,用于获取 Apple 的公钥
|
||||
this.jwksClient = jwksClient({
|
||||
jwksUri: 'https://appleid.apple.com/auth/keys',
|
||||
cache: true,
|
||||
cacheMaxAge: 24 * 60 * 60 * 1000, // 24小时缓存
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 10,
|
||||
});
|
||||
|
||||
if (!this.bundleId) {
|
||||
this.logger.warn('APPLE_BUNDLE_ID 环境变量未设置,Apple登录验证可能失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Apple 的公钥
|
||||
*/
|
||||
private async getApplePublicKey(kid: string): Promise<string> {
|
||||
try {
|
||||
const key = await this.jwksClient.getSigningKey(kid);
|
||||
return key.getPublicKey();
|
||||
} catch (error) {
|
||||
this.logger.error(`获取Apple公钥失败: ${error.message}`);
|
||||
throw new UnauthorizedException('无法验证Apple身份令牌');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Apple Identity Token
|
||||
*/
|
||||
async verifyAppleToken(identityToken: string): Promise<AppleTokenPayload> {
|
||||
try {
|
||||
// 解码 token header 获取 kid
|
||||
const decodedHeader = jwt.decode(identityToken, { complete: true });
|
||||
if (!decodedHeader || !decodedHeader.header.kid) {
|
||||
throw new BadRequestException('无效的身份令牌格式');
|
||||
}
|
||||
|
||||
const kid = decodedHeader.header.kid;
|
||||
|
||||
// 获取 Apple 公钥
|
||||
const publicKey = await this.getApplePublicKey(kid);
|
||||
|
||||
this.logger.log(`Apple 公钥: ${publicKey}`);
|
||||
this.logger.log(`this.bundleId: ${this.bundleId}`);
|
||||
// 验证 token
|
||||
const payload = jwt.verify(identityToken, publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
audience: this.bundleId,
|
||||
issuer: 'https://appleid.apple.com',
|
||||
}) as AppleTokenPayload;
|
||||
|
||||
// 验证必要字段
|
||||
if (!payload.sub) {
|
||||
throw new BadRequestException('身份令牌缺少用户标识符');
|
||||
}
|
||||
|
||||
// 验证 token 是否过期
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (payload.exp < now) {
|
||||
throw new UnauthorizedException('身份令牌已过期');
|
||||
}
|
||||
|
||||
this.logger.log(`Apple身份令牌验证成功,用户ID: ${payload.sub}`);
|
||||
return payload;
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException || error instanceof BadRequestException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error(`验证Apple身份令牌失败: ${error.message}`);
|
||||
throw new UnauthorizedException('身份令牌验证失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成访问令牌
|
||||
*/
|
||||
generateAccessToken(userId: string, email?: string): string {
|
||||
const payload: AccessTokenPayload = {
|
||||
sub: userId,
|
||||
email,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + this.accessTokenExpiresIn,
|
||||
type: 'access',
|
||||
};
|
||||
|
||||
return jwt.sign(payload, this.accessTokenSecret, {
|
||||
algorithm: 'HS256',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成刷新令牌
|
||||
*/
|
||||
generateRefreshToken(userId: string): string {
|
||||
const payload: RefreshTokenPayload = {
|
||||
sub: userId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + this.refreshTokenExpiresIn,
|
||||
type: 'refresh',
|
||||
};
|
||||
|
||||
return jwt.sign(payload, this.refreshTokenSecret, {
|
||||
algorithm: 'HS256',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证访问令牌
|
||||
*/
|
||||
verifyAccessToken(token: string): AccessTokenPayload {
|
||||
try {
|
||||
const payload = jwt.verify(token, this.accessTokenSecret, {
|
||||
algorithms: ['HS256'],
|
||||
}) as AccessTokenPayload;
|
||||
|
||||
if (payload.type !== 'access') {
|
||||
throw new UnauthorizedException('无效的令牌类型');
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
this.logger.error(`验证访问令牌失败: ${error.message}`);
|
||||
throw new UnauthorizedException('访问令牌无效或已过期');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证刷新令牌
|
||||
*/
|
||||
verifyRefreshToken(token: string): RefreshTokenPayload {
|
||||
try {
|
||||
const payload = jwt.verify(token, this.refreshTokenSecret, {
|
||||
algorithms: ['HS256'],
|
||||
}) as RefreshTokenPayload;
|
||||
|
||||
if (payload.type !== 'refresh') {
|
||||
throw new UnauthorizedException('无效的令牌类型');
|
||||
}
|
||||
|
||||
return payload;
|
||||
} catch (error) {
|
||||
this.logger.error(`验证刷新令牌失败: ${error.message}`);
|
||||
throw new UnauthorizedException('刷新令牌无效或已过期');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
async refreshAccessToken(refreshToken: string, userEmail?: string): Promise<{ accessToken: string; expiresIn: number }> {
|
||||
const payload = this.verifyRefreshToken(refreshToken);
|
||||
|
||||
const newAccessToken = this.generateAccessToken(payload.sub, userEmail);
|
||||
|
||||
return {
|
||||
accessToken: newAccessToken,
|
||||
expiresIn: this.accessTokenExpiresIn,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Bearer token 中提取 token
|
||||
*/
|
||||
extractTokenFromHeader(authHeader: string): string {
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('无效的授权头格式');
|
||||
}
|
||||
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取访问令牌的过期时间(秒)
|
||||
*/
|
||||
getAccessTokenExpiresIn(): number {
|
||||
return this.accessTokenExpiresIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取刷新令牌的过期时间(秒)
|
||||
*/
|
||||
getRefreshTokenExpiresIn(): number {
|
||||
return this.refreshTokenExpiresIn;
|
||||
}
|
||||
}
|
||||
494
src/users/services/apple-purchase.service.ts
Normal file
494
src/users/services/apple-purchase.service.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user