feat: 新增多个脚本用于监控抖音直播、店铺评价、售后及体验分数据

新增了多个脚本文件,用于监控抖音直播间的弹幕、店铺评价、售后数据及商家体验分。这些脚本通过飞书多维表格进行数据存储,并支持定时任务自动更新数据。具体包括:
1. 直播间弹幕监控脚本
2. 店铺评价监控脚本
3. 售后数据监控脚本
4. 商家体验分监控脚本
5. 竞品、行业及跨行业热门千川素材获取脚本

这些脚本通过飞书API进行数据写入,并支持去重和定时任务调度。
This commit is contained in:
intelligrow
2025-04-25 15:15:29 +08:00
commit 3c3ecf8947
10 changed files with 3689 additions and 0 deletions
@@ -0,0 +1,169 @@
// ==UserScript==
// @name 抖音直播间评论次数监控(独立版)
// @namespace https://bbs.tampermonkey.net.cn/
// @version 1.2
// @description 每五分钟获取一次直播间的评论次数,并写入飞书多维表格中展示
// @author 汪喜
// @match https://compass.jinritemai.com/screen/live/shop?live_room_id=*
// @match https://compass.jinritemai.com/screen/live/*
// @match https://compass.jinritemai.com/shop/live-detail?live_room_id=*
// @grant GM_xmlhttpRequest
// @connect open.feishu.cn
// @connect compass.jinritemai.com
// @crontab */5 * * * *
// ==/UserScript==
const CONFIG = {
id: "cli_a6f25876ea28100d",
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
appId: 'Wx1Jb6chwagzmfsOtyycaYmLnpf',
tableId: 'tbl0Ig2UVbIRuRFZ', // 修改为评论数据的表格ID
rooms: [
{ name: "夸迪官方旗舰店" },
{ name: "夸迪官方旗舰店直播间" } // 添加更多直播间
],
urls: {
tenantAccessToken: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
bitableUpdateRecords: (appId, tableId) => `https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records`,
fetchLiveRoomId: 'https://compass.jinritemai.com/compass_api/shop/live/live_list/realtime?page_no=1&page_size=100',
fetchCommentsData: (roomId) => `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/five_min_data?room_id=${roomId}`
}
};
function log(message) {
console.log(message);
}
const retry = async (fn, retries = 3, delay = 10000) => {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i < retries - 1) {
console.warn(`Retrying... (${i + 1}/${retries})`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
async function sendHttpRequest(method, url, body = null, headers = {
'Content-Type': 'application/json'
}) {
return retry(() => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
data: body,
headers: headers,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const jsonResponse = JSON.parse(response.responseText);
resolve(jsonResponse);
} catch (e) {
reject(`Failed to parse JSON: ${e}`);
}
} else {
reject(`Error: ${response.status} ${response.statusText}`);
}
},
onerror: function(error) {
reject(`Network Error: ${error}`);
}
});
}));
}
async function fetchTenantAccessToken(id, secret) {
try {
const response = await sendHttpRequest("POST", CONFIG.urls.tenantAccessToken, JSON.stringify({
"app_id": id,
"app_secret": secret
}));
return response.tenant_access_token;
} catch (error) {
throw new Error(`Error fetching Tenant Access Token: ${error}`);
}
}
async function updateBitableRecords(accessToken, appId, tableId, items) {
try {
const response = await sendHttpRequest("POST", CONFIG.urls.bitableUpdateRecords(appId, tableId), JSON.stringify(items), {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
});
log('Updated record: ' + JSON.stringify(response));
return response;
} catch (error) {
throw new Error(`Failed to update record in Bitable: ${error}`);
}
}
async function fetchLiveRoomId(authorNickName) {
try {
const response = await sendHttpRequest("GET", CONFIG.urls.fetchLiveRoomId);
const liveRooms = response.data.card_list;
const targetRoom = liveRooms.find(room => room.author.author_nick_name === authorNickName);
if (targetRoom) {
return targetRoom.live_room_id;
} else {
throw new Error(`未找到符合条件的直播间: ${authorNickName}`);
}
} catch (error) {
log('Error fetching live room list: ' + error.message);
return null;
}
}
async function fetchCommentsData(roomId) {
try {
const response = await sendHttpRequest('GET', CONFIG.urls.fetchCommentsData(roomId));
const comments = response.data.card.find(d => d.index_display === '评论次数').value.value;
const updateTimeStr = response.data.update_time.replace("更新", "").trim();
const updateTime = new Date(updateTimeStr).getTime();
return { count: comments, timestamp: updateTime };
} catch (error) {
log('Error fetching interaction data: ' + error.message);
return { count: 0, timestamp: new Date().getTime() };
}
}
async function fetchComments() {
try {
for (const room of CONFIG.rooms) {
log(`Fetching comments data for ${room.name}...`);
const liveRoomId = await fetchLiveRoomId(room.name);
if (!liveRoomId) {
log(`未找到符合条件的直播间 ${room.name},跳过执行`);
continue;
}
log(`Live room id for ${room.name}: ${liveRoomId}`);
const commentsData = await fetchCommentsData(liveRoomId);
log(`Fetched comments count for ${room.name}: ${commentsData.count} at ${new Date(commentsData.timestamp).toLocaleString()}`);
// 上传数据到多维表格
log(`Uploading comments data to bitable for ${room.name}...`);
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
const itemsToWrite = {
fields: {
"评论次数": commentsData.count,
"时间": commentsData.timestamp, // 直接使用毫秒级时间戳
"直播间名": room.name,
"直播间id": liveRoomId
}
};
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId, itemsToWrite);
log(`Comments data for ${room.name} uploaded successfully.`);
}
} catch (error) {
log('Error fetching comments data: ' + error.message);
}
}
(async function() {
log('Script started');
await fetchComments();
})();
@@ -0,0 +1,149 @@
// ==UserScript==
// @name 罗盘直播大屏弹幕监听
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.3.0
// @description 监听直播间弹幕
// @author 汪喜
// @match https://compass.jinritemai.com/screen/live*
// @grant GM_xmlhttpRequest
// @require https://scriptcat.org/lib/637/1.4.3/ajaxHooker.js#sha256=y1sWy1M/U5JP1tlAY5e80monDp27fF+GMRLsOiIrSUY=
// ==/UserScript==
(function () {
'use strict';
//一些更改
// 添加一个按钮来控制监听状态
function addToggleButton() {
// 使用setTimeout来延迟按钮的添加
setTimeout(() => {
const targetContainerSelector = '.header--XNxuk';
const targetContainer = document.querySelector(targetContainerSelector);
if (!targetContainer) {
console.error('The target container for the toggle button was not found.');
return;
}
const button = document.createElement('button');
button.textContent = '开始监听';
button.style.marginLeft = '10px';
let isListening = false;
button.addEventListener('click', () => {
if (isListening) {
ah.unProxy();
button.textContent = '开始监听';
isListening = false;
alert('监听结束');
} else {
startListening();
button.textContent = '监听中 点击停止';
isListening = true;
alert('监听开始');
}
});
targetContainer.appendChild(button);
}, 3000); // 延迟3000毫秒(3秒)后执行
}
window.addEventListener('load', addToggleButton);
const appId = "cli_a6f25876ea28100d";
const appSecret = "raLC56ZLIara07nKigpysfoDxHTAeyJf";
const appToken = 'HyV1bstZra3P1xstDiecfCSjnLe'; //多维表格appToken
const tableId = 'tblEqZ2x8eujgl0c'; //多维表格tableId
class FeishuAPI {
constructor() {
this.tenantAccessToken = null;
}
async getTenantAccessToken() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/",
headers: {
"Content-Type": "application/json",
"accept": "application/json, text/plain, */*"
},
data: JSON.stringify({
"app_id": appId,
"app_secret": appSecret
}),
onload: (response) => {
if (response.status === 200 && response.responseText) {
let responseData = JSON.parse(response.responseText);
this.tenantAccessToken = responseData.tenant_access_token;
console.log('Tenant Access Token:', this.tenantAccessToken);
resolve(this.tenantAccessToken);
} else {
console.error('Failed to get tenant access token:', response.statusText);
reject(new Error('Failed to get tenant access token'));
}
}
});
});
}
async writeIntoBase(dataItem) {
if (!this.tenantAccessToken) {
await this.getTenantAccessToken();
}
return new Promise((resolve, reject) => {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.tenantAccessToken}`,
};
const requestBody = {
fields: dataItem
};
GM_xmlhttpRequest({
method: 'POST',
url: `https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`,
headers: headers,
data: JSON.stringify(requestBody),
onload: function (response) {
if (response.status === 200) {
console.log('Data written to Feishu successfully:', response.responseText);
resolve(response.responseText);
} else {
console.error('Failed to write data:', response.statusText);
reject(new Error(`HTTP error! Status: ${response.status}`));
}
},
onerror: function (error) {
console.error('Request failed:', error);
reject(new Error('Request failed'));
}
});
});
}
}
function startListening() {
ajaxHooker.hook(request => {
if (request.url.includes('https://compass.jinritemai.com/business_api/shop/live_bigscreen/comment')) {
request.response = async res => {
const jsonResponse = await res.json();
const comments = jsonResponse.data.comments;
comments.forEach(async (comment) => {
const dataItem = {
nickname: comment.nick_name,
content: comment.content,
createtime: Math.round(new Date().getTime())
};
console.log(dataItem);
const feishuAPI = new FeishuAPI();
await feishuAPI.writeIntoBase(dataItem);
});
};
}
});
}
})();
+119
View File
@@ -0,0 +1,119 @@
// ==UserScript==
// @name 罗盘直播大屏弹幕监听
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.3.0
// @description 监听直播间弹幕
// @author 汪喜
// @match https://compass.jinritemai.com/screen/*
// @grant GM_xmlhttpRequest
// @require https://scriptcat.org/lib/637/1.4.3/ajaxHooker.js#sha256=y1sWy1M/U5JP1tlAY5e80monDp27fF+GMRLsOiIrSUY
// ==/UserScript==
(function () {
'use strict';
const appId = "cli_a6f25876ea28100d";
const appSecret = "raLC56ZLIara07nKigpysfoDxHTAeyJf";
const appToken = 'LzcmbMqJxakIjFstWplcF26lnOh'; //多维表格appToken
const tableId = 'tblkwX3Sl7hzzYWb'; //多维表格tableId
class FeishuAPI {
constructor() {
this.tenantAccessToken = null;
}
async getTenantAccessToken() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/",
headers: {
"Content-Type": "application/json",
"accept": "application/json, text/plain, */*"
},
data: JSON.stringify({
"app_id": appId,
"app_secret": appSecret
}),
onload: (response) => {
if (response.status === 200 && response.responseText) {
let responseData = JSON.parse(response.responseText);
this.tenantAccessToken = responseData.tenant_access_token;
console.log('Tenant Access Token:', this.tenantAccessToken);
resolve(this.tenantAccessToken);
} else {
console.error('Failed to get tenant access token:', response.statusText);
reject(new Error('Failed to get tenant access token'));
}
}
});
});
}
async writeIntoBase(dataItem) {
if (!this.tenantAccessToken) {
await this.getTenantAccessToken();
}
return new Promise((resolve, reject) => {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.tenantAccessToken}`,
};
const requestBody = {
fields: dataItem
};
GM_xmlhttpRequest({
method: 'POST',
url: `https://open.feishu.cn/open-apis/bitable/v1/apps/${appToken}/tables/${tableId}/records`,
headers: headers,
data: JSON.stringify(requestBody),
onload: function (response) {
if (response.status === 200) {
console.log('Data written to Feishu successfully:', response.responseText);
resolve(response.responseText);
} else {
console.error('Failed to write data:', response.statusText);
reject(new Error(`HTTP error! Status: ${response.status}`));
}
},
onerror: function (error) {
console.error('Request failed:', error);
reject(new Error('Request failed'));
}
});
});
}
}
function startListening() {
console.log('Starting to listen for API requests...');
ajaxHooker.hook(request => {
if (request.url.includes('https://compass.jinritemai.com/business_api/shop/live_bigscreen/comment')) {
console.log('Intercepted API request:', request.url);
request.response = async res => {
const jsonResponse = await res.json();
const comments = jsonResponse.data.comments;
comments.forEach(async (comment) => {
const dataItem = {
nickname: comment.nick_name,
content: comment.content,
createtime: Math.round(new Date().getTime())
};
console.log('Extracted comment:', dataItem);
const feishuAPI = new FeishuAPI();
await feishuAPI.writeIntoBase(dataItem);
});
};
}
});
}
// 自动开始监听
window.addEventListener('load', () => {
console.log('Page loaded, starting listener...');
startListening();
});
})();
@@ -0,0 +1,598 @@
// ==UserScript==
// @name 抖音直播间数据监控(全自动)半小时
// @namespace https://bbs.tampermonkey.net.cn/
// @version 2.3
// @description 后台定时获取直播间数据,并在多维表格中显示
// @author 汪喜
// @require https://cdn.staticfile.net/sweetalert2/11.10.3/sweetalert2.all.min.js
// @match https://compass.jinritemai.com/screen/live/shop?live_room_id=*
// @match https://compass.jinritemai.com/screen/live/*
// @match https://compass.jinritemai.com/shop/live-detail?live_room_id=*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @crontab */30 * * * *
// @connect open.feishu.cn
// @connect compass.jinritemai.com
// @connect compass.jinritemai.com
// @connect qianchuan.jinritemai.com
// ==/UserScript==
/*
更新日志:
1. 支持多个直播间
*/
const CONFIG = {
rooms: [{
name: "夸迪官方旗舰店",
aadvid: "1794468035923978",
type: "official"
}, {
name: "夸迪官方旗舰店直播间",
aadvid: "1798388578394176",
type: "shop"
}],
id: "cli_a6f25876ea28100d",
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
appId: 'Wx1Jb6chwagzmfsOtyycaYmLnpf',
tableId: 'tblixcMnCd4GfF08',
logTableId: 'tbld90KC73YBSu0B',
rawDataTableId: 'tblbzx4nG0PbZ6US',
urls: {
tenantAccessToken: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
bitableUpdateRecords: (appId, tableId) => `https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records`,
fetchLiveRoomId: 'https://compass.jinritemai.com/compass_api/shop/live/live_list/realtime?page_no=1&page_size=100',
/*fetchLiveRoomData: (roomId) => {
const indexSelected = encodeURIComponent("pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt");
return `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
},
*/
fetchQianchuanData: (aadvid) => `https://qianchuan.jinritemai.com/ad/api/pmc/v1/standard/get_summary_info?aavid=${aadvid}`,
fetchCommentsData: (roomId) => `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/five_min_data?room_id=${roomId}`, //使用五分钟数据来加总评论次数
fetchConvertData: (roomId) => `https://compass.jinritemai.com/business_api/shop/live_room/flow/gmv_interaction?live_room_id=${roomId}`
}
};
let logs = [];
let liveRoomId = null;
let intervalId = null;
let timeoutId = null;
//let commentCounts = []; // 用于存储每五分钟获取到的评论次数
function log(message) {
logs.push(message);
console.log(message);
}
const retry = async (fn, retries = 3, delay = 10000) => {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i < retries - 1) {
console.warn(`Retrying... (${i + 1}/${retries})`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
async function sendHttpRequest(method, url, body = null, headers = {
'Content-Type': 'application/json'
}) {
return retry(() => new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
data: body,
headers: headers,
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.msg) {
log(jsonResponse.msg);
}
resolve(jsonResponse);
} catch (e) {
reject(`Failed to parse JSON: ${e}`);
}
} else {
reject(`Error: ${response.status} ${response.statusText}`);
}
},
onerror: function(error) {
reject(`Network Error: ${error}`);
}
});
}));
}
async function fetchTenantAccessToken(id, secret) {
try {
const response = await sendHttpRequest("POST", CONFIG.urls.tenantAccessToken, JSON.stringify({
"app_id": id,
"app_secret": secret
}));
return response.tenant_access_token;
} catch (error) {
throw new Error(`Error fetching Tenant Access Token: ${error}`);
}
}
// 更新记录
async function updateBitableRecords(accessToken, appId, tableId, items) {
try {
const response = await sendHttpRequest("POST", CONFIG.urls.bitableUpdateRecords(appId, tableId), JSON.stringify(items), {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
});
log('Updated record: ' + JSON.stringify(response));
return response;
} catch (error) {
throw new Error(`Failed to update record in Bitable: ${error}`);
}
}
//获取正在直播的直播间id
async function fetchLiveRoomId(authorNickName) {
const url = CONFIG.urls.fetchLiveRoomId;
try {
const response = await sendHttpRequest("GET", url);
const liveRooms = response.data.card_list;
const targetRoom = liveRooms.find(room => room.author.author_nick_name === authorNickName);
if (targetRoom) {
return targetRoom.live_room_id;
} else {
throw new Error("未找到符合条件的直播间");
}
} catch (error) {
log('Error fetching live room list: ' + error.message);
return null;
}
}
// 获取直播间大屏数据
async function fetchLiveRoomData(roomId, roomType) {
try {
let url;
if (roomType === "official") {
const indexSelected = encodeURIComponent("pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt");
url = `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
} else if (roomType === "shop") {
const indexSelected = encodeURIComponent("pay_ucnt,watch_cnt_show_ratio,avg_watch_duration,current_cnt,live_show_cnt,real_refund_amt");
url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
} else {
throw new Error(`Unknown room type: ${roomType}`);
}
const headers = {
"accept": "application/json, text/plain, */*"
};
const response = await sendHttpRequest("GET", url, null, headers);
const data = response.data;
if (roomType === "official") {
return {
pay_amt: data.pay_amt.value,
pay_ucnt: data.core_data.find(d => d.index_display === "成交人数").value.value,
real_refund_amt: data.core_data.find(d => d.index_display === "退款金额").value.value,
avg_watch_duration: data.core_data.find(d => d.index_display === "人均观看时长").value.value,
current_cnt: data.core_data.find(d => d.index_display === "实时在线人数").value.value,
live_show_watch_cnt_ratio: data.core_data.find(d => d.index_display === "曝光-观看率(次数)").value.value,
live_show_cnt: data.core_data.find(d => d.index_display === "曝光次数").value.value
};
} else if (roomType === "shop") {
// 根据实际返回的数据结构,提取所需字段
// 这里假设返回的数据结构与官方直播间类似
return {
pay_amt: data.pay_amt.value,
pay_ucnt: data.core_data.find(d => d.index_display === "成交人数").value.value,
real_refund_amt: data.core_data.find(d => d.index_display === "退款金额").value.value,
avg_watch_duration: data.core_data.find(d => d.index_display === "人均观看时长").value.value,
current_cnt: data.core_data.find(d => d.index_display === "实时在线人数").value.value,
live_show_watch_cnt_ratio: data.core_data.find(d => d.index_display === "曝光-观看率(次数)").value.value,
live_show_cnt: data.core_data.find(d => d.index_display === "曝光次数").value.value
};
}
} catch (error) {
log('Error fetching live room data: ' + error.message);
return {
pay_amt: 0,
pay_ucnt: 0,
real_refund_amt: 0,
avg_watch_duration: 0,
current_cnt: 0,
live_show_watch_cnt_ratio: 0,
live_show_cnt: 0
};
}
}
/*
//获取评论次数的数据
async function fetchCommentsData(roomId) {
try {
const url = CONFIG.urls.fetchCommentsData(roomId);
const response = await sendHttpRequest('GET', url);
const comments = response.data.card.find(d => d.index_display === '评论次数').value.value;
const updateTimeStr = response.data.update_time.replace("更新", "").trim();
const updateTime = new Date(updateTimeStr).getTime();
return { count: comments, timestamp: updateTime };
} catch (error) {
log('Error fetching interaction data: ' + error.message);
return { count: 0, timestamp: new Date().getTime() };
}
}
// 每五分钟获取一次评论数据
async function fetchComments() {
try {
log('Fetching comments data...');
const commentsData = await fetchCommentsData(liveRoomId);
commentCounts.push(commentsData);
log(commentCounts)
log(`Fetched comments count: ${commentsData.count} at ${new Date(commentsData.timestamp).toLocaleString()}`);
} catch (error) {
log('Error fetching comments data: ' + error.message);
}
}
// 初始化评论次数数组并设置定时器
async function initializeCommentFetching() {
if (intervalId !== null) {
clearInterval(intervalId);
}
commentCounts = [];
await fetchComments(); // 立即执行一次
intervalId = setInterval(fetchComments, 5 * 60 * 1000); // 每五分钟重新获取一次评论数据
}
// 计算过去一小时的评论次数
function getCommentsCountLastHour() {
log(`Current commentCounts array: ${JSON.stringify(commentCounts)}`);
const oneHourAgo = new Date().getTime() - (60 * 60 * 1000);
const filteredComments = commentCounts.filter(comment => comment.timestamp >= oneHourAgo);
log(`Filtered comments array: ${JSON.stringify(filteredComments)}`);
const totalComments = filteredComments.reduce((total, comment) => total + comment.count, 0);
log(`Total comments in the last hour: ${totalComments}`);
return totalComments;
}
*/
//获取直播间开始时间数据
async function fetchLiveRoomStartTime(roomId) {
const url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/live_base_info?room_id=${roomId}`;
try {
const response = await sendHttpRequest("GET", url);
return response.data.live_start_time;
} catch (error) {
log('Error fetching live room start time: ' + error.message);
return null; // 默认返回 null
}
}
// 获取千川消耗金额数据
async function fetchQianchuanData(aadvid) {
const now = new Date();
const formatNumber = (num) => num.toString().padStart(2, '0');
// 结束时间为动态的当前时间
const year = now.getFullYear();
const month = formatNumber(now.getMonth() + 1); // 月份从0开始
const day = formatNumber(now.getDate());
const hours = formatNumber(now.getHours());
const minutes = formatNumber(now.getMinutes());
const seconds = formatNumber(now.getSeconds());
const end_time = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
let start_time;
log('Failed to fetch start time, using default start time.');
if (now.getHours() >= 6) {
// 时间超过6点,则使用当天6点作为开始时间
start_time = `${year}-${month}-${day} 06:00:00`;
} else {
// 如果当前时间在0点到6点之间,则开始时间为昨天的6点
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
const yesterdayYear = yesterday.getFullYear();
const yesterdayMonth = formatNumber(yesterday.getMonth() + 1);
const yesterdayDay = formatNumber(yesterday.getDate());
start_time = `${yesterdayYear}-${yesterdayMonth}-${yesterdayDay} 06:00:00`;
}
const body = {
"mar_goal": 2,
"page": 1,
"page_size": 10,
"order_by_type": 2,
"order_by_field": "create_time",
"start_time": start_time,
"end_time": end_time,
"smartBidType": 0,
"metrics": "stat_cost",
"adlab_mode": 1,
"dataSetKey": "site_promotion_list",
"smart_bid_type": 0,
"aavid": aadvid
};
try {
const response = await sendHttpRequest("POST", CONFIG.urls.fetchQianchuanData(aadvid), JSON.stringify(body), {
'Content-Type': 'application/json'
});
return response.data.totalMetrics.metrics.statCost.value;
} catch (error) {
log('Error fetching Qianchuan data: ' + error.message);
return 0;
}
}
// 获取直播间转化漏斗数据
async function fetchConvertData(roomId) {
try {
const url = CONFIG.urls.fetchConvertData(roomId);
const response = await sendHttpRequest("GET", url);
return {
exposure_ucnt: response.data.gmv_change.find(d => d.index_name === "直播间曝光人数").value,
watch_ucnt: response.data.gmv_change.find(d => d.index_name === "直播间观看人数").value,
product_exposure_ucnt: response.data.gmv_change.find(d => d.index_name === "商品曝光人数").value,
product_click_ucnt: response.data.gmv_change.find(d => d.index_name === "商品点击人数").value
};
} catch (error) {
log('Error fetching flow data: ' + error.message);
return {
exposure_ucnt: 0,
watch_ucnt: 0,
product_exposure_ucnt: 0,
product_click_ucnt: 0
};
}
}
// 存储数据,每个直播间单独一个key,不覆盖之前的数据
function saveDataUsingGM(roomId, data) {
let existingData = GM_getValue(roomId, []);
if (!Array.isArray(existingData)) {
existingData = [];
}
existingData.push(data); // 将新的数据推入到现有数据数组中
GM_setValue(roomId, existingData);
}
// 读取数据
function getDataUsingGM(roomId) {
let data = GM_getValue(roomId, []);
return Array.isArray(data) ? data : [];
}
//延迟函数
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
//计算直播场次所在的日期
function calculateSessionDate(timestamp) {
const date = new Date(timestamp);
// 直播时间在凌晨2点前,则认为是前一天的场次
if (date.getHours() <= 2) {
date.setDate(date.getDate() - 1);
}
// 创建一个新的日期对象,表示计算后的日期,并返回其时间戳
const sessionDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
return sessionDate.getTime();
}
async function fetchAndUploadLiveRoomData() {
for (const room of CONFIG.rooms) {
try {
log(`Fetching live room id for ${room.name}`);
const liveRoomId = await fetchLiveRoomId(room.name);
if (!liveRoomId) {
log(`未找到符合条件的直播间 ${room.name},跳过执行`);
continue; // 未找到符合条件的直播间,跳过执行
}
log(`Fetching live room start time for ${room.name}...`);
const start_time = await fetchLiveRoomStartTime(liveRoomId);
if (!start_time) {
Swal.fire({
icon: 'error',
title: `无法获取直播间 ${room.name} 开始时间,请检查直播间ID`,
text: '使用默认开始时间 06:00:00',
});
}
log(`Fetching live room data for ${room.name}...`);
const liveRoomData = await fetchLiveRoomData(liveRoomId, room.type);
log('Live room data fetched successfully.');
await delay(2000);
log(`Fetching Qianchuan data for ${room.name}...`);
const qianchuanData = await fetchQianchuanData(room.aadvid);
log('Qianchuan data fetched successfully.');
await delay(2000);
log(`Fetching flow data for ${room.name}...`);
const convertData = await fetchConvertData(liveRoomId);
log('Flow data fetched successfully.');
const previousData = getDataUsingGM(liveRoomId);
const currentTime = new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai'
});
let incrementData = {};
if (previousData.length > 0) {
const lastData = previousData[previousData.length - 1];
incrementData = {
"直播间": room.name, // 添加直播间名字字段
"GMV": liveRoomData.pay_amt / 100 - lastData.pay_amt,
"商品成交人数": liveRoomData.pay_ucnt - lastData.pay_ucnt,
"实时在线人数": liveRoomData.current_cnt,
"退款金额": liveRoomData.real_refund_amt / 100 - lastData.real_refund_amt,
"千川消耗": qianchuanData - lastData.qianchuan_cost,
"直播间曝光人数": convertData.exposure_ucnt - lastData.exposure_ucnt,
"直播观看人数": convertData.watch_ucnt - lastData.watch_ucnt,
"商品曝光人数": convertData.product_exposure_ucnt - lastData.product_exposure_ucnt,
"商品点击人数": convertData.product_click_ucnt - lastData.product_click_ucnt,
"直播间ID": liveRoomId,
"时间": new Date(currentTime).getTime(),
"直播间曝光次数": liveRoomData.live_show_cnt - lastData.live_show_cnt,
"直播观看次数": liveRoomData.live_show_watch_cnt_ratio * liveRoomData.live_show_cnt - lastData.live_show_cnt * lastData.live_show_watch_cnt_ratio,
"人均停留时长": (liveRoomData.avg_watch_duration * convertData.watch_ucnt - lastData.avg_watch_duration * lastData.watch_ucnt) / (convertData.watch_ucnt - lastData.watch_ucnt),
"直播场次日期": calculateSessionDate(new Date(currentTime).getTime()),
"差值时间": lastData.timestamp
};
} else {
incrementData = {
"直播间": room.name, // 添加直播间名字字段
"GMV": liveRoomData.pay_amt / 100,
"商品成交人数": liveRoomData.pay_ucnt,
"实时在线人数": liveRoomData.current_cnt,
"退款金额": liveRoomData.real_refund_amt / 100,
"千川消耗": qianchuanData,
"直播间曝光人数": convertData.exposure_ucnt,
"直播观看人数": convertData.watch_ucnt,
"商品曝光人数": convertData.product_exposure_ucnt,
"商品点击人数": convertData.product_click_ucnt,
"直播间ID": liveRoomId,
"时间": new Date(currentTime).getTime(),
"直播间曝光次数": liveRoomData.live_show_cnt,
"直播观看次数": liveRoomData.live_show_watch_cnt_ratio * liveRoomData.live_show_cnt,
"人均停留时长": liveRoomData.avg_watch_duration,
"直播场次日期": calculateSessionDate(new Date(currentTime).getTime()),
"差值时间": new Date(start_time).getTime()
};
}
log('Data extracted successfully');
log(incrementData);
// 获取tenant access token
log('Fetching tenant access token...');
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
log('Tenant access token fetched successfully.');
// 把计算后的数据写入到多维表格
log(`Uploading live room data to bitable for ${room.name}...`);
const itemsToWrite = {
fields: incrementData
};
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId, itemsToWrite);
log('Live room data uploaded successfully.');
log(`Uploading live room data to localstorage for ${room.name}...`);
// 写入localStorage
const dataToSave = {
room_name: room.name,
pay_amt: liveRoomData.pay_amt / 100,
pay_ucnt: liveRoomData.pay_ucnt,
real_refund_amt: liveRoomData.real_refund_amt / 100,
avg_watch_duration: liveRoomData.avg_watch_duration,
current_cnt: liveRoomData.current_cnt,
qianchuan_cost: qianchuanData,
exposure_ucnt: convertData.exposure_ucnt,
watch_ucnt: convertData.watch_ucnt,
product_exposure_ucnt: convertData.product_exposure_ucnt,
product_click_ucnt: convertData.product_click_ucnt,
liveRoomId: liveRoomId,
timestamp: new Date(currentTime).getTime(),
timestring: currentTime,
live_show_watch_cnt_ratio: liveRoomData.live_show_watch_cnt_ratio,
live_show_cnt: liveRoomData.live_show_cnt
};
log(dataToSave);
saveDataUsingGM(liveRoomId, dataToSave);
log('Writing to localstorage successfully');
// 写入rawdata多维表格
log(`Uploading saved data to another bitable table for ${room.name}...`);
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.rawDataTableId, {
fields: dataToSave
});
log('Saved data uploaded to another bitable table successfully.');
} catch (error) {
log(`Error for ${room.name}: ` + error.message);
Swal.fire({
icon: 'error',
title: `直播间 ${room.name} 出错了,请联系@汪喜`,
text: error.message,
});
}
}
}
(() => {
let runResult = "成功"; // 默认值为成功
let logs = [];
const log = message => {
logs.push(message);
console.log(message);
};
async function logRunResult() {
try {
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
const runTime = new Date().getTime();
const logData = {
fields: {
"表名": "直播间数据",
"表格id": CONFIG.tableId,
"最近运行时间": runTime,
"运行结果": runResult,
"日志": logs.join('\n')
}
};
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.logTableId, logData);
log('Run result logged successfully.');
} catch (error) {
log('Failed to log run result: ' + error.message);
}
}
(async function() {
try {
log('Script started');
await fetchAndUploadLiveRoomData();
log('Data fetched and uploaded');
} catch (error) {
runResult = "失败";
log('Script execution failed: ' + error.message);
} finally {
await logRunResult();
log('Run result logged');
}
})();
})();
@@ -0,0 +1,696 @@
// ==UserScript==
// @name 抖音直播间数据监控(全自动)半小时
// @namespace https://bbs.tampermonkey.net.cn/
// @version 2.3
// @description 后台定时获取直播间数据,并在多维表格中显示
// @author 汪喜
// @require https://cdn.staticfile.net/sweetalert2/11.10.3/sweetalert2.all.min.js
// @match https://compass.jinritemai.com/screen/live/shop?live_room_id=*
// @match https://compass.jinritemai.com/screen/live/*
// @match https://compass.jinritemai.com/shop/live-detail?live_room_id=*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @crontab */30 * * * *
// @connect open.feishu.cn
// @connect compass.jinritemai.com
// @connect compass.jinritemai.com
// @connect qianchuan.jinritemai.com
// ==/UserScript==
/*
更新日志:
1. 支持多个直播间
*/
const CONFIG = {
rooms: [
{
name: "夸迪官方旗舰店",
aadvid: "1794468035923978",
type: "official",
},
{
name: "夸迪官方旗舰店直播间",
aadvid: "1798388578394176",
type: "shop",
},
],
id: "cli_a6f25876ea28100d",
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
appId: "Wx1Jb6chwagzmfsOtyycaYmLnpf",
tableId: "tblixcMnCd4GfF08",
logTableId: "tbld90KC73YBSu0B",
rawDataTableId: "tblbzx4nG0PbZ6US",
urls: {
tenantAccessToken:
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
bitableUpdateRecords: (appId, tableId) =>
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records`,
fetchLiveRoomId:
"https://compass.jinritemai.com/compass_api/shop/live/live_list/realtime?page_no=1&page_size=100",
/*fetchLiveRoomData: (roomId) => {
const indexSelected = encodeURIComponent("pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt");
return `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
},
*/
fetchQianchuanData: (aadvid) =>
`https://qianchuan.jinritemai.com/ad/api/pmc/v1/standard/get_summary_info?aavid=${aadvid}`,
fetchCommentsData: (roomId) =>
`https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/five_min_data?room_id=${roomId}`, //使用五分钟数据来加总评论次数
fetchConvertData: (roomId) =>
`https://compass.jinritemai.com/business_api/shop/live_room/flow/gmv_interaction?live_room_id=${roomId}`,
},
};
let logs = [];
let liveRoomId = null;
let intervalId = null;
let timeoutId = null;
//let commentCounts = []; // 用于存储每五分钟获取到的评论次数
function log(message) {
logs.push(message);
console.log(message);
}
const retry = async (fn, retries = 3, delay = 10000) => {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i < retries - 1) {
console.warn(`Retrying... (${i + 1}/${retries})`);
await new Promise((resolve) => setTimeout(resolve, delay));
} else {
throw error;
}
}
}
};
async function sendHttpRequest(
method,
url,
body = null,
headers = {
"Content-Type": "application/json",
}
) {
return retry(
() =>
new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
data: body,
headers: headers,
onload: function (response) {
if (response.status >= 200 && response.status < 300) {
try {
const jsonResponse = JSON.parse(response.responseText);
if (jsonResponse.msg) {
log(jsonResponse.msg);
}
resolve(jsonResponse);
} catch (e) {
reject(`Failed to parse JSON: ${e}`);
}
} else {
reject(`Error: ${response.status} ${response.statusText}`);
}
},
onerror: function (error) {
reject(`Network Error: ${error}`);
},
});
})
);
}
async function fetchTenantAccessToken(id, secret) {
try {
const response = await sendHttpRequest(
"POST",
CONFIG.urls.tenantAccessToken,
JSON.stringify({
app_id: id,
app_secret: secret,
})
);
return response.tenant_access_token;
} catch (error) {
throw new Error(`Error fetching Tenant Access Token: ${error}`);
}
}
// 更新记录
async function updateBitableRecords(accessToken, appId, tableId, items) {
try {
const response = await sendHttpRequest(
"POST",
CONFIG.urls.bitableUpdateRecords(appId, tableId),
JSON.stringify(items),
{
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
}
);
log("Updated record: " + JSON.stringify(response));
return response;
} catch (error) {
throw new Error(`Failed to update record in Bitable: ${error}`);
}
}
//获取正在直播的直播间id
async function fetchLiveRoomId(authorNickName) {
const url = CONFIG.urls.fetchLiveRoomId;
try {
const response = await sendHttpRequest("GET", url);
const liveRooms = response.data.card_list;
const targetRoom = liveRooms.find(
(room) => room.author.author_nick_name === authorNickName
);
if (targetRoom) {
return targetRoom.live_room_id;
} else {
throw new Error("未找到符合条件的直播间");
}
} catch (error) {
log("Error fetching live room list: " + error.message);
return null;
}
}
// 获取直播间大屏数据
async function fetchLiveRoomData(roomId, roomType) {
try {
let url;
if (roomType === "official") {
const indexSelected = encodeURIComponent(
"pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt"
);
url = `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
} else if (roomType === "shop") {
const indexSelected = encodeURIComponent(
"pay_ucnt,watch_cnt_show_ratio,avg_watch_duration,current_cnt,live_show_cnt,real_refund_amt"
);
url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
} else {
throw new Error(`Unknown room type: ${roomType}`);
}
const headers = {
accept: "application/json, text/plain, */*",
};
const response = await sendHttpRequest("GET", url, null, headers);
const data = response.data;
if (roomType === "official") {
return {
pay_amt: data.pay_amt.value,
pay_ucnt: data.core_data.find((d) => d.index_display === "成交人数")
.value.value,
real_refund_amt: data.core_data.find(
(d) => d.index_display === "退款金额"
).value.value,
avg_watch_duration: data.core_data.find(
(d) => d.index_display === "人均观看时长"
).value.value,
current_cnt: data.core_data.find(
(d) => d.index_display === "实时在线人数"
).value.value,
live_show_watch_cnt_ratio: data.core_data.find(
(d) => d.index_display === "曝光-观看率(次数)"
).value.value,
live_show_cnt: data.core_data.find(
(d) => d.index_display === "曝光次数"
).value.value,
};
} else if (roomType === "shop") {
// 根据实际返回的数据结构,提取所需字段
// 这里假设返回的数据结构与官方直播间类似
return {
pay_amt: data.pay_amt.value,
pay_ucnt: data.core_data.find((d) => d.index_display === "成交人数")
.value.value,
real_refund_amt: data.core_data.find(
(d) => d.index_display === "退款金额"
).value.value,
avg_watch_duration: data.core_data.find(
(d) => d.index_display === "人均观看时长"
).value.value,
current_cnt: data.core_data.find(
(d) => d.index_display === "实时在线人数"
).value.value,
live_show_watch_cnt_ratio: data.core_data.find(
(d) => d.index_display === "曝光-观看率(次数)"
).value.value,
live_show_cnt: data.core_data.find(
(d) => d.index_display === "曝光次数"
).value.value,
};
}
} catch (error) {
log("Error fetching live room data: " + error.message);
return {
pay_amt: 0,
pay_ucnt: 0,
real_refund_amt: 0,
avg_watch_duration: 0,
current_cnt: 0,
live_show_watch_cnt_ratio: 0,
live_show_cnt: 0,
};
}
}
/*
//获取评论次数的数据
async function fetchCommentsData(roomId) {
try {
const url = CONFIG.urls.fetchCommentsData(roomId);
const response = await sendHttpRequest('GET', url);
const comments = response.data.card.find(d => d.index_display === '评论次数').value.value;
const updateTimeStr = response.data.update_time.replace("更新", "").trim();
const updateTime = new Date(updateTimeStr).getTime();
return { count: comments, timestamp: updateTime };
} catch (error) {
log('Error fetching interaction data: ' + error.message);
return { count: 0, timestamp: new Date().getTime() };
}
}
// 每五分钟获取一次评论数据
async function fetchComments() {
try {
log('Fetching comments data...');
const commentsData = await fetchCommentsData(liveRoomId);
commentCounts.push(commentsData);
log(commentCounts)
log(`Fetched comments count: ${commentsData.count} at ${new Date(commentsData.timestamp).toLocaleString()}`);
} catch (error) {
log('Error fetching comments data: ' + error.message);
}
}
// 初始化评论次数数组并设置定时器
async function initializeCommentFetching() {
if (intervalId !== null) {
clearInterval(intervalId);
}
commentCounts = [];
await fetchComments(); // 立即执行一次
intervalId = setInterval(fetchComments, 5 * 60 * 1000); // 每五分钟重新获取一次评论数据
}
// 计算过去一小时的评论次数
function getCommentsCountLastHour() {
log(`Current commentCounts array: ${JSON.stringify(commentCounts)}`);
const oneHourAgo = new Date().getTime() - (60 * 60 * 1000);
const filteredComments = commentCounts.filter(comment => comment.timestamp >= oneHourAgo);
log(`Filtered comments array: ${JSON.stringify(filteredComments)}`);
const totalComments = filteredComments.reduce((total, comment) => total + comment.count, 0);
log(`Total comments in the last hour: ${totalComments}`);
return totalComments;
}
*/
//获取直播间开始时间数据
async function fetchLiveRoomStartTime(roomId) {
const url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/live_base_info?room_id=${roomId}`;
try {
const response = await sendHttpRequest("GET", url);
return response.data.live_start_time;
} catch (error) {
log("Error fetching live room start time: " + error.message);
return null; // 默认返回 null
}
}
// 获取千川消耗金额数据
async function fetchQianchuanData(aadvid) {
const now = new Date();
const formatNumber = (num) => num.toString().padStart(2, "0");
// 结束时间为动态的当前时间
const year = now.getFullYear();
const month = formatNumber(now.getMonth() + 1); // 月份从0开始
const day = formatNumber(now.getDate());
const hours = formatNumber(now.getHours());
const minutes = formatNumber(now.getMinutes());
const seconds = formatNumber(now.getSeconds());
const end_time = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
let start_time;
log("Failed to fetch start time, using default start time.");
if (now.getHours() >= 6) {
// 时间超过6点,则使用当天6点作为开始时间
start_time = `${year}-${month}-${day} 06:00:00`;
} else {
// 如果当前时间在0点到6点之间,则开始时间为昨天的6点
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
const yesterdayYear = yesterday.getFullYear();
const yesterdayMonth = formatNumber(yesterday.getMonth() + 1);
const yesterdayDay = formatNumber(yesterday.getDate());
start_time = `${yesterdayYear}-${yesterdayMonth}-${yesterdayDay} 06:00:00`;
}
const body = {
DataSetKey: "home_cost_total_prom",
Dimensions: ["adlab_mode", "pricing_category", "qcpx_category"],
Metrics: ["stat_cost"],
StartTime: start_time,
EndTime: end_time,
Filters: {
Conditions: [
{
Field: "advertiser_id",
Values: [aadvid],
Operator: 7,
},
{
Field: "ecp_app_id",
Values: ["1", "2"],
Operator: 7,
},
{
Field: "query_self_data",
Values: ["off"],
Operator: 7,
},
],
ConditionRelationshipType: 1,
},
};
try {
const response = await sendHttpRequest(
"POST",
`https://qianchuan.jinritemai.com/ad/api/data/v1/common/statQuery?reqFrom=data-summary&aavid=${aadvid}`,
JSON.stringify(body),
{
"Content-Type": "application/json",
}
);
return response.data.StatsData.Totals.stat_cost.Value;
} catch (error) {
log("Error fetching Qianchuan data: " + error.message);
return 0;
}
}
// 获取直播间转化漏斗数据
async function fetchConvertData(roomId) {
try {
const url = CONFIG.urls.fetchConvertData(roomId);
const response = await sendHttpRequest("GET", url);
return {
exposure_ucnt: response.data.gmv_change.find(
(d) => d.index_name === "直播间曝光人数"
).value,
watch_ucnt: response.data.gmv_change.find(
(d) => d.index_name === "直播间观看人数"
).value,
product_exposure_ucnt: response.data.gmv_change.find(
(d) => d.index_name === "商品曝光人数"
).value,
product_click_ucnt: response.data.gmv_change.find(
(d) => d.index_name === "商品点击人数"
).value,
};
} catch (error) {
log("Error fetching flow data: " + error.message);
return {
exposure_ucnt: 0,
watch_ucnt: 0,
product_exposure_ucnt: 0,
product_click_ucnt: 0,
};
}
}
// 存储数据,每个直播间单独一个key,不覆盖之前的数据
function saveDataUsingGM(roomId, data) {
let existingData = GM_getValue(roomId, []);
if (!Array.isArray(existingData)) {
existingData = [];
}
existingData.push(data); // 将新的数据推入到现有数据数组中
GM_setValue(roomId, existingData);
}
// 读取数据
function getDataUsingGM(roomId) {
let data = GM_getValue(roomId, []);
return Array.isArray(data) ? data : [];
}
//延迟函数
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
//计算直播场次所在的日期
function calculateSessionDate(timestamp) {
const date = new Date(timestamp);
// 直播时间在凌晨2点前,则认为是前一天的场次
if (date.getHours() <= 2) {
date.setDate(date.getDate() - 1);
}
// 创建一个新的日期对象,表示计算后的日期,并返回其时间戳
const sessionDate = new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
);
return sessionDate.getTime();
}
async function fetchAndUploadLiveRoomData() {
for (const room of CONFIG.rooms) {
try {
log(`Fetching live room id for ${room.name}`);
const liveRoomId = await fetchLiveRoomId(room.name);
if (!liveRoomId) {
log(`未找到符合条件的直播间 ${room.name},跳过执行`);
continue; // 未找到符合条件的直播间,跳过执行
}
log(`Fetching live room start time for ${room.name}...`);
const start_time = await fetchLiveRoomStartTime(liveRoomId);
if (!start_time) {
Swal.fire({
icon: "error",
title: `无法获取直播间 ${room.name} 开始时间,请检查直播间ID`,
text: "使用默认开始时间 06:00:00",
});
}
log(`Fetching live room data for ${room.name}...`);
const liveRoomData = await fetchLiveRoomData(liveRoomId, room.type);
log("Live room data fetched successfully.");
await delay(2000);
log(`Fetching Qianchuan data for ${room.name}...`);
const qianchuanData = await fetchQianchuanData(room.aadvid);
log("Qianchuan data fetched successfully.");
await delay(2000);
log(`Fetching flow data for ${room.name}...`);
const convertData = await fetchConvertData(liveRoomId);
log("Flow data fetched successfully.");
const previousData = getDataUsingGM(liveRoomId);
const currentTime = new Date().toLocaleString("zh-CN", {
timeZone: "Asia/Shanghai",
});
let incrementData = {};
if (previousData.length > 0) {
const lastData = previousData[previousData.length - 1];
incrementData = {
直播间: room.name, // 添加直播间名字字段
GMV: liveRoomData.pay_amt / 100 - lastData.pay_amt,
商品成交人数: liveRoomData.pay_ucnt - lastData.pay_ucnt,
实时在线人数: liveRoomData.current_cnt,
退款金额:
liveRoomData.real_refund_amt / 100 - lastData.real_refund_amt,
千川消耗: qianchuanData - lastData.qianchuan_cost,
直播间曝光人数: convertData.exposure_ucnt - lastData.exposure_ucnt,
直播观看人数: convertData.watch_ucnt - lastData.watch_ucnt,
商品曝光人数:
convertData.product_exposure_ucnt - lastData.product_exposure_ucnt,
商品点击人数:
convertData.product_click_ucnt - lastData.product_click_ucnt,
直播间ID: liveRoomId,
时间: new Date(currentTime).getTime(),
直播间曝光次数: liveRoomData.live_show_cnt - lastData.live_show_cnt,
直播观看次数:
liveRoomData.live_show_watch_cnt_ratio *
liveRoomData.live_show_cnt -
lastData.live_show_cnt * lastData.live_show_watch_cnt_ratio,
人均停留时长:
(liveRoomData.avg_watch_duration * convertData.watch_ucnt -
lastData.avg_watch_duration * lastData.watch_ucnt) /
(convertData.watch_ucnt - lastData.watch_ucnt),
直播场次日期: calculateSessionDate(new Date(currentTime).getTime()),
差值时间: lastData.timestamp,
};
} else {
incrementData = {
直播间: room.name, // 添加直播间名字字段
GMV: liveRoomData.pay_amt / 100,
商品成交人数: liveRoomData.pay_ucnt,
实时在线人数: liveRoomData.current_cnt,
退款金额: liveRoomData.real_refund_amt / 100,
千川消耗: qianchuanData,
直播间曝光人数: convertData.exposure_ucnt,
直播观看人数: convertData.watch_ucnt,
商品曝光人数: convertData.product_exposure_ucnt,
商品点击人数: convertData.product_click_ucnt,
直播间ID: liveRoomId,
时间: new Date(currentTime).getTime(),
直播间曝光次数: liveRoomData.live_show_cnt,
直播观看次数:
liveRoomData.live_show_watch_cnt_ratio * liveRoomData.live_show_cnt,
人均停留时长: liveRoomData.avg_watch_duration,
直播场次日期: calculateSessionDate(new Date(currentTime).getTime()),
差值时间: new Date(start_time).getTime(),
};
}
log("Data extracted successfully");
log(incrementData);
// 获取tenant access token
log("Fetching tenant access token...");
const accessToken = await fetchTenantAccessToken(
CONFIG.id,
CONFIG.secret
);
log("Tenant access token fetched successfully.");
// 把计算后的数据写入到多维表格
log(`Uploading live room data to bitable for ${room.name}...`);
const itemsToWrite = {
fields: incrementData,
};
await updateBitableRecords(
accessToken,
CONFIG.appId,
CONFIG.tableId,
itemsToWrite
);
log("Live room data uploaded successfully.");
log(`Uploading live room data to localstorage for ${room.name}...`);
// 写入localStorage
const dataToSave = {
room_name: room.name,
pay_amt: liveRoomData.pay_amt / 100,
pay_ucnt: liveRoomData.pay_ucnt,
real_refund_amt: liveRoomData.real_refund_amt / 100,
avg_watch_duration: liveRoomData.avg_watch_duration,
current_cnt: liveRoomData.current_cnt,
qianchuan_cost: qianchuanData,
exposure_ucnt: convertData.exposure_ucnt,
watch_ucnt: convertData.watch_ucnt,
product_exposure_ucnt: convertData.product_exposure_ucnt,
product_click_ucnt: convertData.product_click_ucnt,
liveRoomId: liveRoomId,
timestamp: new Date(currentTime).getTime(),
timestring: currentTime,
live_show_watch_cnt_ratio: liveRoomData.live_show_watch_cnt_ratio,
live_show_cnt: liveRoomData.live_show_cnt,
};
log(dataToSave);
saveDataUsingGM(liveRoomId, dataToSave);
log("Writing to localstorage successfully");
// 写入rawdata多维表格
log(`Uploading saved data to another bitable table for ${room.name}...`);
await updateBitableRecords(
accessToken,
CONFIG.appId,
CONFIG.rawDataTableId,
{
fields: dataToSave,
}
);
log("Saved data uploaded to another bitable table successfully.");
} catch (error) {
log(`Error for ${room.name}: ` + error.message);
Swal.fire({
icon: "error",
title: `直播间 ${room.name} 出错了,请联系@汪喜`,
text: error.message,
});
}
}
}
(() => {
let runResult = "成功"; // 默认值为成功
let logs = [];
const log = (message) => {
logs.push(message);
console.log(message);
};
async function logRunResult() {
try {
const accessToken = await fetchTenantAccessToken(
CONFIG.id,
CONFIG.secret
);
const runTime = new Date().getTime();
const logData = {
fields: {
表名: "直播间数据",
表格id: CONFIG.tableId,
最近运行时间: runTime,
运行结果: runResult,
日志: logs.join("\n"),
},
};
await updateBitableRecords(
accessToken,
CONFIG.appId,
CONFIG.logTableId,
logData
);
log("Run result logged successfully.");
} catch (error) {
log("Failed to log run result: " + error.message);
}
}
(async function () {
try {
log("Script started");
await fetchAndUploadLiveRoomData();
log("Data fetched and uploaded");
} catch (error) {
runResult = "失败";
log("Script execution failed: " + error.message);
} finally {
await logRunResult();
log("Run result logged");
}
})();
})();