Skip to content

Commit 27d8ce9

Browse files
committed
REFACTOR: 版本比较只用 versionName,丢弃 buildNumber
设计:versionName(如 1.0.12)唯一标识一次正式发布,buildNumber 是 Android versionCode / iOS CFBundleVersion 等平台内部升降级机制, 对用户和业务版本判断无意义。 改动: - version_compare.dart: parseVersion 只返回 [major, minor, patch] 三段,compareVersions 只比 3 段 - app_update_provider.dart: localVersion 只用 packageInfo.version, 不再拼接 buildNumber - 测试:原来'本地 1.0.9+1 < 远程 1.0.9+2 需要更新'变成'相等无需更新', 断言全部更新 这使得 version.json 的 latestVersion 可以直接写 "1.0.12", 跟用户在'关于'页看到的版本号完全一致。
1 parent 74d400c commit 27d8ce9

4 files changed

Lines changed: 66 additions & 125 deletions

File tree

lib/providers/app_update_provider.dart

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,9 @@ class AppUpdate extends _$AppUpdate {
115115
}
116116

117117
final packageInfo = ref.read(packageInfoProvider);
118-
final buildNumber = packageInfo.buildNumber;
119-
// 仅当 buildNumber 是纯数字时组合为完整版本号(空时默认 +1)
120-
final buildNumberInt = int.tryParse(buildNumber);
121-
final effectiveBuildNumber = buildNumberInt?.toString() ?? '1';
122-
final localVersion = '${packageInfo.version}+$effectiveBuildNumber';
118+
// 仅用 versionName 做版本比较;buildNumber 是平台内部升降级机制,
119+
// 不参与业务版本判断(同一 versionName 唯一对应一次正式发布)。
120+
final localVersion = packageInfo.version;
123121
final updateType = determineUpdateType(localVersion, info);
124122

125123
// 非手动检查时,检查是否已忽略此版本

lib/utils/version_compare.dart

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
11
/// 版本号比较工具
22
///
3-
/// 提供 semver 风格的版本号比较,支持构建号(+N 后缀):
4-
/// - `1.0.8`[1, 0, 8, 0]
5-
/// - `1.0.8+2`[1, 0, 8, 2]
6-
/// - null / "" → [0, 0, 0, 0]
7-
/// - "1.0" → [1, 0, 0, 0](自动补零)
8-
/// - "1.0.0-beta" → [1, 0, 0, 0](去除 pre-release 后缀)
3+
/// 按 SemVer 主版本号 [major, minor, patch] 三段比较,**不考虑构建号**。
4+
///
5+
/// 设计:构建号(Flutter pubspec 的 `+N`、Android versionCode)是平台内部
6+
/// 升降级机制,对用户和业务版本判断无意义。同一 versionName(如 `1.0.11`
7+
/// 在我们的发布规范下唯一对应一次正式发布,构建号只是 CI 自动生成的内部数字。
8+
///
9+
/// 解析示例:
10+
/// - `1.0.8`[1, 0, 8]
11+
/// - `1.0.8+2`[1, 0, 8](构建号被丢弃)
12+
/// - null / "" → [0, 0, 0]
13+
/// - "1.0" → [1, 0, 0](自动补零)
14+
/// - "1.0.0-beta" → [1, 0, 0](去除 pre-release 后缀)
915
/// - "abc" / "1.x.0" → 对应段解析为 0
1016
/// - 任何输入都不抛异常
1117
library;
1218

