唯品会联盟实时订单采集器-crazyunix

在唯品会联盟订单报表页面,采集订单数据并发送到指定接口,同时展示状态面板和日志,支持自动翻页、重试、手动控制等功能。

// ==UserScript==
// @name         唯品会联盟实时订单采集器-crazyunix
// @namespace    http://tampermonkey.net/
// @version      2025.06.26.1
// @description  在唯品会联盟订单报表页面,采集订单数据并发送到指定接口,同时展示状态面板和日志,支持自动翻页、重试、手动控制等功能。
// @author       Your Name
// @match        https://union.vip.com/v/index*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=vip.com
// @connect      ff.wxmob.cn
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    const TARGET_API_URL = 'https://ff.wxmob.cn/api/market/report/data';
    const VIP_ORDER_API_KEYWORD = 'v2/order/list';
    const CUSTOM_HEADERS = { 'Content-Type': 'application/json' };
    const MAX_RETRY = 3;

    let globalStopFlag = false;
    let currentPage = 1;
    let sendSuccessCount = 0;
    let lastFailedData = null;

    // 1. 注入浮动状态面板
    const panel = document.createElement('div');
    panel.id = 'vip-order-panel';
    panel.style.cssText = `
        position: fixed; top: 100px; right: 20px; width: 300px; background: #f9f9f9;
        border: 1px solid #ccc; border-radius: 8px; padding: 12px; z-index: 9999;
        box-shadow: 0 0 10px rgba(0,0,0,0.2); font-size: 14px; font-family: sans-serif;
    `;
    panel.innerHTML = `
        <div id="panel-header" style="cursor: move; font-weight: bold; background: #eee; padding: 5px 10px; border-radius: 6px 6px 0 0;">
            📊 实时采集状态
            <button id="close-panel" style="float: right; color: red; border: none; background: none; font-weight: bold; cursor: pointer;">✖</button>
        </div>
        <div style="margin-top: 10px">
            <label>📇 Advertiser ID:</label>
            <input id="advertiser-id" type="text" placeholder="请输入广告主ID"
                style="width: 100%; padding: 4px; margin-top: 4px; border-radius: 4px; border: 1px solid #ccc;"/>
        </div>
        <div id="status-panel" style="margin: 10px 0;"></div>
        <div style="display: flex; justify-content: space-between;">
            <button id="stop-btn">🛑 停止</button>
            <button id="retry-btn">🔁 重试</button>
            <button id="resume-btn">▶️ 继续</button>
        </div>
        <div id="log-panel" style="margin-top: 10px; max-height: 200px; overflow-y: auto; background: #fff; padding: 6px; border: 1px solid #ddd;"></div>
    `;
    document.body.appendChild(panel);

    // 2. 拖动支持
    const drag = document.getElementById('panel-header');
    drag.onmousedown = function (e) {
        const offsetX = e.clientX - panel.offsetLeft;
        const offsetY = e.clientY - panel.offsetTop;
        document.onmousemove = function (e) {
            panel.style.left = e.clientX - offsetX + 'px';
            panel.style.top = e.clientY - offsetY + 'px';
        };
        document.onmouseup = function () {
            document.onmousemove = null;
            document.onmouseup = null;
        };
    };

    // 3. 控件绑定
    document.getElementById('stop-btn').onclick = () => globalStopFlag = true;
    document.getElementById('retry-btn').onclick = () => {
        if (lastFailedData) {
            globalStopFlag = false;
            sendDataWithRetry(lastFailedData, MAX_RETRY, tryAutoTurnPage);
        }
    };
    document.getElementById('resume-btn').onclick = () => {
        globalStopFlag = false;
        tryAutoTurnPage();
    };
    document.getElementById('close-panel').onclick = () => panel.style.display = 'none';

    function updateStatusPanel({ page, status, retriesLeft, error }) {
        document.getElementById('status-panel').innerHTML =
            `📄 第 ${page} 页<br/>状态: ${status}<br/>重试剩余: ${retriesLeft}<br/>错误: ${error}`;
    }

    function addTaskLog(text) {
        const log = document.getElementById('log-panel');
        const line = document.createElement('div');
        const timestamp = new Date().toLocaleTimeString();
        line.textContent = `[${timestamp}] ${text}`;
        log.appendChild(line);
        log.scrollTop = log.scrollHeight;
        console.log(`[LOG][${timestamp}] ${text}`);
    }

    // 4. 劫持 XHR
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        this._requestURL = url;
        return originalOpen.apply(this, arguments);
    };
    const originalSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function() {
        this.addEventListener('load', function() {
            if (this._requestURL && this._requestURL.includes(VIP_ORDER_API_KEYWORD)) {
                if (this.status === 200 && this.responseText) {
                    try {
                        const responseData = JSON.parse(this.responseText);
                        sendDataWithRetry(responseData, MAX_RETRY, tryAutoTurnPage);
                    } catch (e) {
                        console.error('解析 JSON 失败:', e);
                    }
                }
            }
        });
        return originalSend.apply(this, arguments);
    };

    function sendDataWithRetry(data, retries, callback) {
        const advertiserId = document.getElementById('advertiser-id')?.value?.trim();

        if (!advertiserId) {
            updateStatusPanel({ page: currentPage, status: '❌ 未填写广告主ID', retriesLeft: 0, error: '缺失ID' });
            addTaskLog('🚫 未发送:广告主ID为空');
            return;
        }

        lastFailedData = data;
        const headers = { ...CUSTOM_HEADERS, advertiserId };
        updateStatusPanel({ page: currentPage, status: '发送中...', retriesLeft: retries, error: '' });

        console.log(`🛰️ 正在发送第 ${currentPage} 页数据到 ${TARGET_API_URL}`);
        console.log(`添加的 header advertiserId=${advertiserId}`);

        GM_xmlhttpRequest({
            method: 'POST',
            url: TARGET_API_URL,
            headers,
            data: JSON.stringify(data),
            onload: function (resp) {
                if (resp.status >= 200 && resp.status < 300) {
                    sendSuccessCount++;
                    lastFailedData = null;
                    updateStatusPanel({ page: currentPage, status: '✅ 成功', retriesLeft: retries, error: '' });
                    addTaskLog(`✅ 第 ${currentPage} 页发送成功`);
                    callback && callback();
                } else {
                    retryOrFail(`HTTP状态 ${resp.status}`);
                }
            },
            onerror: () => retryOrFail('网络错误')
        });

        function retryOrFail(msg) {
            if (retries > 1) {
                updateStatusPanel({ page: currentPage, status: '重试中...', retriesLeft: retries - 1, error: msg });
                setTimeout(() => sendDataWithRetry(data, retries - 1, callback), 1500);
            } else {
                updateStatusPanel({ page: currentPage, status: '❌ 失败', retriesLeft: 0, error: msg });
                globalStopFlag = true;
                addTaskLog(`❌ 第 ${currentPage} 页失败:${msg}`);
                alert('❌ 数据发送失败,请检查接口或广告主ID');
            }
        }
    }

    function tryAutoTurnPage() {
        if (globalStopFlag) return;
        const nextBtn = document.querySelector('button.btn-next');
        if (!nextBtn) {
            console.warn('⚠️ 未找到翻页按钮');
            return;
        }
        if (nextBtn.disabled) {
            updateStatusPanel({ page: currentPage, status: '✅ 全部完成', retriesLeft: 0, error: '' });
            addTaskLog(`🎉 全部采集完成,成功 ${sendSuccessCount} 页`);
            alert('✅ 所有订单数据已采集完成!');
        } else {
            currentPage++;
            updateStatusPanel({ page: currentPage, status: '翻页中...', retriesLeft: MAX_RETRY, error: '' });
            // 👇 添加翻页延迟,1 秒后点击下一页
            setTimeout(() => {
                nextBtn.click();
            }, 1000);
        }
    }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。