Files
plates-server/src/users/users.service.ts

3175 lines
106 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Injectable,
NotFoundException,
ConflictException,
Logger,
BadRequestException,
Inject,
} from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger as WinstonLogger } from 'winston';
import { InjectModel, InjectConnection } from '@nestjs/sequelize';
import { Gender, User } from './models/user.model';
import { UserResponseDto } from './dto/user-response.dto';
import { ResponseCode } from 'src/base.dto';
import { Transaction, Op } from 'sequelize';
import { Sequelize } from 'sequelize-typescript';
import { UpdateUserDto, UpdateUserResponseDto } from './dto/update-user.dto';
import { ConfigService } from '@nestjs/config';
import { VersionCheckDto, VersionCheckResponseDto, VersionInfo } from './dto/version-check.dto';
import { UserPurchase, PurchaseType, PurchaseStatus, PurchasePlatform } from './models/user-purchase.model';
import { ApplePurchaseService } from './services/apple-purchase.service';
import * as dayjs from 'dayjs';
import * as isoWeek from 'dayjs/plugin/isoWeek';
dayjs.extend(isoWeek);
import { AccessTokenPayload, AppleAuthService, AppleTokenPayload } from './services/apple-auth.service';
import { AppleLoginDto, AppleLoginResponseDto, RefreshTokenDto, RefreshTokenResponseDto } from './dto/apple-login.dto';
import { DeleteAccountDto, DeleteAccountResponseDto } from './dto/delete-account.dto';
import { GuestLoginDto, GuestLoginResponseDto, RefreshGuestTokenDto, RefreshGuestTokenResponseDto } from './dto/guest-login.dto';
import { AppStoreServerNotificationDto, ProcessNotificationResponseDto, NotificationType } from './dto/app-store-notification.dto';
import { RevenueCatEvent } from './models/revenue-cat-event.model';
import { UserProfile } from './models/user-profile.model';
import { RevenueCatWebhookDto, RevenueCatEventType } from './dto/revenue-cat-webhook.dto';
import { RestorePurchaseDto, RestorePurchaseResponseDto, RestoredPurchaseInfo, ActiveEntitlement, NonSubscriptionTransaction } from './dto/restore-purchase.dto';
import { PurchaseRestoreLog, RestoreStatus, RestoreSource } from './models/purchase-restore-log.model';
import { BlockedTransaction, BlockReason } from './models/blocked-transaction.model';
import { UserWeightHistory, WeightUpdateSource } from './models/user-weight-history.model';
import { UserBodyMeasurementHistory, BodyMeasurementType, MeasurementUpdateSource } from './models/user-body-measurement-history.model';
import { UserDailyHealth } from './models/user-daily-health.model';
import { UpdateDailyHealthDto, UpdateDailyHealthResponseDto } from './dto/daily-health.dto';
import { ActivityLogsService } from '../activity-logs/activity-logs.service';
import { UserActivityService } from './services/user-activity.service';
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model';
import { BadgeService } from './services/badge.service';
const DEFAULT_FREE_USAGE_COUNT = 5;
// 会员同步验证的频率限制5分钟
const MEMBERSHIP_SYNC_INTERVAL_MS = 5 * 60 * 1000;
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
// 用于存储最后一次验证时间的内存缓存
private lastSyncTimestamps: Map<string, number> = new Map();
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly winstonLogger: WinstonLogger,
@InjectModel(User)
private userModel: typeof User,
@InjectModel(UserPurchase)
private userPurchaseModel: typeof UserPurchase,
@InjectModel(RevenueCatEvent)
private revenueCatEventModel: typeof RevenueCatEvent,
@InjectModel(PurchaseRestoreLog)
private purchaseRestoreLogModel: typeof PurchaseRestoreLog,
private appleAuthService: AppleAuthService,
private applePurchaseService: ApplePurchaseService,
@InjectModel(BlockedTransaction)
private blockedTransactionModel: typeof BlockedTransaction,
@InjectModel(UserProfile)
private userProfileModel: typeof UserProfile,
@InjectModel(UserWeightHistory)
private userWeightHistoryModel: typeof UserWeightHistory,
@InjectModel(UserBodyMeasurementHistory)
private userBodyMeasurementHistoryModel: typeof UserBodyMeasurementHistory,
@InjectModel(UserDailyHealth)
private userDailyHealthModel: typeof UserDailyHealth,
@InjectConnection()
private sequelize: Sequelize,
private readonly activityLogsService: ActivityLogsService,
private readonly userActivityService: UserActivityService,
private readonly badgeService: BadgeService,
private readonly configService: ConfigService,
) { }
async getProfile(user: AccessTokenPayload, appVersion?: string): Promise<UserResponseDto> {
try {
// 使用NestJS Logger (会通过winston输出)
this.logger.log(`getProfile: ${JSON.stringify(user)}, appVersion: ${appVersion}`);
// 也可以直接使用winston logger
this.winstonLogger.info('getProfile method called', {
context: 'UsersService',
userId: user.sub,
email: user.email
});
// 检查email是否已存在
const existingUser = await this.userModel.findOne({
where: { mail: user.email },
});
if (!existingUser) {
return {
code: ResponseCode.ERROR,
message: `用户不存在,请先注册`,
data: null as any,
};
}
// 更新用户最后登录时间和版本信息
existingUser.lastLogin = new Date();
if (appVersion && existingUser.appVersion !== appVersion) {
const oldVersion = existingUser.appVersion;
existingUser.appVersion = appVersion;
this.logger.log(`用户 ${existingUser.id} 版本更新: ${oldVersion || '无'} -> ${appVersion}`);
}
await existingUser.save();
const [profile] = await this.userProfileModel.findOrCreate({
where: { userId: existingUser.id },
defaults: { userId: existingUser.id },
});
// 检查并记录今日登录活跃
await this.userActivityService.checkAndRecordTodayLogin(existingUser.id);
// 异步触发会员状态同步验证(不等待结果,不阻塞响应)
// 使用 Promise.resolve().then() 确保在当前事件循环后执行
Promise.resolve().then(() => {
this.syncMembershipFromRevenueCat(existingUser.id).catch(err => {
// 错误已在方法内部处理,这里只是确保不会有未捕获的 Promise rejection
this.logger.error(`异步会员验证出错: ${err instanceof Error ? err.message : '未知错误'}`);
});
});
const returnData = {
...existingUser.toJSON(),
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
isVip: existingUser.isVip,
gender: existingUser.gender,
appVersion: existingUser.appVersion,
dailyStepsGoal: profile?.dailyStepsGoal,
dailyCaloriesGoal: profile?.dailyCaloriesGoal,
pilatesPurposes: profile?.pilatesPurposes,
weight: profile?.weight,
initialWeight: profile?.initialWeight,
targetWeight: profile?.targetWeight,
height: profile?.height,
activityLevel: profile?.activityLevel,
dailyWaterGoal: profile?.dailyWaterGoal,
chestCircumference: profile?.chestCircumference,
waistCircumference: profile?.waistCircumference,
upperHipCircumference: profile?.upperHipCircumference,
armCircumference: profile?.armCircumference,
thighCircumference: profile?.thighCircumference,
calfCircumference: profile?.calfCircumference,
}
this.logger.log(`getProfile returnData: ${JSON.stringify(returnData, null, 2)}`);
// 返回用户信息,包含购买状态
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: returnData,
};
} catch (error) {
this.logger.error(`getProfile error: ${error instanceof Error ? error.message : '未知错误'}`);
if (error instanceof ConflictException) {
throw error;
}
return {
code: ResponseCode.ERROR,
message: `获取用户信息失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: null as any,
};
}
}
/**
* @desc 获取用户剩余的聊天次数
*/
async getUserUsageCount(userId: string): Promise<number> {
try {
const user = await this.userModel.findOne({ where: { id: userId } });
if (!user) {
this.logger.log(`getUserUsageCount: ${userId} not found, return 0`);
return 0
}
if (user.isVip) {
// 会员用户无限次
this.logger.log(`getUserUsageCount: ${userId} is vip, return 999`);
return 999
}
this.logger.log(`getUserUsageCount: ${userId} freeUsageCount: ${user.freeUsageCount}`);
return user.freeUsageCount || 0;
} catch (error) {
this.logger.error(`getUserUsageCount error: ${error instanceof Error ? error.message : String(error)}`);
return 0
}
}
async getUserLanguage(userId: string): Promise<string> {
try {
const user = await this.userModel.findOne({ where: { id: userId } });
if (!user) {
this.logger.warn(`getUserLanguage: ${userId} not found, default zh-CN`);
return 'zh-CN';
}
return user.language || 'zh-CN';
} catch (error) {
this.logger.error(`getUserLanguage error: ${error instanceof Error ? error.message : String(error)}`);
return 'zh-CN';
}
}
// 扣减用户免费次数
async deductUserUsageCount(userId: string, count: number = 1): Promise<void> {
try {
this.logger.log(`deductUserUsageCount: ${userId} deduct ${count} times`);
const user = await this.userModel.findOne({ where: { id: userId } });
if (!user) {
throw new NotFoundException(`ID为${userId}的用户不存在`);
}
// 会员用户不扣减
if (user.isVip) {
return
}
user.freeUsageCount -= count;
await user.save();
} catch (error) {
this.logger.error(`deductUserUsageCount error: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
// 更新用户昵称、头像
async updateUser(updateUserDto: UpdateUserDto, userId: string): Promise<UpdateUserResponseDto> {
const { name, avatar, gender, birthDate, language, dailyStepsGoal, dailyCaloriesGoal, pilatesPurposes, weight, initialWeight, targetWeight, height, activityLevel } = updateUserDto;
this.logger.log(`updateUser: ${JSON.stringify(updateUserDto, null, 2)}`);
const user = await this.userModel.findOne({
where: { id: userId },
});
if (!user) {
throw new NotFoundException(`ID为${userId}的用户不存在`);
}
const profileChanges: Record<string, any> = {};
const userChanges: Record<string, any> = {};
if (name) {
user.name = name;
userChanges.name = name;
}
if (avatar) {
user.avatar = avatar;
userChanges.avatar = avatar;
}
if (gender) {
user.gender = gender;
userChanges.gender = gender;
}
if (birthDate) {
user.birthDate = dayjs(birthDate * 1000).startOf('day').toDate();
userChanges.birthDate = birthDate;
}
if (language) {
user.language = language;
userChanges.language = language;
}
this.logger.log(`updateUser user: ${JSON.stringify(user, null, 2)}`);
await user.save();
const [profile] = await this.userProfileModel.findOrCreate({
where: { userId },
defaults: { userId },
});
// 更新或创建扩展信息
if (dailyStepsGoal !== undefined || dailyCaloriesGoal !== undefined || pilatesPurposes !== undefined || weight !== undefined || initialWeight !== undefined || targetWeight !== undefined || height !== undefined || activityLevel !== undefined) {
if (dailyStepsGoal !== undefined) { profile.dailyStepsGoal = dailyStepsGoal as any; profileChanges.dailyStepsGoal = dailyStepsGoal; }
if (dailyCaloriesGoal !== undefined) { profile.dailyCaloriesGoal = dailyCaloriesGoal as any; profileChanges.dailyCaloriesGoal = dailyCaloriesGoal; }
if (pilatesPurposes !== undefined) { profile.pilatesPurposes = pilatesPurposes as any; profileChanges.pilatesPurposes = pilatesPurposes; }
if (weight !== undefined) {
profile.weight = weight;
try {
await this.userWeightHistoryModel.create({ userId, weight, source: WeightUpdateSource.Manual });
} catch (e) {
this.logger.error(`记录体重历史失败: ${e instanceof Error ? e.message : String(e)}`);
}
profileChanges.weight = weight;
}
if (initialWeight !== undefined) {
profile.initialWeight = initialWeight;
profileChanges.initialWeight = initialWeight;
}
if (targetWeight !== undefined) {
profile.targetWeight = targetWeight;
profileChanges.targetWeight = targetWeight;
}
if (height !== undefined) {
profile.height = height;
profileChanges.height = height;
}
if (activityLevel !== undefined) {
profile.activityLevel = activityLevel;
profileChanges.activityLevel = activityLevel;
}
await profile.save();
}
// 记录用户基础与扩展信息更新
if (Object.keys(userChanges).length > 0) {
await this.activityLogsService.record({
userId,
entityType: ActivityEntityType.USER,
action: ActivityActionType.UPDATE,
entityId: userId,
changes: userChanges,
});
}
if (Object.keys(profileChanges).length > 0) {
await this.activityLogsService.record({
userId,
entityType: ActivityEntityType.USER_PROFILE,
action: ActivityActionType.UPDATE,
entityId: userId,
changes: profileChanges,
});
}
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: {
...user.toJSON(),
...profile.toJSON(),
isNew: false,
} as any,
};
}
async addWeightByVision(userId: string, weight: number): Promise<void> {
const t = await this.sequelize.transaction();
try {
const [profile] = await this.userProfileModel.findOrCreate({ where: { userId }, defaults: { userId }, transaction: t });
profile.weight = weight;
await profile.save({ transaction: t });
await this.userWeightHistoryModel.create({ userId, weight, source: WeightUpdateSource.Vision }, { transaction: t });
await t.commit();
await this.activityLogsService.record({
userId,
entityType: ActivityEntityType.USER_PROFILE,
action: ActivityActionType.UPDATE,
entityId: userId,
changes: { weight },
metadata: { source: 'vision' },
});
} catch (e) {
await t.rollback();
this.logger.error(`addWeightByVision error: ${e instanceof Error ? e.message : String(e)}`);
}
}
async getWeightHistory(userId: string, params: { start?: Date; end?: Date; limit?: number } = {}) {
const where: any = { userId };
if (params.start || params.end) {
where.createdAt = {} as any;
if (params.start) where.createdAt.$gte = params.start as any;
if (params.end) where.createdAt.$lte = params.end as any;
}
const limit = params.limit && params.limit > 0 ? Math.min(1000, params.limit) : 200;
const rows = await this.userWeightHistoryModel.findAll({
where,
order: [['created_at', 'DESC']],
limit,
});
return rows.map(r => ({
id: r.id,
weight: r.weight,
source: r.source,
createdAt: r.createdAt
}));
}
/**
* 更新体重记录
*/
async updateWeightRecord(userId: string, recordId: number, updateData: { weight: number; source?: WeightUpdateSource }) {
const t = await this.sequelize.transaction();
try {
// 查找并验证体重记录是否存在且属于当前用户
const weightRecord = await this.userWeightHistoryModel.findOne({
where: { id: recordId, userId },
transaction: t,
});
if (!weightRecord) {
throw new NotFoundException('体重记录不存在');
}
const oldWeight = weightRecord.weight;
const oldSource = weightRecord.source;
// 更新体重记录
await weightRecord.update({
weight: updateData.weight,
source: updateData.source || weightRecord.source,
}, { transaction: t });
// 如果这是最新的体重记录,同时更新用户档案中的体重
const latestRecord = await this.userWeightHistoryModel.findOne({
where: { userId },
order: [['created_at', 'DESC']],
transaction: t,
});
if (latestRecord && latestRecord.id === recordId) {
const profile = await this.userProfileModel.findOne({
where: { userId },
transaction: t,
});
if (profile) {
profile.weight = updateData.weight;
await profile.save({ transaction: t });
}
}
await t.commit();
// 记录活动日志
await this.activityLogsService.record({
userId,
entityType: ActivityEntityType.USER_PROFILE,
action: ActivityActionType.UPDATE,
entityId: recordId.toString(),
changes: {
weight: { from: oldWeight, to: updateData.weight },
...(updateData.source && updateData.source !== oldSource ? { source: { from: oldSource, to: updateData.source } } : {}),
},
});
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: {
id: weightRecord.id,
userId: weightRecord.userId,
weight: weightRecord.weight,
source: weightRecord.source,
createdAt: weightRecord.createdAt,
updatedAt: weightRecord.updatedAt,
},
};
} catch (error) {
await t.rollback();
this.logger.error(`更新体重记录失败: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* 删除体重记录
*/
async deleteWeightRecord(userId: string, recordId: number): Promise<boolean> {
const t = await this.sequelize.transaction();
try {
// 查找并验证体重记录是否存在且属于当前用户
const weightRecord = await this.userWeightHistoryModel.findOne({
where: { id: recordId, userId },
transaction: t,
});
if (!weightRecord) {
return false;
}
const recordData = {
id: weightRecord.id,
weight: weightRecord.weight,
source: weightRecord.source,
createdAt: weightRecord.createdAt,
};
// 删除体重记录
await weightRecord.destroy({ transaction: t });
// 如果删除的是最新记录,需要更新用户档案中的体重为倒数第二新的记录
const latestRecord = await this.userWeightHistoryModel.findOne({
where: { userId },
order: [['created_at', 'DESC']],
transaction: t,
});
const profile = await this.userProfileModel.findOne({
where: { userId },
transaction: t,
});
if (profile) {
if (latestRecord) {
// 有其他体重记录,更新为最新的体重
profile.weight = latestRecord.weight;
} else {
// 没有其他体重记录,清空体重字段
profile.weight = null;
}
await profile.save({ transaction: t });
}
await t.commit();
// 记录活动日志
await this.activityLogsService.record({
userId,
entityType: ActivityEntityType.USER_PROFILE,
action: ActivityActionType.DELETE,
entityId: recordId.toString(),
changes: recordData,
});
return true;
} catch (error) {
await t.rollback();
this.logger.error(`删除体重记录失败: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Apple 登录
*/
async appleLogin(appleLoginDto: AppleLoginDto): Promise<AppleLoginResponseDto> {
try {
this.logger.log(`appleLogin: ${JSON.stringify(appleLoginDto)}`);
// 验证 Apple Identity Token
const applePayload: AppleTokenPayload = await this.appleAuthService.verifyAppleToken(appleLoginDto.identityToken);
// 构造用户ID使用 Apple 的 sub 作为用户ID添加前缀以区分
const userId = applePayload.sub
this.logger.log(`appleLogin userId: ${userId}`);
// 查找或创建用户
let user = await this.userModel.findOne({
where: { id: userId },
});
let isNewUser = false;
if (!user) {
// 创建新用户
const userName = appleLoginDto.name || applePayload.email?.split('@')[0] || '用户';
// 如果无法获取用户邮箱,生成一个随机邮箱
let userEmail = appleLoginDto.email || applePayload.email || '';
if (!userEmail) {
// 使用用户ID生成唯一的随机邮箱
const randomString = Math.random().toString(36).substring(2, 10);
userEmail = `${userId.substring(0, 8)}_${randomString}@outlive.com`;
this.logger.log(`为用户 ${userId} 生成随机邮箱: ${userEmail}`);
}
const memberNumber = await this.assignMemberNumber();
user = await this.userModel.create({
id: userId,
name: userName,
mail: userEmail,
gender: Gender.MALE,
memberNumber,
freeUsageCount: DEFAULT_FREE_USAGE_COUNT,
lastLogin: new Date(),
avatar: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/seal-avatar/1.jpeg'
});
isNewUser = true;
this.logger.log(`创建新的Apple用户: ${userId}`);
// 创建默认扩展记录
await this.userProfileModel.findOrCreate({ where: { userId }, defaults: { userId } });
} else {
// 更新现有用户的登录时间
user.lastLogin = new Date();
await user.save();
this.logger.log(`Apple用户登录: ${userId}`);
}
// 生成访问令牌和刷新令牌
const accessToken = this.appleAuthService.generateAccessToken(userId, user.mail);
const refreshToken = this.appleAuthService.generateRefreshToken(userId);
// 构造用户数据
const profileForLogin = await this.userProfileModel.findByPk(user.id);
const userData = {
...user.toJSON(),
isNew: isNewUser,
isVip: user.isVip,
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
profile: profileForLogin ? {
dailyStepsGoal: profileForLogin.dailyStepsGoal,
dailyCaloriesGoal: profileForLogin.dailyCaloriesGoal,
pilatesPurposes: profileForLogin.pilatesPurposes,
weight: profileForLogin.weight,
initialWeight: profileForLogin.initialWeight,
targetWeight: profileForLogin.targetWeight,
height: profileForLogin.height,
activityLevel: profileForLogin.activityLevel,
} : undefined,
};
// 登录行为也可视为活动(可选,记录创建或登录行为)
await this.activityLogsService.record({
userId,
entityType: ActivityEntityType.USER,
action: ActivityActionType.UPDATE,
entityId: userId,
changes: { lastLogin: user.lastLogin },
metadata: { source: 'appleLogin' },
});
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: {
accessToken,
refreshToken,
expiresIn: this.appleAuthService.getAccessTokenExpiresIn(),
user: userData,
},
};
} catch (error) {
this.logger.error(`Apple登录失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `Apple登录失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: null as any,
};
}
}
/**
* 刷新访问令牌
*/
async refreshToken(refreshTokenDto: RefreshTokenDto): Promise<RefreshTokenResponseDto> {
try {
this.logger.log(`refreshToken: ${JSON.stringify(refreshTokenDto)}`);
// 验证刷新令牌并获取用户信息
const payload = this.appleAuthService.verifyRefreshToken(refreshTokenDto.refreshToken);
// 查找用户以获取邮箱信息
const user = await this.userModel.findByPk(payload.sub);
if (!user) {
throw new BadRequestException('用户不存在');
}
// 生成新的访问令牌
const result = await this.appleAuthService.refreshAccessToken(refreshTokenDto.refreshToken, user.mail);
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: result,
};
} catch (error) {
this.logger.error(`刷新令牌失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `刷新令牌失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: null as any,
};
}
}
/**
* 删除用户账号
*/
async deleteAccount(deleteAccountDto: DeleteAccountDto): Promise<DeleteAccountResponseDto> {
this.logger.log(`deleteAccount: ${JSON.stringify(deleteAccountDto)}`);
const transaction: Transaction = await this.sequelize.transaction();
try {
const { userId } = deleteAccountDto;
// 查找用户
const user = await this.userModel.findByPk(userId, { transaction });
if (!user) {
await transaction.rollback();
return {
code: ResponseCode.ERROR,
message: `ID为${userId}的用户不存在`,
data: { success: false },
};
}
// 开始删除用户相关数据(使用事务确保数据一致性)
// 1. 删除用户购买记录
await this.userPurchaseModel.destroy({
where: { userId },
transaction,
});
// 2. 删除用户扩展信息
await this.userProfileModel.destroy({
where: { userId },
transaction,
});
// 最后删除用户本身
await this.userModel.destroy({
where: { id: userId },
transaction,
});
// 提交事务
await transaction.commit();
this.logger.log(`用户账号删除成功: ${userId}`);
// 记录删除账户行为
await this.activityLogsService.record({
userId,
entityType: ActivityEntityType.USER,
action: ActivityActionType.DELETE,
entityId: userId,
changes: null,
metadata: { reason: 'deleteAccount' },
});
return {
code: ResponseCode.SUCCESS,
message: '账号删除成功',
data: { success: true },
};
} catch (error) {
// 回滚事务
await transaction.rollback();
this.logger.error(`删除账号失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `删除账号失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: { success: false },
};
}
}
/**
* 游客登录
*/
async guestLogin(guestLoginDto: GuestLoginDto): Promise<GuestLoginResponseDto> {
try {
this.logger.log(`guestLogin: ${JSON.stringify(guestLoginDto)}`);
const { deviceId, deviceName, deviceModel, iosVersion } = guestLoginDto;
// 构造游客用户ID使用设备ID加前缀
const guestUserId = `guest_${deviceId}`;
// 查找是否已存在该游客用户
let user = await this.userModel.findOne({
where: { id: guestUserId },
});
let isNewUser = false;
if (!user) {
// 创建新的游客用户
const memberNumber = await this.assignMemberNumber();
user = await this.userModel.create({
id: guestUserId,
name: `游客_${deviceName}`,
gender: Gender.MALE, // 默认性别
memberNumber,
isGuest: true,
deviceId,
deviceName,
deviceModel,
iosVersion,
freeUsageCount: DEFAULT_FREE_USAGE_COUNT,
lastLogin: new Date(),
mail: `guest_${deviceId}@gmail.com`,
});
isNewUser = true;
this.logger.log(`创建新的游客用户: ${guestUserId}`);
await this.userProfileModel.findOrCreate({ where: { userId: guestUserId }, defaults: { userId: guestUserId } });
} else {
// 更新现有游客用户的登录时间和设备信息
user.lastLogin = new Date();
user.deviceName = deviceName;
await user.save();
this.logger.log(`游客用户登录: ${guestUserId}`);
}
// 生成访问令牌和刷新令牌
const accessToken = this.appleAuthService.generateAccessToken(guestUserId, user.mail);
const refreshToken = this.appleAuthService.generateRefreshToken(guestUserId);
// 构造用户数据
const profileForGuest = await this.userProfileModel.findByPk(user.id);
const userData = {
...user.toJSON(),
isNew: isNewUser,
isVip: user.membershipExpiration ? dayjs(user.membershipExpiration).isAfter(dayjs()) : false,
isGuest: true,
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
profile: profileForGuest ? {
dailyStepsGoal: profileForGuest.dailyStepsGoal,
dailyCaloriesGoal: profileForGuest.dailyCaloriesGoal,
pilatesPurposes: profileForGuest.pilatesPurposes,
weight: profileForGuest.weight,
initialWeight: profileForGuest.initialWeight,
targetWeight: profileForGuest.targetWeight,
height: profileForGuest.height,
activityLevel: profileForGuest.activityLevel,
} : undefined,
};
await this.activityLogsService.record({
userId: guestUserId,
entityType: ActivityEntityType.USER,
action: ActivityActionType.UPDATE,
entityId: guestUserId,
changes: { lastLogin: user.lastLogin },
metadata: { source: 'guestLogin' },
});
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: {
accessToken,
refreshToken,
expiresIn: this.appleAuthService.getAccessTokenExpiresIn(),
user: userData,
},
};
} catch (error) {
this.logger.error(`游客登录失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `游客登录失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: null as any,
};
}
}
/**
* 刷新游客令牌
*/
async refreshGuestToken(refreshGuestTokenDto: RefreshGuestTokenDto): Promise<RefreshGuestTokenResponseDto> {
try {
this.logger.log(`refreshGuestToken: ${JSON.stringify(refreshGuestTokenDto)}`);
// 验证刷新令牌并获取用户信息
const payload = this.appleAuthService.verifyRefreshToken(refreshGuestTokenDto.refreshToken);
// 查找用户以获取邮箱信息
const user = await this.userModel.findByPk(payload.sub);
if (!user) {
throw new BadRequestException('用户不存在');
}
// 验证是否为游客用户
if (!user.isGuest) {
throw new BadRequestException('非游客用户不能使用此接口');
}
// 生成新的访问令牌
const result = await this.appleAuthService.refreshAccessToken(refreshGuestTokenDto.refreshToken, user.mail);
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: result,
};
} catch (error) {
this.logger.error(`刷新游客令牌失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `刷新游客令牌失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: null as any,
};
}
}
/**
* 处理App Store服务器通知
*/
async processAppStoreNotification(notificationDto: AppStoreServerNotificationDto): Promise<ProcessNotificationResponseDto> {
try {
this.logger.log(`processAppStoreNotification start: ${JSON.stringify(notificationDto)}`);
// 使用ApplePurchaseService处理通知
const { notificationPayload, transactionInfo, renewalInfo } = await this.applePurchaseService.processServerNotification(
notificationDto.signedPayload
);
if (!notificationPayload) {
return {
code: ResponseCode.ERROR,
message: '无法解析通知负载',
data: { processed: false },
};
}
// 提取基本信息
const notificationType = notificationPayload.notificationType as NotificationType;
const notificationUUID = notificationPayload.notificationUUID;
const bundleId = notificationPayload.data?.bundleId;
this.logger.log(`processAppStoreNotification 通知类型: ${notificationType}, UUID: ${notificationUUID}, Bundle ID: ${bundleId}`);
this.logger.log(`processAppStoreNotification transactionInfo: ${JSON.stringify(transactionInfo)}`);
this.logger.log(`processAppStoreNotification renewalInfo: ${JSON.stringify(renewalInfo)}`);
// 根据通知类型进行具体处理
const handlerResult = await this.applePurchaseService.handleNotificationByType(
notificationType,
transactionInfo,
renewalInfo
);
// 如果有交易信息,尝试找到对应的用户
let userId: string | undefined;
let transactionId: string | undefined;
if (transactionInfo) {
transactionId = transactionInfo.transactionId || transactionInfo.originalTransactionId;
// 通过appAccountToken查找用户如果有的话
if (transactionInfo.appAccountToken) {
const user = await this.userModel.findOne({
where: { id: transactionInfo.appAccountToken },
});
if (user) {
userId = user.id;
}
}
}
this.logger.log(`processAppStoreNotification userId: ${userId}`);
if (!userId) {
this.logger.error(`未找到用户ID通知处理失败`);
return {
code: ResponseCode.ERROR,
message: '未找到用户ID通知处理失败',
data: {
processed: false,
},
};
}
// 根据通知类型执行相应的业务逻辑
await this.handleNotificationBusinessLogic(notificationType, transactionInfo, renewalInfo, userId);
this.logger.log(`processAppStoreNotification App Store通知处理完成: ${JSON.stringify(handlerResult)}`);
return {
code: ResponseCode.SUCCESS,
message: '通知处理成功',
data: {
processed: true,
notificationType,
notificationUUID,
userId,
transactionId,
},
};
} catch (error) {
this.logger.error(`processAppStoreNotification 处理App Store服务器通知失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `处理通知失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: { processed: false },
};
}
}
/**
* 根据通知类型执行具体的业务逻辑
*/
private async handleNotificationBusinessLogic(
notificationType: NotificationType,
transactionInfo: any,
renewalInfo: any,
userId?: string
): Promise<void> {
this.logger.log(`执行业务逻辑 - 通知类型: ${notificationType}, 用户ID: ${userId}`);
try {
switch (notificationType) {
case NotificationType.SUBSCRIBED:
await this.handleSubscribedNotification(transactionInfo, renewalInfo, userId);
break;
case NotificationType.DID_RENEW:
await this.handleRenewalNotification(transactionInfo, renewalInfo, userId);
break;
case NotificationType.EXPIRED:
await this.handleExpirationNotification(transactionInfo, renewalInfo, userId);
break;
case NotificationType.DID_FAIL_TO_RENEW:
await this.handleRenewalFailureNotification(transactionInfo, renewalInfo, userId);
break;
case NotificationType.REFUND:
await this.handleRefundNotification(transactionInfo, renewalInfo, userId);
break;
case NotificationType.REVOKE:
await this.handleRevokeNotification(transactionInfo, renewalInfo, userId);
break;
case NotificationType.DID_CHANGE_RENEWAL_STATUS:
await this.handleRenewalStatusChangeNotification(transactionInfo, renewalInfo, userId);
break;
case NotificationType.TEST:
this.logger.log('收到测试通知,无需特殊处理');
break;
default:
this.logger.warn(`未处理的通知类型: ${notificationType}`);
}
} catch (error) {
this.logger.error(`执行业务逻辑失败: ${error instanceof Error ? error.message : '未知错误'}`);
throw error;
}
}
/**
* 处理订阅通知
*/
private async handleSubscribedNotification(transactionInfo: any, renewalInfo: any, userId?: string): Promise<void> {
this.logger.log(`handleSubscribedNotification 处理订阅通知 - 用户ID: ${userId}`);
if (!userId || !transactionInfo) {
this.logger.warn('handleSubscribedNotification 缺少用户ID或交易信息跳过订阅处理');
return;
}
// 更新用户订阅状态
const user = await this.userModel.findByPk(userId);
if (user && transactionInfo.expiresDate) {
user.membershipExpiration = new Date(transactionInfo.expiresDate);
await user.save();
this.logger.log(`handleSubscribedNotification 用户 ${userId} 订阅状态已更新,到期时间: ${user.membershipExpiration}`);
}
}
/**
* 处理续订通知
*/
private async handleRenewalNotification(transactionInfo: any, renewalInfo: any, userId?: string): Promise<void> {
this.logger.log(`handleRenewalNotification 处理续订通知 - 用户ID: ${userId}`);
if (!userId || !transactionInfo) {
this.logger.warn('handleRenewalNotification 缺少用户ID或交易信息跳过续订处理');
return;
}
// 更新用户订阅到期时间
const user = await this.userModel.findByPk(userId);
if (user && transactionInfo.expiresDate) {
user.membershipExpiration = new Date(transactionInfo.expiresDate);
await user.save();
this.logger.log(`handleRenewalNotification 用户 ${userId} 订阅已续订,新的到期时间: ${user.membershipExpiration}`);
}
}
/**
* 处理过期通知
*/
private async handleExpirationNotification(transactionInfo: any, renewalInfo: any, userId?: string): Promise<void> {
this.logger.log(`handleExpirationNotification 处理过期通知 - 用户ID: ${userId}`);
if (!userId) {
this.logger.warn('handleExpirationNotification 缺少用户ID跳过过期处理');
return;
}
// 可以在这里实现过期后的处理逻辑
// 例如:发送邮件通知、降级用户权限等
this.logger.log(`用户 ${userId} 的订阅已过期`);
}
/**
* 处理续订失败通知
*/
private async handleRenewalFailureNotification(transactionInfo: any, renewalInfo: any, userId?: string): Promise<void> {
this.logger.log(`处理续订失败通知 - 用户ID: ${userId}`);
if (!userId) {
this.logger.warn('缺少用户ID跳过续订失败处理');
return;
}
// 可以在这里实现续订失败的处理逻辑
// 例如:发送付款提醒邮件
this.logger.log(`用户 ${userId} 的订阅续订失败`);
}
/**
* 处理退款通知
*/
private async handleRefundNotification(transactionInfo: any, renewalInfo: any, userId?: string): Promise<void> {
this.logger.log(`处理退款通知 - 用户ID: ${userId}`);
if (!userId) {
this.logger.warn('缺少用户ID跳过退款处理');
return;
}
// 处理退款逻辑:撤销用户的订阅权限
const user = await this.userModel.findByPk(userId);
if (user) {
// 将会员到期时间设置为当前时间,立即取消会员权限
user.membershipExpiration = new Date();
await user.save();
this.logger.log(`用户 ${userId} 的订阅因退款已被撤销`);
}
}
/**
* 处理撤销通知
*/
private async handleRevokeNotification(transactionInfo: any, renewalInfo: any, userId?: string): Promise<void> {
this.logger.log(`处理撤销通知 - 用户ID: ${userId}`);
if (!userId) {
this.logger.warn('缺少用户ID跳过撤销处理');
return;
}
// 处理撤销逻辑:撤销用户的订阅权限
const user = await this.userModel.findByPk(userId);
if (user) {
user.membershipExpiration = new Date();
await user.save();
this.logger.log(`用户 ${userId} 的订阅已被撤销`);
}
}
/**
* 处理续订状态变更通知
*/
private async handleRenewalStatusChangeNotification(transactionInfo: any, renewalInfo: any, userId?: string): Promise<void> {
this.logger.log(`处理续订状态变更通知 - 用户ID: ${userId}`);
if (!userId || !renewalInfo) {
this.logger.warn('缺少用户ID或续订信息跳过续订状态变更处理');
return;
}
// 可以在这里实现续订状态变更的处理逻辑
// 例如根据autoRenewStatus更新用户的自动续订设置
const autoRenewEnabled = renewalInfo.autoRenewStatus === 1;
this.logger.log(`用户 ${userId} 的自动续订状态: ${autoRenewEnabled ? '启用' : '禁用'}`);
}
async handleRevenueCatWebhook(webhook: RevenueCatWebhookDto): Promise<void> {
const { event } = webhook;
// 1. Check for duplicate event
const existingEvent = await this.revenueCatEventModel.findOne({
where: { eventId: event.id },
});
if (existingEvent) {
this.winstonLogger.warn('RevenueCat event already processed', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
reason: 'duplicate_event'
});
return;
}
this.winstonLogger.info('RevenueCat event processing started', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
eventTimestamp: event.event_timestamp_ms
});
// 2. Save the event to the database
await this.revenueCatEventModel.create({
eventId: event.id,
type: event.type,
appUserId: event.app_user_id,
eventTimestamp: new Date(event.event_timestamp_ms),
payload: event,
});
// 3. Process the event based on its type
const user = await this.userModel.findByPk(event.app_user_id);
if (!user) {
this.winstonLogger.warn('User not found for RevenueCat event', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
reason: 'user_not_found'
});
// Even if user not found, we mark event as processed to avoid retries
return;
}
this.winstonLogger.info('User found for RevenueCat event', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
userId: user.id,
userEmail: user.mail,
currentMembershipExpiration: user.membershipExpiration
});
switch (event.type) {
case RevenueCatEventType.INITIAL_PURCHASE:
case RevenueCatEventType.RENEWAL:
case RevenueCatEventType.UNCANCELLATION:
case RevenueCatEventType.PRODUCT_CHANGE:
case RevenueCatEventType.NON_RENEWING_PURCHASE:
case RevenueCatEventType.TRANSFER:
if (user && typeof event.expiration_at_ms === 'number') {
const previousExpiration = user.membershipExpiration;
user.membershipExpiration = new Date(event.expiration_at_ms);
await user.save();
this.winstonLogger.info('Membership updated for purchase event', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
userId: user.id,
previousExpiration: previousExpiration,
newExpiration: user.membershipExpiration,
productId: event.product_id,
expirationAtMs: event.expiration_at_ms
});
}
break;
case RevenueCatEventType.CANCELLATION:
// For cancellations, you might want to record the cancellation reason
// but not immediately revoke access. Access is typically revoked on EXPIRATION.
this.winstonLogger.warn('Subscription cancelled', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
userId: user.id,
cancelReason: event.cancel_reason,
productId: event.product_id,
currentMembershipExpiration: user.membershipExpiration,
note: 'Access not immediately revoked - will be revoked on EXPIRATION event'
});
break;
case RevenueCatEventType.EXPIRATION:
// The subscription has truly expired.
// You could set membershipExpiration to now or a past date.
if (user) {
// Check if the current expiration date is in the future.
// If so, it means the user might have had a longer plan from a previous purchase
// and we should not shorten it.
const now = new Date();
const previousExpiration = user.membershipExpiration;
if (user.membershipExpiration && user.membershipExpiration > now) {
this.winstonLogger.info('Expiration event ignored - user has future expiration', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
userId: user.id,
currentExpiration: user.membershipExpiration,
eventExpirationMs: event.expiration_at_ms,
reason: 'current_expiration_in_future'
});
} else if (typeof event.expiration_at_ms === 'number') {
user.membershipExpiration = new Date(event.expiration_at_ms);
await user.save();
this.winstonLogger.warn('Membership expired', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
userId: user.id,
previousExpiration: previousExpiration,
newExpiration: user.membershipExpiration,
productId: event.product_id,
expirationAtMs: event.expiration_at_ms
});
}
}
break;
case RevenueCatEventType.TEST:
this.winstonLogger.info('RevenueCat test event received', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
note: 'Test event - no processing required'
});
break;
default:
this.winstonLogger.warn('Unhandled RevenueCat event type', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
reason: 'unknown_event_type',
eventData: event
});
}
// 4. Mark event as processed
const savedEvent = await this.revenueCatEventModel.findOne({ where: { eventId: event.id } });
if (savedEvent) {
savedEvent.processed = true;
savedEvent.processedAt = new Date();
await savedEvent.save();
this.winstonLogger.info('RevenueCat event processing completed', {
context: 'UsersService',
method: 'handleRevenueCatWebhook',
eventId: event.id,
eventType: event.type,
appUserId: event.app_user_id,
userId: user?.id,
processedAt: savedEvent.processedAt,
status: 'completed'
});
}
}
/**
* 恢复购买 - 根据 RevenueCat 客户信息恢复用户的购买记录(安全版本)
*/
async restorePurchase(
restorePurchaseDto: RestorePurchaseDto,
currentUserId: string,
clientIp?: string,
userAgent?: string
): Promise<RestorePurchaseResponseDto> {
// 输入验证
if (!restorePurchaseDto?.customerInfo) {
return this.createErrorResponse('客户信息不能为空');
}
if (!currentUserId?.trim()) {
return this.createErrorResponse('用户ID不能为空');
}
const dbTransaction = await this.sequelize.transaction();
try {
this.winstonLogger.info('Restoring purchase', {
context: 'UsersService',
method: 'restorePurchase',
customerInfo: restorePurchaseDto.customerInfo
});
// 验证用户存在性
const user = await this.validateUserExists(currentUserId, dbTransaction);
if (!user) {
await dbTransaction.rollback();
return this.createErrorResponse('用户不存在');
}
const { customerInfo } = restorePurchaseDto;
this.winstonLogger.info('RevenueCat customer info', {
context: 'UsersService',
method: 'restorePurchase',
customerInfo: customerInfo
});
// 🔒 安全验证检查RevenueCat原始用户ID关联性
const securityValidation = await this.validateRevenueCatUserAssociation(
customerInfo.originalAppUserId,
currentUserId,
dbTransaction
);
this.winstonLogger.info('RevenueCat security validation', {
context: 'UsersService',
method: 'restorePurchase',
customerInfo: customerInfo,
securityValidation: securityValidation
});
if (!securityValidation.isValid) {
await this.logRestoreAttempt(
currentUserId,
'',
'',
customerInfo.originalAppUserId,
RestoreStatus.FRAUD_DETECTED,
securityValidation.reason || '安全验证失败',
restorePurchaseDto,
clientIp,
userAgent,
dbTransaction
);
await dbTransaction.rollback();
return this.createErrorResponse('安全验证失败:用户身份验证不通过');
}
// 🔒 安全验证:检查被阻止的交易
const blockedCheck = await this.checkForBlockedTransactions(
customerInfo,
dbTransaction
);
this.winstonLogger.info('RevenueCat blocked check', {
context: 'UsersService',
method: 'restorePurchase',
customerInfo: customerInfo,
blockedCheck: blockedCheck
});
if (blockedCheck.hasBlocked) {
await this.logRestoreAttempt(
currentUserId,
'',
'',
customerInfo.originalAppUserId,
RestoreStatus.FRAUD_DETECTED,
`检测到被阻止的交易: ${blockedCheck.blockedTransactions.join(', ')}`,
restorePurchaseDto,
clientIp,
userAgent,
dbTransaction
);
await dbTransaction.rollback();
return this.createErrorResponse(`检测到被阻止的交易,恢复失败。涉及交易: ${blockedCheck.blockedTransactions.join(', ')}`);
}
this.winstonLogger.info('RevenueCat blocked check', {
context: 'UsersService',
method: 'restorePurchase',
customerInfo: customerInfo,
blockedCheck: blockedCheck
});
// 🔒 安全验证检查交易ID全局唯一性
const duplicateCheck = await this.checkForDuplicateTransactions(
customerInfo,
currentUserId,
dbTransaction
);
if (duplicateCheck.hasDuplicates) {
// 自动阻止重复使用的交易
await this.autoBlockDuplicateTransactions(
duplicateCheck.duplicateTransactions,
'Automatic block due to duplicate usage across multiple accounts',
dbTransaction
);
await this.logRestoreAttempt(
currentUserId,
'',
'',
customerInfo.originalAppUserId,
RestoreStatus.DUPLICATE,
`检测到重复交易: ${duplicateCheck.duplicateTransactions.join(', ')}`,
restorePurchaseDto,
clientIp,
userAgent,
dbTransaction
);
await dbTransaction.rollback();
return this.createErrorResponse(`检测到重复交易,恢复失败。涉及交易: ${duplicateCheck.duplicateTransactions.join(', ')}`);
}
const restoredPurchases: RestoredPurchaseInfo[] = [];
// 处理活跃权益(带安全日志)
const activeEntitlementResults = await this.processActiveEntitlements(
customerInfo.activeEntitlements,
currentUserId,
dbTransaction
);
this.winstonLogger.info('RevenueCat active entitlements', {
context: 'UsersService',
method: 'restorePurchase',
customerInfo: customerInfo,
activeEntitlements: activeEntitlementResults
});
// 记录成功的恢复日志
await this.logSuccessfulRestores(
activeEntitlementResults.purchases,
currentUserId,
customerInfo.originalAppUserId,
restorePurchaseDto,
clientIp,
userAgent,
dbTransaction
);
restoredPurchases.push(...activeEntitlementResults.purchases);
// 处理非订阅交易(带安全日志)
const nonSubResults = await this.processNonSubscriptionTransactions(
customerInfo.nonSubscriptionTransactions,
currentUserId,
dbTransaction
);
// 记录成功的恢复日志
await this.logSuccessfulRestores(
nonSubResults.purchases,
currentUserId,
customerInfo.originalAppUserId,
restorePurchaseDto,
clientIp,
userAgent,
dbTransaction
);
restoredPurchases.push(...nonSubResults.purchases);
// 更新用户会员状态
const allExpirationDates = [
...activeEntitlementResults.expirationDates,
...nonSubResults.expirationDates
];
const membershipExpiration = await this.updateUserMembership(
user,
allExpirationDates,
dbTransaction
);
await dbTransaction.commit();
const totalRestoredCount = restoredPurchases.length;
const message = this.generateSuccessMessage(totalRestoredCount, membershipExpiration);
this.logger.log(`恢复购买完成 - 用户ID: ${currentUserId}, 恢复了 ${totalRestoredCount} 个购买记录`);
return {
code: ResponseCode.SUCCESS,
message: totalRestoredCount > 0 ? '购买记录恢复成功' : '未找到可恢复的购买记录',
data: {
restoredPurchases,
membershipExpiration: membershipExpiration?.toISOString(),
message,
totalRestoredCount
}
};
} catch (error) {
await dbTransaction.rollback();
this.logger.error(`恢复购买失败: ${error instanceof Error ? error.message : '未知错误'}`);
return this.createErrorResponse(`恢复购买失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 🔒 验证RevenueCat用户关联性
*/
private async validateRevenueCatUserAssociation(
originalAppUserId: string,
currentUserId: string,
transaction: Transaction
): Promise<{ isValid: boolean; reason?: string }> {
// 检查是否是同一个用户ID直接匹配
if (originalAppUserId === currentUserId) {
return { isValid: true };
}
// 检查是否是游客用户升级为正式用户的情况
if (originalAppUserId.startsWith('guest_') && !currentUserId.startsWith('guest_')) {
// 可以添加更复杂的验证逻辑比如设备ID匹配等
this.winstonLogger.warn(`游客用户升级检测: ${originalAppUserId} -> ${currentUserId}`);
return { isValid: true }; // 暂时允许,但应该添加更严格的验证
}
// 检查是否已经有其他用户使用过这个RevenueCat用户ID
const existingLog = await this.purchaseRestoreLogModel.findOne({
where: {
originalAppUserId,
status: RestoreStatus.SUCCESS
},
transaction
});
if (existingLog && existingLog.userId !== currentUserId) {
return {
isValid: false,
reason: `RevenueCat用户ID ${originalAppUserId} 已被其他用户 ${existingLog.userId} 使用`
};
}
return { isValid: true };
}
/**
* 🔒 检查交易ID重复使用
*/
private async checkForDuplicateTransactions(
customerInfo: any,
currentUserId: string,
transaction: Transaction
): Promise<{ hasDuplicates: boolean; duplicateTransactions: string[] }> {
const allTransactionIds: string[] = [];
// 收集所有交易ID
Object.values(customerInfo.activeEntitlements).forEach((entitlement: any) => {
if (entitlement.latestPurchaseDate) {
allTransactionIds.push(entitlement.latestPurchaseDate);
}
});
customerInfo.nonSubscriptionTransactions.forEach((nonSub: any) => {
if (nonSub.transactionIdentifier) {
allTransactionIds.push(nonSub.transactionIdentifier);
}
});
if (allTransactionIds.length === 0) {
return { hasDuplicates: false, duplicateTransactions: [] };
}
// 检查这些交易ID是否已被其他用户使用
const existingLogs = await this.purchaseRestoreLogModel.findAll({
where: {
transactionId: allTransactionIds,
status: RestoreStatus.SUCCESS,
userId: { [Op.ne]: currentUserId } // 排除当前用户
},
transaction
});
const duplicateTransactions = existingLogs.map(log => log.transactionId);
return {
hasDuplicates: duplicateTransactions.length > 0,
duplicateTransactions
};
}
/**
* 🔒 记录恢复尝试日志
*/
private async logRestoreAttempt(
userId: string,
transactionId: string,
productId: string,
originalAppUserId: string,
status: RestoreStatus,
failureReason: string | null,
rawData: any,
clientIp?: string,
userAgent?: string,
transaction?: Transaction
): Promise<void> {
try {
await this.purchaseRestoreLogModel.create({
userId,
transactionId,
productId,
originalAppUserId,
status,
source: RestoreSource.REVENUE_CAT,
rawData: JSON.stringify(rawData),
clientIp,
userAgent,
failureReason
}, { transaction });
} catch (error) {
this.logger.error(`记录恢复日志失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 🔒 记录成功的恢复日志
*/
private async logSuccessfulRestores(
restoredPurchases: RestoredPurchaseInfo[],
userId: string,
originalAppUserId: string,
rawData: any,
clientIp?: string,
userAgent?: string,
transaction?: Transaction
): Promise<void> {
try {
const logPromises = restoredPurchases.map(purchase =>
this.purchaseRestoreLogModel.create({
userId,
transactionId: purchase.transactionId,
productId: purchase.productId,
originalAppUserId,
status: RestoreStatus.SUCCESS,
source: RestoreSource.REVENUE_CAT,
rawData: JSON.stringify(rawData),
clientIp,
userAgent,
failureReason: null
}, { transaction })
);
await Promise.all(logPromises);
this.logger.log(`记录了 ${restoredPurchases.length} 个成功恢复的购买日志`);
} catch (error) {
this.logger.error(`记录成功恢复日志失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 🔒 检查被阻止的交易
*/
private async checkForBlockedTransactions(
customerInfo: any,
transaction: Transaction
): Promise<{ hasBlocked: boolean; blockedTransactions: string[] }> {
const allTransactionIds: string[] = [];
// 收集所有交易ID
Object.values(customerInfo.activeEntitlements).forEach((entitlement: any) => {
if (entitlement.latestPurchaseDate) {
allTransactionIds.push(entitlement.latestPurchaseDate);
}
});
customerInfo.nonSubscriptionTransactions.forEach((nonSub: any) => {
if (nonSub.transactionIdentifier) {
allTransactionIds.push(nonSub.transactionIdentifier);
}
});
if (allTransactionIds.length === 0) {
return { hasBlocked: false, blockedTransactions: [] };
}
// 检查这些交易ID是否被阻止
const blockedTransactions = await this.blockedTransactionModel.findAll({
where: {
transactionId: allTransactionIds,
isActive: true,
[Op.or]: [
{ expiresAt: null }, // 永久阻止
{ expiresAt: { [Op.gt]: new Date() } } // 未过期的临时阻止
]
},
transaction
});
const blockedTransactionIds = blockedTransactions.map(bt => bt.transactionId);
return {
hasBlocked: blockedTransactionIds.length > 0,
blockedTransactions: blockedTransactionIds
};
}
/**
* 🔒 自动阻止重复使用的交易
*/
private async autoBlockDuplicateTransactions(
transactionIds: string[],
reason: string,
transaction?: Transaction
): Promise<void> {
try {
const blockPromises = transactionIds.map(transactionId =>
this.blockedTransactionModel.findOrCreate({
where: { transactionId },
defaults: {
transactionId,
blockReason: BlockReason.DUPLICATE_USAGE,
description: reason,
isActive: true,
expiresAt: null // 永久阻止
},
transaction
})
);
await Promise.all(blockPromises);
this.logger.warn(`自动阻止了 ${transactionIds.length} 个重复使用的交易: ${transactionIds.join(', ')}`);
} catch (error) {
this.logger.error(`自动阻止交易失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 🔒 手动阻止交易
*/
async blockTransaction(
transactionId: string,
reason: string,
operatorId: string,
expiresAt?: Date
): Promise<{ success: boolean; message: string }> {
try {
const [blockedTransaction, created] = await this.blockedTransactionModel.findOrCreate({
where: { transactionId },
defaults: {
transactionId,
blockReason: BlockReason.MANUAL_BLOCK,
description: reason,
operatorId,
isActive: true,
expiresAt: expiresAt || null
}
});
if (!created) {
// 更新现有记录
blockedTransaction.description = reason;
blockedTransaction.operatorId = operatorId;
blockedTransaction.isActive = true;
if (expiresAt) {
blockedTransaction.expiresAt = expiresAt;
} else {
blockedTransaction.expiresAt = null;
}
await blockedTransaction.save();
}
this.logger.warn(`交易 ${transactionId} 已被手动阻止,操作员: ${operatorId}, 原因: ${reason}`);
return { success: true, message: '交易已成功阻止' };
} catch (error) {
this.logger.error(`阻止交易失败: ${error instanceof Error ? error.message : '未知错误'}`);
return { success: false, message: `阻止交易失败: ${error instanceof Error ? error.message : '未知错误'}` };
}
}
/**
* 🔒 解除交易阻止
*/
async unblockTransaction(
transactionId: string,
operatorId: string
): Promise<{ success: boolean; message: string }> {
try {
const blockedTransaction = await this.blockedTransactionModel.findOne({
where: { transactionId, isActive: true }
});
if (!blockedTransaction) {
return { success: false, message: '未找到被阻止的交易' };
}
blockedTransaction.isActive = false;
blockedTransaction.operatorId = operatorId;
await blockedTransaction.save();
this.logger.log(`交易 ${transactionId} 已被解除阻止,操作员: ${operatorId}`);
return { success: true, message: '交易阻止已成功解除' };
} catch (error) {
this.logger.error(`解除交易阻止失败: ${error instanceof Error ? error.message : '未知错误'}`);
return { success: false, message: `解除交易阻止失败: ${error instanceof Error ? error.message : '未知错误'}` };
}
}
/**
* 验证用户是否存在
*/
private async validateUserExists(userId: string, transaction: Transaction): Promise<User | null> {
return await this.userModel.findByPk(userId, { transaction });
}
/**
* 处理活跃权益
*/
private async processActiveEntitlements(
activeEntitlements: { [key: string]: ActiveEntitlement },
userId: string,
transaction: Transaction
): Promise<{ purchases: RestoredPurchaseInfo[]; expirationDates: Date[] }> {
const purchases: RestoredPurchaseInfo[] = [];
const expirationDates: Date[] = [];
this.logger.log(`处理活跃权益 - 数量: ${Object.keys(activeEntitlements).length}`);
// 批量查询现有购买记录以避免N+1问题
const entitlementKeys = Object.keys(activeEntitlements);
const existingPurchases = await this.getExistingPurchasesForEntitlements(
userId,
activeEntitlements,
transaction
);
for (const [entitlementKey, entitlement] of Object.entries(activeEntitlements)) {
if (!entitlement.isActive) continue;
this.logger.log(`处理权益: ${entitlementKey}, 产品ID: ${entitlement.productIdentifier}`);
const purchaseType = this.determinePurchaseType(entitlement.productIdentifier);
const existingPurchase = existingPurchases.find(p =>
p.productId === entitlement.productIdentifier &&
p.transactionId === entitlement.latestPurchaseDate
);
if (!existingPurchase) {
const expirationDate = await this.createPurchaseRecord(
userId,
entitlement,
purchaseType,
transaction
);
if (expirationDate) {
expirationDates.push(expirationDate);
}
}
// 添加到恢复列表
purchases.push(this.createRestoredPurchaseInfo(entitlement, entitlementKey, purchaseType));
}
return { purchases, expirationDates };
}
/**
* 处理非订阅交易
*/
private async processNonSubscriptionTransactions(
nonSubTransactions: NonSubscriptionTransaction[],
userId: string,
transaction: Transaction
): Promise<{ purchases: RestoredPurchaseInfo[]; expirationDates: Date[] }> {
const purchases: RestoredPurchaseInfo[] = [];
const expirationDates: Date[] = [];
this.logger.log(`处理非订阅交易 - 数量: ${nonSubTransactions.length}`);
// 批量查询现有购买记录
const existingPurchases = await this.getExistingPurchasesForNonSub(
userId,
nonSubTransactions,
transaction
);
for (const nonSubTransaction of nonSubTransactions) {
this.logger.log(`处理非订阅交易: ${nonSubTransaction.productIdentifier}`);
const purchaseType = this.determinePurchaseType(nonSubTransaction.productIdentifier);
const existingPurchase = existingPurchases.find(p =>
p.productId === nonSubTransaction.productIdentifier &&
p.transactionId === nonSubTransaction.transactionIdentifier
);
if (!existingPurchase) {
const expirationDate = await this.createNonSubPurchaseRecord(
userId,
nonSubTransaction,
purchaseType,
transaction
);
if (expirationDate) {
expirationDates.push(expirationDate);
}
}
// 添加到恢复列表
purchases.push(this.createRestoredPurchaseInfoFromNonSub(nonSubTransaction, purchaseType));
}
return { purchases, expirationDates };
}
/**
* 批量查询活跃权益的现有购买记录
*/
private async getExistingPurchasesForEntitlements(
userId: string,
entitlements: { [key: string]: ActiveEntitlement },
transaction: Transaction
): Promise<UserPurchase[]> {
const conditions = Object.values(entitlements).map(entitlement => ({
userId,
productId: entitlement.productIdentifier,
transactionId: entitlement.latestPurchaseDate
}));
if (conditions.length === 0) return [];
return await this.userPurchaseModel.findAll({
where: {
[Op.or]: conditions
},
transaction
});
}
/**
* 批量查询非订阅交易的现有购买记录
*/
private async getExistingPurchasesForNonSub(
userId: string,
nonSubTransactions: NonSubscriptionTransaction[],
transaction: Transaction
): Promise<UserPurchase[]> {
const conditions = nonSubTransactions.map(transaction => ({
userId,
productId: transaction.productIdentifier,
transactionId: transaction.transactionIdentifier
}));
if (conditions.length === 0) return [];
return await this.userPurchaseModel.findAll({
where: {
[Op.or]: conditions
},
transaction
});
}
/**
* 确定购买类型
*/
private determinePurchaseType(productIdentifier: string): PurchaseType {
const productId = productIdentifier.toLowerCase();
if (productId.includes('lifetime')) {
return PurchaseType.LIFETIME;
} else if (productId.includes('quarterly')) {
return PurchaseType.QUARTERLY;
} else if (productId.includes('weekly')) {
return PurchaseType.WEEKLY;
}
return PurchaseType.WEEKLY; // 默认值
}
/**
* 创建购买记录(活跃权益)
*/
private async createPurchaseRecord(
userId: string,
entitlement: ActiveEntitlement,
purchaseType: PurchaseType,
transaction: Transaction
): Promise<Date | null> {
const expirationDate = entitlement.expirationDate ? new Date(entitlement.expirationDate) : null;
let finalExpirationDate: Date;
if (purchaseType === PurchaseType.LIFETIME && !expirationDate) {
finalExpirationDate = this.getLifetimeExpirationDate();
} else if (expirationDate) {
finalExpirationDate = expirationDate;
} else {
return null;
}
await this.userPurchaseModel.create({
userId,
purchaseType,
status: PurchaseStatus.ACTIVE,
platform: PurchasePlatform.IOS,
transactionId: entitlement.latestPurchaseDate,
productId: entitlement.productIdentifier,
expiresAt: finalExpirationDate
}, { transaction });
this.logger.log(`创建购买记录: ${entitlement.productIdentifier}, 类型: ${purchaseType}`);
return finalExpirationDate;
}
/**
* 创建非订阅购买记录
*/
private async createNonSubPurchaseRecord(
userId: string,
nonSubTransaction: NonSubscriptionTransaction,
purchaseType: PurchaseType,
transaction: Transaction
): Promise<Date> {
const lifetimeExpiration = this.getLifetimeExpirationDate();
await this.userPurchaseModel.create({
userId,
purchaseType,
status: PurchaseStatus.ACTIVE,
platform: PurchasePlatform.IOS,
transactionId: nonSubTransaction.transactionIdentifier,
productId: nonSubTransaction.productIdentifier,
expiresAt: lifetimeExpiration
}, { transaction });
this.logger.log(`创建非订阅购买记录: ${nonSubTransaction.productIdentifier}, 类型: ${purchaseType}`);
return lifetimeExpiration;
}
/**
* 获取终身购买的过期时间
*/
private getLifetimeExpirationDate(): Date {
const LIFETIME_YEARS = 100; // 常量化魔法数字
const lifetimeExpiration = new Date();
lifetimeExpiration.setFullYear(lifetimeExpiration.getFullYear() + LIFETIME_YEARS);
return lifetimeExpiration;
}
/**
* 创建恢复购买信息(活跃权益)
*/
private createRestoredPurchaseInfo(
entitlement: ActiveEntitlement,
entitlementKey: string,
purchaseType: PurchaseType
): RestoredPurchaseInfo {
return {
productId: entitlement.productIdentifier,
transactionId: entitlement.latestPurchaseDate,
purchaseDate: entitlement.originalPurchaseDate,
expirationDate: entitlement.expirationDate || undefined,
entitlementId: entitlementKey,
store: entitlement.store,
purchaseType: purchaseType
};
}
/**
* 创建恢复购买信息(非订阅交易)
*/
private createRestoredPurchaseInfoFromNonSub(
nonSubTransaction: NonSubscriptionTransaction,
purchaseType: PurchaseType
): RestoredPurchaseInfo {
return {
productId: nonSubTransaction.productIdentifier,
transactionId: nonSubTransaction.transactionIdentifier,
purchaseDate: nonSubTransaction.purchaseDate,
entitlementId: nonSubTransaction.productIdentifier,
store: 'APP_STORE',
purchaseType: purchaseType
};
}
/**
* 更新用户会员状态
*/
private async updateUserMembership(
user: User,
expirationDates: Date[],
transaction: Transaction
): Promise<Date | null> {
if (expirationDates.length === 0) return null;
const latestExpirationDate = new Date(Math.max(...expirationDates.map(date => date.getTime())));
const currentExpiration = user.membershipExpiration;
if (!currentExpiration || latestExpirationDate > currentExpiration) {
user.membershipExpiration = latestExpirationDate;
await user.save({ transaction });
this.winstonLogger.info(`用户 ${user.id} 的会员状态已更新,过期时间: ${latestExpirationDate}`, {
context: 'UsersService',
method: 'updateUserMembership',
user
});
return latestExpirationDate;
}
return currentExpiration;
}
/**
* 生成成功消息
*/
private generateSuccessMessage(totalRestoredCount: number, membershipExpiration: Date | null): string {
if (totalRestoredCount === 0) {
return '未找到可恢复的购买记录';
}
let message = `成功恢复 ${totalRestoredCount} 个购买记录`;
if (membershipExpiration) {
message += `,会员有效期至 ${dayjs(membershipExpiration).format('YYYY-MM-DD HH:mm:ss')}`;
}
return message;
}
/**
* 创建错误响应
*/
private createErrorResponse(message: string): RestorePurchaseResponseDto {
return {
code: ResponseCode.ERROR,
message,
data: {
restoredPurchases: [],
message,
totalRestoredCount: 0
}
};
}
/**
* 调用 RevenueCat API 获取用户订阅信息subscriptions endpoint
*/
private async getRevenueCatSubscriptions(userId: string): Promise<any> {
try {
const REVENUECAT_PUBLIC_API_KEY = process.env.REVENUECAT_PUBLIC_API_KEY;
const REVENUECAT_PROJECT_ID = process.env.REVENUECAT_PROJECT_ID || 'proje92e464f';
if (!REVENUECAT_PUBLIC_API_KEY) {
throw new Error('RevenueCat API key 未配置');
}
const url = `https://api.revenuecat.com/v2/projects/${REVENUECAT_PROJECT_ID}/customers/${userId}/subscriptions`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${REVENUECAT_PUBLIC_API_KEY}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
if (response.status === 404) {
this.logger.warn(`RevenueCat subscriptions 中未找到用户: ${userId}`);
return null;
}
throw new Error(`RevenueCat Subscriptions API 请求失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
this.logger.log(`RevenueCat Subscriptions API 响应: ${JSON.stringify(data)}`);
return data;
} catch (error) {
this.logger.error(`调用 RevenueCat Subscriptions API 失败: ${error instanceof Error ? error.message : '未知错误'}`);
return null; // 返回 null 而不是抛出错误,让调用方继续处理
}
}
/**
* 调用 RevenueCat API 获取用户购买信息purchases endpoint
*/
private async getRevenueCatPurchases(userId: string): Promise<any> {
try {
const REVENUECAT_PUBLIC_API_KEY = process.env.REVENUECAT_PUBLIC_API_KEY;
const REVENUECAT_PROJECT_ID = process.env.REVENUECAT_PROJECT_ID || 'proje92e464f';
if (!REVENUECAT_PUBLIC_API_KEY) {
throw new Error('RevenueCat API key 未配置');
}
const url = `https://api.revenuecat.com/v2/projects/${REVENUECAT_PROJECT_ID}/customers/${userId}/purchases`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${REVENUECAT_PUBLIC_API_KEY}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
if (response.status === 404) {
this.logger.warn(`RevenueCat purchases 中未找到用户: ${userId}`);
return null;
}
throw new Error(`RevenueCat Purchases API 请求失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
this.logger.log(`RevenueCat Purchases API 响应: ${JSON.stringify(data)}`);
return data;
} catch (error) {
this.logger.error(`调用 RevenueCat Purchases API 失败: ${error instanceof Error ? error.message : '未知错误'}`);
return null; // 返回 null 而不是抛出错误,让调用方继续处理
}
}
/**
* 异步同步用户会员状态(从 RevenueCat 获取最新数据)
* 带频率限制,避免过度调用 API
*/
private async syncMembershipFromRevenueCat(userId: string): Promise<void> {
try {
// 检查频率限制
const lastSyncTime = this.lastSyncTimestamps.get(userId);
const now = Date.now();
if (lastSyncTime && (now - lastSyncTime) < MEMBERSHIP_SYNC_INTERVAL_MS) {
this.logger.log(`用户 ${userId} 在频率限制内,跳过会员验证(距上次验证 ${Math.floor((now - lastSyncTime) / 1000)} 秒)`);
return;
}
this.winstonLogger.info('开始同步用户会员状态', {
context: 'UsersService',
method: 'syncMembershipFromRevenueCat',
userId,
lastSyncTime: lastSyncTime ? new Date(lastSyncTime).toISOString() : 'never'
});
// 并行调用两个 RevenueCat API 接口获取用户信息
const [subscriptionsData, purchasesData] = await Promise.all([
this.getRevenueCatSubscriptions(userId),
this.getRevenueCatPurchases(userId)
]);
// 更新最后验证时间
this.lastSyncTimestamps.set(userId, now);
// 如果两个接口都没有数据,说明用户没有任何购买记录
if (!subscriptionsData && !purchasesData) {
this.logger.log(`用户 ${userId} 在 RevenueCat 中未找到任何购买信息`);
return;
}
let latestExpirationDate: Date | null = null;
let isLifetimeMember = false;
// 1. 处理订阅数据subscriptions endpoint
if (subscriptionsData && subscriptionsData.items) {
this.logger.log(`用户 ${userId} 订阅数据: ${subscriptionsData.items.length} 个订阅`);
for (const subscription of subscriptionsData.items) {
// subscription 可能包含 current_period_ends_at 字段
const currentPeriodEndsAt = subscription.current_period_ends_at;
if (currentPeriodEndsAt) {
const expiration = new Date(currentPeriodEndsAt);
// 只考虑未过期的订阅
if (expiration > new Date()) {
if (!latestExpirationDate || expiration > latestExpirationDate) {
latestExpirationDate = expiration;
this.logger.log(`找到有效订阅,过期时间: ${expiration.toISOString()}`);
}
}
}
}
}
// 2. 处理一次性购买数据purchases endpoint
if (purchasesData && purchasesData.items) {
this.logger.log(`用户 ${userId} 购买数据: ${purchasesData.items.length} 个购买`);
for (const purchase of purchasesData.items) {
// 一次性购买通常没有过期时间,或者 status 为 "owned"
// 如果有 store_transaction_id 且 status 为 active/owned认为是有效的终身购买
if (purchase.status === 'owned' || purchase.status === 'active') {
// 检查是否是终身购买(通常一次性购买没有 expires_at 或者 expires_at 是很远的未来)
const expiresAt = purchase.expires_at;
if (!expiresAt) {
// 没有过期时间,认为是终身购买
isLifetimeMember = true;
this.logger.log(`找到终身购买: ${purchase.product_id || 'unknown'}`);
break; // 找到终身购买就不需要继续了
} else {
// 有过期时间,比较是否比当前最晚的更晚
const expiration = new Date(expiresAt);
if (expiration > new Date()) {
if (!latestExpirationDate || expiration > latestExpirationDate) {
latestExpirationDate = expiration;
this.logger.log(`找到有效购买,过期时间: ${expiration.toISOString()}`);
}
}
}
}
}
}
// 获取用户当前数据
const user = await this.userModel.findByPk(userId);
if (!user) {
this.logger.warn(`用户 ${userId} 在数据库中不存在`);
return;
}
const currentExpiration = user.membershipExpiration;
// 决定最终的会员过期时间
let finalExpirationDate: Date | null = null;
if (isLifetimeMember) {
// 终身会员设置为100年后
finalExpirationDate = new Date();
finalExpirationDate.setFullYear(finalExpirationDate.getFullYear() + 100);
this.logger.log(`用户 ${userId} 是终身会员`);
} else if (latestExpirationDate) {
finalExpirationDate = latestExpirationDate;
}
// 比较并更新
if (finalExpirationDate) {
// RevenueCat 显示用户有有效的会员
const needsUpdate = !currentExpiration ||
Math.abs(finalExpirationDate.getTime() - currentExpiration.getTime()) > 60000; // 允许1分钟误差
if (needsUpdate) {
const oldExpiration = currentExpiration?.toISOString() || 'null';
user.membershipExpiration = finalExpirationDate;
await user.save();
this.winstonLogger.info('会员状态已同步更新', {
context: 'UsersService',
method: 'syncMembershipFromRevenueCat',
userId,
oldExpiration,
newExpiration: finalExpirationDate.toISOString(),
isLifetimeMember,
source: 'revenuecat_sync'
});
} else {
this.logger.log(`用户 ${userId} 会员状态一致,无需更新`);
}
} else {
// RevenueCat 显示没有有效会员
if (currentExpiration && currentExpiration > new Date()) {
// 但数据库显示会员未过期,可能需要人工确认
this.winstonLogger.warn('会员状态不一致:数据库显示有效但 RevenueCat 无有效权益', {
context: 'UsersService',
method: 'syncMembershipFromRevenueCat',
userId,
dbExpiration: currentExpiration.toISOString(),
subscriptionsCount: subscriptionsData?.items?.length || 0,
purchasesCount: purchasesData?.items?.length || 0
});
} else {
this.logger.log(`用户 ${userId} 在 RevenueCat 和数据库中均无有效会员`);
}
}
} catch (error) {
// 错误不应影响主流程,只记录日志
this.winstonLogger.error('同步会员状态失败', {
context: 'UsersService',
method: 'syncMembershipFromRevenueCat',
userId,
error: error instanceof Error ? error.message : '未知错误',
stack: error instanceof Error ? error.stack : undefined
});
}
}
/**
* 关联 RevenueCat 用户ID 与当前用户ID
*/
private async linkRevenueCatUser(oldRevenueCatUserId: string, newUserId: string): Promise<void> {
try {
const REVENUECAT_SECRET_API_KEY = process.env.REVENUECAT_SECRET_API_KEY;
if (!REVENUECAT_SECRET_API_KEY) {
this.logger.warn('RevenueCat Secret API key 未配置,跳过用户关联');
return;
}
// 调用 RevenueCat API 创建用户别名
const url = `https://api.revenuecat.com/v1/subscribers/${oldRevenueCatUserId}/alias`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${REVENUECAT_SECRET_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
new_app_user_id: newUserId
}),
});
if (response.ok) {
this.logger.log(`成功关联 RevenueCat 用户: ${oldRevenueCatUserId} -> ${newUserId}`);
} else {
this.logger.warn(`RevenueCat 用户关联失败: ${response.status} ${response.statusText}`);
}
} catch (error) {
this.logger.error(`关联 RevenueCat 用户失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 获取用户最近六个月的活跃情况
*/
async getUserActivityHistory(userId: string): Promise<GetUserActivityHistoryResponseDto> {
try {
const activityHistory = await this.userActivityService.getUserActivityHistory(userId);
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: activityHistory,
};
} catch (error) {
this.logger.error(`获取用户活跃历史失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取用户活跃历史失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: [],
};
}
}
/**
* 更新用户围度信息
*/
async updateBodyMeasurements(userId: string, measurements: any): Promise<{ code: any; message: string }> {
const transaction = await this.sequelize.transaction();
try {
// 获取或创建用户档案
const [profile] = await this.userProfileModel.findOrCreate({
where: { userId },
defaults: { userId },
transaction,
});
const updateFields: any = {};
const historyRecords: any[] = [];
// 映射字段名到围度类型
const fieldMappings = {
chestCircumference: BodyMeasurementType.ChestCircumference,
waistCircumference: BodyMeasurementType.WaistCircumference,
upperHipCircumference: BodyMeasurementType.UpperHipCircumference,
armCircumference: BodyMeasurementType.ArmCircumference,
thighCircumference: BodyMeasurementType.ThighCircumference,
calfCircumference: BodyMeasurementType.CalfCircumference,
};
// 处理每个传入的围度字段
for (const [fieldName, measurementType] of Object.entries(fieldMappings)) {
if (measurements[fieldName] !== undefined) {
const value = measurements[fieldName];
updateFields[fieldName] = value;
// 准备历史记录
historyRecords.push({
userId,
measurementType,
value,
source: MeasurementUpdateSource.Manual,
});
}
}
// 更新用户档案
if (Object.keys(updateFields).length > 0) {
await profile.update(updateFields, { transaction });
// 批量创建历史记录
if (historyRecords.length > 0) {
await this.userBodyMeasurementHistoryModel.bulkCreate(historyRecords, { transaction });
}
}
await transaction.commit();
this.logger.log(`用户 ${userId} 围度更新成功: ${JSON.stringify(updateFields)}`);
return {
code: ResponseCode.SUCCESS,
message: 'success',
};
} catch (error) {
await transaction.rollback();
this.logger.error(`更新用户围度失败: ${error instanceof Error ? error.message : '未知错误'}`);
throw new BadRequestException(`更新围度信息失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 获取用户围度历史记录
*/
async getBodyMeasurementHistory(userId: string, measurementType?: BodyMeasurementType): Promise<any> {
try {
const whereCondition: any = { userId };
if (measurementType) {
whereCondition.measurementType = measurementType;
}
const history = await this.userBodyMeasurementHistoryModel.findAll({
where: whereCondition,
order: [['createdAt', 'DESC']],
limit: 100, // 限制返回最近100条记录
});
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: history,
};
} catch (error) {
this.logger.error(`获取围度历史记录失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取围度历史记录失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: [],
};
}
}
/**
* 获取用户围度分析报表
*/
async getBodyMeasurementAnalysis(userId: string, period: 'week' | 'month' | 'year'): Promise<any> {
try {
const now = dayjs();
let startDate: Date;
let dataPoints: Date[] = [];
// 根据时间范围计算起始日期和数据点
switch (period) {
case 'week':
// 获取本周7天按中国习惯从周一开始
const startOfWeek = now.startOf('isoWeek'); // ISO周从周一开始
for (let i = 0; i < 7; i++) {
const date = startOfWeek.add(i, 'day');
dataPoints.push(date.toDate());
}
startDate = startOfWeek.toDate();
break;
case 'month':
// 获取本月的4周以周日结束
const startOfMonth = now.startOf('month');
// 本月1日作为起始日期
startDate = startOfMonth.toDate();
// 生成4个数据点每个代表一周以该周的周日为准
for (let i = 0; i < 4; i++) {
// 每周从本月1日开始算第i周取该周的周日
const weekStart = startOfMonth.add(i * 7, 'day');
const weekEnd = weekStart.endOf('week'); // dayjs默认周日结束
dataPoints.push(weekEnd.toDate());
}
break;
case 'year':
// 获取今年1月到12月每月取最后一天
const startOfYear = now.startOf('year'); // 今年1月1日
for (let i = 0; i < 12; i++) {
const date = startOfYear.add(i, 'month').endOf('month');
dataPoints.push(date.toDate());
}
startDate = startOfYear.toDate();
break;
}
// 获取从开始日期到数据点最大时间的所有围度数据
const maxDataPointDate = dataPoints.reduce((max, date) => date > max ? date : max, startDate);
const measurements = await this.userBodyMeasurementHistoryModel.findAll({
where: {
userId,
createdAt: {
[Op.gte]: startDate,
[Op.lte]: maxDataPointDate,
},
},
order: [['createdAt', 'ASC']],
});
// 初始化结果数组
const analysisData = dataPoints.map(date => {
const label = this.formatDateLabel(date, period);
return {
label,
chestCircumference: null,
waistCircumference: null,
upperHipCircumference: null,
armCircumference: null,
thighCircumference: null,
calfCircumference: null,
};
});
// 为每个数据点找到最新的围度数据
analysisData.forEach((point, index) => {
const targetDate = dataPoints[index];
// 为每种围度类型找到到目标日期为止的最新值
Object.values(BodyMeasurementType).forEach(measurementType => {
const relevantMeasurements = measurements.filter(m =>
m.measurementType === measurementType &&
new Date(m.createdAt) <= targetDate
);
if (relevantMeasurements.length > 0) {
// 取时间最新的记录(离目标日期最近且不超过目标日期的最新记录)
const latestMeasurement = relevantMeasurements
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
// 将数据库字段映射到响应字段
const fieldMapping = {
[BodyMeasurementType.ChestCircumference]: 'chestCircumference',
[BodyMeasurementType.WaistCircumference]: 'waistCircumference',
[BodyMeasurementType.UpperHipCircumference]: 'upperHipCircumference',
[BodyMeasurementType.ArmCircumference]: 'armCircumference',
[BodyMeasurementType.ThighCircumference]: 'thighCircumference',
[BodyMeasurementType.CalfCircumference]: 'calfCircumference',
};
const fieldName = fieldMapping[measurementType];
if (fieldName) {
point[fieldName] = latestMeasurement.value;
}
}
});
});
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: analysisData,
};
} catch (error) {
this.logger.error(`获取围度分析报表失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取围度分析报表失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: [],
};
}
}
/**
* 格式化日期标签
*/
private formatDateLabel(date: Date, period: 'week' | 'month' | 'year'): string {
const dayjsDate = dayjs(date);
switch (period) {
case 'week':
return dayjsDate.format('YYYY-MM-DD');
case 'month':
return dayjsDate.format('YYYY-MM-DD');
case 'year':
return dayjsDate.format('YYYY-MM');
default:
return dayjsDate.format('YYYY-MM-DD');
}
}
/**
* 自动分配会员编号
* @returns Promise<number> 新的会员编号
*/
private async assignMemberNumber(): Promise<number> {
try {
// 查找当前最大的会员编号
const result = await this.userModel.findOne({
attributes: [[this.sequelize.fn('MAX', this.sequelize.col('member_number')), 'maxMemberNumber']],
raw: true
});
const maxMember = result as unknown as { maxMemberNumber: number | null };
// 返回下一个编号如果没有现有编号则从1开始
const nextMemberNumber = (maxMember?.maxMemberNumber || 0) + 1;
this.logger.log(`分配新会员编号: ${nextMemberNumber}`);
return nextMemberNumber;
} catch (error) {
this.logger.error(`assignMemberNumber error: ${error instanceof Error ? error.message : String(error)}`);
// 如果查询失败,返回基于时间戳的编号作为备选
return Math.floor(Date.now() / 1000) % 100000;
}
}
/**
* 获取用户勋章列表
*/
async getUserBadges(userId: string): Promise<any> {
try {
const badges = await this.badgeService.getUserBadges(userId);
const total = badges.length;
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: {
badges,
total,
},
};
} catch (error) {
this.logger.error(`获取用户勋章列表失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取用户勋章列表失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: {
badges: [],
total: 0,
},
};
}
}
/**
* 获取所有可用勋章(包含用户是否已获得)
*/
async getAvailableBadges(userId?: string): Promise<any> {
try {
const badges = await this.badgeService.getAvailableBadges(userId);
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: badges,
};
} catch (error) {
this.logger.error(`获取可用勋章列表失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取可用勋章列表失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: [],
};
}
}
/**
* 检查应用版本更新
*/
async checkVersion(query: VersionCheckDto): Promise<VersionCheckResponseDto> {
try {
this.logger.log(`版本检查请求 - 当前版本: ${query.currentVersion}, 平台: ${query.platform}`);
const currentVersion = query.currentVersion
if (!currentVersion) {
this.logger.log('当前版本号为空,返回默认版本信息');
return {
code: ResponseCode.SUCCESS,
message: '当前版本号为空',
data: null as any,
};
}
// 从环境变量获取配置
const latestVersion = this.configService.get<string>('APP_VERSION', '1.0.0');
const appStoreUrl = this.configService.get<string>('APP_STORE_URL', '');
// 版本比较
const needsUpdate = this.compareVersions(latestVersion, currentVersion) > 0;
// 构建响应数据
const versionInfo: VersionInfo = {
latestVersion,
appStoreUrl,
needsUpdate: needsUpdate,
updateMessage: this.getUpdateMessage(needsUpdate),
releaseNotes: this.getReleaseNotes(latestVersion),
};
this.logger.log(`版本检查结果: ${JSON.stringify(versionInfo)}`);
return {
code: ResponseCode.SUCCESS,
message: '版本检查成功',
data: versionInfo,
};
} catch (error) {
this.logger.error(`版本检查失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `版本检查失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: null as any,
};
}
}
/**
* 比较两个语义化版本号
* @param version1 版本1
* @param version2 版本2
* @returns 1: version1 > version2, 0: version1 = version2, -1: version1 < version2
*/
private compareVersions(version1: string, version2: string): number {
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part) return 1;
if (v1Part < v2Part) return -1;
}
return 0;
}
/**
* 获取更新消息
*/
private getUpdateMessage(needsUpdate: boolean): string {
if (needsUpdate) {
return '发现新版本,建议更新到最新版本以获得更好的体验';
}
return '当前已是最新版本';
}
/**
* 获取版本发布说明
*/
private getReleaseNotes(version: string): string {
// 这里可以从数据库或配置文件中获取版本发布说明
// 暂时返回示例数据
return '1. 优化多语言配置\n2. 锻炼通知点击直接查看锻炼详情\n3. 修复已知问题';
}
/**
* 标记勋章已展示
*/
async markBadgeAsShown(userId: string, badgeCode: string): Promise<any> {
try {
const success = await this.badgeService.markBadgeAsShown(userId, badgeCode);
if (!success) {
return {
code: ResponseCode.ERROR,
message: '勋章不存在或标记失败',
data: { success: false },
};
}
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: { success: true },
};
} catch (error) {
this.logger.error(`标记勋章已展示失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `标记勋章已展示失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: { success: false },
};
}
}
// ==================== 每日健康数据相关方法 ====================
/**
* 获取用户指定日期的健康数据
* @param userId 用户ID
* @param date 日期,格式 YYYY-MM-DD默认为今天
* @returns 健康数据记录,如果不存在则返回 null
*/
async getDailyHealth(userId: string, date?: string): Promise<UserDailyHealth | null> {
const recordDate = date || dayjs().format('YYYY-MM-DD');
this.logger.log(`获取每日健康数据 - 用户ID: ${userId}, 日期: ${recordDate}`);
const record = await this.userDailyHealthModel.findOne({
where: { userId, recordDate },
});
return record;
}
/**
* 更新用户每日健康数据
* 每日每个用户只会生成一条数据,如果已存在则更新
*/
async updateDailyHealth(userId: string, updateDto: UpdateDailyHealthDto): Promise<UpdateDailyHealthResponseDto> {
try {
// 确定记录日期,默认为今天
const recordDate = updateDto.date || dayjs().format('YYYY-MM-DD');
this.logger.log(`更新每日健康数据 - 用户ID: ${userId}, 日期: ${recordDate}`);
// 准备更新字段
const updateFields: Partial<UserDailyHealth> = {};
if (updateDto.waterIntake !== undefined) updateFields.waterIntake = updateDto.waterIntake;
if (updateDto.exerciseMinutes !== undefined) updateFields.exerciseMinutes = updateDto.exerciseMinutes;
if (updateDto.caloriesBurned !== undefined) updateFields.caloriesBurned = updateDto.caloriesBurned;
if (updateDto.standingMinutes !== undefined) updateFields.standingMinutes = updateDto.standingMinutes;
if (updateDto.basalMetabolism !== undefined) updateFields.basalMetabolism = updateDto.basalMetabolism;
if (updateDto.sleepMinutes !== undefined) updateFields.sleepMinutes = updateDto.sleepMinutes;
if (updateDto.bloodOxygen !== undefined) updateFields.bloodOxygen = updateDto.bloodOxygen;
if (updateDto.stressLevel !== undefined) updateFields.stressLevel = Math.round(updateDto.stressLevel * 10) / 10; // 保留一位小数
if (updateDto.steps !== undefined) updateFields.steps = updateDto.steps;
// 使用 upsert 实现创建或更新
const [record, created] = await this.userDailyHealthModel.findOrCreate({
where: { userId, recordDate },
defaults: {
userId,
recordDate,
...updateFields,
},
});
// 如果记录已存在,则更新
if (!created && Object.keys(updateFields).length > 0) {
await record.update(updateFields);
}
this.logger.log(`每日健康数据${created ? '创建' : '更新'}成功 - 记录ID: ${record.id}`);
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: {
id: record.id,
userId: record.userId,
recordDate: record.recordDate,
waterIntake: record.waterIntake,
exerciseMinutes: record.exerciseMinutes,
caloriesBurned: record.caloriesBurned,
standingMinutes: record.standingMinutes,
basalMetabolism: record.basalMetabolism,
sleepMinutes: record.sleepMinutes,
bloodOxygen: record.bloodOxygen,
stressLevel: record.stressLevel,
steps: record.steps,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
},
};
} catch (error) {
this.logger.error(`更新每日健康数据失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `更新每日健康数据失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: null as any,
};
}
}
/**
* 获取用户健康邀请码
* 如果用户没有邀请码,则生成一个新的
*/
async getHealthInviteCode(userId: string): Promise<{ code: ResponseCode; message: string; data: { healthInviteCode: string } }> {
try {
const user = await this.userModel.findByPk(userId);
if (!user) {
return {
code: ResponseCode.ERROR,
message: '用户不存在',
data: { healthInviteCode: '' },
};
}
// 如果用户已有邀请码,直接返回
if (user.healthInviteCode) {
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: { healthInviteCode: user.healthInviteCode },
};
}
// 生成唯一的邀请码8位随机字母数字组合
const healthInviteCode = await this.generateUniqueInviteCode(8);
// 保存到数据库
user.healthInviteCode = healthInviteCode;
await user.save();
this.logger.log(`为用户 ${userId} 生成健康邀请码: ${healthInviteCode}`);
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: { healthInviteCode },
};
} catch (error) {
this.logger.error(`获取健康邀请码失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取健康邀请码失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: { healthInviteCode: '' },
};
}
}
/**
* 生成唯一的邀请码,确保不与数据库中已有的重复
*/
private async generateUniqueInviteCode(length: number, maxAttempts = 10): Promise<string> {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for (let attempt = 0; attempt < maxAttempts; attempt++) {
// 生成随机邀请码
let code = '';
for (let i = 0; i < length; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
// 检查是否已存在
const existing = await this.userModel.findOne({
where: { healthInviteCode: code },
});
if (!existing) {
return code;
}
this.logger.warn(`邀请码 ${code} 已存在,重新生成(第 ${attempt + 1} 次尝试)`);
}
// 如果多次尝试都失败,使用时间戳+随机数确保唯一性
const timestamp = Date.now().toString(36).toUpperCase();
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
return `${timestamp}${random}`.substring(0, length);
}
}