13-
/// 将版本号字符串解析为整数列表 [major, minor, patch, build]
19+
/// 将版本号字符串解析为整数列表 [major, minor, patch]
1420
///
15-
/// 容错处理:去除 v 前缀、去除 pre-release 后缀、自动补零、非法段解析为 0。
16-
/// 构建号(+N 后缀)会被解析为第 4 个元素,无构建号则为 0。
21+
/// 容错处理:去除 v 前缀、丢弃 +N 构建号、去除 pre-release 后缀、自动补零。
1722
List<int> parseVersion(String? version) {
18-
if (version == null || version.isEmpty) return [0, 0, 0, 0];
23+
if (version == null || version.isEmpty) return [0, 0, 0];
1924

20-
// 去除 "v" 前缀(如 "v1.0.0")
25+
// 去除 "v" 前缀
2126
final cleaned = version.startsWith('v') || version.startsWith('V')
2227
? version.substring(1)
2328
: version;
2429

25-
// 先分离构建号(+ 后缀)
26-
final buildParts = cleaned.split('+');
27-
final coreWithPre = buildParts.first;
28-
final buildNumber = buildParts.length > 1
29-
? (int.tryParse(buildParts[1].split('-').first) ?? 0)
30-
: 0;
30+
// 丢弃 +N 构建号(不参与比较)
31+
final coreWithPre = cleaned.split('+').first;
3132

32-
// 去除 pre-release 后缀(- 后缀)
33+
// 去除 pre-release 后缀
3334
final core = coreWithPre.split('-').first;
3435

3536
final parts = core.split('.');
3637
final result = <int>[];
3738
for (var i = 0; i < 3; i++) {
3839
result.add(i < parts.length ? (int.tryParse(parts[i]) ?? 0) : 0);
3940
}
40-
result.add(buildNumber);
4141
return result;
4242
}
4343

@@ -50,7 +50,7 @@ List<int> parseVersion(String? version) {
5050
int compareVersions(String? a, String? b) {
5151
final va = parseVersion(a);
5252
final vb = parseVersion(b);
53-
for (var i = 0; i < 4; i++) {
53+
for (var i = 0; i < 3; i++) {
5454
if (va[i] != vb[i]) return va[i] - vb[i];
5555
}
5656
return 0;

test/providers/app_update_provider_test.dart

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,13 @@ void main() {
6161
});
6262
});
6363

