From 586dfda8f73bbdde6a0400e67766f6f5cb22cf64 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 9 Dec 2025 06:54:55 -0800 Subject: [PATCH] fix(admin): support 4-segment version in domain layer & make changelog optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VersionName value object now accepts x.y.z.w format - changelog field is now optional in upload version DTO - profile page: ensure expired section has full width 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/api/dto/request/upload-version.dto.ts | 5 ++-- .../domain/value-objects/version-name.vo.ts | 18 +++++++++++---- .../presentation/pages/profile_page.dart | 23 +++++++++++-------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/backend/services/admin-service/src/api/dto/request/upload-version.dto.ts b/backend/services/admin-service/src/api/dto/request/upload-version.dto.ts index 6a904544..d64d71f9 100644 --- a/backend/services/admin-service/src/api/dto/request/upload-version.dto.ts +++ b/backend/services/admin-service/src/api/dto/request/upload-version.dto.ts @@ -27,9 +27,10 @@ export class UploadVersionDto { @IsString() buildNumber?: string - @ApiProperty({ description: '更新日志' }) + @ApiPropertyOptional({ description: '更新日志', default: '' }) + @IsOptional() @IsString() - changelog: string + changelog?: string @ApiPropertyOptional({ description: '是否强制更新', default: false }) @IsOptional() diff --git a/backend/services/admin-service/src/domain/value-objects/version-name.vo.ts b/backend/services/admin-service/src/domain/value-objects/version-name.vo.ts index 31f8032b..7049acbd 100644 --- a/backend/services/admin-service/src/domain/value-objects/version-name.vo.ts +++ b/backend/services/admin-service/src/domain/value-objects/version-name.vo.ts @@ -2,19 +2,21 @@ import { DomainException } from '../../shared/exceptions/domain.exception'; /** * 版本名称值对象 - * 格式: major.minor.patch (e.g., 1.0.0, 2.3.1) + * 格式: major.minor.patch 或 major.minor.patch.build (e.g., 1.0.0, 1.0.0.4) */ export class VersionName { private readonly _value: string; private readonly _major: number; private readonly _minor: number; private readonly _patch: number; + private readonly _build?: number; - private constructor(value: string, major: number, minor: number, patch: number) { + private constructor(value: string, major: number, minor: number, patch: number, build?: number) { this._value = value; this._major = major; this._minor = minor; this._patch = patch; + this._build = build; } get value(): string { @@ -33,24 +35,30 @@ export class VersionName { return this._patch; } + get build(): number | undefined { + return this._build; + } + static create(value: string): VersionName { if (!value || value.trim() === '') { throw new DomainException('VersionName cannot be empty'); } const trimmed = value.trim(); - const versionPattern = /^(\d+)\.(\d+)\.(\d+)$/; + // 支持 x.y.z 或 x.y.z.w 格式 + const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:\.(\d+))?$/; const match = trimmed.match(versionPattern); if (!match) { - throw new DomainException('VersionName must follow semantic versioning format (e.g., 1.0.0)'); + throw new DomainException('VersionName must follow semantic versioning format (e.g., 1.0.0 or 1.0.0.4)'); } const major = parseInt(match[1], 10); const minor = parseInt(match[2], 10); const patch = parseInt(match[3], 10); + const build = match[4] ? parseInt(match[4], 10) : undefined; - return new VersionName(trimmed, major, minor, patch); + return new VersionName(trimmed, major, minor, patch, build); } equals(other: VersionName): boolean { diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart index de29788b..9ae86749 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart @@ -1005,17 +1005,19 @@ class _ProfilePageState extends ConsumerState { /// 构建已过期区域 Widget _buildExpiredSection() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFFFF5E6), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: const Color(0x33D4AF37), - width: 1, + return SizedBox( + width: double.infinity, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFFFF5E6), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0x33D4AF37), + width: 1, + ), ), - ), - child: Column( + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( @@ -1083,6 +1085,7 @@ class _ProfilePageState extends ConsumerState { ], ), ], + ), ), ); }