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