64-
group('构建号场景', () {
65-
test('本地 1.0.9+1 与远程 1.0.9+1 相等,无需更新', () {
66-
const info = AppUpdateInfo(
67-
latestVersion: '1.0.9+1',
68-
minimumVersion: '1.0.0',
69-
);
70-
expect(
71-
AppUpdate.determineUpdateType('1.0.9+1', info),
72-
AppUpdateType.none,
73-
);
74-
});
64+
group('构建号场景(不参与比较)', () {
65+
// 设计:versionName 唯一标识一次发布,buildNumber 是平台内部机制
66+
// (Android versionCode、iOS CFBundleVersion)。版本比较只看 versionName。
7567

76-
test('本地 1.0.9+1 与远程 1.0.9+0 相等,无需更新', () {
68+
test('同 versionName 不同构建号视为相等,无需更新', () {
7769
const info = AppUpdateInfo(
78-
latestVersion: '1.0.9',
70+
latestVersion: '1.0.9+2',
7971
minimumVersion: '1.0.0',
8072
);
8173
expect(
@@ -84,31 +76,20 @@ void main() {
8476
);
8577
});
8678

87-
test('本地 1.0.9+1 低于远程 1.0.9+2,需要更新', () {
79+
test('本地带构建号、远端不带,相等无需更新', () {
8880
const info = AppUpdateInfo(
89-
latestVersion: '1.0.9+2',
81+
latestVersion: '1.0.9',
9082
minimumVersion: '1.0.0',
9183
);
9284
expect(
9385
AppUpdate.determineUpdateType('1.0.9+1', info),
94-
AppUpdateType.softUpdate,
95-
);
96-
});
97-
98-
test('本地 1.0.9+2 高于远程 1.0.9+1,无需更新', () {
99-
const info = AppUpdateInfo(
100-
latestVersion: '1.0.9+1',
101-
minimumVersion: '1.0.0',
102-
);
103-
expect(
104-
AppUpdate.determineUpdateType('1.0.9+2', info),
10586
AppUpdateType.none,
10687
);
10788
});
10889

109-
test('本地 1.0.9 高于远程 1.0.8+5,无需更新(patch 高于构建号)', () {
90+
test('本地不带构建号、远端带,相等无需更新', () {
11091
const info = AppUpdateInfo(
111-
latestVersion: '1.0.8+5',
92+
latestVersion: '1.0.9+5',
11293
minimumVersion: '1.0.0',
11394
);
11495
expect(
@@ -117,25 +98,24 @@ void main() {
11798
);
11899
});
119100

120-
test('本地 1.0.9+1 等于远程 1.0.9+1,无需更新', () {
101+
test('patch 升级,构建号被忽略', () {
121102
const info = AppUpdateInfo(
122-
latestVersion: '1.0.9+1',
103+
latestVersion: '1.0.9+2',
123104
minimumVersion: '1.0.0',
124105
);
125106
expect(
126-
AppUpdate.determineUpdateType('1.0.9+1', info),
127-
AppUpdateType.none,
107+
AppUpdate.determineUpdateType('1.0.8+99', info),
108+
AppUpdateType.softUpdate,
128109
);
129110
});
130111

131-
test('buildNumber 为空时默认 +1', () {
112+
test('本地 patch 更高,构建号被忽略', () {
132113
const info = AppUpdateInfo(
133-
latestVersion: '1.0.9+1',
114+
latestVersion: '1.0.8+5',
134115
minimumVersion: '1.0.0',
135116
);
136-
// 空 buildNumber 时 localVersion 会是 "1.0.9+1"
137117
expect(
138-
AppUpdate.determineUpdateType('1.0.9+1', info),
118+
AppUpdate.determineUpdateType('1.0.9', info),
139119
AppUpdateType.none,
140120
);
141121
});

test/utils/version_compare_test.dart

Lines changed: 27 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,71 +4,47 @@ import 'package:echo_loop/utils/version_compare.dart';
44
void main() {
55
group('parseVersion', () {
66
test('解析标准版本号', () {
7-
expect(parseVersion('1.2.3'), [1, 2, 3, 0]);
7+
expect(parseVersion('1.2.3'), [1, 2, 3]);
88
});
99

10-
test('解析带构建号的版本号', () {
11-
expect(parseVersion('1.0.8+2'), [1, 0, 8, 2]);
10+
test('丢弃构建号', () {
11+
expect(parseVersion('1.0.8+2'), [1, 0, 8]);
1212
});
1313

14-
test('null 返回 [0, 0, 0, 0]', () {
15-
expect(parseVersion(null), [0, 0, 0, 0]);
14+
test('null 返回 [0, 0, 0]', () {
15+
expect(parseVersion(null), [0, 0, 0]);
1616
});
1717

18-
test('空字符串返回 [0, 0, 0, 0]', () {
19-
expect(parseVersion(''), [0, 0, 0, 0]);
18+
test('空字符串返回 [0, 0, 0]', () {
19+
expect(parseVersion(''), [0, 0, 0]);
2020
});
2121

2222
test('两段版本号自动补零', () {
23-
expect(parseVersion('1.0'), [1, 0, 0, 0]);
23+
expect(parseVersion('1.0'), [1, 0, 0]);
2424
});
2525

2626
test('单段版本号自动补零', () {
27-
expect(parseVersion('3'), [3, 0, 0, 0]);
27+
expect(parseVersion('3'), [3, 0, 0]);
2828
});
2929

3030
test('去除 pre-release 后缀', () {
31-
expect(parseVersion('1.0.0-beta'), [1, 0, 0, 0]);
32-
});
33-
34-
test('去除 build 后缀保留构建号', () {
35-
expect(parseVersion('2.1.0+42'), [2, 1, 0, 42]);
31+
expect(parseVersion('1.0.0-beta'), [1, 0, 0]);
3632
});
3733

3834
test('去除 v 前缀', () {
39-
expect(parseVersion('v1.2.3'), [1, 2, 3, 0]);
35+
expect(parseVersion('v1.2.3'), [1, 2, 3]);
4036
});
4137

4238
test('非法段解析为 0', () {
43-
expect(parseVersion('1.x.0'), [1, 0, 0, 0]);
39+
expect(parseVersion('1.x.0'), [1, 0, 0]);
4440
});
4541

4642
test('完全非法字符串', () {
47-
expect(parseVersion('abc'), [0, 0, 0, 0]);
48-
});
49-
50-
test('构建号非法段解析为 0', () {
51-
expect(parseVersion('1.0.8+abc'), [1, 0, 8, 0]);
52-
});
53-
54-
test('构建号带 pre-release 后缀', () {
55-
expect(parseVersion('1.0.8+2-beta'), [1, 0, 8, 2]);
56-
});
57-
58-
test('构建号超大', () {
59-
expect(parseVersion('1.0.8+999999'), [1, 0, 8, 999999]);
60-
});
61-
62-
test('只有构建号没有版本号', () {
63-
expect(parseVersion('+1'), [0, 0, 0, 1]);
43+
expect(parseVersion('abc'), [0, 0, 0]);
6444
});
6545

66-
test('多个 + 号取第一个', () {
67-
expect(parseVersion('1.0.9+1+2'), [1, 0, 9, 1]);
68-
});
69-
70-
test('构建号为负数解析为 0', () {
71-
expect(parseVersion('1.0.8+-1'), [1, 0, 8, 0]);
46+
test('超大构建号也被丢弃', () {
47+
expect(parseVersion('1.0.8+999999'), [1, 0, 8]);
7248
});
7349
});
7450

@@ -77,22 +53,16 @@ void main() {
7753
expect(compareVersions('1.0.0', '1.0.0'), 0);
7854
});
7955

80-
test('相同版本相同构建号返回 0', () {
81-
expect(compareVersions('1.0.8+2', '1.0.8+2'), 0);
82-
});
83-
84-
test('相同版本不同构建号', () {
85-
expect(compareVersions('1.0.8+1', '1.0.8+2'), lessThan(0));
86-
expect(compareVersions('1.0.8+2', '1.0.8+1'), greaterThan(0));
87-
});
88-
89-
test('无构建号与构建号 0 相等', () {
90-
expect(compareVersions('1.0.8', '1.0.8+0'), 0);
56+
test('构建号不影响比较(同 versionName)', () {
57+
expect(compareVersions('1.0.8+1', '1.0.8+2'), 0);
58+
expect(compareVersions('1.0.8+2', '1.0.8+1'), 0);
59+
expect(compareVersions('1.0.8', '1.0.8+5'), 0);
9160
expect(compareVersions('1.0.8+0', '1.0.8'), 0);
9261
});
9362

94-
test('patch 版本高于构建号差异', () {
63+
test('patch 版本比较', () {
9564
expect(compareVersions('1.0.9+0', '1.0.8+5'), greaterThan(0));
65+
expect(compareVersions('1.0.1', '1.0.0'), greaterThan(0));
9666
});
9767

9868
test('a > b 返回正数', () {
@@ -103,10 +73,6 @@ void main() {
10373
expect(compareVersions('1.0.0', '1.1.0'), lessThan(0));
10474
});
10575

106-
test('patch 版本比较', () {
107-
expect(compareVersions('1.0.1', '1.0.0'), greaterThan(0));
108-
});
109-
11076
test('major 版本比较', () {
11177
expect(compareVersions('2.0.0', '1.9.9'), greaterThan(0));
11278
});
@@ -142,29 +108,26 @@ void main() {
142108
);
143109
});
144110

145-
test('相同版本号构建号更新时返回 true', () {
111+
test('相同 versionName 不同构建号视为相等,无需更新', () {
146112
expect(
147113
isNewerVersion(localVersion: '1.0.8+1', remoteVersion: '1.0.8+2'),
148-
isTrue,
114+
isFalse,
149115
);
150-
});
151-
152-
test('相同版本号构建号相同时返回 false', () {
153116
expect(
154-
isNewerVersion(localVersion: '1.0.8+2', remoteVersion: '1.0.8+2'),
117+
isNewerVersion(localVersion: '1.0.8+2', remoteVersion: '1.0.8+1'),
155118
isFalse,
156119
);
157120
});
158121

159-
test('无构建号版本与构建号版本比较', () {
122+
test('无构建号 vs 带构建号视为相等', () {
160123
expect(
161124
isNewerVersion(localVersion: '1.0.8', remoteVersion: '1.0.8+1'),
162-
isTrue,
125+
isFalse,
163126
);
164127
expect(
165128
isNewerVersion(localVersion: '1.0.8+1', remoteVersion: '1.0.8'),
166129
isFalse,
167130
);
168131
});
169132
});
170-
}
133+
}

0 commit comments

Comments
 (0)