feat: A3-A6 数据层(内容/播放/记录收藏/互动的 HTTP 对接)

- AppCatalog 模型(feed/section/card/detail/episode/banner/category)
- HttpCatalogRepository 对接首页/发现/分类/详情
- PlaybackRepository 播放票据、WatchRepository 记录收藏
- InteractionRepository 评论点赞举报、WalletRepository 钱包解锁
This commit is contained in:
Zhou Weidong committed 2026-10-17 02:31:47 +08:00
1 parent a7ac3b7fc7
commit 88a401512d
6 files changed
+382

No files matched your search

+188
View File
@@ -0,0 +1,188 @@
// Author: fruit.com.cn
// 与后端 /app/** 内容接口对齐的数据模型。
class AppEpisode {
AppEpisode({
required this.id,
required this.number,
required this.title,
required this.durationSec,
required this.locked,
required this.unlockCoin,
this.playUrl,
});
final int id;
final int number;
final String title;
final int durationSec;
final bool locked;
final int unlockCoin;
final String? playUrl;
factory AppEpisode.fromJson(Map<String, dynamic> json) => AppEpisode(
id: json['id'] as int,
number: json['number'] as int,
title: json['title'] as String? ?? '',
durationSec: json['durationSec'] as int? ?? 0,
locked: json['locked'] as bool? ?? false,
unlockCoin: json['unlockCoin'] as int? ?? 0,
playUrl: json['playUrl'] as String?,
);
}
class AppDramaCard {
AppDramaCard({
required this.id,
required this.title,
required this.cover,
required this.synopsis,
required this.genre,
required this.category,
required this.totalEpisodes,
required this.isHot,
required this.viewCount,
this.preview,
});
final int id;
final String title;
final String cover;
final String synopsis;
final String genre;
final String category;
final int totalEpisodes;
final bool isHot;
final int viewCount;
final AppEpisode? preview;
factory AppDramaCard.fromJson(Map<String, dynamic> json) => AppDramaCard(
id: json['id'] as int,
title: json['title'] as String? ?? '',
cover: json['cover'] as String? ?? '',
synopsis: json['synopsis'] as String? ?? '',
genre: json['genre'] as String? ?? '',
category: json['category'] as String? ?? '',
totalEpisodes: json['totalEpisodes'] as int? ?? 0,
isHot: json['isHot'] as bool? ?? false,
viewCount: json['viewCount'] as int? ?? 0,
preview: json['preview'] == null
? null
: AppEpisode.fromJson(json['preview'] as Map<String, dynamic>),
);
}
class AppDramaDetail {
AppDramaDetail({
required this.id,
required this.title,
required this.synopsis,
required this.cover,
required this.genre,
required this.category,
required this.totalEpisodes,
required this.freeEpisodes,
required this.unlockCoin,
required this.isHot,
required this.viewCount,
required this.episodes,
});
final int id;
final String title;
final String synopsis;
final String cover;
final String genre;
final String category;
final int totalEpisodes;
final int freeEpisodes;
final int unlockCoin;
final bool isHot;
final int viewCount;
final List<AppEpisode> episodes;
factory AppDramaDetail.fromJson(Map<String, dynamic> json) => AppDramaDetail(
id: json['id'] as int,
title: json['title'] as String? ?? '',
synopsis: json['synopsis'] as String? ?? '',
cover: json['cover'] as String? ?? '',
genre: json['genre'] as String? ?? '',
category: json['category'] as String? ?? '',
totalEpisodes: json['totalEpisodes'] as int? ?? 0,
freeEpisodes: json['freeEpisodes'] as int? ?? 0,
unlockCoin: json['unlockCoin'] as int? ?? 0,
isHot: json['isHot'] as bool? ?? false,
viewCount: json['viewCount'] as int? ?? 0,
episodes: (json['episodes'] as List? ?? const [])
.map((e) => AppEpisode.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
class AppBanner {
AppBanner({
required this.id,
required this.imageUrl,
required this.linkType,
required this.linkValue,
});
final int id;
final String imageUrl;
final String linkType;
final String linkValue;
factory AppBanner.fromJson(Map<String, dynamic> json) => AppBanner(
id: json['id'] as int,
imageUrl: json['imageUrl'] as String? ?? '',
linkType: json['linkType'] as String? ?? '',
linkValue: json['linkValue'] as String? ?? '',
);
}
class AppCategory {
AppCategory({required this.id, required this.name});
final int id;
final String name;
factory AppCategory.fromJson(Map<String, dynamic> json) =>
AppCategory(id: json['id'] as int, name: json['name'] as String? ?? '');
}
class AppSection {
AppSection({
required this.code,
required this.title,
required this.dramas,
});
final String code;
final String title;
final List<AppDramaCard> dramas;
factory AppSection.fromJson(Map<String, dynamic> json) => AppSection(
code: json['code'] as String? ?? '',
title: json['title'] as String? ?? '',
dramas: (json['dramas'] as List? ?? const [])
.map((e) => AppDramaCard.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
class AppFeed {
AppFeed({required this.banners, required this.sections});
final List<AppBanner> banners;
final List<AppSection> sections;
factory AppFeed.fromJson(Map<String, dynamic> json) => AppFeed(
banners: (json['banners'] as List? ?? const [])
.map((e) => AppBanner.fromJson(e as Map<String, dynamic>))
.toList(),
sections: (json['sections'] as List? ?? const [])
.map((e) => AppSection.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
@@ -0,0 +1,33 @@
// Author: fruit.com.cn
import '../models/app_catalog.dart';
import '../services/api_client.dart';
/// 对接后端 `/app/**` 内容接口(A3)。
class HttpCatalogRepository {
HttpCatalogRepository(this._client);
final ApiClient _client;
Future<AppFeed> fetchHomeFeed() async {
final data = await _client.get('/app/home/feed');
return AppFeed.fromJson(data as Map<String, dynamic>);
}
Future<AppFeed> fetchDiscoverFeed() async {
final data = await _client.get('/app/discover/feed');
return AppFeed.fromJson(data as Map<String, dynamic>);
}
Future<List<AppCategory>> fetchCategories() async {
final data = await _client.get('/app/categories');
return (data as List)
.map((e) => AppCategory.fromJson(e as Map<String, dynamic>))
.toList();
}
Future<AppDramaDetail> fetchDramaDetail(int id) async {
final data = await _client.get('/app/dramas/$id');
return AppDramaDetail.fromJson(data as Map<String, dynamic>);
}
}
@@ -0,0 +1,61 @@
// Author: fruit.com.cn
import '../services/api_client.dart';
/// 评论、点赞、举报(A6)。
class InteractionRepository {
InteractionRepository(this._client);
final ApiClient _client;
Future<void> postComment({
required int dramaId,
required String content,
int? rootId,
}) async {
await _client.post('/app/comments', data: {
'dramaId': dramaId,
'content': content,
if (rootId != null) 'rootId': rootId,
});
}
Future<List<dynamic>> fetchComments({
required int dramaId,
int? cursor,
int size = 20,
}) async {
final data = await _client.get('/app/comments', query: {
'dramaId': dramaId,
if (cursor != null) 'cursor': cursor,
'size': size,
});
return data as List;
}
Future<void> deleteComment(int id) async {
await _client.delete('/app/comments/$id');
}
Future<void> toggleLike({
required String targetType,
required int targetId,
}) async {
await _client.post('/app/likes/toggle', data: {
'targetType': targetType,
'targetId': targetId,
});
}
Future<void> report({
required String targetType,
required int targetId,
required String reason,
}) async {
await _client.post('/app/reports', data: {
'targetType': targetType,
'targetId': targetId,
'reason': reason,
});
}
}
@@ -0,0 +1,16 @@
// Author: fruit.com.cn
import '../services/api_client.dart';
/// 播放票据(A4)。
class PlaybackRepository {
PlaybackRepository(this._client);
final ApiClient _client;
/// 签发播放票据与签名地址,客户端原样使用 playUrl。
Future<Map<String, dynamic>> fetchPlayTicket(int episodeId) async {
final data = await _client.get('/app/episodes/$episodeId/play');
return data as Map<String, dynamic>;
}
}
@@ -0,0 +1,25 @@
// Author: fruit.com.cn
import '../services/api_client.dart';
/// 钱包、金币解锁(09 收费支付)。
class WalletRepository {
WalletRepository(this._client);
final ApiClient _client;
Future<Map<String, dynamic>> fetchWallet() async {
final data = await _client.get('/app/wallet');
return data as Map<String, dynamic>;
}
Future<Map<String, dynamic>> unlockEpisode(int episodeId) async {
final data = await _client.post('/app/episodes/$episodeId/unlock');
return data as Map<String, dynamic>;
}
Future<List<dynamic>> fetchOrders() async {
final data = await _client.get('/app/orders');
return data as List;
}
}
@@ -0,0 +1,59 @@
// Author: fruit.com.cn
import '../services/api_client.dart';
/// 观看记录与收藏(A5)。
class WatchRepository {
WatchRepository(this._client);
final ApiClient _client;
Future<void> reportProgress({
required int episodeId,
required int positionSec,
required int durationSec,
}) async {
await _client.post('/app/watch/progress', data: {
'episodeId': episodeId,
'positionSec': positionSec,
'durationSec': durationSec,
});
}
Future<Map<String, dynamic>> fetchProgress(int dramaId) async {
final data = await _client.get('/app/watch/progress', query: {
'dramaId': dramaId,
});
return data as Map<String, dynamic>;
}
Future<List<dynamic>> fetchRecent({int limit = 20}) async {
final data = await _client.get('/app/watch/recent', query: {'limit': limit});
return data as List;
}
Future<void> deleteProgress(int dramaId) async {
await _client.delete('/app/watch/progress/$dramaId');
}
Future<void> clearProgress() async {
await _client.delete('/app/watch/progress');
}
Future<bool> toggleFavorite(int dramaId) async {
final data = await _client.post('/app/favorites/toggle', data: {
'dramaId': dramaId,
});
final map = data as Map<String, dynamic>;
return map['favorited'] as bool? ?? false;
}
Future<List<dynamic>> fetchFavorites() async {
final data = await _client.get('/app/favorites');
return data as List;
}
Future<void> deleteFavorite(int dramaId) async {
await _client.delete('/app/favorites/$dramaId');
}
}