// ==UserScript== // @name 抖音商家体验分监控 // @namespace https://bbs.tampermonkey.net.cn/ // @version 0.3.4 // @description 每天更新前一日的体验分数据 // @author wanxi // @crontab 0 8-19 * * * // @grant GM_xmlhttpRequest // ==/UserScript== const CONFIG = { id: "", secret: "", appId: '', tableId1: '', // 维度表 tableId2: '', // 指标表 logTableId: '', // 运行记录表 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?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(); } })();