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