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?> 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.fromJson(json), ); return result.isSuccess ? result.data : null; } /// 短剧专题列表(热门短剧 Tab,按 sort 倒序已由服务端排好) static Future?> fetchTopics() async { final result = await httpManager.fetchResponseByGET( '/media/drama/topics', jsonTransformation: (json) => (json['list'] as List?) ?.map((e) => DramaTopic.fromJson(e)) .toList() ?? [], ); return result.isSuccess ? result.data : null; } /// 专题作品列表(热门短剧橱窗) static Future?> 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.fromJson(json), ); return result.isSuccess ? result.data : null; } /// 搜索短剧:`GET /media/search`,[kind] 固定 4 /// [sortType] 1 最新上架、2 最多观看;默认 1 static Future 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 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?> searchLibrary({ int page = 1, int size = 20, List? tagIds, int? sortType, }) async { final param = { '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.fromJson(json), ); return result.isSuccess ? result.data : null; } /// 分集列表(选集面板用) [sortType] 0-正序 1-倒序 static Future?> 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.fromJson(json), ); return result.isSuccess ? result.data : null; } /// 分集详情:播放地址 + canPlay + 未解锁时的 paywall。 /// 列表里的播放地址会过期,每次起播都得重新拉这个接口,不能跨时长缓存 static Future 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?> fetchLikes(int page, {int size = 20}) async { final result = await httpManager.fetchResponseByGET( '/mine/like', param: {'likeType': 'drama', 'pageNumber': page, 'pageSize': size}, jsonTransformation: (json) => ListBaseModel.fromJson(json), ); return result.isSuccess ? result.data : null; } /// 我的收藏-短剧 [sortType] 0-最新收藏 1-最近更新 static Future?> 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.fromJson(json), ); return result.isSuccess ? result.data : null; } /// 我的已购-短剧(按剧去重,点进去看每集的 hasBuy) static Future?> 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.fromJson(json), ); return result.isSuccess ? result.data : null; } /// 短剧单集下载授权:**下载链路唯一入口**。 /// 登录态、上下架、短剧权益、钱包下载次数校验 + 扣 1 次 + 幂等,服务端一次做完并回下载地址, /// 前端不要再调 `/mine/privilege/consume` 或 `/mine/download/use`——那两个只扣次不给资源,会重复扣。 /// [requestId] 同一次下载(含超时重试、刷新过期地址)必须复用,换新的会再扣一次; /// 返回整个 [BaseRespBean]:失败要按 code 分流引导,不能只看有没有数据 static Future 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 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; } }