#47 合并

Otevřený
liulidong chce sloučit 5 revizí z větve Web/master do větve Web/feature_260509_replace_upload_new_datasource

+ 150 - 0
src/hooks/useLogger.ts

@@ -0,0 +1,150 @@
+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 { VideoSortType } from '@src/views/publishContent/weCom/components/videoSelectModal';
+import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
+
+type NormalLogParams = {
+	traceId: number;
+	planType?: VideoSearchPlanType;
+	gzhAccountId?: number;
+	subChannel?: string;
+}
+
+type VideoListQueryLogParams = {
+	category?: string;
+	title?: string;
+	sortType?: VideoSortType;
+} & NormalLogParams;
+
+type VideoLogParams = {
+	videoId: number;
+	playTime?: 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,
+		planType,
+		subChannel,
+		category,
+		title,
+		sortType,
+	}:VideoListQueryLogParams) => {
+		return uploadLog({
+			businessType: 'video_list_query',
+			objectType: 'video_list',
+			extParams: {
+				traceId,
+				planType,
+				subChannel,
+				category,
+				title,
+				sortType,
+			},
+		});
+	}
+
+	const uploadLogVideoPlay = ({videoId, traceId, planType, subChannel}: VideoLogParams) => {
+		return uploadLog({
+			businessType: 'video_play',
+			objectType: 'video',
+			extParams: {
+				videoId,
+				traceId,
+				planType,
+				subChannel,
+			},
+		});
+	}
+
+	const uploadLogVideoPlayEnd = ({videoId, playTime, traceId, planType, subChannel}: VideoLogParams) => {
+		return uploadLog({
+			businessType: 'video_play_end',
+			objectType: 'video',
+			extParams: {
+				videoId,
+				playTime,
+				traceId,
+				planType,
+				subChannel,
+			},
+		});
+	}
+
+	const uploadLogVideoCollect = ({videoId, traceId, collect, planType, subChannel}: VideoLogParams) => {
+		return uploadLog({
+			businessType: 'video_collect',
+			objectType: 'video',
+			extParams: {
+				videoId,
+				traceId,
+				collect,
+				planType,
+				subChannel,
+			},
+		});
+	}
+
+	const uploadLogVideoCreatePublish = ({
+		videoList,
+		traceId,
+		planType,
+		gzhAccountId,
+		subChannel
+	}: PublishPlanLogParams) => {
+		return uploadLog({
+			businessType: 'publish_plan_create',
+			objectType: 'publish_plan',
+			extParams: {
+				videoList,
+				traceId,
+				planType,
+				gzhAccountId,
+				subChannel,
+			},
+		});
+	}
+
+	return {
+		uploadLog,
+		uploadLogVideoPlay,
+		uploadLogVideoPlayEnd,
+		uploadLogVideoCollect,
+		uploadLogVideoListQuery,
+		uploadLogVideoCreatePublish,
+	};
+};
+
+export default useLogger;

+ 3 - 0
src/http/api.ts

