feat(users): add version checking endpoint
Add app version checking functionality to notify users when updates are available. The feature extracts the current version from the x-App-Version header, compares it with the latest configured version, and returns update information including download links and release notes.
This commit is contained in:
12
src/common/decorators/app-version.decorator.ts
Normal file
12
src/common/decorators/app-version.decorator.ts
Normal file
@@ -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'];
|
||||||
|
},
|
||||||
|
);
|
||||||
68
src/users/dto/version-check.dto.ts
Normal file
68
src/users/dto/version-check.dto.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -32,13 +32,15 @@ import { DeleteAccountDto, DeleteAccountResponseDto } from './dto/delete-account
|
|||||||
import { GuestLoginDto, GuestLoginResponseDto, RefreshGuestTokenDto, RefreshGuestTokenResponseDto } from './dto/guest-login.dto';
|
import { GuestLoginDto, GuestLoginResponseDto, RefreshGuestTokenDto, RefreshGuestTokenResponseDto } from './dto/guest-login.dto';
|
||||||
import { AppStoreServerNotificationDto, ProcessNotificationResponseDto } from './dto/app-store-notification.dto';
|
import { AppStoreServerNotificationDto, ProcessNotificationResponseDto } from './dto/app-store-notification.dto';
|
||||||
import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.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 { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
|
||||||
import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.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 { GetUserBadgesResponseDto, GetAvailableBadgesResponseDto, MarkBadgeShownDto, MarkBadgeShownResponseDto } from './dto/badge.dto';
|
||||||
|
|
||||||
import { Public } from '../common/decorators/public.decorator';
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
import { CurrentUser } from '../common/decorators/current-user.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 { AccessTokenPayload } from './services/apple-auth.service';
|
||||||
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/common/guards/jwt-auth.guard';
|
||||||
import { ResponseCode } from 'src/base.dto';
|
import { ResponseCode } from 'src/base.dto';
|
||||||
@@ -444,4 +446,30 @@ export class UsersController {
|
|||||||
return this.usersService.markBadgeAsShown(user.sub, markBadgeShownDto.badgeCode);
|
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<VersionCheckResponseDto> {
|
||||||
|
this.logger.log(`版本检查请求 - 当前版本: ${appVersion}, 平台: ${platform}`);
|
||||||
|
|
||||||
|
// 构造查询对象,保持与原有服务的兼容性
|
||||||
|
const query: VersionCheckDto = {
|
||||||
|
currentVersion: appVersion,
|
||||||
|
platform: platform,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.usersService.checkVersion(query);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { ResponseCode } from 'src/base.dto';
|
|||||||
import { Transaction, Op } from 'sequelize';
|
import { Transaction, Op } from 'sequelize';
|
||||||
import { Sequelize } from 'sequelize-typescript';
|
import { Sequelize } from 'sequelize-typescript';
|
||||||
import { UpdateUserDto, UpdateUserResponseDto } from './dto/update-user.dto';
|
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 { UserPurchase, PurchaseType, PurchaseStatus, PurchasePlatform } from './models/user-purchase.model';
|
||||||
import { ApplePurchaseService } from './services/apple-purchase.service';
|
import { ApplePurchaseService } from './services/apple-purchase.service';
|
||||||
@@ -82,6 +84,7 @@ export class UsersService {
|
|||||||
private readonly activityLogsService: ActivityLogsService,
|
private readonly activityLogsService: ActivityLogsService,
|
||||||
private readonly userActivityService: UserActivityService,
|
private readonly userActivityService: UserActivityService,
|
||||||
private readonly badgeService: BadgeService,
|
private readonly badgeService: BadgeService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async getProfile(user: AccessTokenPayload): Promise<UserResponseDto> {
|
async getProfile(user: AccessTokenPayload): Promise<UserResponseDto> {
|
||||||
@@ -2868,6 +2871,98 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查应用版本更新
|
||||||
|
*/
|
||||||
|
async checkVersion(query: VersionCheckDto): Promise<VersionCheckResponseDto> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`版本检查请求 - 当前版本: ${query.currentVersion}, 平台: ${query.platform}`);
|
||||||
|
|
||||||
|
const currentVersion = query.currentVersion
|
||||||
|
|
||||||
|
if (!currentVersion) {
|
||||||
|
this.logger.log('当前版本号为空,返回默认版本信息');
|
||||||
|
return {
|
||||||
|
code: ResponseCode.SUCCESS,
|
||||||
|
message: '当前版本号为空',
|
||||||
|
data: null as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 从环境变量获取配置
|
||||||
|
const latestVersion = this.configService.get<string>('APP_VERSION', '1.0.0');
|
||||||
|
const appStoreUrl = this.configService.get<string>('APP_STORE_URL', '');
|
||||||
|
|
||||||
|
// 版本比较
|
||||||
|
const needsUpdate = this.compareVersions(latestVersion, currentVersion) > 0;
|
||||||
|
|
||||||
|
// 构建响应数据
|
||||||
|
const versionInfo: VersionInfo = {
|
||||||
|
latestVersion,
|
||||||
|
appStoreUrl,
|
||||||
|
needsUpdate: needsUpdate,
|
||||||
|
updateMessage: this.getUpdateMessage(needsUpdate),
|
||||||
|
releaseNotes: this.getReleaseNotes(latestVersion),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.log(`版本检查结果: ${JSON.stringify(versionInfo)}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: ResponseCode.SUCCESS,
|
||||||
|
message: '版本检查成功',
|
||||||
|
data: versionInfo,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`版本检查失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
|
return {
|
||||||
|
code: ResponseCode.ERROR,
|
||||||
|
message: `版本检查失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||||
|
data: null as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较两个语义化版本号
|
||||||
|
* @param version1 版本1
|
||||||
|
* @param version2 版本2
|
||||||
|
* @returns 1: version1 > version2, 0: version1 = version2, -1: version1 < version2
|
||||||
|
*/
|
||||||
|
private compareVersions(version1: string, version2: string): number {
|
||||||
|
const v1Parts = version1.split('.').map(Number);
|
||||||
|
const v2Parts = version2.split('.').map(Number);
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
|
||||||
|
const v1Part = v1Parts[i] || 0;
|
||||||
|
const v2Part = v2Parts[i] || 0;
|
||||||
|
|
||||||
|
if (v1Part > v2Part) return 1;
|
||||||
|
if (v1Part < v2Part) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取更新消息
|
||||||
|
*/
|
||||||
|
private getUpdateMessage(needsUpdate: boolean): string {
|
||||||
|
if (needsUpdate) {
|
||||||
|
return '发现新版本,建议更新到最新版本以获得更好的体验';
|
||||||
|
}
|
||||||
|
return '当前已是最新版本';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取版本发布说明
|
||||||
|
*/
|
||||||
|
private getReleaseNotes(version: string): string {
|
||||||
|
// 这里可以从数据库或配置文件中获取版本发布说明
|
||||||
|
// 暂时返回示例数据
|
||||||
|
return '1. 优化多语言配置\n2. 锻炼通知点击直接查看锻炼详情\n3. 修复已知问题';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 标记勋章已展示
|
* 标记勋章已展示
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user