fix(update): 修复 app 版本更新检查路径 + 解决 MinIO presigned URL 24h 过期

Mobile 端:
- version_checker.dart: /api/app/... → /api/v1/app/... (与 Kong 路由匹配)

Backend (admin-service):
- AppVersion 实体新增 storage_key 字段(已执行 ALTER TABLE)
- FileStorageService: uploadFile 不再返回 presigned URL,只返回 objectName
- AdminVersionController: upload 后保存 storageKey,downloadUrl 设为
  /api/v1/app/version/download/{id}(稳定 API 地址,不过期)
- AppVersionController.downloadVersion: storageKey 存在时每次请求动态
  生成 presigned URL(1小时有效,只需够下载完成即可)
- AppVersionService.checkUpdate: 有 storageKey 的版本统一返回 API 下载地址

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-03 21:13:13 -08:00
parent 50d6c77dfd
commit c6c434a07a
6 changed files with 23 additions and 14 deletions

View File

@ -24,7 +24,10 @@ export class AppVersionService {
forceUpdate: latest.isForceUpdate && latest.isEnabled, forceUpdate: latest.isForceUpdate && latest.isEnabled,
version: latest.versionName, version: latest.versionName,
versionCode: latest.versionCode, versionCode: latest.versionCode,
downloadUrl: latest.downloadUrl, // Use stable API download endpoint; storageKey versions generate fresh presigned URL on download
downloadUrl: latest.storageKey
? `/api/v1/app/version/download/${latest.id}`
: latest.downloadUrl,
fileSize: Number(latest.fileSize), fileSize: Number(latest.fileSize),
fileSizeFriendly: this.formatFileSize(BigInt(latest.fileSize)), fileSizeFriendly: this.formatFileSize(BigInt(latest.fileSize)),
sha256: latest.fileSha256, sha256: latest.fileSha256,
@ -54,6 +57,7 @@ export class AppVersionService {
versionName: string; versionName: string;
buildNumber: string; buildNumber: string;
downloadUrl: string; downloadUrl: string;
storageKey?: string;
fileSize: string; fileSize: string;
fileSha256: string; fileSha256: string;
changelog: string; changelog: string;

View File

@ -49,16 +49,8 @@ export class FileStorageService {
: 'application/octet-stream', : 'application/octet-stream',
}); });
// Generate presigned download URL (24h)
const downloadUrl = await this.minio.presignedGetObject(
BUCKET,
objectName,
24 * 3600,
);
return { return {
objectName, objectName,
downloadUrl,
fileSize: buffer.length.toString(), fileSize: buffer.length.toString(),
sha256, sha256,
}; };

View File

@ -29,6 +29,9 @@ export class AppVersion {
@Column({ name: 'download_url', type: 'text' }) @Column({ name: 'download_url', type: 'text' })
downloadUrl: string; downloadUrl: string;
@Column({ name: 'storage_key', type: 'text', nullable: true })
storageKey: string | null;
@Column({ name: 'file_size', type: 'bigint', default: 0 }) @Column({ name: 'file_size', type: 'bigint', default: 0 })
fileSize: string; // bigint as string in TypeORM fileSize: string; // bigint as string in TypeORM

View File

@ -128,7 +128,8 @@ export class AdminVersionController {
versionCode, versionCode,
versionName, versionName,
buildNumber, buildNumber,
downloadUrl: uploadResult.downloadUrl, storageKey: uploadResult.objectName,
downloadUrl: '', // will be updated after id is known
fileSize: uploadResult.fileSize, fileSize: uploadResult.fileSize,
fileSha256: uploadResult.sha256, fileSha256: uploadResult.sha256,
changelog: body.changelog || '', changelog: body.changelog || '',
@ -138,7 +139,12 @@ export class AdminVersionController {
createdBy: req.user?.sub, createdBy: req.user?.sub,
}); });
return { code: 0, data: version }; // Set stable download URL pointing to this service's download endpoint
const updated = await this.versionService.updateVersion(version.id, {
downloadUrl: `/api/v1/app/version/download/${version.id}`,
});
return { code: 0, data: updated };
} }
@Post('parse') @Post('parse')

View File

@ -35,10 +35,14 @@ export class AppVersionController {
} }
@Get('download/:id') @Get('download/:id')
@ApiOperation({ summary: 'Download app package' }) @ApiOperation({ summary: 'Download app package (regenerates fresh presigned URL each request)' })
async downloadVersion(@Param('id') id: string, @Res() res: Response) { async downloadVersion(@Param('id') id: string, @Res() res: Response) {
const version = await this.versionService.getVersion(id); const version = await this.versionService.getVersion(id);
// Redirect to the download URL (presigned MinIO URL or external URL) 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);
}
return res.redirect(302, version.downloadUrl); return res.redirect(302, version.downloadUrl);
} }
} }

View File

@ -28,7 +28,7 @@ class VersionChecker {
debugPrint('[VersionChecker] 当前版本: ${currentInfo.version}, buildNumber: ${currentInfo.buildNumber}'); debugPrint('[VersionChecker] 当前版本: ${currentInfo.version}, buildNumber: ${currentInfo.buildNumber}');
final response = await _dio.get( final response = await _dio.get(
'/api/app/version/check', '/api/v1/app/version/check',
queryParameters: { queryParameters: {
'platform': 'android', 'platform': 'android',
'current_version': currentInfo.version, 'current_version': currentInfo.version,