feat(mining-admin): 新建公开版本管理接口供 mobile-upgrade 前端使用

mining-admin-service 有全局 AdminAuthGuard,导致 mobile-upgrade 前端
调用版本接口返回 401。新建 UpgradeVersionController (@Public) 作为
独立的公开接口,路径为 /api/v2/upgrade-versions,不影响现有需认证的
/api/v2/versions 接口。前端 apiPrefix 同步更新。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-29 12:53:29 -08:00
parent c6137078ff
commit 4112b45b9e
3 changed files with 244 additions and 2 deletions

View File

@ -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,
],
})

View File

@ -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<VersionResponseDto[]> {
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<VersionResponseDto> {
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<VersionResponseDto> {
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<VersionResponseDto> {
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<void> {
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<VersionResponseDto> {
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)
}
}

View File

@ -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<AppType, ApiConfig> = {
@ -63,7 +63,7 @@ const APP_CONFIGS: Record<AppType, ApiConfig> = {
// 股行 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',
},
}