Greasy Fork

Greasy Fork is available in English.

黎騒君の蹲票

蹲开票之后的第n波票或掉落余票,暂不清楚是否可蹲开票

当前为 2024-06-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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


    // ==UserScript==
    // @name         黎騒君の蹲票
    // @namespace    http://tampermonkey.net/
    // @version      1.0
    // @description  蹲开票之后的第n波票或掉落余票,暂不清楚是否可蹲开票
    // @author       黎騒君
    // @match        https://*.youzan.com/*
    // @icon         https://img01.yzcdn.cn/v2/image/yz_fc.ico
    // @grant        GM_setValue
    // @grant        GM_getValue
    // @license      MIT
    // ==/UserScript==
     
     
    (async function () {
     
        // 初始化购票信息
        var ticketToBuy_regex
        try {
            ticketToBuy_regex = GM_getValue('autoBuyRegex') ? new RegExp(GM_getValue('autoBuyRegex')) : /电子预售票/;
        } catch (error) {
            console.log('正则表达式解析失败,正在使用默认值')
            ticketToBuy_regex = /电子预售票/;
        }
        const idcard = GM_getValue('autoBuyIDCard') ? GM_getValue('autoBuyIDCard') : '';
        const realname = GM_getValue('autoBuyRealname') ? GM_getValue('autoBuyRealname') : '';
        const phonenumber = GM_getValue('autoBuyPhonenumber') ? GM_getValue('autoBuyPhonenumber') : '';
     
        function checkAutoBuyInfo() {
            return idcard && idcard.length == 18 && realname && realname.length >= 2 && phonenumber && phonenumber.length == 11
        }
     
        function saveAutoBuyInfo() {
            console.log('正在保存蹲票配置')
            GM_setValue('autoBuyRegex', document.getElementById('settingsRegex').value)
            GM_setValue('autoBuyIDCard', document.getElementById('settingsIDCard').value)
            GM_setValue('autoBuyRealname', document.getElementById('settingsRealname').value)
            GM_setValue('autoBuyPhonenumber', document.getElementById('settingsPhonenumber').value)
            location.reload()
        }
     
        // 设置按钮和设置界面
        const settingsEl = document.createElement('template')
        settingsEl.innerHTML = `
            <button id="settingsBtn" onclick="document.getElementById('settingsOverlay').style.display = 'block'" style="
                position: fixed;
                left: 4px;
                top: 30%;
                border-radius: 25%;
                background-color: #0f0;
                padding: 8px;
                line-height: 1em;
                z-index: 999;
            ">蹲票<br>设置</button>
            <div id="settingsOverlay" style="
                display: none;
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                padding: 8px;
                border: 2px solid;
                background-color: white;
                overflow: auto;
                z-index: 999;
            ">
                <p>票名(支持正则表达式匹配):</p>
                <input id="settingsRegex" style="height: initial; background-color: #ff0;" value="${ticketToBuy_regex.source}">
                <p>
                    举例:<br>
                    所有普票:电子预售票<br>
                    所有32nd普票:电子预售票.*32<br>
                    所有32nd 5月1号普票:5月1日电子预售票.*32<br>
                    所有32nd所有票(包括VIP):32<br>
                    (正则表达式用于匹配商品标题,如果不会请百度)
                </p><br><br>
                <p>身份证:</p>
                <input id="settingsIDCard" style="height: initial; background-color: #ff0;" value="${idcard}"><br><br>
                <p>姓名:</p>
                <input id="settingsRealname" style="height: initial; background-color: #ff0;" value="${realname}"><br><br>
                <p>手机号:</p>
                <input id="settingsPhonenumber" style="height: initial; background-color: #ff0;" value="${phonenumber}"><br>
                <button id="settingsSetBtn" style="float: right">确定</button>
            </div>
        `
        document.body.appendChild(settingsEl.content)
        document.getElementById('settingsSetBtn').onclick = saveAutoBuyInfo
     
     
        // 所有商品页
        //
        if (location.href.includes('/wscshop/feature/goods/all')) {
            // 蹲票状态覆盖层
            const overlayEl = document.createElement('template')
            overlayEl.innerHTML = `
                <div id="autoBuyStatus" style="
                    position: fixed;
                    left: 8px;
                    bottom: 8px;
                    box-sizing: border-box;
                    max-width: 60%;
                    max-height: 30%;
                    padding: 8px;
                    background-color: rgba(0, 0, 0, 0.5);
                    color: white;
                    font-size: 12px;
                    line-height: 1.2;
                    overflow: auto;
                "></div>
            `
            document.body.appendChild(overlayEl.content)
     
            // 检查蹲票信息是否正确,不正确则留下提示然后退出
            if (!checkAutoBuyInfo()) {
                console.log('购票信息有误')
                document.getElementById('autoBuyStatus').innerHTML = '请先正确填写蹲票信息!'
                document.getElementById('autoBuyStatus').style.fontSize = '20px'
                document.getElementById('autoBuyStatus').style.color = '#ff0'
                return
            }
     
     
            // 申请浏览器通知权限
            Notification.requestPermission().then(() => {
                new Notification('蹲票脚本已启动', {
                    body: '有票时将通过浏览器通知提醒支付',
                });
            })
     
            // 启动 worker 以便页面挂起时也能继续蹲票
            function workerInterval() {
                setInterval(() => {
                    postMessage('')
                }, 1000);
            }
            const workerBlob = URL.createObjectURL(new Blob([`(${workerInterval.toString()})()`], { type: 'application/javascript' }));
            const worker = new Worker(workerBlob)
     
            let count = 0
     
            worker.onmessage = () => {
                /*
                    https://shop46404892.youzan.com/wscshop/showcase/goods/allGoods.json?
                    pageSize=20&
                    page=1&
                    offlineId=0&
                    openIndependentPrice=0&
                    order=&
                    json=1&
                    uuid=a5f75232-3ec1-66c4-5ec2-b0db133d7765&
                    activityPriceIndependent=1&
                    order_by=algPVD30&
                    goodsType=2&
                    needActivity=0&
                    clientSource=2&
                    tagAlias=&
                    needGroupFilter=false&
                    needGoodsRank=true&
                    supportCombo=true&
                    excludedComboSubType=%5B%22none%22%5D", {
                */
                // 获取前20个商品
                fetch("/wscshop/showcase/goods/allGoods.json?json=1&order_by=algPVD30", {
                    "method": "GET",
                    "credentials": "include"
                }).then(response => {
                    return response.json();
                }).then(data => {
                    // 清空蹲票信息显示
                    document.getElementById('autoBuyStatus').innerHTML = `正在蹲以下票(${Notification.permission == "granted" ? '浏览器通知已启用' : '请启用浏览器通知以便接收结果'}):<br>`
                    // 遍历商品
                    data.data.list.forEach((item) => {
                        // 匹配商品标题
                        if (item.title.match(ticketToBuy_regex)) {
                            // 蹲票状态显示在覆盖层
                            document.getElementById('autoBuyStatus').innerHTML += `${item.title}&emsp;余票: ${item.totalStock}<br>`
                            if (item.totalStock > 0) {
                                // 有票时发送浏览器通知并跳转商品详情页
                                new Notification('发现有票,请10分钟内前往手动支付', {
                                    body: item.title,
                                });
                                document.querySelectorAll('.cap-goods-layout__container>.cap-goods-layout__wrapper>.cap-goods-layout__item').forEach((el) => {
                                    if (el.innerText.match(item.title)) {
                                        console.log('正在打开商品详情页')
                                        el.click()
                                    }
                                })
                            }
                        }
                    })
                    count++
                    document.getElementById('autoBuyStatus').innerHTML += `已蹲票次数:${count}<br>`
                })
            }
        }
     
     
       
        if (!checkAutoBuyInfo()) {
            console.log('购票信息有误')
            return
        }
     
      
    
        if (location.href.includes('/wscgoods/detail') || location.href.includes('/pay/wscgoods_order')) {
            await new Promise(r => setTimeout(r, 1000))
     
            if (!document.querySelector('.goods-title__main-text').innerText.match(ticketToBuy_regex)) {
                console.log('商品不匹配,跳过自动点击')
                return
            }
            if (document.querySelector('.mrb__no-goods')) {
                console.log('没票了!')
                return
            }
     
            console.log('正在打开购买页面')
            // 两种“立即购买”按钮,其中一种要点击更内层的 div 才可触发购买界面
            try {
                document.querySelector('.goods-btns__button.button--last .goods-btns__authorize .user-authorize__btn-empty').click()
            } catch (error) {
                document.querySelector('.goods-btns__button.button--last .goods-btns__authorize').click()
            }
     
            await new Promise(r => setTimeout(r, 500))
     
            console.log('正在填写信息')
            // 填实名信息(需手动触发 input 刷新变量,blur 事件确认购买价),两种页面通用
            document.querySelectorAll('.sku-messages>.tee-view').forEach((el) => {
                el.querySelector('.t-cell__value input').focus()
                if (el.querySelector('.t-cell__title').innerText.includes('身份证')) {
                    el.querySelector('.t-cell__value input').value = idcard
                }
                if (el.querySelector('.t-cell__title').innerText.includes('姓名')) {
                    el.querySelector('.t-cell__value input').value = realname
                }
                if (el.querySelector('.t-cell__title').innerText.includes('电话') || el.querySelector('.t-cell__title').innerText.includes('手机')) {
                    el.querySelector('.t-cell__value input').value = phonenumber
                }
                el.querySelector('.t-cell__value input').dispatchEvent(new Event('input'))
                el.querySelector('.t-cell__value input').dispatchEvent(new Event('blur'))
            })
     
            setInterval(() => {
                console.log('正在跳转提交订单界面')
                // 两种确认购买按钮
                try {
                    document.querySelector('.pay-btn-order').click()
                } catch (error) {
                    document.querySelector('.sku-actions__btn--main').click()
                }
            }, 500);
        }
     
     
        // 提交订单页面
        //
        if (location.href.includes('/pay/wsctrade_buy')) {
            await new Promise(r => setTimeout(r, 1000))
     
            if (!document.querySelector('.new-goods-list-card__title__text').innerText.match(ticketToBuy_regex)) {
                console.log('商品不匹配,跳过自动点击')
                return
            }
     
            console.log('正在提交订单')
            document.querySelector('.submit-bar__button').click()
        }
     
    })();