@@ -60,3 +60,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`

+ 8 - 0
src/utils/helper.ts

@@ -36,4 +36,12 @@ export function enumToOptions(enumObj: Record<string, any>): OptionItem[] {
       label: key,
       value: value
     }));
+}
+
+export function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
+  let timer: ReturnType<typeof setTimeout> | null = null;
+  return ((...args: Parameters<T>) => {
+    if (timer) clearTimeout(timer);
+    timer = setTimeout(() => fn(...args), delay);
+  }) as T;
 }

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

@@ -0,0 +1,31 @@
+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;
+}

+ 10 - 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,13 @@ const AddPlanModal: React.FC<{
 				message.error('请选择发布内容');
 				return;
 			}
+			// 上报日志
+			uploadLogVideoCreatePublish({
+				videoList: selectedVideos,
+				traceId: Date.now(),
+				planType: type === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+				subChannel: values.subChannel || 'weCom',
+			});
 			onOk({
 				type: values.type,
 				subChannel: values.subChannel,

+ 48 - 1
src/views/publishContent/weCom/components/videoSelectModal/index.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
+import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
 import {
 	Drawer,
 	Button,
@@ -18,6 +18,8 @@ import http from '@src/http';
 import { getVideoContentListApi } from '@src/http/api';
 import { useVideoCategoryOptions } from '@src/views/publishContent/weGZH/hooks/useVideoCategoryOptions';
 import { WeComPlanType, WeVideoItem, VideoSearchPlanType } from '@src/views/publishContent/weCom/type'
+import useLogger from '@src/hooks/useLogger';
+import { debounce } from '@src/utils/helper';
 
 const { Text, Paragraph } = Typography;
 
@@ -66,6 +68,8 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 	const currentPageRef = useRef(1);
 	const getVideoListRef = useRef<(pageNum: number, mode: LoadMode) => Promise<void>>();
 	const MAX_SELECTION = 3;
+	const { uploadLogVideoPlay, uploadLogVideoPlayEnd, uploadLogVideoCollect, uploadLogVideoListQuery } = useLogger();
+	const debouncedUploadLogVideoListQuery = useMemo(() => debounce(uploadLogVideoListQuery, 500), [uploadLogVideoListQuery]);
 
 	useEffect(() => { hasMoreRef.current = hasMore; }, [hasMore]);
 	useEffect(() => { loadingRef.current = loading; }, [loading]);
@@ -82,6 +86,16 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 		}
 		setCurrentPage(pageNum);
 
+		// 上报视频列表查询日志(使用防抖)
+		debouncedUploadLogVideoListQuery({
+			traceId: Date.now(),
+			planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+			subChannel: 'weCom',
+			category: category ?? '',
+			title: searchTerm,
+			sortType: sort,
+		});
+
 		const requestParams = {
 			category,
 			title: searchTerm,
@@ -215,12 +229,28 @@ 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(),
+					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(),
+					collect: 1,
+					planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+					subChannel: 'weCom',
+				});
 			}
 			return newSet;
 		});
@@ -237,9 +267,26 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ visible, onClose, o
 
 	const playVideo = (video: WeVideoItem) => {
 		setPlayingVideo(video);
+		// 上报视频播放日志
+		uploadLogVideoPlay({
+			videoId: video.videoId,
+			traceId: Date.now(),
+			planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+			subChannel: 'weCom',
+		});
 	};
 
 	const closeVideoPlayer = () => {
+		if (playingVideo) {
+			// 上报视频播放结束日志
+			uploadLogVideoPlayEnd({
+				videoId: playingVideo.videoId,
+				playTime: 0, // 实际播放时间可以从视频元素中获取,这里暂时设为0
+				traceId: Date.now(),
+				planType: planType === WeComPlanType.社群 ? VideoSearchPlanType.企微社群 : VideoSearchPlanType.企微自动回复,
+				subChannel: 'weCom',
+			});
+		}
 		setPlayingVideo(null);
 	};
 

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

@@ -6,6 +6,8 @@ import EditTitleCoverModal from '../editTitleCoverModal';
 import { VideoItem } from '../types'; // Import from common types
 import { GzhPlanDataType, GzhPlanType } from '../../hooks/useGzhPlanList';
 import { useAccountOptions } from '../../hooks/useAccountOptions';
+import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
+import useLogger from '@src/hooks/useLogger';
 
 const { Option } = Select;
 const { Paragraph } = Typography;
@@ -31,6 +33,17 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSu
 	const [editingVideo, setEditingVideo] = useState<VideoItem | null>(null); // State for editing modal
 	const [initialSelectedVideoId, setInitialSelectedVideoId] = useState<string | null>(null); // State for initial video selection
 	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.服务号推送;
+		}
+	};
 
 	useEffect(() => {
 		if (actionType === 'edit') {
@@ -89,6 +102,14 @@ const AddPunlishPlanModal: React.FC<AddPunlishPlanModalProps> = ({ visible, isSu
 				}
 				formData.videoList = selectedVideos;
 				onOk(formData);
+				// 上报日志
+				uploadLogVideoCreatePublish({
+					videoList: selectedVideos,
+					traceId: Date.now(),
+					planType: getVideoListType(planType),
+					gzhAccountId: formData.accountId,
+					subChannel: 'weGZH'
+				});
 			})
 			.catch((info) => {
 				console.log('Validate Failed:', info);

+ 48 - 1
src/views/publishContent/weGZH/components/videoSelectModal/index.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useRef, useCallback } from 'react';
+import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
 import {
 	Drawer,
 	Button,
@@ -20,6 +20,8 @@ import { useVideoCategoryOptions } from '../../hooks/useVideoCategoryOptions';
 import { VideoSortType } from '@src/views/publishContent/weCom/components/videoSelectModal';
 import { GzhPlanType } from '../../hooks/useGzhPlanList';
 import { VideoSearchPlanType } from '@src/views/publishContent/weCom/type';
+import useLogger from '@src/hooks/useLogger';
+import { debounce } from '@src/utils/helper';
 
 const { Paragraph, Text } = Typography;
 
@@ -36,6 +38,8 @@ interface VideoSelectModalProps {
 
 const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible, onClose, onOk, initialSelectedIds = [], selectedVideos = [] }) => {
 	const { videoCategoryOptions } = useVideoCategoryOptions();
+	const { uploadLogVideoListQuery, uploadLogVideoPlay, uploadLogVideoPlayEnd, uploadLogVideoCollect } = useLogger();
+	const debouncedUploadLogVideoListQuery = useMemo(() => debounce(uploadLogVideoListQuery, 500), [uploadLogVideoListQuery]);
 	const [category, setCategory] = useState<string>();
 	const sort = VideoSortType.推荐指数;
 	const PAGE_SIZE = 10;
@@ -88,6 +92,16 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 		}
 		setCurrentPage(pageNum);
 
+		// 上报视频列表查询日志(使用防抖)
+		debouncedUploadLogVideoListQuery({
+			traceId: Date.now(),
+			planType: getVideoListType(planType),
+			subChannel: 'weGZH',
+			category: category ?? '',
+			title: searchTerm,
+			sortType: sort,
+		});
+
 		const requestParams = {
 			category,
 			title: searchTerm,
@@ -232,12 +246,28 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 			const newSet = new Set(prev);
 			if (newSet.has(videoId)) {
 				newSet.delete(videoId);
+				// 上报取消收藏日志
+				uploadLogVideoCollect({
+					videoId,
+					traceId: 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(),
+					collect: 1,
+					planType: getVideoListType(planType),
+					subChannel: 'weGZH',
+				});
 			}
 			return newSet;
 		});
@@ -250,10 +280,27 @@ const VideoSelectModal: React.FC<VideoSelectModalProps> = ({ planType, visible,
 	};
 
 	const playVideo = (video: VideoItem) => {
+		// 上报视频播放日志
+		uploadLogVideoPlay({
+			videoId: video.videoId,
+			traceId: Date.now(),
+			planType: getVideoListType(planType),
+			subChannel: 'weGZH',
+		});
 		setPlayingVideo(video);
 	};
 
 	const closeVideoPlayer = () => {
+		if (playingVideo) {
+			// 上报视频播放结束日志
+			uploadLogVideoPlayEnd({
+				videoId: playingVideo.videoId,
+				playTime: 0,
+				traceId: Date.now(),
+				planType: getVideoListType(planType),
+				subChannel: 'weGZH',
+			});
+		}
 		setPlayingVideo(null);
 	};