阿里巴巴国际站客户信息导出--树洞先生

更新加入了公海客户和我的客户导出并对客户行为字段进行爬取,"产品浏览数", "有效询盘数", "有效RFQ数", "登录天数", "垃圾询盘数", "被加为黑名单数", "订单总数", "订单总金额", "交易供应商数", "最近搜索词", "最常采购行业", "最近询盘产品链接"

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         阿里巴巴国际站客户信息导出--树洞先生
// @namespace    http://tampermonkey.net/
// @version      1.7
// @license      MIT
// @description  更新加入了公海客户和我的客户导出并对客户行为字段进行爬取,"产品浏览数", "有效询盘数", "有效RFQ数", "登录天数", "垃圾询盘数", "被加为黑名单数", "订单总数", "订单总金额", "交易供应商数", "最近搜索词", "最常采购行业", "最近询盘产品链接"
// @author       树洞先生
// @match        https://alicrm.alibaba.com/*
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js
// ==/UserScript==

(function() {
    'use strict';

    // 字段映射
    const fields = [
        "客户姓名", "旺旺ID", "业务员", "来源", "性别", "客户国家", "客户等级", "职位",
        "头像", "客户名片链接", "邮箱", "手机号码", "座机号码", "社交账号", "建档时间", "注册时间",
        "年采购额", "公司名称", "公司部门", "公司网址", "公司地址",
        // 行为字段
        "产品浏览数", "有效询盘数", "有效RFQ数", "登录天数", "垃圾询盘数", "被加为黑名单数", "订单总数", "订单总金额", "交易供应商数", "最近搜索词", "最常采购行业", "最近询盘产品链接"
    ];

    // 英文与中文字段映射
    const behaviorFieldMap = {
        productViewCount: "产品浏览数",
        validInquiryCount: "有效询盘数",
        validRfqCount: "有效RFQ数",
        loginDays: "登录天数",
        spamInquiryMarkedBySupplierCount: "垃圾询盘数",
        addedToBlacklistCount: "被加为黑名单数",
        totalOrderCount: "订单总数",
        totalOrderVolume: "订单总金额",
        tradeSupplierCount: "交易供应商数",
        searchWords: "最近搜索词",
        preferredIndustries: "最常采购行业",
        latestInquiryProducts: "最近询盘产品链接"
    };

    // 判断是否为公海客户页面
    function isPublicCustomerPage() {
        // 直接根据URL hash判断
        return location.hash === '#public-customer';
    }

    // 采集公海客户ID(分页采集,支持筛选参数)
    async function getPublicCustomerIds(_tb_token_, progressCb) {
        let customerIds = [];
        let pageNum = 1;
        const filterParams = getCurrentFilterParams(); // 获取筛选参数
        const pageSize = 10;
        let total = 0;
        while (true) {
            const body = {
                jsonArray: filterParams.jsonArray || '[]',
                orderDescs: [{col: 'opp_gmt_modified', asc: false}],
                pageNum: pageNum,
                pageSize: pageSize
            };
            const resp = await fetch('https://alicrm.alibaba.com/eggCrmQn/crm/customerQueryServiceI/queryPublicCustomerList.json?_tb_token_=' + encodeURIComponent(_tb_token_), {
                method: 'POST',
                headers: {
                    'content-type': 'application/json;charset=UTF-8',
                    'accept': '*/*'
                },
                credentials: 'include',
                body: JSON.stringify(body)
            });
            const data = await resp.json();
            if (pageNum === 1) {
                total = data?.data?.total || 0;
            }
            const customers = data?.data?.data || [];
            if (!customers.length) break;
            for (const c of customers) {
                if (c.customerId) customerIds.push(c.customerId);
            }
            if (progressCb) progressCb(pageNum, customerIds.length, total);
            if (customerIds.length >= total) break;
            pageNum++;
            await new Promise(r => setTimeout(r, 300));
        }
        return customerIds;
    }

    // 获取cookie
    function getCookie(name) {
        const value = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
        return value ? value.pop() : '';
    }

    // --- 自动捕获最新筛选参数 ---
    let lastJsonArray = null;
    window._lastJsonArray = lastJsonArray;
    if (typeof unsafeWindow !== 'undefined') {
        unsafeWindow._lastJsonArray = lastJsonArray;
    }
    (function() {
        const oldFetch = window.fetch;
        window.fetch = function(input, init) {
            if (typeof input === 'string' && input.includes('queryCustomerList.json') && init && init.body) {
                try {
                    const body = JSON.parse(init.body);
                    if (body.jsonArray) {
                        lastJsonArray = body.jsonArray;
                        window._lastJsonArray = lastJsonArray;
                        if (typeof unsafeWindow !== 'undefined') {
                            unsafeWindow._lastJsonArray = lastJsonArray;
                        }
                    }
                } catch (e) {}
            }
            return oldFetch.apply(this, arguments);
        };
    })();

    // 获取当前页面筛选参数(如有特殊参数可补充)
    function getCurrentFilterParams() {
        if (lastJsonArray) {
            return { jsonArray: lastJsonArray };
        }
        // fallback: 可选,返回空或默认
        return {};
    }

    // 采集所有筛选结果的客户ID(分页采集)
    async function getFilteredCustomerIds(_tb_token_, progressCb) {
        let customerIds = [];
        let pageNum = 1;
        const filterParams = getCurrentFilterParams();
        const pageSize = 10; // 和页面一致
        let total = 0;
        while (true) {
            const body = {
                jsonArray: filterParams.jsonArray || '[]',
                orderDescs: [{col: 'opp_gmt_modified', asc: false}],
                pageNum: pageNum,
                pageSize: pageSize
            };
            const resp = await fetch('https://alicrm.alibaba.com/eggCrmQn/crm/customerQueryServiceI/queryCustomerList.json?_tb_token_=' + encodeURIComponent(_tb_token_), {
                method: 'POST',
                headers: {
                    'content-type': 'application/json;charset=UTF-8',
                    'accept': '*/*'
                },
                credentials: 'include',
                body: JSON.stringify(body)
            });
            const data = await resp.json();
            if (pageNum === 1) {
                total = data?.data?.total || 0;
            }
            const customers = data?.data?.data || [];
            if (!customers.length) break;
            for (const c of customers) {
                if (c.customerId) customerIds.push(c.customerId);
            }
            if (progressCb) progressCb(pageNum, customerIds.length, total);
            if (customerIds.length >= total) break;
            pageNum++;
            await new Promise(r => setTimeout(r, 300));
        }
        return customerIds;
    }

    // 获取客户行为
    async function getCustomerBehavior(customerId, _tb_token_) {
        const url = `https://alicrm.alibaba.com/eggCrmQn/crm/customerQueryServiceI/queryCustomerBehavior.json?customerId=${customerId}&_tb_token_=${encodeURIComponent(_tb_token_)}`;
        const resp = await fetch(url, {
            method: 'GET',
            credentials: 'include'
        });
        const data = await resp.json();
        const behavior = data?.data?.data || {};
        // 处理映射和数组字段
        const result = {};
        for (const key in behaviorFieldMap) {
            let val = behavior[key];
            if (Array.isArray(val)) {
                if (key === "latestInquiryProducts") {
                    // 拼接图片和链接
                    val = val.map(item => item.productUrl).join(", ");
                } else {
                    val = val.join(", ");
                }
            }
            // 特殊处理-1为客户隐藏
            if (["totalOrderCount", "totalOrderVolume", "tradeSupplierCount"].includes(key) && val === -1) {
                val = "客户隐藏";
            }
            result[behaviorFieldMap[key]] = val !== undefined ? val : '';
        }
        return result;
    }

    // 获取客户详情
    async function getCustomerDetail(customerId, _tb_token_) {
        const url = `https://alicrm.alibaba.com/eggCrmQn/crm/customerQueryServiceI/queryCustomerAndContacts.json?customerId=${customerId}&_tb_token_=${encodeURIComponent(_tb_token_)}`;
        const resp = await fetch(url, {
            method: 'GET',
            credentials: 'include'
        });
        const data = await resp.json();
        const customer = data?.data?.customerDetailCO || {};
        const contacts = data?.data?.contactQueryCOList || [];
        const contact = contacts[0] || {};

        // growthLevel优先取联系人,没有再取客户
        const growthLevel = contact.growthLevel || (customer.growthLevelInfo?.growthLevel || "");

        // address优先取客户,没有再取联系人
        let address = customer.address || contact.address || "";
        if (typeof address === 'object' && address !== null) {
            address = ["country", "province", "city", "district", "street"].map(k => address[k] || "None").join(",");
        }

        // 合并姓名
        const name = [contact.firstName || "", contact.lastName || ""].join(" ").trim();

        // registerDate 字段处理
        let registerDate = "";
        if (customer.registerDate && /^\d+$/.test(customer.registerDate)) {
            registerDate = new Date(Number(customer.registerDate) * 1000).toISOString().slice(0,10);
        }

        function list2str(val) {
            if (Array.isArray(val)) {
                if (val.length && typeof val[0] === 'object') {
                    return val.map(item => ["countryCode", "areaCode", "number"].map(k => item[k] || "").join("-")).join(",");
                } else {
                    return val.join(",");
                }
            }
            return val || "";
        }

        // 字段映射
        const result = {
            "客户姓名": name,
            "旺旺ID": contact.loginId || "",
            "业务员": customer.oppModifier || "",
            "来源": customer.source || "",
            "性别": contact.gender || "",
            "客户国家": customer.country || "",
            "客户等级": growthLevel,
            "职位": contact.position || "",
            "头像": contact.avatar || "",
            "客户名片链接": contact.profileLink || "",
            "邮箱": list2str(contact.email),
            "手机号码": list2str(contact.mobiles),
            "座机号码": list2str(contact.phoneNumbers),
            "社交账号": list2str(contact.ims),
            "建档时间": customer.gmtCreate || "",
            "注册时间": registerDate,
            "年采购额": customer.annualProcurement || "",
            "公司名称": customer.companyName || "",
            "公司部门": contact.department || "",
            "公司网址": customer.website || "",
            "公司地址": address
        };
        // 合并行为数据
        const behavior = await getCustomerBehavior(customerId, _tb_token_);
        return {
            ...result,
            ...behavior
        };
    }

    // 导出为Excel
    function exportExcel(data, filename) {
        const ws = XLSX.utils.json_to_sheet(data, {header: fields});
        const wb = XLSX.utils.book_new();
        XLSX.utils.book_append_sheet(wb, ws, "客户信息");
        const wbout = XLSX.write(wb, {bookType:'xlsx', type:'array'});
        const blob = new Blob([wbout], {type: "application/octet-stream"});
        const url = URL.createObjectURL(blob);

        // 用 a 标签下载
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        setTimeout(() => {
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }, 100);
    }

    // 并发控制函数
    async function concurrentMap(inputs, limit, asyncFn, progressCb) {
        const results = [];
        let idx = 0;
        let running = 0;
        return new Promise((resolve, reject) => {
            function next() {
                if (idx >= inputs.length && running === 0) {
                    resolve(results);
                    return;
                }
                while (running < limit && idx < inputs.length) {
                    const curIdx = idx;
                    running++;
                    asyncFn(inputs[curIdx], curIdx)
                        .then(res => {
                            results[curIdx] = res;
                            if (progressCb) progressCb(curIdx + 1, inputs.length);
                        })
                        .catch(e => {
                            results[curIdx] = null;
                        })
                        .finally(() => {
                            running--;
                            next();
                        });
                    idx++;
                }
            }
            next();
        });
    }

    // 等待页面 loading 状态消失
    async function waitForLoadingToFinish() {
        let count = 0;
        while (document.querySelector('.loading-indicator, .next-loading, .ant-spin-spinning')) {
            if (count++ > 50) { // 最多等10秒
                console.log('loading 等待超时,强制继续');
                break;
            }
            await new Promise(r => setTimeout(r, 200));
        }
    }

    // 主入口
    async function run() {
        if (!confirm("是否开始导出客户信息?")) return;
        const _tb_token_ = getCookie('_tb_token_');
        if (!_tb_token_) {
            alert("未检测到 _tb_token_,请先登录并刷新页面!");
            return;
        }
        // 等待页面 loading 状态消失,确保筛选请求已完成
        await waitForLoadingToFinish();
        // 获取进度元素
        const progress = document.getElementById('exportCustomerProgress');
        if (progress) {
            progress.style.display = 'block';
            progress.textContent = `正在采集客户ID,请耐心等待...`;
        }
        // 采集客户ID(根据页面类型自动切换)
        let customerIds;
        if (isPublicCustomerPage()) {
            customerIds = await getPublicCustomerIds(_tb_token_, (page, total, all) => {
                if (progress) progress.textContent = `正在采集公海客户ID,请耐心等待...(第${page}页,已采集${total}个客户${all ? `/共${all}个` : ''})`;
            });
        } else {
            customerIds = await getFilteredCustomerIds(_tb_token_, (page, total, all) => {
                if (progress) progress.textContent = `正在采集客户ID,请耐心等待...(第${page}页,已采集${total}个客户${all ? `/共${all}个` : ''})`;
            });
        }
        if (!customerIds.length) {
            alert("未采集到客户ID,请确认筛选条件下有客户。");
            if (progress) progress.style.display = 'none';
            return;
        }
        alert(`共采集到 ${customerIds.length} 个客户ID,开始采集详情...`);
        if (progress) {
            progress.textContent = `正在导出 0/${customerIds.length}`;
        }
        // 并发采集详情
        const concurrentLimit = 5;
        const results = await concurrentMap(
            customerIds,
            concurrentLimit,
            (id, i) => getCustomerDetail(id, _tb_token_),
            (done, total) => {
                if (progress) progress.textContent = `正在导出 ${done}/${total}`;
                console.log(`已采集详情 ${done}/${total}`);
            }
        );
        const today = new Date().toISOString().slice(0,10).replace(/-/g, "");
        const exportType = isPublicCustomerPage() ? "公海客户" : "客户列表";
        const filename = `${exportType}信息导出${today}.xlsx`;
        exportExcel(results.filter(Boolean), filename);
        if (progress) {
            progress.textContent = `${exportType}导出完成!`;
            setTimeout(() => { progress.style.display = 'none'; }, 3000);
        }
        alert(`${exportType}导出完成!`);
    }

    // 页面添加按钮
    function addButton() {
        if (document.getElementById('exportCustomerBtn')) return;
        const btn = document.createElement('button');
        btn.id = 'exportCustomerBtn';
        // 根据页面类型设置按钮名称
        btn.textContent = isPublicCustomerPage() ? '公海客户导出--树洞先生' : '我的客户导出--树洞先生';
        btn.style.position = 'fixed';
        btn.style.top = '100px';
        btn.style.right = '40px';
        btn.style.zIndex = 9999;
        btn.style.background = '#4CAF50';
        btn.style.color = '#fff';
        btn.style.padding = '10px 20px';
        btn.style.border = 'none';
        btn.style.borderRadius = '5px';
        btn.style.cursor = 'pointer';
        btn.onclick = run;
        document.body.appendChild(btn);

        // 添加进度显示元素
        const progress = document.createElement('span');
        progress.id = 'exportCustomerProgress';
        progress.style.position = 'fixed';
        progress.style.top = '140px';
        progress.style.right = '40px';
        progress.style.background = 'rgba(0,0,0,0.7)';
        progress.style.color = '#fff';
        progress.style.padding = '6px 16px';
        progress.style.borderRadius = '5px';
        progress.style.fontSize = '16px';
        progress.style.zIndex = 9999;
        progress.style.display = 'none';
        document.body.appendChild(progress);
    }

    // 动态更新按钮名称(监听hash变化)
    function updateButtonName() {
        const btn = document.getElementById('exportCustomerBtn');
        if (btn) {
            btn.textContent = isPublicCustomerPage() ? '公海客户导出--树洞先生' : '我的客户导出--树洞先生';
        }
    }
    window.addEventListener('hashchange', updateButtonName);

    // 等待页面加载
    window.addEventListener('load', () => {
        addButton();
        updateButtonName();
    });

})();