diff --git a/backend/services/mining-admin-service/src/api/api.module.ts b/backend/services/mining-admin-service/src/api/api.module.ts index 25c32319..9ea32dcd 100644 --- a/backend/services/mining-admin-service/src/api/api.module.ts +++ b/backend/services/mining-admin-service/src/api/api.module.ts @@ -13,6 +13,7 @@ import { ManualMiningController } from './controllers/manual-mining.controller'; import { PendingContributionsController } from './controllers/pending-contributions.controller'; import { BatchMiningController } from './controllers/batch-mining.controller'; import { VersionController } from './controllers/version.controller'; +import { UpgradeVersionController } from './controllers/upgrade-version.controller'; import { MobileVersionController } from './controllers/mobile-version.controller'; @Module({ @@ -37,6 +38,7 @@ import { MobileVersionController } from './controllers/mobile-version.controller PendingContributionsController, BatchMiningController, VersionController, + UpgradeVersionController, MobileVersionController, ], }) diff --git a/backend/services/mining-admin-service/src/api/controllers/upgrade-version.controller.ts b/backend/services/mining-admin-service/src/api/controllers/upgrade-version.controller.ts new file mode 100644 index 00000000..3da3b14c --- /dev/null +++ b/backend/services/mining-admin-service/src/api/controllers/upgrade-version.controller.ts @@ -0,0 +1,240 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Patch, + Body, + Param, + Query, + HttpCode, + HttpStatus, + UseInterceptors, + UploadedFile, + ParseFilePipe, + MaxFileSizeValidator, + BadRequestException, +} from '@nestjs/common' +import { FileInterceptor } from '@nestjs/platform-express' +import { ApiTags, ApiOperation, ApiResponse, ApiQuery, ApiConsumes, ApiBody } from '@nestjs/swagger' +import { Public } from '../../shared/guards/admin-auth.guard' +import { VersionService } from '../../application/services/version.service' +import { Platform, AppVersion } from '../../domain/version-management' +import { + CreateVersionDto, + UploadVersionDto, + UpdateVersionDto, + ToggleVersionDto, + VersionResponseDto, +} from '../dto/version' + +const MAX_FILE_SIZE = 500 * 1024 * 1024 + +/** + * 公开版本管理接口 - 供 mobile-upgrade 前端使用(无需认证) + * + * 与 VersionController 共用 VersionService,但路径为 /api/v2/upgrade-versions + * 不影响需认证的 /api/v2/versions 接口 + */ +@Public() +@ApiTags('Upgrade Version Management (Public)') +@Controller('upgrade-versions') +export class UpgradeVersionController { + constructor(private readonly versionService: VersionService) {} + + private toVersionDto(version: AppVersion): VersionResponseDto { + return { + id: version.id, + platform: version.platform, + versionCode: version.versionCode.value, + versionName: version.versionName.value, + buildNumber: version.buildNumber.value, + downloadUrl: version.downloadUrl.value, + fileSize: version.fileSize.bytes.toString(), + fileSha256: version.fileSha256.value, + changelog: version.changelog.value, + isForceUpdate: version.isForceUpdate, + isEnabled: version.isEnabled, + minOsVersion: version.minOsVersion?.value ?? null, + releaseDate: version.releaseDate, + createdAt: version.createdAt, + updatedAt: version.updatedAt, + } + } + + @Get() + @ApiOperation({ summary: '获取版本列表' }) + @ApiQuery({ name: 'platform', required: false, enum: ['ANDROID', 'IOS', 'android', 'ios'] }) + @ApiQuery({ name: 'includeDisabled', required: false, type: Boolean }) + @ApiResponse({ status: 200, type: [VersionResponseDto] }) + async listVersions( + @Query('platform') platform?: string, + @Query('includeDisabled') includeDisabled?: string, + ): Promise { + const platformEnum = platform ? (platform.toUpperCase() as Platform) : undefined + const versions = await this.versionService.findAll(platformEnum, includeDisabled === 'true') + return versions.map((v) => this.toVersionDto(v)) + } + + @Get(':id') + @ApiOperation({ summary: '获取版本详情' }) + @ApiResponse({ status: 200, type: VersionResponseDto }) + @ApiResponse({ status: 404, description: '版本不存在' }) + async getVersion(@Param('id') id: string): Promise { + const version = await this.versionService.findById(id) + return this.toVersionDto(version) + } + + @Post() + @ApiOperation({ summary: '创建新版本' }) + @ApiResponse({ status: 201, type: VersionResponseDto }) + async createVersion(@Body() dto: CreateVersionDto): Promise { + const version = await this.versionService.create({ + ...dto, + releaseDate: dto.releaseDate ? new Date(dto.releaseDate) : undefined, + createdBy: 'upgrade-admin', + }) + return this.toVersionDto(version) + } + + @Put(':id') + @ApiOperation({ summary: '更新版本' }) + @ApiResponse({ status: 200, type: VersionResponseDto }) + @ApiResponse({ status: 404, description: '版本不存在' }) + async updateVersion( + @Param('id') id: string, + @Body() dto: UpdateVersionDto, + ): Promise { + const version = await this.versionService.update(id, { + ...dto, + releaseDate: dto.releaseDate ? new Date(dto.releaseDate) : dto.releaseDate === null ? null : undefined, + updatedBy: 'upgrade-admin', + }) + return this.toVersionDto(version) + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: '删除版本' }) + @ApiResponse({ status: 204, description: '删除成功' }) + @ApiResponse({ status: 404, description: '版本不存在' }) + async deleteVersion(@Param('id') id: string): Promise { + await this.versionService.delete(id) + } + + @Patch(':id/toggle') + @ApiOperation({ summary: '启用/禁用版本' }) + @ApiResponse({ status: 200, description: '操作成功' }) + @ApiResponse({ status: 404, description: '版本不存在' }) + async toggleVersion( + @Param('id') id: string, + @Body() dto: ToggleVersionDto, + ): Promise<{ success: boolean }> { + await this.versionService.toggleEnabled(id, dto.isEnabled, 'upgrade-admin') + return { success: true } + } + + @Post('parse') + @UseInterceptors(FileInterceptor('file')) + @ApiOperation({ summary: '解析APK/IPA包信息 (不保存)' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + required: ['file', 'platform'], + properties: { + file: { type: 'string', format: 'binary', description: 'APK或IPA安装包文件' }, + platform: { type: 'string', enum: ['android', 'ios', 'ANDROID', 'IOS'], description: '平台' }, + }, + }, + }) + @ApiResponse({ status: 200, description: '解析成功' }) + @ApiResponse({ status: 400, description: '解析失败' }) + async parsePackage( + @UploadedFile( + new ParseFilePipe({ + validators: [new MaxFileSizeValidator({ maxSize: MAX_FILE_SIZE })], + fileIsRequired: true, + }), + ) + file: Express.Multer.File, + @Body('platform') platform: string, + ): Promise<{ + packageName: string + versionCode: number + versionName: string + minSdkVersion?: string + targetSdkVersion?: string + }> { + const platformEnum = platform.toUpperCase() as Platform + const ext = file.originalname.toLowerCase().split('.').pop() + if (platformEnum === Platform.ANDROID && ext !== 'apk') { + throw new BadRequestException('Android平台只能上传APK文件') + } + if (platformEnum === Platform.IOS && ext !== 'ipa') { + throw new BadRequestException('iOS平台只能上传IPA文件') + } + const parsed = await this.versionService.parsePackage(file.buffer, platformEnum) + if (!parsed) { + throw new BadRequestException('无法解析安装包信息,请确认文件格式正确') + } + return parsed + } + + @Post('upload') + @UseInterceptors(FileInterceptor('file')) + @ApiOperation({ summary: '上传APK/IPA并创建版本' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + required: ['file', 'platform', 'changelog'], + properties: { + file: { type: 'string', format: 'binary', description: 'APK或IPA安装包文件' }, + platform: { type: 'string', enum: ['android', 'ios', 'ANDROID', 'IOS'], description: '平台' }, + versionCode: { type: 'integer', description: '版本号 (可选)' }, + versionName: { type: 'string', description: '版本名称 (可选)' }, + buildNumber: { type: 'string', description: '构建号 (可选)' }, + changelog: { type: 'string', description: '更新日志' }, + isForceUpdate: { type: 'boolean', description: '是否强制更新', default: false }, + minOsVersion: { type: 'string', description: '最低操作系统版本 (可选)' }, + releaseDate: { type: 'string', format: 'date-time', description: '发布日期' }, + }, + }, + }) + @ApiResponse({ status: 201, type: VersionResponseDto }) + @ApiResponse({ status: 400, description: '无效的文件或参数' }) + async uploadVersion( + @UploadedFile( + new ParseFilePipe({ + validators: [new MaxFileSizeValidator({ maxSize: MAX_FILE_SIZE })], + fileIsRequired: true, + }), + ) + file: Express.Multer.File, + @Body() dto: UploadVersionDto, + ): Promise { + const ext = file.originalname.toLowerCase().split('.').pop() + if (dto.platform === Platform.ANDROID && ext !== 'apk') { + throw new BadRequestException('Android平台只能上传APK文件') + } + if (dto.platform === Platform.IOS && ext !== 'ipa') { + throw new BadRequestException('iOS平台只能上传IPA文件') + } + const version = await this.versionService.upload({ + platform: dto.platform, + fileBuffer: file.buffer, + originalFilename: file.originalname, + changelog: dto.changelog ?? '', + isForceUpdate: dto.isForceUpdate ?? false, + createdBy: 'upgrade-admin', + versionCode: dto.versionCode, + versionName: dto.versionName, + buildNumber: dto.buildNumber, + minOsVersion: dto.minOsVersion, + releaseDate: dto.releaseDate ? new Date(dto.releaseDate) : undefined, + }) + return this.toVersionDto(version) + } +} diff --git a/frontend/mobile-upgrade/src/infrastructure/http/api-client.ts b/frontend/mobile-upgrade/src/infrastructure/http/api-client.ts index fb542302..4d977194 100644 --- a/frontend/mobile-upgrade/src/infrastructure/http/api-client.ts +++ b/frontend/mobile-upgrade/src/infrastructure/http/api-client.ts @@ -51,7 +51,7 @@ export interface ApiConfig { * * 注意: * - mobile 使用 /api/v1/versions 前缀 (admin-service) - * - mining 使用 /api/v2/versions 前缀 (mining-admin-service) + * - mining 使用 /api/v2/upgrade-versions 前缀 (mining-admin-service 公开接口) * - API前缀不同是因为两个后端是独立的服务 */ const APP_CONFIGS: Record = { @@ -63,7 +63,7 @@ const APP_CONFIGS: Record = { // 股行 App - 连接新的 mining-admin-service 后端 mining: { baseURL: process.env.NEXT_PUBLIC_MINING_API_URL || 'http://localhost:3023', - apiPrefix: '/api/v2/versions', + apiPrefix: '/api/v2/upgrade-versions', }, }