后端 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。
129 lines
3.9 KiB
Dart
129 lines
3.9 KiB
Dart
import 'json_numbers.dart';
|
||
|
||
/// 短剧与剧集,字段与后端 `/app/**` 的下发结构对应。
|
||
///
|
||
/// 封面与播放地址都是后端返回的相对路径(`/files/...`),
|
||
/// 渲染前用 `AppConfig.mediaUrl` 补上服务地址。
|
||
class Episode {
|
||
const Episode({
|
||
required this.id,
|
||
required this.number,
|
||
required this.title,
|
||
required this.isFree,
|
||
this.videoUrl = '',
|
||
this.durationSec = 0,
|
||
this.locked = false,
|
||
});
|
||
|
||
final String id;
|
||
final int number;
|
||
final String title;
|
||
final bool isFree;
|
||
|
||
/// 播放地址:详情接口不下发,由 `/app/episodes/{id}/play` 返回后写入。
|
||
final String videoUrl;
|
||
final int durationSec;
|
||
|
||
/// 后端判定需要解锁;解锁能力(金币/VIP)在 M3 接入。
|
||
final bool locked;
|
||
|
||
Episode withVideoUrl(String url) => Episode(
|
||
id: id,
|
||
number: number,
|
||
title: title,
|
||
isFree: isFree,
|
||
videoUrl: url,
|
||
durationSec: durationSec,
|
||
locked: locked,
|
||
);
|
||
|
||
factory Episode.fromJson(Map<String, dynamic> json) {
|
||
return Episode(
|
||
id: (json['id'] ?? '').toString(),
|
||
number: (json['number'] as num?)?.toInt() ?? 0,
|
||
title: json['title'] as String? ?? '',
|
||
isFree: json['isFree'] == true,
|
||
durationSec: (json['durationSec'] as num?)?.toInt() ?? 0,
|
||
locked: json['locked'] == true,
|
||
videoUrl: json['playUrl'] as String? ?? '',
|
||
);
|
||
}
|
||
}
|
||
|
||
class Drama {
|
||
const Drama({
|
||
required this.id,
|
||
required this.title,
|
||
required this.synopsis,
|
||
required this.posterUrl,
|
||
this.episodes = const [],
|
||
this.category = '',
|
||
this.genre = '',
|
||
this.totalEpisodes = 0,
|
||
this.freeEpisodes = 0,
|
||
this.isHot = false,
|
||
this.viewCount = 0,
|
||
this.detailLoaded = false,
|
||
});
|
||
|
||
final String id;
|
||
final String title;
|
||
final String synopsis;
|
||
final String posterUrl;
|
||
final List<Episode> episodes;
|
||
final String category;
|
||
final String genre;
|
||
final int totalEpisodes;
|
||
final int freeEpisodes;
|
||
final bool isHot;
|
||
final int viewCount;
|
||
|
||
/// 详情接口是否已经取过完整剧集列表。
|
||
/// 列表卡片只带一集可播预览,需要完整剧集时再进详情。
|
||
final bool detailLoaded;
|
||
|
||
/// 卡片只带一集预览,集数以后端给的总集数为准。
|
||
int get episodeCount => totalEpisodes > 0 ? totalEpisodes : episodes.length;
|
||
|
||
/// 列表卡片:带一集可播预览(首页直接起播),完整剧集进详情再取。
|
||
factory Drama.fromCardJson(Map<String, dynamic> json) {
|
||
final preview = json['preview'];
|
||
return Drama(
|
||
id: (json['id'] ?? '').toString(),
|
||
title: json['title'] as String? ?? '',
|
||
synopsis: json['synopsis'] as String? ?? '',
|
||
posterUrl: json['cover'] as String? ?? '',
|
||
episodes: preview is Map<String, dynamic>
|
||
? [Episode.fromJson(preview)]
|
||
: const [],
|
||
category: json['category'] as String? ?? '',
|
||
genre: json['genre'] as String? ?? '',
|
||
totalEpisodes: (json['totalEpisodes'] as num?)?.toInt() ?? 0,
|
||
isHot: json['isHot'] == true,
|
||
// viewCount 后端是 Long,按字符串下发(见 json_numbers.dart)
|
||
viewCount: jsonToInt(json['viewCount']),
|
||
);
|
||
}
|
||
|
||
/// 详情:带剧集列表。
|
||
factory Drama.fromDetailJson(Map<String, dynamic> json) {
|
||
return Drama(
|
||
id: (json['id'] ?? '').toString(),
|
||
title: json['title'] as String? ?? '',
|
||
synopsis: json['synopsis'] as String? ?? '',
|
||
posterUrl: json['cover'] as String? ?? '',
|
||
category: json['category'] as String? ?? '',
|
||
genre: json['genre'] as String? ?? '',
|
||
totalEpisodes: (json['totalEpisodes'] as num?)?.toInt() ?? 0,
|
||
freeEpisodes: (json['freeEpisodes'] as num?)?.toInt() ?? 0,
|
||
isHot: json['isHot'] == true,
|
||
viewCount: jsonToInt(json['viewCount']),
|
||
detailLoaded: true,
|
||
episodes: (json['episodes'] as List? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(Episode.fromJson)
|
||
.toList(),
|
||
);
|
||
}
|
||
}
|