Files
plates-server/src/users/users.service.ts
2025-09-24 18:03:32 +08:00

2533 lines
82 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 { 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 { 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';
const DEFAULT_FREE_USAGE_COUNT = 5;
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
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,
@InjectConnection()
private sequelize: Sequelize,
private readonly activityLogsService: ActivityLogsService,
private readonly userActivityService: UserActivityService,
) { }
async getProfile(user: AccessTokenPayload): Promise<UserResponseDto> {
try {
// 使用NestJS Logger (会通过winston输出)
this.logger.log(`getProfile: ${JSON.stringify(user)}`);
// 也可以直接使用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,
};
}
const [profile] = await this.userProfileModel.findOrCreate({
where: { userId: existingUser.id },
defaults: { userId: existingUser.id },
});
// 检查并记录今日登录活跃
await this.userActivityService.checkAndRecordTodayLogin(existingUser.id);
const returnData = {
...existingUser.toJSON(),
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
isVip: existingUser.isVip,
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 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}的用户不存在`);
}
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, 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;
}
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] || '用户';
const userEmail = appleLoginDto.email || applePayload.email || '';
if (!userEmail) {
throw new BadRequestException('无法获取用户邮箱信息');
}
user = await this.userModel.create({
id: userId,
name: userName,
mail: userEmail,
gender: Gender.MALE,
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) {
// 创建新的游客用户
user = await this.userModel.create({
id: guestUserId,
name: `游客_${deviceName}`,
gender: Gender.MALE, // 默认性别
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 获取用户信息
*/
private async getRevenueCatCustomerInfo(userId: string): Promise<any> {
try {
const REVENUECAT_PUBLIC_API_KEY = process.env.REVENUECAT_PUBLIC_API_KEY;
const REVENUECAT_APP_USER_ID = userId;
if (!REVENUECAT_PUBLIC_API_KEY) {
throw new Error('RevenueCat API key 未配置');
}
// RevenueCat REST API v1 endpoint
const url = `https://api.revenuecat.com/v2/subscribers/${REVENUECAT_APP_USER_ID}`;
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 中未找到用户: ${userId}`);
return null;
}
throw new Error(`RevenueCat API 请求失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
this.logger.log(`RevenueCat API 响应: ${JSON.stringify(data)}`);
return data;
} catch (error) {
this.logger.error(`调用 RevenueCat API 失败: ${error instanceof Error ? error.message : '未知错误'}`);
throw error;
}
}
/**
* 关联 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');
}
}
}