feat: 新增多个脚本用于监控抖音直播、店铺评价、售后及体验分数据
新增了多个脚本文件,用于监控抖音直播间的弹幕、店铺评价、售后数据及商家体验分。这些脚本通过飞书多维表格进行数据存储,并支持定时任务自动更新数据。具体包括: 1. 直播间弹幕监控脚本 2. 店铺评价监控脚本 3. 售后数据监控脚本 4. 商家体验分监控脚本 5. 竞品、行业及跨行业热门千川素材获取脚本 这些脚本通过飞书API进行数据写入,并支持去重和定时任务调度。
This commit is contained in:
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
@@ -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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user