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:
parent
50d6c77dfd
commit
c6c434a07a
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue