// ==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(); } })();