fix(admin): support 4-segment version in domain layer & make changelog optional

- 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-09 06:54:55 -08:00
parent 4307d1eb91
commit 586dfda8f7
3 changed files with 29 additions and 17 deletions

View File

@ -27,9 +27,10 @@ export class UploadVersionDto {
@IsString() @IsString()
buildNumber?: string buildNumber?: string
@ApiProperty({ description: '更新日志' }) @ApiPropertyOptional({ description: '更新日志', default: '' })
@IsOptional()
@IsString() @IsString()
changelog: string changelog?: string
@ApiPropertyOptional({ description: '是否强制更新', default: false }) @ApiPropertyOptional({ description: '是否强制更新', default: false })
@IsOptional() @IsOptional()

View File

@ -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 { export class VersionName {
private readonly _value: string; private readonly _value: string;
private readonly _major: number; private readonly _major: number;
private readonly _minor: number; private readonly _minor: number;
private readonly _patch: 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._value = value;
this._major = major; this._major = major;
this._minor = minor; this._minor = minor;
this._patch = patch; this._patch = patch;
this._build = build;
} }
get value(): string { get value(): string {
@ -33,24 +35,30 @@ export class VersionName {
return this._patch; return this._patch;
} }
get build(): number | undefined {
return this._build;
}
static create(value: string): VersionName { static create(value: string): VersionName {
if (!value || value.trim() === '') { if (!value || value.trim() === '') {
throw new DomainException('VersionName cannot be empty'); throw new DomainException('VersionName cannot be empty');
} }
const trimmed = value.trim(); 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); const match = trimmed.match(versionPattern);
if (!match) { 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 major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10); const minor = parseInt(match[2], 10);
const patch = parseInt(match[3], 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 { equals(other: VersionName): boolean {

View File

@ -1005,17 +1005,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
/// ///
Widget _buildExpiredSection() { Widget _buildExpiredSection() {
return Container( return SizedBox(
padding: const EdgeInsets.all(16), width: double.infinity,
decoration: BoxDecoration( child: Container(
color: const Color(0xFFFFF5E6), padding: const EdgeInsets.all(16),
borderRadius: BorderRadius.circular(8), decoration: BoxDecoration(
border: Border.all( color: const Color(0xFFFFF5E6),
color: const Color(0x33D4AF37), borderRadius: BorderRadius.circular(8),
width: 1, border: Border.all(
color: const Color(0x33D4AF37),
width: 1,
),
), ),
), child: Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text(
@ -1083,6 +1085,7 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
], ],
), ),
], ],
),
), ),
); );
} }