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 = 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 { 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 { 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 { 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 { 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 { 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 = {}; const userChanges: Record = {}; 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { // 输入验证 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 新的会员编号 */ private async assignMemberNumber(): Promise { 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 { 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 { 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 { 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('APP_VERSION', '1.0.0'); const appStoreUrl = this.configService.get('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 { 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 { 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 { try { // 确定记录日期,默认为今天 const recordDate = updateDto.date || dayjs().format('YYYY-MM-DD'); this.logger.log(`更新每日健康数据 - 用户ID: ${userId}, 日期: ${recordDate}`); // 准备更新字段 const updateFields: Partial = {}; 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 { 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); } }