446 lines
16 KiB
Dart
446 lines
16 KiB
Dart
import 'package:analytics_sdk/analytics_sdk.dart';
|
|
import 'package:analytics_sdk/manager/page_name_manager.dart';
|
|
import 'package:analytics_sdk/entity/ad_click_event.dart';
|
|
import 'package:analytics_sdk/entity/ad_impression_event.dart';
|
|
import 'package:analytics_sdk/entity/comic_event.dart';
|
|
import 'package:analytics_sdk/entity/keyword_click_event.dart';
|
|
import 'package:analytics_sdk/entity/keyword_search_event.dart';
|
|
import 'package:analytics_sdk/entity/novel_event.dart';
|
|
import 'package:analytics_sdk/entity/video_event.dart';
|
|
import 'package:analytics_sdk/enum/click_item_type_enum.dart';
|
|
import 'package:analytics_sdk/enum/read_behavior_enum.dart';
|
|
import 'package:analytics_sdk/enum/video_content_type_enum.dart';
|
|
import 'package:analytics_sdk/enum/video_event_enum.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:jydj/tools_base/debug_log.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
import '../hj_model/cartoon_media_info.dart';
|
|
import '../hj_model/media_content.dart';
|
|
import '../hj_model/splash/ads_model.dart';
|
|
import '../hj_model/video_model.dart';
|
|
|
|
//页面枚举
|
|
enum TrackEventPageType {
|
|
start('start', '启动页'), // 启动页广告
|
|
home('home', '首页'), // 首页公告弹窗
|
|
acgHome('acg_home', 'acg首页'), // 首页acg
|
|
live('live', '直播列表'), // 直播顶部广告
|
|
liveDetail('live_detail', '直播详情页'), // 直播详情页广告
|
|
commPost('comm_post', '社区首页'), // 顶部公共广告
|
|
commImg('comm_img', '社区图集'), // 顶部公共广告
|
|
commNovel('comm_novel', '社区小说'), // 顶部公共广告
|
|
commGroup('comm_group', '社区群聊'), // 顶部公共广告
|
|
darkweb('darkweb', '暗网'), // 直播顶部广告
|
|
mine('mine', '我的'), // 我的页面广告
|
|
search('search', '搜索页'), // 搜索页广告
|
|
videoPlay('video_play', '视频播放页'), // 视频播放页广告
|
|
topicDetail('topic_detail', '专题详情页'), // 专题详情顶部广告
|
|
appList('app_list', '应用推荐'), // 应用等
|
|
postDetail('post_detail', '社区详情页'), // 应用等
|
|
imChatCenter('im_chat_center', '私信中心'), // IM 私信会话列表
|
|
imChatDetail('im_chat_detail', '私信详情'); // IM 私信聊天页
|
|
|
|
final String pagekey;
|
|
final String pageName;
|
|
|
|
const TrackEventPageType(this.pagekey, this.pageName);
|
|
}
|
|
|
|
//搜索枚举
|
|
enum TrackEventSearchType {
|
|
video('video', 'video'), // 视频
|
|
comic('comic', '漫画'), // 漫画
|
|
acg('acg', 'acg'), // acg
|
|
user('user', '用户'), // 用户
|
|
image('image', 'image'), // image
|
|
post('post', '帖子'); // 帖子
|
|
|
|
final String clickTypeKey;
|
|
final String clickTypeName;
|
|
|
|
const TrackEventSearchType(this.clickTypeKey, this.clickTypeName);
|
|
}
|
|
|
|
/// 广告埋点类型(对应 payload 的 ad_type 字段)
|
|
enum AdType {
|
|
smallIcon('small_icon'),
|
|
verticalIcon('vertical_icon'),
|
|
moreIcon('more_icon'),
|
|
bigBanner('big_banner'),
|
|
smallBanner('small_banner'),
|
|
banner('banner'),
|
|
feed('feed'),
|
|
splash('splash'),
|
|
dialog('dialog');
|
|
|
|
/// 上报字符串值
|
|
final String value;
|
|
|
|
const AdType(this.value);
|
|
|
|
/// 字符串反查枚举,找不到返回 null
|
|
static AdType? fromValue(String? value) => values.firstWhereOrNull((e) => e.value == value);
|
|
}
|
|
|
|
class AdExposureModel {
|
|
final TrackEventPageType? pageType; //页面位置信息
|
|
AdType? adType; //广告类型,banner 类组件会就地改写(如 ..adType = AdType.bigBanner)
|
|
final int? tabIndex; //tabIndex
|
|
|
|
AdExposureModel({
|
|
this.pageType,
|
|
this.adType,
|
|
this.tabIndex,
|
|
});
|
|
}
|
|
|
|
/// 广告曝光统一管理(业务级)
|
|
class AdExposureManager {
|
|
static final Set<String> _exposedKeys = {};
|
|
|
|
/// 返回 true = 允许曝光
|
|
static bool tryExpose(String key) => _exposedKeys.add(key);
|
|
}
|
|
|
|
//广告曝光,只记录一次曝光
|
|
mixin AdExposureMixin {
|
|
void adExposure({
|
|
required AdsInfoModel adInfo,
|
|
AdExposureModel? exposureModel,
|
|
}) {
|
|
final pageKey = exposureModel?.pageType?.pagekey ?? '';
|
|
final slotKey = adInfo.position?.toString() ?? '';
|
|
final adId = adInfo.id?.toString() ?? '';
|
|
|
|
// 业务唯一曝光 Key,最终裁判在这里
|
|
final exposureKey = '$pageKey|$slotKey|$adId|tab${exposureModel?.tabIndex ?? 0}';
|
|
if (!AdExposureManager.tryExpose(exposureKey)) return;
|
|
|
|
AnalyticsSdk.instance.track(AdImpressionEvent(
|
|
pageKey: pageKey,
|
|
pageName: exposureModel?.pageType?.pageName ?? '',
|
|
adSlotKey: slotKey,
|
|
adId: adId,
|
|
creativeId: adId,
|
|
adType: exposureModel?.adType?.value ?? '', // 传广告类型字符串,不传默认空字符串
|
|
));
|
|
}
|
|
}
|
|
|
|
final adClickManager = AdClickManager();
|
|
|
|
//广告点击
|
|
class AdClickManager {
|
|
// 单例
|
|
static final AdClickManager _ins = AdClickManager._();
|
|
|
|
factory AdClickManager() => _ins;
|
|
|
|
AdClickManager._();
|
|
|
|
//广告点击事件
|
|
void adClicks({
|
|
required AdsInfoModel? adInfo,
|
|
required Map<String, dynamic> payload,
|
|
TrackEventPageType? page,
|
|
}) {
|
|
if (adInfo == null) return;
|
|
try {
|
|
final adId = adInfo.id?.toString() ?? '';
|
|
AnalyticsSdk.instance.track(AdClickEvent(
|
|
pageKey: page?.pagekey ?? '',
|
|
pageName: page?.pageName ?? '',
|
|
adSlotKey: adInfo.position?.toString() ?? '',
|
|
adId: adId,
|
|
creativeId: adId,
|
|
adType: payload['ad_type'] ?? '',
|
|
));
|
|
} catch (e) {
|
|
debugLog('adClicks', e.toString());
|
|
}
|
|
}
|
|
}
|
|
|
|
final searchClickManager = SearchClickManager();
|
|
|
|
class SearchClickManager {
|
|
// 单例
|
|
static final SearchClickManager _ins = SearchClickManager._();
|
|
|
|
factory SearchClickManager() => _ins;
|
|
|
|
SearchClickManager._();
|
|
|
|
//搜索埋点
|
|
void searchKeyword({
|
|
required String? keyword, //搜索关键词
|
|
required int? count, //搜索结果数量
|
|
required String? traceId, //搜索引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。
|
|
required String? searchId, //搜索引擎列表的video_id(视频),novel_id(小说),comic_id(漫画), 可视窗口有多个,使用英文逗号分隔。
|
|
}) {
|
|
AnalyticsSdk.instance.track(KeywordSearchEvent(
|
|
keyword: keyword ?? '',
|
|
searchResultCount: count ?? 0,
|
|
searchTraceId: traceId ?? '', // 未接搜索引擎时传空字符串
|
|
searchId: searchId ?? '', // 未接搜索引擎时传空字符串
|
|
));
|
|
}
|
|
|
|
//搜索点击事件
|
|
void searchClicks({
|
|
required String? keyword,
|
|
required TrackEventSearchType clickType,
|
|
required String? id,
|
|
required int? index,
|
|
required String? traceId, //搜索引擎的trace_id。没有对接搜索推荐引擎的上报空字符串。
|
|
}) {
|
|
AnalyticsSdk.instance.track(KeywordClickEvent(
|
|
keyword: keyword ?? "",
|
|
clickItemId: id ?? '',
|
|
clickItemType: ClickItemTypeEnum(clickType.clickTypeKey, clickType.clickTypeName),
|
|
clickPosition: (index ?? 0) + 1,
|
|
searchTraceId: traceId ?? '', // 未接搜索引擎时传空字符串
|
|
));
|
|
}
|
|
}
|
|
|
|
class TrackVideoManager {
|
|
//上次上报播放完成的秒级时间戳,用于防重
|
|
static int _lastComplete = 0;
|
|
|
|
//埋点字段不收空值,统一兜默认值
|
|
static String _or(String? value, String fallback) => (value?.isEmpty ?? true) ? fallback : value!;
|
|
|
|
//视频事件
|
|
static void send(
|
|
VideoEventEnum event,
|
|
VideoModel? model,
|
|
VideoPlayerController? ctr, {
|
|
bool isPreVideo = false,
|
|
}) {
|
|
if (isPreVideo) return; //预览视频的不上报
|
|
if (ctr == null) return; //播放器为空不上报
|
|
final duration = ctr.value.duration.inSeconds;
|
|
if (duration == 0) return; //时长为0不上报,避免除0异常
|
|
final position = ctr.value.position.inSeconds;
|
|
final progress = position * 100 ~/ duration;
|
|
|
|
if (event == VideoEventEnum.VIDEO_COMPLETE) {
|
|
if (progress < 90) {
|
|
//播放进度不足90%, 转为播放事件上报
|
|
event = VideoEventEnum.VIDEO_PLAY;
|
|
} else {
|
|
// 自动播放完,业务监听会多次处理, 视频播放完成事件防重
|
|
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
|
if (now - _lastComplete < 2) {
|
|
debugLog("video complete 重复处理");
|
|
return;
|
|
}
|
|
_lastComplete = now;
|
|
}
|
|
}
|
|
|
|
//新增SDK埋点
|
|
AnalyticsSdk.instance.track(VideoEvent(
|
|
videoId: model?.id ?? '',
|
|
videoTitle: model?.title ?? '',
|
|
videoTypeId: _or(model?.videoTypeId, 'default'),
|
|
videoTypeName: _or(model?.videoTypeName, '默认分类'),
|
|
videoTagKey: _or(model?.tags?.map((e) => e.id).join(','), 'default'),
|
|
videoTagName: _or(model?.tags?.map((e) => e.name).join(','), '默认标签'),
|
|
videoDuration: duration,
|
|
playDuration: position,
|
|
playProgress: progress,
|
|
videoBehavior: event,
|
|
videoContentType: model?.contentType == 'video' ? VideoContentTypeEnum.video : VideoContentTypeEnum.shortVideo,
|
|
recommendTraceId: '',
|
|
// 未接推荐引擎传 ''
|
|
mediaId: model?.lsjId ?? '',
|
|
));
|
|
}
|
|
|
|
//漫画行为 展示, 下一页, 上一页,读完
|
|
static void comicEvent(
|
|
ReadBehaviorEnum behavior, //行为
|
|
{
|
|
CartoonMediaInfo? model,
|
|
int? readProgress = 0, //章节百分比
|
|
int? pageNo = 1,
|
|
MediaContent? mediaContent,
|
|
}) {
|
|
AnalyticsSdk.instance.track(ComicEvent(
|
|
comicId: model?.id ?? '',
|
|
comicTitle: model?.title ?? '',
|
|
comicTypeId: _or(model?.mId, 'default'),
|
|
comicTypeName: _or(model?.moduleName, '默认分类'),
|
|
comicTagKey: _or(model?.tagIds, 'default'),
|
|
comicTagName: _or(model?.tagNames, '默认标签'),
|
|
readProgress: readProgress ?? 0,
|
|
pageNo: pageNo ?? 0,
|
|
comicBehavior: behavior,
|
|
mediaId: model?.lsjId ?? '',
|
|
chapterId: _or(mediaContent?.id, '1'),
|
|
chapterName: _or(mediaContent?.name, '第一章'),
|
|
));
|
|
}
|
|
|
|
//小说行为 展示, 下一页, 上一页,读完
|
|
static void novelEvent(
|
|
ReadBehaviorEnum behavior, {
|
|
CartoonMediaInfo? model, //小说模型
|
|
int? readProgress = 0, //章节百分比
|
|
int? pageNo = 1,
|
|
String? chapterId,
|
|
String? chapterName,
|
|
}) {
|
|
AnalyticsSdk.instance.track(NovelEvent(
|
|
novelId: model?.id ?? '',
|
|
novelTitle: model?.title ?? '',
|
|
novelTypeId: _or(model?.mId, 'default'),
|
|
novelTypeName: _or(model?.moduleName, '默认分类'),
|
|
novelTagKey: _or(model?.tagIds, 'default'),
|
|
novelTagName: _or(model?.tagNames, '默认标签'),
|
|
readProgress: readProgress ?? 0,
|
|
pageNo: pageNo ?? 0,
|
|
novelBehavior: behavior,
|
|
mediaId: model?.lsjId ?? '',
|
|
chapterId: _or(chapterId, '1'),
|
|
chapterName: _or(chapterName, '第一章'),
|
|
));
|
|
}
|
|
}
|
|
|
|
/// 页面 pageKey -> 中文名映射,app 启动时注册一次(main.dart)
|
|
/// key 就是 Get.to 自动生成的路由名(去掉斜杠后的页面类名),改类名要同步改这里
|
|
void addMappings() {
|
|
PageNameMapper.addMappings({
|
|
'AiHomePage': 'AI首页',
|
|
'AiSquarePage': 'AI广场',
|
|
'CartoonDetailPage': '漫画详情页',
|
|
'CartoonReadPage': '漫画阅读页',
|
|
'CartoonRecommendPage': '漫画推荐页',
|
|
'CartoonSectionListPage': '漫画专题更多页',
|
|
'CartoonSectionSubPage': '漫画专题列表页',
|
|
'CartoonSubGridPage': '漫画海角页',
|
|
'CartoonSubModulePage': '漫画亚模块页',
|
|
'NovelDetailPage': '小说详情页',
|
|
'TextNovelReadPage': '文字小说阅读页',
|
|
'VoiceNovelReadPage': '有声小说阅读页',
|
|
'CommunityModulePage': '社区模块页',
|
|
'CommunityTabPage': '社区Tab页',
|
|
'SourceSubModulePage': '社区资源亚模块页',
|
|
'CommunityMainPage': '社区主页',
|
|
'TagDetailSubView': '标签详情页',
|
|
'CommunityTagDetailPage': '社区标签详情页',
|
|
'CommunityImagePage': '社区图片详情页',
|
|
'PostDetailPage': '社区帖子详情页',
|
|
'ChoseTopicPage': '发布选择话题页',
|
|
'PublishPage': '帖子发布页',
|
|
'PublishRulePage': '发布规则页',
|
|
'PicsDetailPage': '色图详情页',
|
|
'HomeTabSectionPage': '首页Tab专题页',
|
|
'HomeTabSortPage': '首页Tab分类页',
|
|
'SearchACGResultPage': '搜索ACG结果页',
|
|
'SearchResultMainPage': '搜索结果主页面',
|
|
'SearchVideoResultPage': '搜索视频结果页',
|
|
'SectionAllPage': '原创达人',
|
|
'SpecialTopicsDetailPage': '视频专题详情页',
|
|
'CartoonTagPage': '漫画标签页',
|
|
'VideoTagPage': '视频标签页',
|
|
'DramaTagPage': '短剧标签页', //DramaTagDetailPage.routeName,改名要同步
|
|
'DarkHomePage': '暗网首页',
|
|
'HomeMainPage': '视频主页',
|
|
'LiveDetailPage': '直播详情页',
|
|
'LiveMainPage': '直播主页',
|
|
'LiveSubPage': '直播子页面',
|
|
'MainPage': '主页面',
|
|
'AiPaintPage': 'AI绘画页',
|
|
'AiItvPage': 'AI图生视频页',
|
|
'AINovelSubPage': 'AI小说首页',
|
|
'AiNovelDetailPage': 'AI小说详情页',
|
|
'AIChangeFacePage': 'AI换脸页',
|
|
'AIFaceChangeSubPage': 'AI换脸子页',
|
|
'AIStripSubPage': 'AI脱衣首页',
|
|
'AIGrilFriendPage': 'AI女友页',
|
|
'AiH5Page': 'AI H5页',
|
|
'AIMainRecordPage': 'AI生成记录主页',
|
|
'AiMainItvPage': 'AI图生视频主页',
|
|
'AIModDetailPage': 'AI换脸模版页',
|
|
'AiRecordPage': 'AI生成记录子页',
|
|
'AdvancePage': '高级设置页',
|
|
'ExchangeVipPage': '兑换会员页',
|
|
'IntegralExchangePage': '积分兑换页',
|
|
'OnlinePayPage': '在线支付页',
|
|
'MineCollectPage': '我的收藏页',
|
|
'MineBuyPage': '我的购买页',
|
|
'MineHistoryPage': '历史记录页',
|
|
'MineLikePage': '我的喜欢页',
|
|
'MineAccountIdentityPage': '账号凭证页',
|
|
'RecordsPage': '明细综合页',
|
|
'WithdrawalPage': '提现页',
|
|
'MessageCenterPage': '消息中心页',
|
|
'ChatDetailPage': '私信详情页',
|
|
'MineDrivePage': '加群开车页',
|
|
'MineFollowSubPage': '关注子页面',
|
|
'MineFollowingPage': '我的关注页',
|
|
'MinePublishPostPage': '创作中心',
|
|
'BankCardHomePage': '银行卡管理页',
|
|
'BindPhonePage': '绑定手机号页',
|
|
'InviteBindPage': '绑定邀请码',
|
|
'MineExchangeCodePage': '兑换码页',
|
|
'MineFindAccountPage': '找回账号',
|
|
'MinePasswordPage': '手势密码',
|
|
'MineScanLoginPage': '扫码登录',
|
|
'MineSettingPage': '设置中心',
|
|
'MineSettingProfilePage': '设置资料页',
|
|
'SettingProfilePage': '编辑资料',
|
|
'SettingAvatarPage': '设置头像',
|
|
'SettingNickNamePage': '设置昵称',
|
|
'MineSharePage': '分享邀请',
|
|
'MineShareRecordPage': '邀请记录',
|
|
'VideoCachePage': '下载缓存',
|
|
'MineChargeCoinPage': '金币充值',
|
|
'MineChargeVipPage': '会员充值',
|
|
'MineFeedbackPage': '意见反馈',
|
|
'MineHelpPage': '帮助反馈',
|
|
'MineQAPage': '常见问题',
|
|
'LouFengAdPage': '应用推荐',
|
|
'MineIncomePage': '分享邀请',
|
|
'RecommendAppPage': '推荐应用页',
|
|
'MoreFreeVideoPage': '更多免费视频',
|
|
'ShareDataListPage': '推广数据列表',
|
|
'ShareDetailsPage': '推广数据',
|
|
'WelfareHomePage': '应用推荐首页',
|
|
'SignDailyPage': '每日签到',
|
|
'HomeMinePage': '我的',
|
|
'PreSalePage': '预售详情页',
|
|
'ShortVideoMainPage': '抖音首页',
|
|
'ShortVideoPage': '抖音',
|
|
'SplashPage': '启动页',
|
|
'RankMainPage': '排行榜主页',
|
|
'RankSubPage': '排行榜子页',
|
|
'UserCenterPage': '用户首页',
|
|
'GroupChatPage': '群聊页',
|
|
'GroupChatDetailPage': '群聊详情页',
|
|
'SimpleVideoPlayerPage': '简单视频播放页',
|
|
'VideoFullPage': '全屏视频播放',
|
|
'VideoPage': '视频播放页',
|
|
'H5Page': 'H5页面',
|
|
'SearchMainPage': '搜索页',
|
|
'VideoAllTypePage': '视频片库',
|
|
'AcgGameRecommendPage': 'ACG游戏推荐页',
|
|
'AcgGameDetailPage': 'ACG游戏详情页',
|
|
'CartoonSubDetailPage': '漫画子详情页',
|
|
'MessageCenterSubPage': '消息中心子页',
|
|
'MinePublishSubPage': '创作中心子页',
|
|
'Home': '主页Tab',
|
|
'Community': '社区Tab',
|
|
'ShortVideo': '抖音Tab',
|
|
'AI': 'AI Tab',
|
|
'Mine': '我的Tab',
|
|
'Pic': ' 色图Tab',
|
|
'Novel': '小说Tab',
|
|
});
|
|
}
|