#44 用户操作日志上报

ادغام شده
nieyuge 3 کامیت ادغام شده از Web/feature_add_report به Web/master 1 ماه پیش

+ 200 - 0
src/hooks/useLogger.ts

@@ -0,0 +1,200 @@
+import http from '@src/http';
+import { uploadLogApi } from '@src/http/api';
+import sso from '@src/http/sso';
+import { VideoItem } from '@src/views/publishContent/types';
+import { VideoLibraryType, VideoSortType } from '@src/views/publishContent/weCom/components/videoSelectModal';
+import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
+
+type NormalLogParams = {
+	traceId: number;
+	requestId: number;
+	planType?: VideoSearchPlanType;
+	gzhAccountId?: number;
+	subChannel?: string;
+}
+
+type VideoListQueryLogParams = {
+	category?: string;
+	title?: string;
+	recentNotUsed?: number;
+	sortType?: VideoSortType;
+	tags?: string[];
+	videoLibraryType?: VideoLibraryType;
+} & NormalLogParams;
+
+type VideoLogParams = {
+	videoId: number;
+	score?: number;
+	tags?: string[];
+	title?: string;
+	cover?: string;
+	libraryType?: VideoLibraryType;
+	playTime?: number;
+	idx?: number;
+	collect?: number;
+} & NormalLogParams;
+
+interface PublishPlanLogParams extends NormalLogParams {
+	videoList: VideoItem[];
+}
+
+const useLogger = () => {
+	const token = sso.getUserInfo()?.token;
+	const channel = sso.getUserInfo()?.channel;
+	const userId = sso.getUserInfo()?.id;
+	const timestamp = Date.now();
+	const userAgent = navigator.userAgent;
+	const page = window.location.pathname;
+	const uploadLog = async ({ businessType, objectType, extParams }: { businessType: string, objectType?: string, extParams?: Record<string, any> }) => {
+		const log = {
+			token,
+			channel,
+			userId,
+			page,
+			userAgent,
+			timestamp,
+			businessType,
+			objectType,
+			extParams,
+		};
+		const response = await http.post(uploadLogApi, {
+			key: 'content-platform-user-behavior',
+			data: log,
+		});
+		return response;
+	}
+
+	const uploadLogVideoListQuery = ({
+		traceId,
+		requestId,
+		planType,
+		gzhAccountId,
+		subChannel,
+		category,
+		title,
+		recentNotUsed,
+		sortType,
+		tags,
+		videoLibraryType,
+	}:VideoListQueryLogParams) => {
+		return uploadLog({
+			businessType: 'video_list_query',
+			objectType: 'video_list',
+			extParams: {
+				traceId,
+				requestId,
+				planType,
+				gzhAccountId,
+				subChannel,
+				category,
+				title,
+				recentNotUsed,
+				sortType,
+				tags,
+				videoLibraryType,
+			},
+		});
+	}
+
+	const uploadLogVideoPlay = ({videoId, traceId, requestId, planType, gzhAccountId, subChannel}: VideoLogParams) => {
+		return uploadLog({
+			businessType: 'video_play',
+			objectType: 'video',
+			extParams: {
+				videoId,
+				traceId,
+				requestId,
+				planType,
+				gzhAccountId,
+				subChannel,
+			},
+		});
+	}
+
+	const uploadLogVideoPlayEnd = ({videoId, playTime, traceId, requestId, planType, gzhAccountId, subChannel}: VideoLogParams) => {
+		return uploadLog({
+			businessType: 'video_play_end',
+			objectType: 'video',
+			extParams: {
+				videoId,
+				playTime,
+				traceId,
+				requestId,
+				planType,
+				gzhAccountId,
+				subChannel,
+			},
+		});
+	}
+
+	const uploadLogVideoView = ({videoId, idx, score, tags, title, cover, libraryType, traceId, requestId, planType, gzhAccountId, subChannel}: VideoLogParams) => {
+		return uploadLog({
+			businessType: 'video_view',
+			objectType: 'video',
+			extParams: {
+				videoId,
+				idx,
+				score,
+				tags,
+				title,
+				cover,
+				libraryType,
+				traceId,
+				requestId,
+				planType,
+				gzhAccountId,
+				subChannel,
+			},
+		});
+	}
+
+	const uploadLogVideoCollect = ({videoId, traceId, requestId, collect, planType, gzhAccountId, subChannel}: VideoLogParams) => {
+		return uploadLog({
+			businessType: 'video_collect',
+			objectType: 'video',
+			extParams: {
+				videoId,
+				traceId,
+				requestId,
+				collect,
+				planType,
+				gzhAccountId,
+				subChannel,
+			},
+		});
+	}
+
+	const uploadLogVideoCreatePublish = ({
+		videoList,
+		traceId,
+		requestId,
+		planType,
+		gzhAccountId,
+		subChannel
+	}: PublishPlanLogParams) => {
+		return uploadLog({
+			businessType: 'publish_plan_create',
+			objectType: 'publish_plan',
+			extParams: {
+				videoList,
+				traceId,
+				requestId,
+				planType,
+				gzhAccountId,
+				subChannel,
+			},
+		});
+	}
+
+	return {
+		uploadLog,
+		uploadLogVideoPlay,
+		uploadLogVideoView,
+		uploadLogVideoPlayEnd,
+		uploadLogVideoCollect,
+		uploadLogVideoListQuery,
+		uploadLogVideoCreatePublish,
+	};
+};
+
+export default useLogger;

