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:
parent
c6137078ff
commit
4112b45b9e
|
|
@ -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,
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue