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

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

// ==UserScript==
// @name         阿里巴巴国际站客户信息导出--树洞先生
// @namespace    http://tampermonkey.net/
// @version      1.5
// @license      MIT
// @description  更新加入了公海客户和我的客户导出并对客户行为字段进行爬取,"产品浏览数", "有效询盘数", "有效RFQ数", "登录天数", "垃圾询盘数", "被加为黑名单数", "订单总数", "订单总金额", "交易供应商数", "最近搜索词", "最常采购行业", "最近询盘产品链接"
// @author       YourName
// @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();
    });

})();