+ 3 - 0
src/http/api.ts

@@ -63,3 +63,6 @@ export const uploadDeleteVideo = `${import.meta.env.VITE_API_URL}/contentPlatfor
 // 设置绑定微信用户
 export const getBindPQUserInfo = `${import.meta.env.VITE_API_URL}/contentPlatform/setting/getBindPQUserInfo`
 export const bindPQUser = `${import.meta.env.VITE_API_URL}/contentPlatform/setting/webLogin`
+
+// 日志上报
+export const uploadLogApi = `${import.meta.env.VITE_API_URL}/contentPlatform/logHub/upload`

+ 32 - 0
src/views/publishContent/types.ts

@@ -0,0 +1,32 @@
+
+
+export enum CollectedStatusEnum {
+	已收藏 = 1,
+	未收藏 = 0,
+}
+
+export enum VideoStatusEnum {
+	已下架 = 0,
+	正常 = 1,
+}
+export interface VideoItem {
+  videoId: number;
+  video: string;
+  title: string;
+  cover: string;
+  customCover?: string;
+  customCoverType?: number;
+  customTitle?: string;
+  score?: number;
+  pageUrl?: string;
+  shareCover?: string;
+  industryFissionRate?: number;
+  channelFissionRate?: number;
+  videoLibraryType?: number;
+  recommendScore?: number;
+  tags?: string[];
+	scene?: 0 | 1;
+	collect?: CollectedStatusEnum;
+	status?: VideoStatusEnum;
+	requestId?: number;
+}

+ 11 - 1
src/views/publishContent/weCom/components/addPlanModal/index.tsx

@@ -2,10 +2,11 @@
 
 import React, { useEffect, useState } from 'react';
 import { Modal, Form, Input, Select, Button, Card, Typography, message } from 'antd';
-import { WeComPlanType, WeVideoItem, AddWeComPlanParam } from '../../type';
+import { WeComPlanType, WeVideoItem, AddWeComPlanParam, VideoSearchPlanType } from '../../type';
 import { CloseOutlined, PlusOutlined, CaretRightFilled } from '@ant-design/icons';
 import VideoSelectModal from '../videoSelectModal';
 import VideoPlayModal from '../videoPlayModal';
+import useLogger from '@src/hooks/useLogger';
 
 const { Paragraph } = Typography;
 
@@ -21,6 +22,7 @@ const AddPlanModal: React.FC<{
 	const [selectedVideos, setSelectedVideos] = useState<WeVideoItem[]>([]);
 	const [isVideoSelectVisible, setIsVideoSelectVisible] = useState(false);
 	const [playingVideo, setPlayingVideo] = useState<WeVideoItem | null>(null);
+	const { uploadLogVideoCreatePublish } = useLogger();
 	
 	useEffect(() => {
 		form.setFieldsValue({
@@ -51,6 +53,14 @@ const AddPlanModal: React.FC<{
 				message.error('请选择发布内容');
 				return;
 			}
+			// 上报日志
+			uploadLogVideoCreatePublish({
+				videoList: selectedVideos,
+				traceId: Date.now(),
+				requestId: Date.now(),
+				planType: type === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+				subChannel: values.subChannel || 'weCom',
+			});
 			onOk({
 				type: values.type,
 				subChannel: values.subChannel,

+ 55 - 6
src/views/publishContent/weCom/components/videoSelectModal/index.tsx

@@ -18,6 +18,7 @@ import { getVideoContentListApi, getUploadVideoContentListApi } from '@src/http/
 import { useVideoCategoryOptions } from '@src/views/publishContent/weGZH/hooks/useVideoCategoryOptions';
 import { WeComPlanType, WeVideoItem, VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
 import { enumToOptions } from '@src/utils/helper';
+import useLogger from '@src/hooks/useLogger';
 
 export enum VideoLibraryType {
 	平台视频库 = 0,
@@ -56,12 +57,25 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 	const [playingVideo, setPlayingVideo] = useState<WeVideoItem | null>(null);
 	const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(VideoLibraryType.平台视频库);
 	const MAX_SELECTION = 3;
+	const { uploadLogVideoPlay, uploadLogVideoPlayEnd, uploadLogVideoCollect, uploadLogVideoListQuery } = useLogger();
 
 	const getVideoList = async (pageNum?: number, _pageSize?: number) => {
 		setLoading(true);
 		setCurrentPage(pageNum || currentPage);
 		setPageSize(_pageSize || pageSize);
 
+		// 上报视频列表查询日志
+		uploadLogVideoListQuery({
+			traceId: Date.now(),
+			requestId: Date.now(),
+			planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+			subChannel: 'weCom',
+			category,
+			title: searchTerm,
+			sortType: sort,
+			videoLibraryType,
+		});
+
 		// 根据视频库类型选择不同的API
 		const apiUrl = videoLibraryType === VideoLibraryType.平台视频库 ? getVideoContentListApi : getUploadVideoContentListApi;
 
@@ -89,12 +103,10 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 
 	// 监听视频库类型变化,重新加载数据
 	useEffect(() => {
-		getVideoList();
-	}, [videoLibraryType]);
-
-	useEffect(() => {
-		getVideoList();
-	}, []);
+		if (visible) {
+			getVideoList();
+		}
+	}, [videoLibraryType, visible]);
 
 	useEffect(() => {
 		if (visible) {
@@ -114,12 +126,30 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 			const newSet = new Set(prev);
 			if (newSet.has(videoId)) {
 				newSet.delete(videoId);
+				// 上报取消收藏日志
+				uploadLogVideoCollect({
+					videoId,
+					traceId: Date.now(),
+					requestId: Date.now(),
+					collect: 0,
+					planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+					subChannel: 'weCom',
+				});
 			} else {
 				if (newSet.size >= MAX_SELECTION) {
 					message.warning(`最多只能选择 ${MAX_SELECTION} 条视频`);
 					return prev;
 				}
 				newSet.add(videoId);
+				// 上报收藏日志
+				uploadLogVideoCollect({
+					videoId,
+					traceId: Date.now(),
+					requestId: Date.now(),
+					collect: 1,
+					planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+					subChannel: 'weCom',
+				});
 			}
 			return newSet;
 		});
@@ -135,10 +165,29 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 	};
 
 	const playVideo = (video: WeVideoItem) => {
+		// 上报视频播放日志
+		uploadLogVideoPlay({
+			videoId: video.videoId,
+			traceId: Date.now(),
+			requestId: Date.now(),
+			planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+			subChannel: 'weCom',
+		});
 		setPlayingVideo(video);
 	};
 
 	const closeVideoPlayer = () => {
+		if (playingVideo) {
+			// 上报视频播放结束日志
+			uploadLogVideoPlayEnd({
+				videoId: playingVideo.videoId,
+				playTime: 0, // 实际播放时间可以从视频元素中获取,这里暂时设为0
+				traceId: Date.now(),
+				requestId: Date.now(),
+				planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+				subChannel: 'weCom',
+			});
+		}
 		setPlayingVideo(null);
 	};
 

+ 22 - 0
src/views/publishContent/weGZH/components/publishPlanModal/index.tsx

@@ -8,6 +8,8 @@ import { VideoItem } from '../types'; // Import from common types
 import { GzhPlanDataType, GzhPlanType } from '../../hooks/useGzhPlanList';
 import { useAccountOptions } from '../../hooks/useAccountOptions';
 import { VideoLibraryType } from '@src/views/publishContent/weCom/components/videoSelectModal';
+import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
+import useLogger from '@src/hooks/useLogger';
 
 const { Option } = Select;
 const { Paragraph } = Typography;
@@ -35,6 +37,17 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSu
 	const [initialSelectedVideoId, setInitialSelectedVideoId] = useState<string | null>(null); // State for initial video selection
 	const [videoLibraryType, setVideoLibraryType] = useState<VideoLibraryType>(VideoLibraryType.平台视频库);
 	const { accountOptions, getAccountList } = useAccountOptions();
+	const { uploadLogVideoCreatePublish } = useLogger();
+
+	const getVideoListType = (planType: GzhPlanType) => {
+		if (planType === GzhPlanType.自动回复) {
+			return VideoSearchPlanType.自动回复;
+		} else if (planType === GzhPlanType.公众号推送) {
+			return VideoSearchPlanType.公众号推送;
+		} else {
+			return VideoSearchPlanType.服务号推送;
+		}
+	};
 
 	// 处理code参数
 	useEffect(() => {
@@ -99,6 +112,15 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSu
 					formData.scene = 0;
 				}
 				formData.videoList = selectedVideos;
+				// 上报日志
+			uploadLogVideoCreatePublish({
+				videoList: selectedVideos,
+				traceId: Date.now(),
+				requestId: Date.now(),
+				planType: getVideoListType(planType),
+				gzhAccountId: formData.accountId,
+				subChannel: 'weGZH'
+			});
 				onOk(formData);
 			})
 			.catch((info) => {

+ 59 - 11
src/views/publishContent/weGZH/components/videoSelectModal/index.tsx

@@ -20,6 +20,7 @@ import { VideoSortType, VideoLibraryType } from '@src/views/publishContent/weCom
 import { GzhPlanType } from '../../hooks/useGzhPlanList';
 import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
 import { enumToOptions } from '@src/utils/helper';
+import useLogger from '@src/hooks/useLogger';
 
 const { Paragraph, Text } = Typography;
 
@@ -35,6 +36,7 @@ interface VideoSelectModalProps {
 
 const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible, onClose, onOk, initialSelectedIds = [], selectedVideos = [], defaultVideoLibraryType }) => {
 	const { videoCategoryOptions } = useVideoCategoryOptions();
+	const { uploadLogVideoListQuery, uploadLogVideoPlay, uploadLogVideoPlayEnd, uploadLogVideoCollect } = useLogger();
 	const [category, setCategory] = useState<string>();
 	const [sort, setSort] = useState<VideoSortType>(VideoSortType.推荐指数);
 	const [searchTerm, setSearchTerm] = useState<string>('');
@@ -70,6 +72,17 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 		setLoading(true);
 		setCurrentPage(pageNum || currentPage);
 		setPageSize(_pageSize || pageSize);
+		// 上报视频列表查询日志
+		uploadLogVideoListQuery({
+			traceId: Date.now(),
+			requestId: Date.now(),
+			planType: getVideoListType(planType),
+			subChannel: 'weGZH',
+			category,
+			title: searchTerm,
+			sortType: sort,
+			videoLibraryType,
+		});
 		// 根据视频库类型选择不同的API
 		const apiUrl = videoLibraryType === VideoLibraryType.平台视频库 ? getVideoContentListApi : getUploadVideoContentListApi;
 
@@ -89,26 +102,24 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 		});
 		if (res && res.code === 0) {
 			selectedVideos = selectedVideos.filter(item => item.videoLibraryType === videoLibraryType)
-			setVideoList([...selectedVideos, ...res.data.objs.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId))]);
-			setVideoListAll(old => [...old, ...res.data.objs.filter(v => !old.find(ov => ov.videoId === v.videoId))]);
+			const newVideos = res.data.objs.filter(v => !selectedVideos.find(ov => ov.videoId === v.videoId));
+			setVideoList([...selectedVideos, ...newVideos]);
+			// 仅在第一页加载时更新videoListAll,避免重复添加
+			if ((pageNum || currentPage) === 1) {
+				setVideoListAll([...selectedVideos, ...newVideos]);
+			}
 			setTotal(res.data.totalSize);
 		}
 	}
 
-	// 监听视频库类型变化,重新加载数据
-	useEffect(() => {
-		if (visible) {
-			getVideoList();
-		}
-	}, [videoLibraryType, visible]);
-
+	// 监听visible和videoLibraryType变化,重新加载数据
 	useEffect(() => {
 		if (visible) {
 			setVideoList(selectedVideos);
 			setVideoListAll(selectedVideos);
-			getVideoList(0);
+			getVideoList();
 		}
-	}, [visible]);
+	}, [visible, videoLibraryType, selectedVideos]);
 
 	useEffect(() => {
 		if (visible) {
@@ -128,12 +139,30 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 			const newSet = new Set(prev);
 			if (newSet.has(videoId)) {
 				newSet.delete(videoId);
+				// 上报取消收藏日志
+				uploadLogVideoCollect({
+					videoId,
+					traceId: Date.now(),
+					requestId: Date.now(),
+					collect: 0,
+					planType: getVideoListType(planType),
+					subChannel: 'weGZH',
+				});
 			} else {
 				if (newSet.size >= MAX_SELECTION) {
 					message.warning(`最多只能选择 ${MAX_SELECTION} 条视频`);
 					return prev;
 				}
 				newSet.add(videoId);
+				// 上报收藏日志
+				uploadLogVideoCollect({
+					videoId,
+					traceId: Date.now(),
+					requestId: Date.now(),
+					collect: 1,
+					planType: getVideoListType(planType),
+					subChannel: 'weGZH',
+				});
 			}
 			return newSet;
 		});
@@ -152,10 +181,29 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 	};
 
 	const playVideo = (video: VideoItem) => {
+		// 上报视频播放日志
+		uploadLogVideoPlay({
+			videoId: video.videoId,
+			traceId: Date.now(),
+			requestId: Date.now(),
+			planType: getVideoListType(planType),
+			subChannel: 'weGZH',
+		});
 		setPlayingVideo(video);
 	};
 
 	const closeVideoPlayer = () => {
+		if (playingVideo) {
+			// 上报视频播放结束日志
+			uploadLogVideoPlayEnd({
+				videoId: playingVideo.videoId,
+				playTime: 0,
+				traceId: Date.now(),
+				requestId: Date.now(),
+				planType: getVideoListType(planType),
+				subChannel: 'weGZH',
+			});
+		}
 		setPlayingVideo(null);
 	};