From a48433f01dd9526e347331ebc742ef91a3d6f797 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Thu, 22 Oct 2026 13:09:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(profile):=20=E4=BF=AE=E5=A4=8D=E6=88=91?= =?UTF-8?q?=E7=9A=84=E6=94=B6=E8=97=8F=E4=B8=8E=E8=A7=82=E7=9C=8B=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E5=88=97=E8=A1=A8=E4=B8=8D=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/data/models/page_result.dart | 32 ++++++++++++-- lib/core/data/remote/library_api.dart | 6 +-- lib/core/data/remote/watch_api.dart | 6 +-- test/core/page_result_parse_test.dart | 59 +++++++++++++++++++++++++ test/library/library_api_test.dart | 63 +++++++++++++++++++++++++++ test/watch/watch_api_test.dart | 48 ++++++++++++++++++++ 6 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 test/core/page_result_parse_test.dart diff --git a/lib/core/data/models/page_result.dart b/lib/core/data/models/page_result.dart index 37511bc..16c446f 100644 --- a/lib/core/data/models/page_result.dart +++ b/lib/core/data/models/page_result.dart @@ -3,6 +3,8 @@ import 'json_numbers.dart'; /// 服务端标准分页响应 `{total, list}`。 +/// +/// 兼容旧版「data 直接是数组」的响应:按数组长度作为 total,避免列表空白。 class PageResult { const PageResult({this.total = 0, this.list = const []}); @@ -14,10 +16,32 @@ class PageResult { Map json, T Function(Map) parseItem, ) { - final items = (json['list'] as List? ?? const []) - .whereType>() - .map(parseItem) - .toList(growable: false); + final items = _parseItemMaps(json['list']).map(parseItem).toList( + growable: false, + ); return PageResult(total: jsonToInt(json['total']), list: items); } + + /// 兼容分页 Map 与旧版纯 List;其它类型视为空页。 + factory PageResult.parse( + Object? data, + T Function(Map) parseItem, + ) { + if (data is Map) { + return PageResult.fromJson(Map.from(data), parseItem); + } + if (data is List) { + final items = _parseItemMaps(data).map(parseItem).toList(growable: false); + return PageResult(total: items.length, list: items); + } + return PageResult(); + } +} + +List> _parseItemMaps(Object? raw) { + if (raw is! List) return const []; + return [ + for (final item in raw) + if (item is Map) Map.from(item), + ]; } diff --git a/lib/core/data/remote/library_api.dart b/lib/core/data/remote/library_api.dart index 3eac576..73588d8 100644 --- a/lib/core/data/remote/library_api.dart +++ b/lib/core/data/remote/library_api.dart @@ -29,10 +29,8 @@ class LibraryApi { '/app/favorites', query: {'pageIndex': '$pageIndex', 'pageSize': '$pageSize'}, ); - if (data is Map) { - return PageResult.fromJson(data, Drama.fromCardJson); - } - return const PageResult(); + // 线上可能仍返回 List;分页壳与纯数组都要能渲染。 + return PageResult.parse(data, Drama.fromCardJson); } Future removeFavorite(String dramaId) async { diff --git a/lib/core/data/remote/watch_api.dart b/lib/core/data/remote/watch_api.dart index f63b5d9..e46e240 100644 --- a/lib/core/data/remote/watch_api.dart +++ b/lib/core/data/remote/watch_api.dart @@ -49,10 +49,8 @@ class WatchApi { '/app/watch/recent', query: {'pageIndex': '$pageIndex', 'pageSize': '$pageSize'}, ); - if (data is Map) { - return PageResult.fromJson(data, WatchProgress.fromJson); - } - return const PageResult(); + // 线上可能仍返回 List;分页壳与纯数组都要能渲染。 + return PageResult.parse(data, WatchProgress.fromJson); } /// 删除某部剧的观看记录(记录页清单条用)。 diff --git a/test/core/page_result_parse_test.dart b/test/core/page_result_parse_test.dart new file mode 100644 index 0000000..43bed86 --- /dev/null +++ b/test/core/page_result_parse_test.dart @@ -0,0 +1,59 @@ +// Author: fruit.com.cn + +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/data/models/page_result.dart'; + +void main() { + group('PageResult.parse', () { + test('should_parse_page_map_when_total_and_list_present', () { + final page = PageResult.parse({ + 'total': '3', + 'list': [ + {'id': '1', 'title': 'A'}, + {'id': '2', 'title': 'B'}, + ], + }, _item); + + expect(page.total, 3); + expect(page.list.map((e) => e['id']), ['1', '2']); + }); + + test('should_parse_legacy_list_when_data_is_array', () { + final page = PageResult.parse([ + {'id': '9', 'title': '收藏剧'}, + {'id': '8', 'title': '另一部'}, + ], _item); + + expect(page.total, 2); + expect(page.list, hasLength(2)); + expect(page.list.first['title'], '收藏剧'); + }); + + test('should_return_empty_page_when_data_is_null_or_empty_list', () { + expect(PageResult.parse(null, _item).total, 0); + expect(PageResult.parse(null, _item).list, isEmpty); + expect(PageResult.parse(const [], _item).total, 0); + expect(PageResult.parse(const [], _item).list, isEmpty); + }); + + test('should_return_empty_page_when_payload_is_invalid', () { + final page = PageResult.parse('broken', _item); + expect(page.total, 0); + expect(page.list, isEmpty); + }); + + test('should_skip_non_map_entries_in_legacy_list', () { + final page = PageResult.parse([ + {'id': '1'}, + 'noise', + 42, + {'id': '2'}, + ], _item); + + expect(page.total, 2); + expect(page.list.map((e) => e['id']), ['1', '2']); + }); + }); +} + +Map _item(Map json) => json; diff --git a/test/library/library_api_test.dart b/test/library/library_api_test.dart index 127c61c..2dbe0e9 100644 --- a/test/library/library_api_test.dart +++ b/test/library/library_api_test.dart @@ -38,4 +38,67 @@ void main() { expect(page.list.single.id, '9'); expect(page.list.single.title, '收藏剧'); }); + + test('should_parse_favorites_when_server_returns_legacy_list', () async { + final adapter = FakeHttpAdapter( + (_) async => FakeHttpResponse( + jsonEncode({ + 'code': 0, + 'message': 'ok', + 'data': [ + {'id': '5', 'title': '闪婚后', 'cover': '/files/5.png'}, + {'id': '4', 'title': '另一部', 'cover': '/files/4.png'}, + ], + }), + ), + ); + final api = LibraryApi( + ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter), + ); + + final page = await api.favorites(); + + expect(page.total, 2); + expect(page.list.map((drama) => drama.id), ['5', '4']); + expect(page.list.first.title, '闪婚后'); + }); + + test('should_return_empty_favorites_when_data_is_null', () async { + final adapter = FakeHttpAdapter( + (_) async => FakeHttpResponse( + jsonEncode({'code': 0, 'message': 'ok', 'data': null}), + ), + ); + final api = LibraryApi( + ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter), + ); + + final page = await api.favorites(); + + expect(page.total, 0); + expect(page.list, isEmpty); + }); + + test('should_throw_api_exception_when_favorites_business_code_failed', () async { + final adapter = FakeHttpAdapter( + (_) async => FakeHttpResponse( + jsonEncode({'code': 401, 'message': '未登录或登录已过期', 'data': null}), + ), + ); + final api = LibraryApi( + ApiClient( + baseUrl: 'http://127.0.0.1:8081', + adapter: adapter, + // 无刷新回调时,401 直接抛业务异常,避免重试逻辑。 + tokenRefresher: null, + ), + ); + + await expectLater( + () => api.favorites(), + throwsA( + isA().having((e) => e.code, 'code', 401), + ), + ); + }); } diff --git a/test/watch/watch_api_test.dart b/test/watch/watch_api_test.dart index 5369cc3..74a9c8a 100644 --- a/test/watch/watch_api_test.dart +++ b/test/watch/watch_api_test.dart @@ -50,4 +50,52 @@ void main() { expect(page.list.single.episodeNumber, 6); expect(page.list.single.totalEpisodes, 68); }); + + test('should_parse_recent_when_server_returns_legacy_list', () async { + final adapter = FakeHttpAdapter( + (_) async => FakeHttpResponse( + jsonEncode({ + 'code': 0, + 'message': 'ok', + 'data': [ + { + 'dramaId': '5', + 'episodeId': '7', + 'positionMs': '203904', + 'durationMs': '227882', + 'finished': false, + 'playCount': 8, + 'updatedAt': '2026-10-21T15:17:05.419', + }, + ], + }), + ), + ); + final api = WatchApi( + ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter), + ); + + final page = await api.recent(); + + expect(page.total, 1); + expect(page.list.single.dramaId, '5'); + expect(page.list.single.episodeId, '7'); + expect(page.list.single.position.inMilliseconds, 203904); + }); + + test('should_return_empty_recent_when_data_is_null', () async { + final adapter = FakeHttpAdapter( + (_) async => FakeHttpResponse( + jsonEncode({'code': 0, 'message': 'ok', 'data': null}), + ), + ); + final api = WatchApi( + ApiClient(baseUrl: 'http://127.0.0.1:8081', adapter: adapter), + ); + + final page = await api.recent(); + + expect(page.total, 0); + expect(page.list, isEmpty); + }); }