feat: 新增多个脚本用于监控抖音直播、店铺评价、售后及体验分数据
新增了多个脚本文件,用于监控抖音直播间的弹幕、店铺评价、售后数据及商家体验分。这些脚本通过飞书多维表格进行数据存储,并支持定时任务自动更新数据。具体包括: 1. 直播间弹幕监控脚本 2. 店铺评价监控脚本 3. 售后数据监控脚本 4. 商家体验分监控脚本 5. 竞品、行业及跨行业热门千川素材获取脚本 这些脚本通过飞书API进行数据写入,并支持去重和定时任务调度。
This commit is contained in:
commit
3c3ecf8947
169
douyinLive/liveRoomCommentCount/liveRoomCommentCount.js
Normal file
169
douyinLive/liveRoomCommentCount/liveRoomCommentCount.js
Normal file
@ -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();
|
||||||
|
})();
|
||||||
149
douyinLive/liveRoomComments/liveRoomComments.js
Normal file
149
douyinLive/liveRoomComments/liveRoomComments.js
Normal file
@ -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
douyinLive/liveRoomComments/test.js
Normal file
119
douyinLive/liveRoomComments/test.js
Normal 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();
|
||||||
|
});
|
||||||
|
})();
|
||||||
598
douyinLive/liveRoomHourData/liveRoomHourData - 全域.js
Normal file
598
douyinLive/liveRoomHourData/liveRoomHourData - 全域.js
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
})();
|
||||||
696
douyinLive/liveRoomHourData/liveRoomHourData.js
Normal file
696
douyinLive/liveRoomHourData/liveRoomHourData.js
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
})();
|
||||||
287
douyinShop/douyinAftersale/douyinAftersale.js
Normal file
287
douyinShop/douyinAftersale/douyinAftersale.js
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 抖音店铺售后监控
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.2.0
|
||||||
|
// @description 每小时获取前2小时的店铺售后数据,并去重后上传到飞书多维表格
|
||||||
|
// @author wanxi
|
||||||
|
// @crontab 5 * * * *
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
id: "",
|
||||||
|
secret: "",
|
||||||
|
appId: "",
|
||||||
|
tableId: "", // 表格ID
|
||||||
|
logTableId: "", // 运行记录表
|
||||||
|
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`,
|
||||||
|
bitableSearchRecords: (appId, tableId) =>
|
||||||
|
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/search`,
|
||||||
|
fetchAfterSales: () => `https://fxg.jinritemai.com/after_sale/pc/list`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let logs = [];
|
||||||
|
let result = "Success";
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
logs.push(message);
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试函数
|
||||||
|
const retry = async (fn, retries = 3, delay = 1000) => {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIfRecordExists(accessToken, appId, tableId, afterSaleId) {
|
||||||
|
const url = CONFIG.urls.bitableSearchRecords(appId, tableId);
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
const body = JSON.stringify({
|
||||||
|
field_names: ["售后单号"],
|
||||||
|
filter: {
|
||||||
|
conjunction: "and",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
field_name: "售后单号",
|
||||||
|
operator: "is",
|
||||||
|
value: [afterSaleId],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
automatic_fields: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest("POST", url, body, headers);
|
||||||
|
return response.data.total > 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error querying Bitable:", error);
|
||||||
|
throw new Error(`Failed to query Bitable: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAfterSales(page, pageSize = 50, startTime, endTime) {
|
||||||
|
const url = CONFIG.urls.fetchAfterSales();
|
||||||
|
const headers = {
|
||||||
|
accept: "application/json, text/plain, */*",
|
||||||
|
"content-type": "application/json;charset=UTF-8",
|
||||||
|
};
|
||||||
|
const body = JSON.stringify({
|
||||||
|
pageSize: pageSize,
|
||||||
|
page: page,
|
||||||
|
apply_time_start: startTime,
|
||||||
|
apply_time_end: endTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest("POST", url, body, headers);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching after sales:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAllAfterSales() {
|
||||||
|
let allAfterSales = [];
|
||||||
|
let page = 0;
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
//秒级时间戳
|
||||||
|
const endTime = Math.floor(Date.now() / 1000);
|
||||||
|
const startTime = endTime - 2 * 3600;
|
||||||
|
//const startTime = 1721516400;
|
||||||
|
//const endTime = 1721523600;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const data = await fetchAfterSales(page, 50, startTime, endTime);
|
||||||
|
if (data.code !== 0) {
|
||||||
|
result = "Failed";
|
||||||
|
throw new Error(`Error fetching after sales: ${data.msg}`);
|
||||||
|
}
|
||||||
|
if (data.data && data.data.items.length > 0) {
|
||||||
|
allAfterSales = allAfterSales.concat(data.data.items);
|
||||||
|
page++;
|
||||||
|
hasMore = data.data.has_more;
|
||||||
|
} else {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allAfterSales;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndUploadAfterSales() {
|
||||||
|
try {
|
||||||
|
log("Fetching after sales data...");
|
||||||
|
const afterSales = await fetchAllAfterSales();
|
||||||
|
log("After sales data fetched successfully.");
|
||||||
|
|
||||||
|
log("Fetching tenant access token...");
|
||||||
|
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
|
||||||
|
log("Tenant access token fetched successfully.");
|
||||||
|
|
||||||
|
log("Uploading after sales data to bitable...");
|
||||||
|
for (const afterSale of afterSales) {
|
||||||
|
const exists = await checkIfRecordExists(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.tableId,
|
||||||
|
afterSale.after_sale_info.after_sale_id
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
const fields = {
|
||||||
|
售后单号: afterSale.after_sale_info.after_sale_id,
|
||||||
|
售后类型: afterSale.after_sale_info.after_sale_tags[0].text,
|
||||||
|
售后原因: afterSale.text_part.reason_text,
|
||||||
|
申请时间: afterSale.after_sale_info.apply_time * 1000,
|
||||||
|
退款金额: afterSale.after_sale_info.refund_amount / 100,
|
||||||
|
订单创建时间:
|
||||||
|
afterSale.order_info.related_order_info[0].create_time * 1000,
|
||||||
|
订单id: afterSale.order_info.related_order_info[0].sku_order_id,
|
||||||
|
订单商品id: afterSale.order_info.related_order_info[0].product_id,
|
||||||
|
订单商品名称: afterSale.order_info.related_order_info[0].product_name,
|
||||||
|
店铺: "夸迪官方旗舰店",
|
||||||
|
};
|
||||||
|
const itemsToWrite = { fields: fields };
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.tableId,
|
||||||
|
itemsToWrite
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("After sales data uploaded successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
log("Error: " + error.message);
|
||||||
|
result = "Failed";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logRunResult() {
|
||||||
|
try {
|
||||||
|
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
|
||||||
|
const runTime = new Date().getTime();
|
||||||
|
const logData = {
|
||||||
|
fields: {
|
||||||
|
表名: "售后",
|
||||||
|
表格id: CONFIG.tableId,
|
||||||
|
最近运行时间: runTime,
|
||||||
|
运行结果: result,
|
||||||
|
日志: 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 {
|
||||||
|
await fetchAndUploadAfterSales();
|
||||||
|
} catch (error) {
|
||||||
|
log("Script execution failed: " + error.message);
|
||||||
|
} finally {
|
||||||
|
await logRunResult();
|
||||||
|
}
|
||||||
|
})();
|
||||||
285
douyinShop/douyinComment/douyinComment.js
Normal file
285
douyinShop/douyinComment/douyinComment.js
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 抖音店铺评价监控
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.2.0
|
||||||
|
// @description 每小时获取前2小时的店铺评价数据,并去重后上传到飞书多维表格
|
||||||
|
// @author wanxi
|
||||||
|
// @crontab 1 * * * *
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @connect open.feishu.cn
|
||||||
|
// @connect fxg.jinritemai.com
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
id: "cli_a6f25876ea28100d",
|
||||||
|
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
|
||||||
|
appId: "GyUUbEzuxajfU4sGieIcNKxvnXd",
|
||||||
|
tableId: "tblfMlqE1lKBEnR4", // 表格ID
|
||||||
|
logTableId: "tbl6eZJpt9GkZjWO", // 运行记录表
|
||||||
|
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`,
|
||||||
|
bitableSearchRecords: (appId, tableId) =>
|
||||||
|
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/search`,
|
||||||
|
fetchComments: (page, pageSize, startTime, endTime) =>
|
||||||
|
`https://fxg.jinritemai.com/product/tcomment/commentList?rank=0&content_search=0&reply_search=0&appeal_search=0&comment_time_from=${startTime}&comment_time_to=${endTime}&pageSize=${pageSize}&page=${page}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let logs = [];
|
||||||
|
let result = "Success";
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
logs.push(message);
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试函数
|
||||||
|
const retry = async (fn, retries = 3, delay = 1000) => {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIfRecordExists(accessToken, appId, tableId, commentId) {
|
||||||
|
const url = CONFIG.urls.bitableSearchRecords(appId, tableId);
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
const body = JSON.stringify({
|
||||||
|
field_names: ["评价id"],
|
||||||
|
filter: {
|
||||||
|
conjunction: "and",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
field_name: "评价id",
|
||||||
|
operator: "is",
|
||||||
|
value: [commentId],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
automatic_fields: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest("POST", url, body, headers);
|
||||||
|
return response.data.total > 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error querying Bitable:", error);
|
||||||
|
throw new Error(`Failed to query Bitable: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchComments(page, pageSize = 50, startTime, endTime) {
|
||||||
|
const url = CONFIG.urls.fetchComments(page, pageSize, startTime, endTime);
|
||||||
|
const headers = {
|
||||||
|
accept: "application/json, text/plain, */*",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest("GET", url, null, headers);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching comments:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAllComments() {
|
||||||
|
let allComments = [];
|
||||||
|
let page = 0;
|
||||||
|
let hasMore = true;
|
||||||
|
//秒级时间戳
|
||||||
|
|
||||||
|
const endTime = Math.floor(Date.now() / 1000);
|
||||||
|
const startTime = endTime - 2 * 3600;
|
||||||
|
|
||||||
|
//const startTime = 1721516400;
|
||||||
|
//const endTime = 1721523600;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
const data = await fetchComments(page, 50, startTime, endTime);
|
||||||
|
if (data.code !== 0) {
|
||||||
|
result = "Failed";
|
||||||
|
throw new Error(`Error fetching comments: ${data.msg}`);
|
||||||
|
}
|
||||||
|
if (data.data && data.data.length > 0) {
|
||||||
|
allComments = allComments.concat(data.data);
|
||||||
|
page++;
|
||||||
|
} else {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allComments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndUploadComments() {
|
||||||
|
try {
|
||||||
|
log("Fetching comments data...");
|
||||||
|
const comments = await fetchAllComments();
|
||||||
|
log("Comments data fetched successfully.");
|
||||||
|
|
||||||
|
log("Fetching tenant access token...");
|
||||||
|
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
|
||||||
|
log("Tenant access token fetched successfully.");
|
||||||
|
|
||||||
|
log("Uploading comments data to bitable...");
|
||||||
|
for (const comment of comments) {
|
||||||
|
const exists = await checkIfRecordExists(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.tableId,
|
||||||
|
comment.id
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
const fields = {
|
||||||
|
评价id: comment.id,
|
||||||
|
评价时间: comment.comment_time * 1000,
|
||||||
|
商品id: comment.product_id,
|
||||||
|
店铺评分: comment.rank_shop,
|
||||||
|
物流评分: comment.rank_logistic,
|
||||||
|
商品评分: comment.rank_product,
|
||||||
|
综合评分: comment.rank,
|
||||||
|
评价标签: comment.tags.rank_info.name,
|
||||||
|
SKU: comment.sku,
|
||||||
|
店铺名: "夸迪官方旗舰店",
|
||||||
|
订单id: comment.order_id,
|
||||||
|
商品名称: comment.product.name,
|
||||||
|
评价内容: comment.content,
|
||||||
|
};
|
||||||
|
const itemsToWrite = { fields: fields };
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.tableId,
|
||||||
|
itemsToWrite
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log("Comments data uploaded successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
log("Error: " + error.message);
|
||||||
|
result = "Failed";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logRunResult() {
|
||||||
|
try {
|
||||||
|
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
|
||||||
|
const runTime = new Date().getTime();
|
||||||
|
const logData = {
|
||||||
|
fields: {
|
||||||
|
表名: "评价",
|
||||||
|
表格id: CONFIG.tableId,
|
||||||
|
最近运行时间: runTime,
|
||||||
|
运行结果: result,
|
||||||
|
日志: 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 {
|
||||||
|
await fetchAndUploadComments();
|
||||||
|
} catch (error) {
|
||||||
|
log("Script execution failed: " + error.message);
|
||||||
|
} finally {
|
||||||
|
await logRunResult();
|
||||||
|
}
|
||||||
|
})();
|
||||||
361
douyinShop/douyinExperienceScore/douyinExperienceScore.js
Normal file
361
douyinShop/douyinExperienceScore/douyinExperienceScore.js
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 抖音商家体验分监控
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.4.0
|
||||||
|
// @description 每天更新前一日的体验分数据
|
||||||
|
// @author wanxi
|
||||||
|
// @crontab 0 8-19 * * *
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
id: "cli_a6f25876ea28100d",
|
||||||
|
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
|
||||||
|
appId: "GyUUbEzuxajfU4sGieIcNKxvnXd",
|
||||||
|
tableId1: "tblyXVQVqwfcyaoK", // 维度表
|
||||||
|
tableId2: "tbl8JEts30wvgWv3", // 指标表
|
||||||
|
logTableId: "tbl6eZJpt9GkZjWO", // 运行记录表
|
||||||
|
dimensionDict: [
|
||||||
|
{
|
||||||
|
维度: "商品体验",
|
||||||
|
指标: ["商品差评率", "商品品质退货率"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
维度: "物流体验",
|
||||||
|
指标: ["运单配送时效达成率", "24小时支付-揽收率", "发货问题负向反馈率"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
维度: "服务体验",
|
||||||
|
指标: [
|
||||||
|
"仅退款自主完结时长",
|
||||||
|
"退货退款自主完结时长",
|
||||||
|
"飞鸽平均响应时长",
|
||||||
|
"飞鸽不满意率",
|
||||||
|
"平台求助率",
|
||||||
|
"售后拒绝率",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
urls: {
|
||||||
|
overview:
|
||||||
|
"https://fxg.jinritemai.com/governance/shop/experiencescore/getOverviewByVersion?exp_version=8.0&source=1",
|
||||||
|
analysisScore:
|
||||||
|
"https://fxg.jinritemai.com/governance/shop/experiencescore/getAnalysisScore?new_dimension=true&time=30&filter_by_industry=true&number_type=30&exp_version=8.0",
|
||||||
|
tenantAccessToken:
|
||||||
|
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||||
|
bitableRecords: (appId, tableId) =>
|
||||||
|
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records`,
|
||||||
|
bitableSearchRecords: (appId, tableId) =>
|
||||||
|
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/search`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let logs = [];
|
||||||
|
let result = "Success";
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
logs.push(message);
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendHttpRequest(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body = null,
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return 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 checkIfRecordExists(accessToken, appId, tableId, date) {
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
CONFIG.urls.bitableSearchRecords(appId, tableId),
|
||||||
|
JSON.stringify({
|
||||||
|
field_names: ["数据时间"],
|
||||||
|
filter: {
|
||||||
|
conjunction: "and",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
field_name: "数据时间",
|
||||||
|
operator: "is",
|
||||||
|
value: [date],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data.total > 0;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to check if record exists in Bitable: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBitableRecords(accessToken, appId, tableId, items) {
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
CONFIG.urls.bitableRecords(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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDimension(indicator) {
|
||||||
|
for (const dimension of CONFIG.dimensionDict) {
|
||||||
|
if (dimension.指标.includes(indicator)) {
|
||||||
|
return dimension.维度;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isYesterday(date) {
|
||||||
|
const today = new Date();
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(today.getDate() - 1);
|
||||||
|
return date.toDateString() === yesterday.toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndUploadData() {
|
||||||
|
try {
|
||||||
|
// 获取TenantAccessToken
|
||||||
|
log("Fetching tenant access token...");
|
||||||
|
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
|
||||||
|
log("Tenant access token fetched successfully.");
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 获取昨天的日期
|
||||||
|
const today = new Date();
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(today.getDate() - 1);
|
||||||
|
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 检查昨天的数据是否已经存在
|
||||||
|
log("Checking if yesterday's data already exists...");
|
||||||
|
//https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/bitable-v1/app-table-record/record-filter-guide
|
||||||
|
const recordExists = await checkIfRecordExists(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.tableId1,
|
||||||
|
"Yesterday"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recordExists) {
|
||||||
|
log("Yesterday's data already exists. Skipping data upload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 细分指标里面有current_date,先请求细分指标
|
||||||
|
log("Fetching analysis score data...");
|
||||||
|
const analysisData = await sendHttpRequest(
|
||||||
|
"GET",
|
||||||
|
CONFIG.urls.analysisScore
|
||||||
|
);
|
||||||
|
log("analysisData: " + JSON.stringify(analysisData));
|
||||||
|
|
||||||
|
if (!analysisData || !analysisData.data) {
|
||||||
|
throw new Error("Invalid analysisData structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = new Date(Date.parse(analysisData.data.current_date));
|
||||||
|
|
||||||
|
if (!isYesterday(currentDate)) {
|
||||||
|
log("Current date is not yesterday. Skipping data upload.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysisScore = analysisData.data.shop_analysis.map((item) => ({
|
||||||
|
指标: item.title,
|
||||||
|
维度: getDimension(item.title),
|
||||||
|
数据时间: new Date(currentDate).getTime(),
|
||||||
|
数值无单位: item.value.value_figure,
|
||||||
|
环比: item.compare_with_self.rise_than_yesterday,
|
||||||
|
超越同行: item.surpass_peers.value_figure,
|
||||||
|
等级: item.level,
|
||||||
|
}));
|
||||||
|
log("Analysis score data fetched successfully.");
|
||||||
|
|
||||||
|
// 将指标分写入多维表格
|
||||||
|
log("Uploading analysis score data to bitable...");
|
||||||
|
for (const item of analysisScore) {
|
||||||
|
const itemsToWrite = {
|
||||||
|
fields: item,
|
||||||
|
};
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.tableId2,
|
||||||
|
itemsToWrite
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log("Analysis score data uploaded successfully.");
|
||||||
|
|
||||||
|
// 维度分
|
||||||
|
log("Fetching overview data...");
|
||||||
|
const overviewData = await sendHttpRequest("GET", CONFIG.urls.overview);
|
||||||
|
log("overviewData: " + JSON.stringify(overviewData));
|
||||||
|
|
||||||
|
if (!overviewData || !overviewData.data) {
|
||||||
|
throw new Error("Invalid overviewData structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
const scores = [
|
||||||
|
{
|
||||||
|
维度: "商品体验分",
|
||||||
|
得分: overviewData.data.goods_score.value,
|
||||||
|
较前一日: overviewData.data.goods_score.rise_than_yesterday,
|
||||||
|
数据时间: new Date(currentDate).getTime(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
维度: "物流体验分",
|
||||||
|
得分: overviewData.data.logistics_score.value,
|
||||||
|
较前一日: overviewData.data.logistics_score.rise_than_yesterday,
|
||||||
|
数据时间: new Date(currentDate).getTime(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
维度: "服务体验分",
|
||||||
|
得分: overviewData.data.service_score.value,
|
||||||
|
较前一日: overviewData.data.service_score.rise_than_yesterday,
|
||||||
|
数据时间: new Date(currentDate).getTime(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
维度: "商家体验分",
|
||||||
|
得分: overviewData.data.experience_score.value,
|
||||||
|
较前一日: overviewData.data.experience_score.rise_than_yesterday,
|
||||||
|
数据时间: new Date(currentDate).getTime(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
log("Overview data fetched successfully.");
|
||||||
|
|
||||||
|
log("Uploading overview data to bitable...");
|
||||||
|
for (const item of scores) {
|
||||||
|
const itemsToWrite = {
|
||||||
|
fields: item,
|
||||||
|
};
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.tableId1,
|
||||||
|
itemsToWrite
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log("Overview data uploaded successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
log("Error: " + error.message);
|
||||||
|
result = "Failed";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logRunResult() {
|
||||||
|
try {
|
||||||
|
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
|
||||||
|
const runTime = new Date().getTime();
|
||||||
|
|
||||||
|
const logData1 = {
|
||||||
|
fields: {
|
||||||
|
表名: "商家体验分-维度",
|
||||||
|
表格id: CONFIG.tableId1,
|
||||||
|
最近运行时间: runTime,
|
||||||
|
运行结果: result,
|
||||||
|
日志: logs.join("\n"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const logData2 = {
|
||||||
|
fields: {
|
||||||
|
表名: "商家体验分-指标",
|
||||||
|
表格id: CONFIG.tableId2,
|
||||||
|
最近运行时间: runTime,
|
||||||
|
运行结果: result,
|
||||||
|
日志: logs.join("\n"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.logTableId,
|
||||||
|
logData1
|
||||||
|
);
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.logTableId,
|
||||||
|
logData2
|
||||||
|
);
|
||||||
|
|
||||||
|
log("Run result logged successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
log("Failed to log run result: " + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
try {
|
||||||
|
await fetchAndUploadData();
|
||||||
|
} catch (error) {
|
||||||
|
log("Script execution failed: " + error.message);
|
||||||
|
} finally {
|
||||||
|
await logRunResult();
|
||||||
|
}
|
||||||
|
})();
|
||||||
409
yuntu/yuntuHotItems/yuntuHotItems.js
Normal file
409
yuntu/yuntuHotItems/yuntuHotItems.js
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 自动获取竞品、行业、跨行业热门千川素材
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 0.2.0
|
||||||
|
// @description try to take over the world!
|
||||||
|
// @author wanxi
|
||||||
|
// @crontab * 8 once * *
|
||||||
|
// @match https://yuntu.oceanengine.com/yuntu_ng/content/creative/content_lab*
|
||||||
|
// @match https://yuntu.oceanengine.com/yuntu_brand/content/creative/content_lab*
|
||||||
|
// @icon https://www.google.com/s2/favicons?domain=oceanengine.com
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
id: "cli_a6f25876ea28100d",
|
||||||
|
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
|
||||||
|
appId: 'EGKVbQPMCarVmWsJEPgcOQ38nKh',
|
||||||
|
tableId1: 'tblSW2DBXwYBWHaT', // 竞品表
|
||||||
|
tableId2: 'tblgwfXH6HD0nMN7', // 同行业
|
||||||
|
tableId3: 'tbl0hQsktecuZP2W', // 跨行业
|
||||||
|
logTableId: 'tblutCXYQl4WYmwY', // 运行记录表
|
||||||
|
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`,
|
||||||
|
bitableSearchRecords: (appId, tableId) => `https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/search`,
|
||||||
|
fetchCompetitorUrl: (brandIdList) => {
|
||||||
|
const industryId = 12;
|
||||||
|
const dateType = 200;
|
||||||
|
const endDate = getTMinus2Date();
|
||||||
|
const startDate = getTMinus2Date();
|
||||||
|
const brandId = 10209634;
|
||||||
|
const competitorType = 1;
|
||||||
|
const itemType = 3;
|
||||||
|
const level1TriggerPointId = 150000;
|
||||||
|
const level2TriggerPointId = 150200;
|
||||||
|
const level3TriggerPointId = 150202;
|
||||||
|
const firstOrderIndex = 10001;
|
||||||
|
const secondOrderIndex = 10001;
|
||||||
|
const secondOrderBy = 'asc';
|
||||||
|
const queryCnt = 20;
|
||||||
|
const baseUrl = 'https://yuntu.oceanengine.com/yuntu_ng/api/v1/CompetitorHotItems';
|
||||||
|
return `${baseUrl}?aadvid=1710507483282439&industry_id=${industryId}&date_type=${dateType}&end_date=${endDate}&start_date=${startDate}&brand_id=${brandId}&brand_id_list=${brandIdList}&competitor_type=${competitorType}&item_type=${itemType}&level_1_trigger_point_id=${level1TriggerPointId}&level_2_trigger_point_id=${level2TriggerPointId}&level_3_trigger_point_id=${level3TriggerPointId}&first_order_index=${firstOrderIndex}&second_order_index=${secondOrderIndex}&second_order_by=${secondOrderBy}&query_cnt=${queryCnt}`;
|
||||||
|
},
|
||||||
|
fetchIndustryUrl: (noCheckIndustry) => {
|
||||||
|
const dateType = 200;
|
||||||
|
const endDate = getTMinus2Date();
|
||||||
|
const startDate = getTMinus2Date();
|
||||||
|
const itemType = 3;
|
||||||
|
const level1TriggerPointId = 150000;
|
||||||
|
const level2TriggerPointId = 150200;
|
||||||
|
const level3TriggerPointId = 150202;
|
||||||
|
const firstOrderIndex = 10001;
|
||||||
|
const secondOrderIndex = 10001;
|
||||||
|
const secondOrderBy = 'asc';
|
||||||
|
const queryCnt = 20;
|
||||||
|
const baseUrl = 'https://yuntu.oceanengine.com/yuntu_ng/api/v1/IndustryHotItems';
|
||||||
|
return `${baseUrl}?aadvid=1710507483282439&no_check_industry=${noCheckIndustry}&date_type=${dateType}&end_date=${endDate}&start_date=${startDate}&item_type=${itemType}&level_1_trigger_point_id=${level1TriggerPointId}&level_2_trigger_point_id=${level2TriggerPointId}&level_3_trigger_point_id=${level3TriggerPointId}&first_order_index=${firstOrderIndex}&second_order_index=${secondOrderIndex}&second_order_by=${secondOrderBy}&query_cnt=${queryCnt}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let logs = [];
|
||||||
|
let result = "Success";
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
logs.push(message);
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试函数
|
||||||
|
const retry = async (fn, retries = 3, delay = 1000) => {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIfRecordExists(accessToken, appId, tableId, videoId) {
|
||||||
|
const url = CONFIG.urls.bitableSearchRecords(appId, tableId);
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
const body = JSON.stringify({
|
||||||
|
"field_names": ["视频后台id"],
|
||||||
|
"filter": {
|
||||||
|
"conjunction": "and",
|
||||||
|
"conditions": [{
|
||||||
|
"field_name": "视频后台id",
|
||||||
|
"operator": "is",
|
||||||
|
"value": [videoId]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"automatic_fields": false
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest("POST", url, body, headers);
|
||||||
|
return response.data.total > 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error querying Bitable:', error);
|
||||||
|
throw new Error(`Failed to query Bitable: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const brandDict = [{
|
||||||
|
brandId: 533686,
|
||||||
|
brandName: "PROYA/珀莱雅"
|
||||||
|
}, {
|
||||||
|
brandId: 885679,
|
||||||
|
brandName: "KANS/韩束"
|
||||||
|
}];
|
||||||
|
|
||||||
|
const industryDict = [{
|
||||||
|
industry_id: 1202,
|
||||||
|
industry_name: "护肤"
|
||||||
|
}, {
|
||||||
|
industry_id: 13,
|
||||||
|
industry_name: "个护清洁(日化)"
|
||||||
|
}, {
|
||||||
|
industry_id: 27,
|
||||||
|
industry_name: "医疗保健"
|
||||||
|
}];
|
||||||
|
|
||||||
|
const getTMinus2Date = () => {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() - 2);
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async (url, getName, isCompetitor) => {
|
||||||
|
console.log(`Fetching data from URL: ${url}`);
|
||||||
|
const fetchDataWithRetry = async () => {
|
||||||
|
const response = await sendHttpRequest('GET', url, null, {
|
||||||
|
'accept': 'application/json, text/plain, */*'
|
||||||
|
});
|
||||||
|
if (response.status === 0 && response.data.hot_items) {
|
||||||
|
return response.data.hot_items
|
||||||
|
.filter(item => item.video_id && item.title && item.id) // 过滤掉任意一个字段为空的条目
|
||||||
|
.map(item => ({
|
||||||
|
视频后台id: item.video_id,
|
||||||
|
[isCompetitor ? '品牌' : '行业']: getName(),
|
||||||
|
日期: getTMinus2Date(),
|
||||||
|
视频标题: item.title,
|
||||||
|
播放量: item.show_cnt,
|
||||||
|
前台id: item.id,
|
||||||
|
点击率: item.click_rate,
|
||||||
|
互动率: item.interact_rate,
|
||||||
|
完播率: item.play_over_rate
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid response');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await retry(fetchDataWithRetry);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchVideoScript = async (video_id, id) => {
|
||||||
|
const fetchVideoScriptWithRetry = async () => {
|
||||||
|
const response = await sendHttpRequest('POST', "https://yuntu.oceanengine.com/yuntu_ng/api/v1/GetContentFormulaAndScript?aadvid=1710507483282439", JSON.stringify({
|
||||||
|
vid: video_id,
|
||||||
|
item_id: id
|
||||||
|
}), {
|
||||||
|
"accept": "application/json, text/plain, */*",
|
||||||
|
"content-type": "application/json"
|
||||||
|
});
|
||||||
|
return response.data.ori_script.text;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await retry(fetchVideoScriptWithRetry);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
return '文案获取失败';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const executeTasks = async () => {
|
||||||
|
console.log('Executing tasks immediately.');
|
||||||
|
|
||||||
|
const fetchCompetitorData = brandDict.map(brand =>
|
||||||
|
fetchData(CONFIG.urls.fetchCompetitorUrl(brand.brandId), () => brand.brandName, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchIndustryData = industryDict
|
||||||
|
.filter(industry => industry.industry_id == 1202) // 不包括护肤行业
|
||||||
|
.map(industry =>
|
||||||
|
fetchData(CONFIG.urls.fetchIndustryUrl(industry.industry_id), () => industry.industry_name, false)
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchCrossIndustryData = industryDict
|
||||||
|
.filter(industry => industry.industry_id !== 1202) // 不包括护肤行业
|
||||||
|
.map(industry =>
|
||||||
|
fetchData(CONFIG.urls.fetchIndustryUrl(industry.industry_id), () => industry.industry_name, false)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [competitorDataArray, industryDataArray, crossIndustryDataArray] = await Promise.all([
|
||||||
|
Promise.all(fetchCompetitorData),
|
||||||
|
Promise.all(fetchIndustryData),
|
||||||
|
Promise.all(fetchCrossIndustryData)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const competitorData = competitorDataArray.flat();
|
||||||
|
const industryData = industryDataArray.flat();
|
||||||
|
const crossIndustryData = crossIndustryDataArray.flat();
|
||||||
|
|
||||||
|
const addScripts = async (data) => {
|
||||||
|
for (const item of data) {
|
||||||
|
item.文案 = await fetchVideoScript(item.视频后台id, item.前台id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await addScripts(competitorData);
|
||||||
|
await addScripts(industryData);
|
||||||
|
await addScripts(crossIndustryData);
|
||||||
|
|
||||||
|
console.log('Competitor Data:', JSON.stringify(competitorData, null, 2));
|
||||||
|
console.log('Industry Data:', JSON.stringify(industryData, null, 2));
|
||||||
|
console.log('Cross-Industry Data:', JSON.stringify(crossIndustryData, null, 2));
|
||||||
|
|
||||||
|
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
|
||||||
|
|
||||||
|
for (const item of competitorData) {
|
||||||
|
const itemsToWrite = {};
|
||||||
|
const exists = await checkIfRecordExists(accessToken, CONFIG.appId, CONFIG.tableId1, item.视频后台id);
|
||||||
|
if (exists === 0) {
|
||||||
|
itemsToWrite.fields = {
|
||||||
|
"视频后台id": item.视频后台id,
|
||||||
|
"品牌/行业": item.品牌 || item.行业,
|
||||||
|
"日期": Math.floor(new Date(item.日期).getTime()),
|
||||||
|
"视频标题": item.视频标题,
|
||||||
|
"播放量": item.播放量,
|
||||||
|
"视频前台id": item.前台id,
|
||||||
|
"点击率": item.点击率,
|
||||||
|
"互动率": item.互动率,
|
||||||
|
"完播率": item.完播率,
|
||||||
|
"文案": item.文案
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (Object.keys(itemsToWrite).length > 0) {
|
||||||
|
console.log(itemsToWrite);
|
||||||
|
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId1, itemsToWrite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of industryData) {
|
||||||
|
const itemsToWrite = {};
|
||||||
|
const exists = await checkIfRecordExists(accessToken, CONFIG.appId, CONFIG.tableId2, item.视频后台id);
|
||||||
|
if (exists === 0) {
|
||||||
|
itemsToWrite.fields = {
|
||||||
|
"视频后台id": item.视频后台id,
|
||||||
|
"品牌/行业": item.品牌 || item.行业,
|
||||||
|
"日期": Math.floor(new Date(item.日期).getTime()),
|
||||||
|
"视频标题": item.视频标题,
|
||||||
|
"播放量": item.播放量,
|
||||||
|
"视频前台id": item.前台id,
|
||||||
|
"点击率": item.点击率,
|
||||||
|
"互动率": item.互动率,
|
||||||
|
"完播率": item.完播率,
|
||||||
|
"文案": item.文案
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (Object.keys(itemsToWrite).length > 0) {
|
||||||
|
console.log(itemsToWrite);
|
||||||
|
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId2, itemsToWrite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of crossIndustryData) {
|
||||||
|
const itemsToWrite = {};
|
||||||
|
const exists = await checkIfRecordExists(accessToken, CONFIG.appId, CONFIG.tableId3, item.视频后台id);
|
||||||
|
if (exists === 0) {
|
||||||
|
itemsToWrite.fields = {
|
||||||
|
"视频后台id": item.视频后台id,
|
||||||
|
"品牌/行业": item.品牌 || item.行业,
|
||||||
|
"日期": Math.floor(new Date(item.日期).getTime()),
|
||||||
|
"视频标题": item.视频标题,
|
||||||
|
"播放量": item.播放量,
|
||||||
|
"视频前台id": item.前台id,
|
||||||
|
"点击率": item.点击率,
|
||||||
|
"互动率": item.互动率,
|
||||||
|
"完播率": item.完播率,
|
||||||
|
"文案": item.文案
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (Object.keys(itemsToWrite).length > 0) {
|
||||||
|
console.log(itemsToWrite);
|
||||||
|
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.tableId3, itemsToWrite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function logRunResult() {
|
||||||
|
try {
|
||||||
|
const accessToken = await fetchTenantAccessToken(CONFIG.id, CONFIG.secret);
|
||||||
|
const runTime = new Date().getTime();
|
||||||
|
|
||||||
|
const logEntries = [{
|
||||||
|
"表名": "竞品表",
|
||||||
|
"表格id": CONFIG.tableId1,
|
||||||
|
"最近运行时间": runTime,
|
||||||
|
"运行结果": result,
|
||||||
|
"日志": logs.join('\n')
|
||||||
|
}, {
|
||||||
|
"表名": "同行业表",
|
||||||
|
"表格id": CONFIG.tableId2,
|
||||||
|
"最近运行时间": runTime,
|
||||||
|
"运行结果": result,
|
||||||
|
"日志": logs.join('\n')
|
||||||
|
}, {
|
||||||
|
"表名": "跨行业表",
|
||||||
|
"表格id": CONFIG.tableId3,
|
||||||
|
"最近运行时间": runTime,
|
||||||
|
"运行结果": result,
|
||||||
|
"日志": logs.join('\n')
|
||||||
|
}];
|
||||||
|
|
||||||
|
for (const logData of logEntries) {
|
||||||
|
await updateBitableRecords(accessToken, CONFIG.appId, CONFIG.logTableId, {
|
||||||
|
fields: logData
|
||||||
|
});
|
||||||
|
log(`Run result for ${logData.表名} logged successfully.`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log('Failed to log run result: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
(async function() {
|
||||||
|
try {
|
||||||
|
await executeTasks();
|
||||||
|
} catch (error) {
|
||||||
|
log('Script execution failed: ' + error.message);
|
||||||
|
result = "Failed";
|
||||||
|
} finally {
|
||||||
|
await logRunResult();
|
||||||
|
}
|
||||||
|
})();
|
||||||
616
yuntu/yuntuLowCPMItems/yuntuLowCPMItems.js
Normal file
616
yuntu/yuntuLowCPMItems/yuntuLowCPMItems.js
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 低流量成本达人监控
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 2.3
|
||||||
|
// @description 每天获取T-2的低CPM CPsearch内容,并给到达人列表,写入多维表格
|
||||||
|
// @author 汪喜
|
||||||
|
// @crontab 53 8 once * *
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @connect open.feishu.cn
|
||||||
|
// @connect yuntu.oceanengine.com
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
feishu: {
|
||||||
|
id: "cli_a6f25876ea28100d",
|
||||||
|
secret: "raLC56ZLIara07nKigpysfoDxHTAeyJf",
|
||||||
|
appId: "Z6Icb88DsaKyLBsOwYhcjHRinTc",
|
||||||
|
videoTableId: "tblfpGtDGUPhteRH",
|
||||||
|
starTableId: "tblrqiSDmdtmBhXu",
|
||||||
|
urls: {
|
||||||
|
tenantAccessToken:
|
||||||
|
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||||
|
bitableBatchCreateRecords: (appId, tableId) =>
|
||||||
|
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/batch_create`,
|
||||||
|
bitableSearchRecords: (appId, tableId) =>
|
||||||
|
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/search`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yuntu: {
|
||||||
|
hotItemApiUrl:
|
||||||
|
"https://yuntu.oceanengine.com/yuntu_ng/api/v1/DoubleHotContentListByTag",
|
||||||
|
aadvid: "1710507483282439",
|
||||||
|
starDataUrl:
|
||||||
|
"https://yuntu.oceanengine.com/yuntu_ng/api/v2/get_talent_filter_v3",
|
||||||
|
industryObj: [
|
||||||
|
{
|
||||||
|
industry_id: 12,
|
||||||
|
industry_name: "美妆",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
industry_id: 13,
|
||||||
|
industry_name: "个护清洁(日化)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
industry_id: 14,
|
||||||
|
industry_name: "食品饮料",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
industry_id: 17,
|
||||||
|
industry_name: "3C数码",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
industry_id: 18,
|
||||||
|
industry_name: "宠物",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
industry_id: 20,
|
||||||
|
industry_name: "母婴",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
industry_id: 27,
|
||||||
|
industry_name: "医疗保健",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
industry_id: 28,
|
||||||
|
industry_name: "家用电器",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
retry: {
|
||||||
|
retries: 3,
|
||||||
|
delay: 10000,
|
||||||
|
},
|
||||||
|
batchSize: 500,
|
||||||
|
requestDelay: Math.floor(Math.random() * 8001) + 2000, // 2-10秒随机延迟
|
||||||
|
webhook: {
|
||||||
|
url: "https://bloomagebiotech.feishu.cn/base/automation/webhook/event/ZFVgaDgM9wwBUuhkma5cTYSOnIU", // Webhook URL
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重试函数
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 延迟函数
|
||||||
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
// HTTP 请求函数
|
||||||
|
async function sendHttpRequest(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body = null,
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
errorHandler = null
|
||||||
|
) {
|
||||||
|
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) {
|
||||||
|
console.log(jsonResponse.msg);
|
||||||
|
}
|
||||||
|
// 使用自定义的错误处理函数
|
||||||
|
if (errorHandler && !errorHandler(jsonResponse)) {
|
||||||
|
reject(`API Error: ${JSON.stringify(jsonResponse)}`);
|
||||||
|
} else {
|
||||||
|
resolve(jsonResponse);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(`Failed to parse JSON: ${e}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(`Error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror: function (error) {
|
||||||
|
reject(`Network Error: ${error}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算日期
|
||||||
|
function formatDate(date) {
|
||||||
|
const d = new Date(date);
|
||||||
|
let month = "" + (d.getMonth() + 1);
|
||||||
|
let day = "" + d.getDate();
|
||||||
|
const year = d.getFullYear();
|
||||||
|
|
||||||
|
if (month.length < 2) month = "0" + month;
|
||||||
|
if (day.length < 2) day = "0" + day;
|
||||||
|
|
||||||
|
return [year, month, day].join("-");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 T-N 的数据
|
||||||
|
function getDynamicDates(n) {
|
||||||
|
const today = new Date();
|
||||||
|
const T_minus_2 = new Date(today);
|
||||||
|
T_minus_2.setDate(today.getDate() - 2);
|
||||||
|
|
||||||
|
const T_minus_n = new Date(T_minus_2);
|
||||||
|
T_minus_n.setDate(T_minus_2.getDate() - n);
|
||||||
|
|
||||||
|
return {
|
||||||
|
start_date: formatDate(T_minus_n),
|
||||||
|
end_date: formatDate(T_minus_2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理云图 API 返回的数据
|
||||||
|
function processYuntuData(item, industryName) {
|
||||||
|
return {
|
||||||
|
视频id: item.item_id,
|
||||||
|
视频链接文本: item.item_link,
|
||||||
|
达人昵称: item.nickname,
|
||||||
|
达人id: item.aweme_id,
|
||||||
|
达人星图id: item.star_uid_id,
|
||||||
|
视频标题: item.title,
|
||||||
|
视频后台id: item.video_id,
|
||||||
|
视频时长: Number(item.video_duration),
|
||||||
|
自然播放次数: item.index_map["80101"] || 0,
|
||||||
|
自然互动次数: item.index_map["80105"] || 0,
|
||||||
|
回搜次数: item.index_map["110001"] || 0,
|
||||||
|
回搜人数: item.index_map["110002"] || 0,
|
||||||
|
看后搜次数: item.index_map["10010"] || 0,
|
||||||
|
看后搜人数: Number(item.key_word_after_search_info.search_uv) || 0,
|
||||||
|
回搜率: item.index_map["110003"] || 0,
|
||||||
|
看后搜率: item.key_word_after_search_info.search_rate || 0,
|
||||||
|
//"search_cost": item.search_cost || 0,
|
||||||
|
发布时间: new Date(
|
||||||
|
item.create_date.slice(0, 4),
|
||||||
|
item.create_date.slice(4, 6) - 1,
|
||||||
|
item.create_date.slice(6, 8)
|
||||||
|
).getTime(),
|
||||||
|
品牌ID: Number(item.brand_id),
|
||||||
|
行业名称: industryName,
|
||||||
|
视频类型: item.content_type, //数组类型 多选
|
||||||
|
搜索关键词:
|
||||||
|
item.key_word_after_search_info.keywords === undefined
|
||||||
|
? ""
|
||||||
|
: item.key_word_after_search_info.keywords.join(", "),
|
||||||
|
完播数: item.index_map["30019"] || 0,
|
||||||
|
总播放次数: item.index_map["80001"] || 0,
|
||||||
|
新增A3率: item.index_map["100002"] || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 云图 API 类
|
||||||
|
class YuntuAPI {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleYuntuResponse(response) {
|
||||||
|
// 检查 status 字段
|
||||||
|
return response.status === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchDoubleHotContentListByTag(body, industryName) {
|
||||||
|
try {
|
||||||
|
const url = `${this.config.hotItemApiUrl}?aadvid=${this.config.aadvid}`;
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
url,
|
||||||
|
JSON.stringify(body),
|
||||||
|
{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
this.handleYuntuResponse
|
||||||
|
);
|
||||||
|
await delay(CONFIG.requestDelay); // 添加延迟
|
||||||
|
return response.data.item_list.map((item) =>
|
||||||
|
processYuntuData(item, industryName)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch data from Yuntu API", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createRequestBody(industryId, industryName) {
|
||||||
|
const dates = getDynamicDates(7);
|
||||||
|
return {
|
||||||
|
general_cond: {
|
||||||
|
industry_id: industryId,
|
||||||
|
brand_id: "10209634",
|
||||||
|
time_range: {
|
||||||
|
date_type: 0,
|
||||||
|
start_date: dates.start_date,
|
||||||
|
end_date: dates.end_date,
|
||||||
|
},
|
||||||
|
time_type: 2,
|
||||||
|
task_type: 1,
|
||||||
|
},
|
||||||
|
hot_type: [1, 0, 2, 3],
|
||||||
|
item_type: 1, // 0 本品牌 1行业 2竞品
|
||||||
|
industry_list: [
|
||||||
|
{
|
||||||
|
industry_id: industryId,
|
||||||
|
industry_name: industryName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAllIndustries() {
|
||||||
|
const results = [];
|
||||||
|
for (const industry of this.config.industryObj) {
|
||||||
|
const { industry_id, industry_name } = industry;
|
||||||
|
const requestBody = this.createRequestBody(industry_id, industry_name);
|
||||||
|
const result = await this.fetchDoubleHotContentListByTag(
|
||||||
|
requestBody,
|
||||||
|
industry_name
|
||||||
|
);
|
||||||
|
results.push(...result);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取达人星图数据
|
||||||
|
async fetchStarData(starIds) {
|
||||||
|
const results = [];
|
||||||
|
await batchProcess(starIds, CONFIG.batchSize, async (batch) => {
|
||||||
|
const body = {
|
||||||
|
mkt_scene: 1,
|
||||||
|
task_type: 1,
|
||||||
|
brand_id: "10209634",
|
||||||
|
industry_id: "12",
|
||||||
|
exact_match_filter: {
|
||||||
|
talent_filter_cond: 1,
|
||||||
|
exact_match_list: batch,
|
||||||
|
},
|
||||||
|
gender: -1,
|
||||||
|
limit: CONFIG.batchSize,
|
||||||
|
};
|
||||||
|
const url = `${this.config.starDataUrl}?aadvid=${this.config.aadvid}`;
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
url,
|
||||||
|
JSON.stringify(body),
|
||||||
|
{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
this.handleYuntuResponse
|
||||||
|
);
|
||||||
|
await delay(CONFIG.requestDelay); // 添加延迟
|
||||||
|
results.push(
|
||||||
|
...response.data.info_item_list.map((item) => ({
|
||||||
|
达人星图id: item.star_uid,
|
||||||
|
达人昵称: item.nick_name,
|
||||||
|
"20s以下视频报价": Number(item.price),
|
||||||
|
"20-60s视频报价": Number(item.price_20_60),
|
||||||
|
"60s以上视频报价": Number(item.price_60),
|
||||||
|
达人uid: item.aweme_id,
|
||||||
|
粉丝量: Number(item.fans_cnt),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 飞书 API 类
|
||||||
|
class FeishuAPI {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
this.accessToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取token
|
||||||
|
async fetchTenantAccessToken() {
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
this.config.urls.tenantAccessToken,
|
||||||
|
JSON.stringify({
|
||||||
|
app_id: this.config.id,
|
||||||
|
app_secret: this.config.secret,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.accessToken = response.tenant_access_token;
|
||||||
|
await delay(CONFIG.requestDelay); // 添加延迟
|
||||||
|
return this.accessToken;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error fetching Tenant Access Token: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//批量更新记录
|
||||||
|
async batchCreateBitableRecords(tableId, items) {
|
||||||
|
if (!this.accessToken) {
|
||||||
|
await this.fetchTenantAccessToken();
|
||||||
|
}
|
||||||
|
const results = [];
|
||||||
|
await batchProcess(items, CONFIG.batchSize, async (batch) => {
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
this.config.urls.bitableBatchCreateRecords(
|
||||||
|
this.config.appId,
|
||||||
|
tableId
|
||||||
|
),
|
||||||
|
JSON.stringify({
|
||||||
|
records: batch,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await delay(CONFIG.requestDelay); // 添加延迟
|
||||||
|
console.log("Batch created records: " + JSON.stringify(response));
|
||||||
|
results.push(response);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to batch create records in Bitable: ${error}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
//返回视频id、recordid和视频标签
|
||||||
|
async fetchRecordByVideoId(videoId) {
|
||||||
|
if (!this.accessToken) {
|
||||||
|
await this.fetchTenantAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
field_names: ["视频id", "标签"],
|
||||||
|
filter: {
|
||||||
|
conjunction: "and",
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
field_name: "视频id",
|
||||||
|
operator: "is",
|
||||||
|
value: [videoId],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
sort: [
|
||||||
|
{
|
||||||
|
field_name: "记录时间",
|
||||||
|
desc: true, //倒序
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = CONFIG.feishu.urls.bitableSearchRecords(
|
||||||
|
CONFIG.feishu.appId,
|
||||||
|
CONFIG.feishu.videoTableId
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await sendHttpRequest("POST", url, JSON.stringify(body), {
|
||||||
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量处理函数
|
||||||
|
async function batchProcess(items, batchSize, processFunction) {
|
||||||
|
for (let i = 0; i < items.length; i += batchSize) {
|
||||||
|
const batch = items.slice(i, i + batchSize);
|
||||||
|
await processFunction(batch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送统计结果到 Webhook 服务器
|
||||||
|
async function sendStatisticsToWebhook(lowCostCount, explosiveCount) {
|
||||||
|
const payload = {
|
||||||
|
lowCostCount: lowCostCount,
|
||||||
|
explosiveCount: explosiveCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendHttpRequest("POST", CONFIG.webhook.url, JSON.stringify(payload), {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
console.log("Statistics sent to webhook successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send statistics to webhook:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
async function processAndUploadData() {
|
||||||
|
try {
|
||||||
|
console.log("Start processing and uploading data...");
|
||||||
|
|
||||||
|
// 1. 请求所有的视频数据
|
||||||
|
const yuntuAPI = new YuntuAPI(CONFIG.yuntu);
|
||||||
|
const allData = await yuntuAPI.fetchAllIndustries();
|
||||||
|
console.log("Fetched all video data.");
|
||||||
|
|
||||||
|
// 2. 根据视频里面的达人星图id字段,请求所有达人的星图报价数据
|
||||||
|
const starIds = allData.map((data) => data["达人星图id"]);
|
||||||
|
const starData = await yuntuAPI.fetchStarData(starIds);
|
||||||
|
console.log("Fetched all star data.");
|
||||||
|
|
||||||
|
// 3. 获取已存在的多维表格记录的视频id和标签信息
|
||||||
|
const feishuAPI = new FeishuAPI(CONFIG.feishu);
|
||||||
|
|
||||||
|
// 4. 根据视频时长,选择合适的报价,如果计算出来的CPM<40,则把视频数据和达人数据push到两个json数组中
|
||||||
|
const filteredVideoData = [];
|
||||||
|
const filteredStarData = [];
|
||||||
|
|
||||||
|
// 记录标签推送次数
|
||||||
|
let lowCostCount = 0;
|
||||||
|
let explosiveCount = 0;
|
||||||
|
|
||||||
|
// 遍历所有视频数据
|
||||||
|
for (const videoData of allData) {
|
||||||
|
// 找到当前视频对应的达人信息
|
||||||
|
const currentStarInfo = starData.find(
|
||||||
|
(star) => star["达人星图id"] === videoData["达人星图id"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果找到了对应的达人信息
|
||||||
|
if (currentStarInfo) {
|
||||||
|
// 根据视频时长选择合适的报价
|
||||||
|
let price;
|
||||||
|
if (videoData["视频时长"] < 20) {
|
||||||
|
price = currentStarInfo["20s以下视频报价"];
|
||||||
|
} else if (videoData["视频时长"] <= 60) {
|
||||||
|
price = currentStarInfo["20-60s视频报价"];
|
||||||
|
} else {
|
||||||
|
price = currentStarInfo["60s以上视频报价"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据过滤部分
|
||||||
|
// 计算自然流量的每千次曝光成本(CPM)
|
||||||
|
const cpm = (price / videoData["自然播放次数"]) * 1000;
|
||||||
|
|
||||||
|
// 计算预估的自然看后搜人数
|
||||||
|
const nativeSearchUv =
|
||||||
|
(videoData["自然播放次数"] / videoData["总播放次数"]) *
|
||||||
|
videoData["看后搜人数"];
|
||||||
|
|
||||||
|
// 计算预估的自然看后搜成本
|
||||||
|
const CPNativeSearchUv = price / nativeSearchUv;
|
||||||
|
|
||||||
|
// 初始化标识字段为数组
|
||||||
|
videoData["标签"] = [];
|
||||||
|
|
||||||
|
// 筛选条件:CPM小于20,CPNativeSearchUv小于10,且报价大于0,或者自然播放次数大于1000万
|
||||||
|
if (
|
||||||
|
(cpm < 20 && CPNativeSearchUv < 10 && price > 0) ||
|
||||||
|
(videoData["自然播放次数"] > 10000000 && nativeSearchUv > 3000)
|
||||||
|
) {
|
||||||
|
// 为视频数据添加新的字段标识“低成本”或“爆量”
|
||||||
|
if (cpm < 20 && CPNativeSearchUv < 10 && price > 0) {
|
||||||
|
if (!videoData["标签"].includes("低成本")) {
|
||||||
|
videoData["标签"].push("低成本");
|
||||||
|
//lowCostCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
videoData["自然播放次数"] > 10000000 &&
|
||||||
|
nativeSearchUv > 3000 &&
|
||||||
|
price > 0
|
||||||
|
) {
|
||||||
|
if (!videoData["标签"].includes("爆量")) {
|
||||||
|
videoData["标签"].push("爆量");
|
||||||
|
// explosiveCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加当前时间字段
|
||||||
|
videoData["记录时间"] = Date.now();
|
||||||
|
|
||||||
|
// 查找多维表格中的记录
|
||||||
|
const existingRecords = await feishuAPI.fetchRecordByVideoId(
|
||||||
|
videoData["视频id"]
|
||||||
|
);
|
||||||
|
const existingRecord =
|
||||||
|
existingRecords.length > 0 ? existingRecords[0] : null;
|
||||||
|
|
||||||
|
// 如果视频ID已经存在,则判断标签是否不同
|
||||||
|
if (existingRecord) {
|
||||||
|
const existingTags = existingRecord.fields["标签"] || [];
|
||||||
|
const newTags = videoData["标签"];
|
||||||
|
// 使用集合比较标签
|
||||||
|
const tagsAreDifferent =
|
||||||
|
new Set([...existingTags, ...newTags]).size !==
|
||||||
|
existingTags.length;
|
||||||
|
|
||||||
|
// 如果标签不同,则更新记录
|
||||||
|
if (tagsAreDifferent) {
|
||||||
|
filteredVideoData.push(videoData);
|
||||||
|
//标签不同不需要再push达人数据,表格中已经有了
|
||||||
|
//filteredStarData.push(currentStarInfo);
|
||||||
|
// 增加计数器
|
||||||
|
if (videoData["标签"].includes("低成本")) {
|
||||||
|
lowCostCount++;
|
||||||
|
}
|
||||||
|
if (videoData["标签"].includes("爆量")) {
|
||||||
|
explosiveCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果视频ID不存在,则新增记录
|
||||||
|
filteredVideoData.push(videoData);
|
||||||
|
filteredStarData.push(currentStarInfo);
|
||||||
|
// 增加计数器
|
||||||
|
if (videoData["标签"].includes("低成本")) {
|
||||||
|
lowCostCount++;
|
||||||
|
}
|
||||||
|
if (videoData["标签"].includes("爆量")) {
|
||||||
|
explosiveCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理:如果没有满足条件的视频,记录日志
|
||||||
|
if (filteredVideoData.length === 0) {
|
||||||
|
console.warn("No videos met the criteria.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoRecordsToUpload = filteredVideoData.map((data) => ({
|
||||||
|
fields: data,
|
||||||
|
}));
|
||||||
|
const starRecordsToUpload = filteredStarData.map((data) => ({
|
||||||
|
fields: data,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 4. 将两个json数组的结果,分别写入达人表和视频表
|
||||||
|
await feishuAPI.batchCreateBitableRecords(
|
||||||
|
CONFIG.feishu.videoTableId,
|
||||||
|
videoRecordsToUpload
|
||||||
|
);
|
||||||
|
await feishuAPI.batchCreateBitableRecords(
|
||||||
|
CONFIG.feishu.starTableId,
|
||||||
|
starRecordsToUpload
|
||||||
|
);
|
||||||
|
|
||||||
|
// 在数据成功写入多维表格后调用
|
||||||
|
await sendStatisticsToWebhook(lowCostCount, explosiveCount);
|
||||||
|
|
||||||
|
console.log("Data successfully uploaded to Bitable.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to process and upload data: ", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动启动脚本
|
||||||
|
(async () => {
|
||||||
|
await processAndUploadData();
|
||||||
|
})();
|
||||||
Loading…
x
Reference in New Issue
Block a user