Files
2026-09-15 15:44:13 +07:00

230 lines
8.4 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:dio/dio.dart';
import 'package:hgdj/hj_model/drama/drama_models.dart';
import 'package:hgdj/hj_model/drama_media_info.dart';
import 'package:hgdj/hj_model/list_base_model.dart';
import 'package:hgdj/hj_model/media_content.dart';
import 'package:hgdj/tools_base/net/base_resp_bean.dart';
import 'package:hgdj/tools_base/net/http_manager.dart';
/// 短剧接口。金币解锁已并入 PayManager/BuyService,这里只剩读操作 + 两个计数型写操作:
/// 信息流翻页去重、分享互动计数,都必须带 `X-Request-ID`——同一次业务的重试要复用同一个 id,
/// 换新 id 就会重复计数
class DramaService {
static Options _idempotent(String requestId) =>
Options(headers: {'X-Request-ID': requestId});
/// AI短剧信息流:一条 = 一部剧 + 该剧第 1 集。
/// 排序、去重、已看沉底全在服务端,客户端不得二次排序,也不分页页码——每次请求都是下一批
static Future<ListBaseModel<DramaFeedItem>?> fetchFeed(
{int size = 20, String? requestId}) async {
final result = await httpManager.fetchResponseByGET(
'/media/drama/feed',
param: {'pageSize': size},
options: requestId == null ? null : _idempotent(requestId),
jsonTransformation: (json) => ListBaseModel<DramaFeedItem>.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 短剧专题列表(热门短剧 Tab,按 sort 倒序已由服务端排好)
static Future<List<DramaTopic>?> fetchTopics() async {
final result = await httpManager.fetchResponseByGET(
'/media/drama/topics',
jsonTransformation: (json) =>
(json['list'] as List?)
?.map((e) => DramaTopic.fromJson(e))
.toList() ??
<DramaTopic>[],
);
return result.isSuccess ? result.data : null;
}
/// 专题作品列表(热门短剧橱窗)
static Future<ListBaseModel<DramaMediaInfo>?> fetchTopicWorks(
String topicId, {
int page = 1,
int size = 20,
}) async {
if (topicId.isEmpty) return null;
final result = await httpManager.fetchResponseByGET(
'/media/drama/topic/works',
param: {
'topicId': topicId,
'pageNumber': page,
'pageSize': size,
},
jsonTransformation: (json) =>
ListBaseModel<DramaMediaInfo>.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 搜索短剧:`GET /media/search`[kind] 固定 4
/// [sortType] 1 最新上架、2 最多观看;默认 1
static Future<DramaSearchResult?> search({
required String keyword,
int page = 1,
int size = 20,
int sortType = 1,
}) async {
final result = await httpManager.fetchResponseByGET(
'/media/search',
param: {
'keyword': keyword,
'kind': 4,
'pageNumber': page,
'pageSize': size,
'sortType': sortType,
},
jsonTransformation: (json) => DramaSearchResult.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 短剧详情(剧信息 + resume 续播位置,不含分集)
static Future<DramaMediaInfo?> fetchDetail(String? dramaId) async {
if (dramaId?.isNotEmpty != true) return null;
final result = await httpManager.fetchResponseByGET(
'/media/info',
param: {'id': dramaId},
jsonTransformation: (json) => DramaMediaInfo.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 短剧片库搜索:标签页用它按标签取剧,走的是和 ACG 片库同一个接口。
/// [tagIds] 空/不传 = 不按标签筛;[sortType] 1热门推荐 2最新上架 4最多收藏
static Future<ListBaseModel<DramaMediaInfo>?> searchLibrary({
int page = 1,
int size = 20,
List<String>? tagIds,
int? sortType,
}) async {
final param = <String, dynamic>{
'pageNumber': page,
'pageSize': size,
'mediaType': 'drama',
'sortType': sortType,
'tagIds': tagIds,
}..removeWhere((k, v) => v == null || (v is List && v.isEmpty));
final result = await httpManager.fetchResponseByPOST(
'/media/library/search',
param: param,
jsonTransformation: (json) =>
ListBaseModel<DramaMediaInfo>.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 分集列表(选集面板用) [sortType] 0-正序 1-倒序
static Future<ListBaseModel<MediaContent>?> fetchEpisodeList(
String dramaId, int page, int size) async {
if (dramaId.isEmpty) return null; // 与 fetchDetail 一致:没有剧 id 就别发空请求
final result = await httpManager.fetchResponseByGET(
'/media_content/list',
param: {
'mediaId': dramaId,
'pageNumber': page,
'pageSize': size,
'sortType': 0
},
jsonTransformation: (json) => ListBaseModel<MediaContent>.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 分集详情:播放地址 + canPlay + 未解锁时的 paywall。
/// 列表里的播放地址会过期,每次起播都得重新拉这个接口,不能跨时长缓存
static Future<MediaContent?> fetchEpisode(String? contentId) async {
if (contentId?.isNotEmpty != true) return null;
final result = await httpManager.fetchResponseByGET(
'/media_content/info',
param: {'id': contentId},
jsonTransformation: (json) => MediaContent.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 我的喜欢-短剧
static Future<ListBaseModel<DramaMediaInfo>?> fetchLikes(int page,
{int size = 20}) async {
final result = await httpManager.fetchResponseByGET(
'/mine/like',
param: {'likeType': 'drama', 'pageNumber': page, 'pageSize': size},
jsonTransformation: (json) =>
ListBaseModel<DramaMediaInfo>.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 我的收藏-短剧 [sortType] 0-最新收藏 1-最近更新
static Future<ListBaseModel<DramaMediaInfo>?> fetchFavorites(int page,
{int size = 20, int sortType = 0}) async {
final result = await httpManager.fetchResponseByGET(
'/media_bookshelf/list',
param: {
'type': 'drama',
'sortType': sortType,
'pageNumber': page,
'pageSize': size
},
jsonTransformation: (json) =>
ListBaseModel<DramaMediaInfo>.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 我的已购-短剧(按剧去重,点进去看每集的 hasBuy)
static Future<ListBaseModel<DramaMediaInfo>?> fetchPurchased(int page,
{int size = 20}) async {
final result = await httpManager.fetchResponseByGET(
'/media/my_buy',
param: {'mediaType': 'drama', 'pageNumber': page, 'pageSize': size},
jsonTransformation: (json) =>
ListBaseModel<DramaMediaInfo>.fromJson(json),
);
return result.isSuccess ? result.data : null;
}
/// 短剧单集下载授权:**下载链路唯一入口**。
/// 登录态、上下架、短剧权益、钱包下载次数校验 + 扣 1 次 + 幂等,服务端一次做完并回下载地址,
/// 前端不要再调 `/mine/privilege/consume` 或 `/mine/download/use`——那两个只扣次不给资源,会重复扣。
/// [requestId] 同一次下载(含超时重试、刷新过期地址)必须复用,换新的会再扣一次;
/// 返回整个 [BaseRespBean]:失败要按 code 分流引导,不能只看有没有数据
static Future<BaseRespBean> authorizeDownload({
required String mediaId,
required String contentId,
required String requestId,
}) {
return httpManager.fetchResponseByPOST(
'/media/drama/download/authorize',
param: {'mediaId': mediaId, 'contentId': contentId},
options: _idempotent(requestId),
jsonTransformation: (json) => DramaDownloadAuth.fromJson(json),
);
}
/// 分享落地:服务端按 [eventId] 幂等累计分享互动数,返回二维码 PNG 的 base64
static Future<String?> shareOutput({
required String content,
required String? mediaId,
String? contentId,
required String eventId,
}) async {
final result = await httpManager.fetchResponseByPOST(
'/share/output',
param: {
'content': content,
'objType': 'drama',
'mediaID': mediaId,
'contentID': contentId ?? '',
'eventId': eventId,
},
options: _idempotent(eventId),
);
return result.isSuccess
? (result.data is Map ? result.data['qrCode'] : null)
: null;
}
}