fix(client): 兼容后端 Long 字段字符串化(id 与计数)
后端 J1 起 Long/long 一律以字符串下发(含雪花 id 与 viewCount/expiresIn)。 客户端 id 方向本来就是 (json['id'] ?? '').toString(),不受影响;会被 as num? 打挂的 是 Long 计数,这里加统一兼容解析并替换 3 处: - lib/core/data/models/json_numbers.dart(新增,接受数字或数字字符串) - lib/core/data/models/auth_session.dart(expiresIn,后端 long) - lib/core/data/models/drama.dart(viewCount,后端 Long ×2) - test/core/json_id_parsing_test.dart(新增 8 条回归用例) 其余 as num?(durationSec/unlockCoin/number/totalEpisodes/freeEpisodes/code)对应后端 Integer/int,仍是数字,未改动;未碰任何业务逻辑与 UI。
This commit is contained in:
1 parent
61454e36b1
commit
94ff9f1177
4 files changed
+125
-3
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import 'json_numbers.dart';
|
||||
|
||||
/// 登录态:服务端下发的令牌与用户摘要。
|
||||
///
|
||||
/// 只保留客户端真正要用的字段(uid / 昵称 / 头像),不做整份 AppUserRespVO 的映射 ——
|
||||
@@ -38,7 +40,8 @@ class AuthSession {
|
||||
factory AuthSession.fromJson(Map<String, dynamic> json) => AuthSession(
|
||||
accessToken: json['accessToken'] as String? ?? '',
|
||||
refreshToken: json['refreshToken'] as String? ?? '',
|
||||
expiresIn: (json['expiresIn'] as num?)?.toInt() ?? 0,
|
||||
// expiresIn 后端是 long,按字符串下发(见 json_numbers.dart)
|
||||
expiresIn: jsonToInt(json['expiresIn']),
|
||||
user: AuthUser.fromJson((json['user'] as Map?)?.cast<String, dynamic>() ?? const {}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'json_numbers.dart';
|
||||
|
||||
/// 短剧与剧集,字段与后端 `/app/**` 的下发结构对应。
|
||||
///
|
||||
/// 封面与播放地址都是后端返回的相对路径(`/files/...`),
|
||||
@@ -98,7 +100,8 @@ class Drama {
|
||||
genre: json['genre'] as String? ?? '',
|
||||
totalEpisodes: (json['totalEpisodes'] as num?)?.toInt() ?? 0,
|
||||
isHot: json['isHot'] == true,
|
||||
viewCount: (json['viewCount'] as num?)?.toInt() ?? 0,
|
||||
// viewCount 后端是 Long,按字符串下发(见 json_numbers.dart)
|
||||
viewCount: jsonToInt(json['viewCount']),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +117,7 @@ class Drama {
|
||||
totalEpisodes: (json['totalEpisodes'] as num?)?.toInt() ?? 0,
|
||||
freeEpisodes: (json['freeEpisodes'] as num?)?.toInt() ?? 0,
|
||||
isHot: json['isHot'] == true,
|
||||
viewCount: (json['viewCount'] as num?)?.toInt() ?? 0,
|
||||
viewCount: jsonToInt(json['viewCount']),
|
||||
detailLoaded: true,
|
||||
episodes: (json['episodes'] as List? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
/// 数值字段的兼容解析。
|
||||
///
|
||||
/// 后端契约(FruitServer/docs/api-phase1.md §0):`Long`/`long` 字段**一律以字符串下发**
|
||||
/// (雪花 id 与计数都算),`Integer`/`int` 仍是数字。所以这里既接受数字也接受数字字符串,
|
||||
/// 否则一个 `"12345"` 就能让 `as num?` 直接抛异常、整页解析失败。
|
||||
int jsonToInt(dynamic value, {int fallback = 0}) {
|
||||
if (value is int) return value;
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) return int.tryParse(value.trim()) ?? fallback;
|
||||
return fallback;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/models/auth_session.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/data/models/json_numbers.dart';
|
||||
import 'package:fruitshorts/core/data/models/play_ticket.dart';
|
||||
|
||||
/// 后端契约(FruitServer/docs/api-phase1.md §0):`Long`/`long` 字段以字符串下发。
|
||||
/// 这里钉住客户端在"收到字符串形式的 id / 计数"时照样能解析,不抛异常。
|
||||
void main() {
|
||||
group('jsonToInt', () {
|
||||
test('should_parse_int_when_value_is_number', () {
|
||||
expect(jsonToInt(12345), 12345);
|
||||
expect(jsonToInt(12.9), 12);
|
||||
});
|
||||
|
||||
test('should_parse_int_when_value_is_numeric_string', () {
|
||||
expect(jsonToInt('2111012753262120962'), 2111012753262120962);
|
||||
expect(jsonToInt(' 42 '), 42);
|
||||
});
|
||||
|
||||
test('should_fall_back_when_value_is_missing_or_invalid', () {
|
||||
expect(jsonToInt(null), 0);
|
||||
expect(jsonToInt(''), 0);
|
||||
expect(jsonToInt('abc'), 0);
|
||||
expect(jsonToInt(null, fallback: 7), 7);
|
||||
});
|
||||
});
|
||||
|
||||
group('drama model', () {
|
||||
test('should_parse_string_ids_when_backend_sends_ids_as_string', () {
|
||||
final drama = Drama.fromCardJson({
|
||||
'id': '2111012753262120962',
|
||||
'title': '闪婚后,傅先生每天都在撒糖',
|
||||
'totalEpisodes': 72,
|
||||
'viewCount': '3100000',
|
||||
'isHot': true,
|
||||
});
|
||||
|
||||
expect(drama.id, '2111012753262120962');
|
||||
expect(drama.title, '闪婚后,傅先生每天都在撒糖');
|
||||
expect(drama.totalEpisodes, 72);
|
||||
// viewCount 后端是 Long → 字符串
|
||||
expect(drama.viewCount, 3100000);
|
||||
});
|
||||
|
||||
test('should_parse_view_count_when_backend_sends_number', () {
|
||||
final drama = Drama.fromCardJson({'id': 5, 'title': 'T', 'viewCount': 1200});
|
||||
|
||||
expect(drama.id, '5');
|
||||
expect(drama.viewCount, 1200);
|
||||
});
|
||||
|
||||
test('should_parse_detail_payload_when_ids_are_strings', () {
|
||||
final drama = Drama.fromDetailJson({
|
||||
'id': '2111012753262120962',
|
||||
'title': 'T',
|
||||
'totalEpisodes': 12,
|
||||
'freeEpisodes': 3,
|
||||
'viewCount': '99',
|
||||
'episodes': [
|
||||
{'id': '2111012753262120963', 'number': 1, 'title': 'E1', 'isFree': true},
|
||||
],
|
||||
});
|
||||
|
||||
expect(drama.id, '2111012753262120962');
|
||||
expect(drama.freeEpisodes, 3);
|
||||
expect(drama.viewCount, 99);
|
||||
expect(drama.episodes.first.id, '2111012753262120963');
|
||||
expect(drama.episodes.first.number, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('auth session', () {
|
||||
test('should_parse_expires_in_when_backend_sends_string', () {
|
||||
final session = AuthSession.fromJson({
|
||||
'accessToken': 'a',
|
||||
'refreshToken': 'r',
|
||||
'expiresIn': '7200',
|
||||
'user': {'uid': 'u-1', 'nickname': 'n'},
|
||||
});
|
||||
|
||||
expect(session.expiresIn, 7200);
|
||||
expect(session.user.uid, 'u-1');
|
||||
});
|
||||
});
|
||||
|
||||
group('play ticket', () {
|
||||
test('should_parse_episode_id_when_backend_sends_string', () {
|
||||
final ticket = PlayTicket.fromJson({
|
||||
'episodeId': '2111012753262120963',
|
||||
'accessible': false,
|
||||
'accessType': PlayTicket.coinRequired,
|
||||
'unlockCoin': 30,
|
||||
});
|
||||
|
||||
expect(ticket.episodeId, '2111012753262120963');
|
||||
expect(ticket.unlockCoin, 30);
|
||||
expect(ticket.needsUnlock, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user