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()
buildNumber?: string
@ApiProperty({ description: '更新日志' })
@ApiPropertyOptional({ description: '更新日志', default: '' })
@IsOptional()
@IsString()
changelog: string
changelog?: string
@ApiPropertyOptional({ description: '是否强制更新', default: false })
@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 {
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 {

View File

@ -1005,17 +1005,19 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
///
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<ProfilePage> {
],
),
],
),
),
);
}