diff --git a/src/common/decorators/app-version.decorator.ts b/src/common/decorators/app-version.decorator.ts new file mode 100644 index 0000000..6c2eece --- /dev/null +++ b/src/common/decorators/app-version.decorator.ts @@ -0,0 +1,12 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +/** + * 从请求头中获取应用版本号的装饰器 + * 从 x-App-Version 请求头中提取版本信息 + */ +export const AppVersion = createParamDecorator( + (data: unknown, ctx: ExecutionContext): string | undefined => { + const request = ctx.switchToHttp().getRequest(); + return request.headers['x-app-version']; + }, +); \ No newline at end of file diff --git a/src/users/dto/version-check.dto.ts b/src/users/dto/version-check.dto.ts new file mode 100644 index 0000000..8d9248e --- /dev/null +++ b/src/users/dto/version-check.dto.ts @@ -0,0 +1,68 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsOptional, IsEnum } from 'class-validator'; +import { ResponseCode } from 'src/base.dto'; + +/** + * 版本检查请求DTO + */ +export class VersionCheckDto { + @ApiProperty({ + description: '当前应用版本号(从请求头 x-App-Version 获取)', + example: '1.0.0', + required: false, + }) + @IsString() + @IsOptional() + currentVersion?: string; + + @ApiProperty({ + description: '设备平台', + example: 'ios', + enum: ['ios', 'android'], + required: false, + }) + @IsString() + @IsOptional() + @IsEnum(['ios', 'android']) + platform?: string; +} + +/** + * 版本信息接口 + */ +export interface VersionInfo { + latestVersion: string; + appStoreUrl: string; + needsUpdate: boolean; + updateMessage?: string; + releaseNotes?: string; +} + +/** + * 版本检查响应DTO + */ +export class VersionCheckResponseDto { + @ApiProperty({ + description: '响应代码', + example: ResponseCode.SUCCESS, + }) + code: ResponseCode; + + @ApiProperty({ + description: '响应消息', + example: '版本检查成功', + }) + message: string; + + @ApiProperty({ + description: '版本信息', + example: { + latestVersion: '1.2.0', + appStoreUrl: 'https://apps.apple.com/app/your-app-id', + needsUpdate: true, + updateMessage: '发现新版本,建议更新到最新版本以获得更好的体验', + releaseNotes: '1. 新增AI健康教练功能\n2. 优化用户体验\n3. 修复已知问题' + }, + }) + data: VersionInfo; +} \ No newline at end of file diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 2a5dadc..82af6a2 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -32,13 +32,15 @@ import { DeleteAccountDto, DeleteAccountResponseDto } from './dto/delete-account import { GuestLoginDto, GuestLoginResponseDto, RefreshGuestTokenDto, RefreshGuestTokenResponseDto } from './dto/guest-login.dto'; import { AppStoreServerNotificationDto, ProcessNotificationResponseDto } from './dto/app-store-notification.dto'; import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.dto'; +import { VersionCheckDto, VersionCheckResponseDto } from './dto/version-check.dto'; import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto'; import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.dto'; -import { UpdateBodyMeasurementDto, UpdateBodyMeasurementResponseDto, GetBodyMeasurementHistoryResponseDto, GetBodyMeasurementAnalysisDto, GetBodyMeasurementAnalysisResponseDto } from './dto/body-measurement.dto'; +import { UpdateBodyMeasurementDto, UpdateBodyMeasurementResponseDto, GetBodyMeasurementHistoryResponseDto, GetBodyMeasurementAnalysisResponseDto } from './dto/body-measurement.dto'; import { GetUserBadgesResponseDto, GetAvailableBadgesResponseDto, MarkBadgeShownDto, MarkBadgeShownResponseDto } from './dto/badge.dto'; import { Public } from '../common/decorators/public.decorator'; import { CurrentUser } from '../common/decorators/current-user.decorator'; +import { AppVersion } from '../common/decorators/app-version.decorator'; import { AccessTokenPayload } from './services/apple-auth.service'; import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard'; import { ResponseCode } from 'src/base.dto'; @@ -444,4 +446,30 @@ export class UsersController { return this.usersService.markBadgeAsShown(user.sub, markBadgeShownDto.badgeCode); } + // ==================== 版本检查相关接口 ==================== + + /** + * 检查应用版本更新 + */ + @Public() + @Get('version-check') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '检查应用版本更新' }) + @ApiQuery({ name: 'platform', required: false, description: '设备平台', enum: ['ios', 'android'] }) + @ApiResponse({ status: 200, description: '成功获取版本信息', type: VersionCheckResponseDto }) + async checkVersion( + @AppVersion() appVersion: string | undefined, + @Query('platform') platform?: string, + ): Promise { + this.logger.log(`版本检查请求 - 当前版本: ${appVersion}, 平台: ${platform}`); + + // 构造查询对象,保持与原有服务的兼容性 + const query: VersionCheckDto = { + currentVersion: appVersion, + platform: platform, + }; + + return this.usersService.checkVersion(query); + } + } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 0c2b62f..0564f69 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -15,6 +15,8 @@ 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'; @@ -82,6 +84,7 @@ export class UsersService { private readonly activityLogsService: ActivityLogsService, private readonly userActivityService: UserActivityService, private readonly badgeService: BadgeService, + private readonly configService: ConfigService, ) { } async getProfile(user: AccessTokenPayload): Promise { @@ -2868,6 +2871,98 @@ export class UsersService { } } + /** + * 检查应用版本更新 + */ + 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. 修复已知问题'; + } + /** * 标记勋章已展示 */