feat(version-service): add GET /api/app/version/check endpoint for Flutter app

Flutter VersionChecker was calling GET /api/app/version/check but this
endpoint didn't exist — only the admin CRUD /api/v1/versions was there.

New: AppVersionCheckController (@Controller('api/app/version'))
  GET /api/app/version/check?platform=android&current_version_code=N
  - Finds latest enabled version for the platform (highest buildNumber)
  - Returns { needUpdate: false } when already up to date
  - Returns full VersionInfo payload when update is available

Response fields match Flutter VersionInfo.fromJson exactly:
  needUpdate, version, versionCode, downloadUrl, fileSize,
  fileSizeFriendly (computed), sha256 (empty — not stored),
  forceUpdate, updateLog, releaseDate

Also: AppVersionRepository.findLatestEnabled(platform) — queries all
enabled versions for platform, picks the one with the highest buildNumber
(parsed as int, robust against varchar storage).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-06 05:24:04 -08:00
parent 95d678ad6b
commit 141c6e984d
3 changed files with 91 additions and 1 deletions

View File

@ -47,4 +47,23 @@ export class AppVersionRepository {
async delete(id: string): Promise<void> {
await this.repo.delete(id);
}
/** 查询指定平台最新启用版本buildNumber 最大的已启用版本) */
async findLatestEnabled(platform: Platform): Promise<AppVersion | null> {
const versions = await this.repo
.createQueryBuilder('v')
.where('v.platform = :platform', { platform })
.andWhere('v.isEnabled = true')
.orderBy('v.createdAt', 'DESC')
.getMany();
if (versions.length === 0) return null;
// 找 buildNumber整数最大的版本兼容字符串存储
return versions.reduce((best, cur) => {
const bestCode = parseInt(best.buildNumber, 10) || 0;
const curCode = parseInt(cur.buildNumber, 10) || 0;
return curCode > bestCode ? cur : best;
});
}
}

View File

@ -0,0 +1,70 @@
import { Controller, Get, Query } from '@nestjs/common';
import { AppVersionRepository } from '../../../infrastructure/repositories/app-version.repository';
import { Platform } from '../../../domain/entities/app-version.entity';
/**
* IT0 App
* GET /api/app/version/check
*
* :
* platform - (android | ios)
* current_version_code - (build number)
*
* Flutter VersionInfo.fromJson :
* needUpdate bool -
* version string - (versionName, e.g. "1.0.0")
* versionCode int - (buildNumber as int)
* downloadUrl string - APK
* fileSize int -
* fileSizeFriendly string - (e.g. "146.9 MB")
* sha256 string - SHA-256
* forceUpdate bool -
* updateLog string? -
* releaseDate string - ISO
*/
@Controller('api/app/version')
export class AppVersionCheckController {
constructor(private readonly versionRepo: AppVersionRepository) {}
@Get('check')
async check(
@Query('platform') platform: string,
@Query('current_version_code') currentVersionCode?: string,
) {
const platformEnum = ((platform || 'android').toUpperCase()) as Platform;
const latest = await this.versionRepo.findLatestEnabled(platformEnum);
if (!latest) {
return { needUpdate: false };
}
const latestCode = parseInt(latest.buildNumber, 10) || 0;
const currentCode = parseInt(currentVersionCode || '0', 10) || 0;
if (latestCode <= currentCode) {
return { needUpdate: false };
}
const fileSizeNum = Number(latest.fileSize) || 0;
return {
needUpdate: true,
version: latest.versionName,
versionCode: latestCode,
downloadUrl: latest.downloadUrl || '',
fileSize: fileSizeNum,
fileSizeFriendly: formatFileSize(fileSizeNum),
sha256: '', // version-service 不存储 sha256留空
forceUpdate: latest.isForceUpdate,
updateLog: latest.changelog ?? null,
releaseDate: (latest.releaseDate ?? latest.createdAt).toISOString(),
};
}
}
function formatFileSize(bytes: number): string {
if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${bytes} B`;
}

View File

@ -5,6 +5,7 @@ import { DatabaseModule } from '@it0/database';
import { AppVersion } from './domain/entities/app-version.entity';
import { AppVersionRepository } from './infrastructure/repositories/app-version.repository';
import { VersionController } from './interfaces/rest/controllers/version.controller';
import { AppVersionCheckController } from './interfaces/rest/controllers/app-version-check.controller';
@Module({
imports: [
@ -12,7 +13,7 @@ import { VersionController } from './interfaces/rest/controllers/version.control
DatabaseModule.forRoot(),
TypeOrmModule.forFeature([AppVersion]),
],
controllers: [VersionController],
controllers: [VersionController, AppVersionCheckController],
providers: [AppVersionRepository],
})
export class VersionModule {}