diff --git a/backend/services/admin-service/src/application/services/file-storage.service.ts b/backend/services/admin-service/src/application/services/file-storage.service.ts index e5a3bce..170fc00 100644 --- a/backend/services/admin-service/src/application/services/file-storage.service.ts +++ b/backend/services/admin-service/src/application/services/file-storage.service.ts @@ -56,8 +56,22 @@ export class FileStorageService { }; } - /** Generate presigned URL for download */ - async getDownloadUrl(objectName: string): Promise { - return this.minio.presignedGetObject(BUCKET, objectName, 24 * 3600); + /** Stream file from MinIO to a writable (e.g. Express Response) */ + async streamFile(objectName: string, destination: NodeJS.WritableStream): Promise { + const stream = await this.minio.getObject(BUCKET, objectName); + await new Promise((resolve, reject) => { + stream.pipe(destination as any); + stream.on('end', resolve); + stream.on('error', reject); + }); + } + + /** Get file metadata (size) from MinIO */ + async statFile(objectName: string): Promise<{ size: number; contentType: string }> { + const stat = await this.minio.statObject(BUCKET, objectName); + return { + size: stat.size, + contentType: (stat.metaData as any)?.['content-type'] || 'application/octet-stream', + }; } } diff --git a/backend/services/admin-service/src/interface/http/controllers/app-version.controller.ts b/backend/services/admin-service/src/interface/http/controllers/app-version.controller.ts index 8edcc9d..0b4615f 100644 --- a/backend/services/admin-service/src/interface/http/controllers/app-version.controller.ts +++ b/backend/services/admin-service/src/interface/http/controllers/app-version.controller.ts @@ -35,13 +35,19 @@ export class AppVersionController { } @Get('download/:id') - @ApiOperation({ summary: 'Download app package (regenerates fresh presigned URL each request)' }) + @ApiOperation({ summary: 'Download app package — streams through service (MinIO not directly exposed)' }) async downloadVersion(@Param('id') id: string, @Res() res: Response) { const version = await this.versionService.getVersion(id); if (version.storageKey) { - // Generate a fresh 1-hour presigned URL per request — no expiry issue - const freshUrl = await this.fileStorage.getDownloadUrl(version.storageKey); - return res.redirect(302, freshUrl); + // Stream from MinIO through this service — MinIO is not publicly accessible + const stat = await this.fileStorage.statFile(version.storageKey); + const ext = version.storageKey.split('.').pop() || 'apk'; + const filename = `app-${version.versionName}.${ext}`; + res.setHeader('Content-Type', stat.contentType); + res.setHeader('Content-Length', stat.size); + res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); + await this.fileStorage.streamFile(version.storageKey, res); + return; } return res.redirect(302, version.downloadUrl); } diff --git a/frontend/genex-mobile/lib/core/updater/models/version_info.dart b/frontend/genex-mobile/lib/core/updater/models/version_info.dart index 10f7c93..64306f8 100644 --- a/frontend/genex-mobile/lib/core/updater/models/version_info.dart +++ b/frontend/genex-mobile/lib/core/updater/models/version_info.dart @@ -24,8 +24,8 @@ class VersionInfo { /// 更新日志 final String? updateLog; - /// 发布时间 - final DateTime releaseDate; + /// 发布时间(可为空,后台未设置时为 null) + final DateTime? releaseDate; const VersionInfo({ required this.version, @@ -36,7 +36,7 @@ class VersionInfo { required this.sha256, this.forceUpdate = false, this.updateLog, - required this.releaseDate, + this.releaseDate, }); factory VersionInfo.fromJson(Map json) { @@ -49,7 +49,9 @@ class VersionInfo { sha256: json['sha256'] as String, forceUpdate: json['forceUpdate'] as bool? ?? false, updateLog: json['updateLog'] as String?, - releaseDate: DateTime.parse(json['releaseDate'] as String), + releaseDate: json['releaseDate'] != null + ? DateTime.parse(json['releaseDate'] as String) + : null, ); } @@ -63,7 +65,7 @@ class VersionInfo { 'sha256': sha256, 'forceUpdate': forceUpdate, 'updateLog': updateLog, - 'releaseDate': releaseDate.toIso8601String(), + 'releaseDate': releaseDate?.toIso8601String(), }; } diff --git a/frontend/genex-mobile/lib/core/updater/version_checker.dart b/frontend/genex-mobile/lib/core/updater/version_checker.dart index 6f2ebfa..d6998c0 100644 --- a/frontend/genex-mobile/lib/core/updater/version_checker.dart +++ b/frontend/genex-mobile/lib/core/updater/version_checker.dart @@ -30,6 +30,7 @@ class VersionChecker { final response = await _dio.get( '/api/v1/app/version/check', queryParameters: { + 'app_type': 'GENEX_MOBILE', 'platform': 'android', 'current_version': currentInfo.version, 'current_version_code': currentInfo.buildNumber, @@ -37,16 +38,23 @@ class VersionChecker { ); if (response.statusCode == 200 && response.data != null) { - final data = response.data is Map - ? response.data as Map - : (response.data['data'] as Map?) ?? response.data; + // API wraps response: {"code":0,"data":{...}} — extract the inner object + final responseMap = response.data as Map; + final data = (responseMap['data'] as Map?) ?? responseMap; - final needUpdate = data['needUpdate'] as bool? ?? true; + final needUpdate = data['needUpdate'] as bool? ?? false; if (!needUpdate) { debugPrint('[VersionChecker] 无需更新'); return null; } - return VersionInfo.fromJson(data); + + // Fix relative downloadUrl → prepend apiBaseUrl for DownloadManager + final rawUrl = data['downloadUrl'] as String? ?? ''; + final fixedData = (rawUrl.isNotEmpty && !rawUrl.startsWith('http')) + ? (Map.from(data)..['downloadUrl'] = '$apiBaseUrl$rawUrl') + : data; + + return VersionInfo.fromJson(fixedData); } return null; } catch (e) {