AutoJs大众点评免费抽自动报名脚本

前言:大众点评的「免费抽」活动每天上线大量免费商品,手速不够快很容易错过。本文介绍一款基于 AutoJs 的自动报名脚本,内置可视化 UI 面板,支持实时日志、轮次统计,文字定位 + 绝对坐标点击双重机制,稳定运行不漂移。

一、效果预览

脚本启动后自动循环执行以下流程:

找到「免费抽」按钮 → 进入详情页 → 点击「我要报名」→ 点击「确认报名」→ 返回列表 → 下滑 → 下一轮

UI 面板实时显示:

  • 🟢 运行状态(待机 / 运行中 / 已停止)
  • 已执行轮次
  • 成功报名次数
  • 带时间戳的滚动日志

二、环境要求

项目要求
运行环境AutoJs / AutoJs Pro(6.x+)
权限无障碍服务(必须开启)
分辨率本脚本坐标基于 2670×1200 屏幕标定 (Xiaomi 14)
前置操作启动前手动打开大众点评,停留在「免费抽」列表页

⚠️ 分辨率不同的设备必须重新标定坐标,详见第五节。


三、完整代码

"ui";

ui.layout(
    <vertical bg="#F2F2F7" h="*" w="*">

        <horizontal bg="#E54B1C" padding="16 14" gravity="center_vertical" w="*">
            <text text="🍴" textSize="22sp" marginRight="10"/>
            <vertical layout_weight="1">
                <text text="大众点评 · 自动报名 v4" textSize="17sp"
                      textColor="#FFFFFF" textStyle="bold"/>
                <text text="绝对坐标版" textSize="10sp"
                      textColor="#FFCCBB"/>
            </vertical>
        </horizontal>

        <vertical bg="#FFFFFF" margin="12 12 12 0" padding="16 14" w="*">

            <horizontal w="*" marginBottom="10">
                <text text="当前状态" textSize="13sp" textColor="#999999" layout_weight="1"/>
                <text id="statusDot" text="●" textSize="14sp" textColor="#CCCCCC" marginRight="4"/>
                <text id="statusText" text="待机" textSize="14sp"
                      textColor="#999999" textStyle="bold"/>
            </horizontal>

            <horizontal w="*" marginBottom="10">
                <text text="已执行轮次" textSize="13sp" textColor="#999999" layout_weight="1"/>
                <text id="roundText" text="0" textSize="18sp"
                      textColor="#E54B1C" textStyle="bold"/>
                <text text=" 轮" textSize="13sp" textColor="#999999"/>
            </horizontal>

            <horizontal w="*">
                <text text="成功报名" textSize="13sp" textColor="#999999" layout_weight="1"/>
                <text id="successText" text="0" textSize="18sp"
                      textColor="#4CAF50" textStyle="bold"/>
                <text text=" 次" textSize="13sp" textColor="#999999"/>
            </horizontal>

        </vertical>

        <horizontal margin="12 8 12 0" w="*">
            <button id="startBtn" text="▶  开始运行"
                    bg="#E54B1C" textColor="#FFFFFF"
                    layout_weight="1" margin="0 0 5 0"
                    textSize="15sp" textStyle="bold"/>
            <button id="stopBtn"  text="⏹  停止"
                    bg="#CCCCCC" textColor="#FFFFFF"
                    layout_weight="1" margin="5 0 0 0"
                    textSize="15sp" textStyle="bold" enabled="false"/>
        </horizontal>

        <button id="clearBtn" text="🗑  清空日志"
                bg="#FFFFFF" textColor="#999999"
                margin="12 6 12 0" w="*" textSize="13sp"/>

        <horizontal margin="14 8 0 4" gravity="center_vertical">
            <text text="📋 运行日志" textSize="12sp" textColor="#AAAAAA" layout_weight="1"/>
            <text id="logCountText" text="0 条" textSize="11sp"
                  textColor="#CCCCCC" marginRight="14"/>
        </horizontal>

        <ScrollView id="logScroll" bg="#FFFFFF"
                    margin="12 0 12 12" padding="0" h="*" w="*">
            <text id="logText"
                  text="— 点击「开始运行」以启动自动化流程 —&#10;— 请确保已授予无障碍服务权限 —&#10;— 启动前请先手动停在「免费抽」列表页 —"
                  textSize="11sp" textColor="#666666"
                  padding="12" lineSpacingExtra="5dp"/>
        </ScrollView>

    </vertical>
);

// ============================================================
// 配置区
// ============================================================
var SCREEN_BOTTOM_LIMIT = 2600;
var running   = false;
var n         = 0;
var successes = 0;
var logCount  = 0;
var worker    = null;

// ============================================================
// 日志工具
// ============================================================
function pad(v) { return ("0" + v).slice(-2); }

function appendLog(msg) {
    var t  = new Date();
    var ts = pad(t.getHours()) + ":" + pad(t.getMinutes()) + ":" + pad(t.getSeconds());
    var line = "[" + ts + "] " + msg;
    logCount++;
    ui.run(function () {
        var prev = ui.logText.getText().toString();
        if (prev.indexOf("点击「开始运行」") >= 0) prev = "";
        ui.logText.setText(prev + (prev ? "\n" : "") + line);
        ui.logCountText.setText(logCount + " 条");
        ui.logScroll.post(function () {
            ui.logScroll.smoothScrollTo(0, ui.logText.getHeight() + 200);
        });
    });
}

function updateCounter() {
    ui.run(function () {
        ui.roundText.setText(String(n));
        ui.successText.setText(String(successes));
    });
}

// ============================================================
// 工具函数
// ============================================================

// 下滑(向上滑动列表)
function swipeUp() {
    var w = device.width, h = device.height;
    var startY = Math.round(h * 0.5);
    var endY = startY - 355;
    swipe(w / 2, startY, w / 2, endY, 2500);
    appendLog("📱 下滑完成");
}

// 找「免费抽」并点击(文字定位 + 超出可视区域自动下滑重试)
function tapFreeDraw() {
    while (running) {
        var btn = text("免费抽").findOne(500);
        if (btn) {
            var bounds = btn.bounds();
            var btnY = bounds.top;
            if (btnY > SCREEN_BOTTOM_LIMIT) {
                appendLog("⚠️ 「免费抽」超出可视区域(Y:" + btnY + "),下滑重试");
                swipeUp();
                sleep(1000);
                continue;
            }
            var targetX = bounds.left - 200;
            var targetY = bounds.top;
            click(targetX, targetY);
            appendLog("✅ 点击「免费抽」(X:" + targetX + " Y:" + targetY + ")");
            return true;
        } else {
            appendLog("⚠️ 未找到「免费抽」,下滑重试...");
            swipeUp();
            sleep(2000);
        }
    }
    return false;
}

// ============================================================
// 单轮流程
// ============================================================
function freeDraw() {
    // 1. 点击「免费抽」(文字定位)
    if (!tapFreeDraw()) return;

    // 2. 等详情页加载
    sleep(1000);
    if (!running) return;

    // 3. 点击「我要报名」(绝对坐标)
    click(892, 2489);
    appendLog("✅ 点击「我要报名」(892, 2489)");

    // 4. 等确认弹窗出现
    sleep(1000);
    if (!running) return;

    // 5. 点击「确认报名」(绝对坐标)
    click(600, 2472);
    appendLog("✅ 点击「确认报名」(600, 2472)");
    successes++;
    updateCounter();
    appendLog("🎉 本轮报名完成");

    // 6. 等待结果页
    sleep(1000);

    // 7. 返回列表页
    back();
    sleep(500);

    // 8. 下滑准备下一轮
    swipeUp();
    sleep(500);
}

// ============================================================
// 按钮:开始
// ============================================================
ui.startBtn.on("click", function () {
    if (running) return;
    running   = true;
    n         = 0;
    successes = 0;
    logCount  = 0;

    ui.startBtn.setEnabled(false);
    ui.stopBtn.setEnabled(true);
    ui.statusDot.setTextColor(colors.parseColor("#4CAF50"));
    ui.statusText.setTextColor(colors.parseColor("#4CAF50"));
    ui.statusText.setText("运行中");
    ui.logText.setText("");

    appendLog("▶ 自动化启动,申请无障碍权限中...");

    worker = threads.start(function () {
        auto.waitFor();
        appendLog("🔓 无障碍权限已就绪");

        for (var i = 5; i > 0; i--) {
            appendLog("⏳ " + i + " 秒后开始,请切换到大众点评...");
            sleep(1000);
        }
        appendLog("🚀 开始运行!");

        while (running) {
            appendLog("─── 第 " + n + " 轮开始 ───");
            updateCounter();
            try {
                freeDraw();
            } catch (e) {
                appendLog("❌ 异常:" + e);
                sleep(1000);
            }
            n++;
        }

        ui.run(function () {
            ui.startBtn.setEnabled(true);
            ui.stopBtn.setEnabled(false);
            ui.statusDot.setTextColor(colors.parseColor("#CCCCCC"));
            ui.statusText.setTextColor(colors.parseColor("#999999"));
            ui.statusText.setText("已停止");
        });
        appendLog("⏹ 已停止,共执行 " + n + " 轮,成功报名 " + successes + " 次");
    });
});

// ============================================================
// 按钮:停止
// ============================================================
ui.stopBtn.on("click", function () {
    running = false;
    if (worker) { worker.interrupt(); worker = null; }
    ui.startBtn.setEnabled(true);
    ui.stopBtn.setEnabled(false);
    ui.statusDot.setTextColor(colors.parseColor("#CCCCCC"));
    ui.statusText.setTextColor(colors.parseColor("#999999"));
    ui.statusText.setText("已停止");
    appendLog("⏹ 手动停止");
});

// ============================================================
// 按钮:清空日志
// ============================================================
ui.clearBtn.on("click", function () {
    logCount = 0;
    ui.logText.setText("");
    ui.logCountText.setText("0 条");
});

四、代码解析

4.1 UI 布局

脚本顶部声明 "ui" 模式,整个界面用 AutoJs 的 XML 布局描述,无需额外 HTML:

"ui";
ui.layout(<vertical bg="#F2F2F7" h="*" w="*"> ... </vertical>);

界面分为四个区域:

  • 顶部标题栏:大众点评品牌色 #E54B1C,展示脚本名和版本
  • 数据面板:实时显示状态、轮次、成功次数(三行 horizontal 布局)
  • 操作按钮:开始 / 停止 / 清空日志
  • 滚动日志区ScrollView 包裹 text,自动滚动到底部

4.2 双线程模型

UI 线程与自动化逻辑线程分离,是本脚本不卡顿的关键:

// 主线程:只负责 UI 交互
ui.startBtn.on("click", function () {
    // 启动子线程执行自动化
    worker = threads.start(function () {
        auto.waitFor();   // 等待无障碍权限就绪
        while (running) {
            freeDraw();   // 循环执行单轮流程
        }
    });
});

所有 UI 更新必须通过 ui.run() 切回主线程执行,否则会报线程安全错误:

function appendLog(msg) {
    ui.run(function () {
        ui.logText.setText(...);         // 更新日志文本
        ui.logScroll.smoothScrollTo(0, ui.logText.getHeight() + 200);  // 滚动到底
    });
}

4.3 文字定位:找「免费抽」按钮

列表页的条目位置会随滑动变化,不能用固定坐标,因此用文字识别定位:

function tapFreeDraw() {
    while (running) {
        var btn = text("免费抽").findOne(500);  // 查找文本节点,超时 500ms
        if (btn) {
            var bounds = btn.bounds();
            var btnY = bounds.top;

            // 超出屏幕可视区域则先下滑
            if (btnY > SCREEN_BOTTOM_LIMIT) {
                swipeUp();
                sleep(1000);
                continue;
            }

            // 向左偏移 200px 点击,避开「免费抽」文字本身落在其他热区
            var targetX = bounds.left - 200;
            click(targetX, btnY);
            return true;
        } else {
            // 未找到则下滑继续搜索
            swipeUp();
            sleep(2000);
        }
    }
}

点击时对 X 坐标向左偏移 200px,目的是点到条目的封面图片区域而非文字区域,防止触发错误的点击响应。

SCREEN_BOTTOM_LIMIT = 2600 是屏幕底部安全线,超过这个 Y 值说明按钮被底部导航栏遮挡,需要先滑动。


4.4 绝对坐标:「我要报名」和「确认报名」

详情页和确认弹窗的布局固定,直接用绝对坐标点击效率最高:

// 详情页底部「我要报名」按钮
click(892, 2489);

// 确认弹窗「确认报名」按钮
click(600, 2472);

两次点击之间 sleep(1000) 等待弹窗动画完成,避免坐标还未渲染就点击失败。


4.5 下滑函数

每轮结束后需要下滑列表,让下一个「免费抽」条目出现:

function swipeUp() {
    var w = device.width, h = device.height;
    var startY = Math.round(h * 0.5);
    var endY = startY - 355;                    // 向上滑动 355px
    swipe(w / 2, startY, w / 2, endY, 2500);   // 持续 2500ms,模拟慢滑
}

X 坐标取屏幕中线,Y 起点取屏幕中部,滑动距离 355px 经过实测是列表单条目高度的合适步进。持续时间 2500ms 模拟人工慢速滑动,减少被 App 检测到机械操作的风险。


4.6 异常捕获与 5 秒倒计时

// 启动后留 5 秒切换到大众点评
for (var i = 5; i > 0; i--) {
    appendLog("⏳ " + i + " 秒后开始,请切换到大众点评...");
    sleep(1000);
}

// 每轮用 try-catch 包裹,单轮报错不影响整体循环
while (running) {
    try {
        freeDraw();
    } catch (e) {
        appendLog("❌ 异常:" + e);
        sleep(1000);
    }
    n++;
}

5 秒倒计时给用户留出手动切换 App 的时间。try-catch 确保某一轮报错(如控件找不到、坐标点击无响应)不会导致整个脚本崩溃退出。


五、不同设备适配(重新标定坐标)

脚本中的绝对坐标基于 2670×1200 分辨率设备标定,其他分辨率需修改以下三处:

第一步:确认自己的分辨率

在 AutoJs 控制台运行:

toast(device.width + " × " + device.height);

第二步:用布局分析工具拿坐标

AutoJs 自带「布局范围分析」工具(悬浮窗 → 布局分析),打开大众点评「我要报名」页面,点击对应按钮查看其坐标,替换代码中的两行:

click(892, 2489);   // 改为「我要报名」按钮的实际 X, Y
click(600, 2472);   // 改为「确认报名」按钮的实际 X, Y

第三步:调整屏幕底部安全线

var SCREEN_BOTTOM_LIMIT = 2600;   // 改为 device.height - 70 左右

第四步:调整下滑距离

var endY = startY - 355;   // 355 是单条目高度,按实际屏幕比例等比缩放

六、使用步骤

1. 将代码保存为 .js 文件,导入 AutoJs
2. 打开手机「无障碍服务」,授权 AutoJs
3. 打开大众点评,进入「免费抽」列表页
4. 切回 AutoJs,点击「▶ 开始运行」
5. 5 秒倒计时内切换回大众点评
6. 脚本自动循环,可随时点「⏹ 停止」终止

七、常见问题

Q:脚本启动后什么都没发生?
A:检查无障碍服务是否已授权给 AutoJs,设置路径:手机设置 → 无障碍 → AutoJs → 开启。

Q:日志显示「未找到『免费抽』」一直循环?
A:确认已停留在大众点评「免费抽」列表页,而不是首页或其他 Tab。

Q:点击了「免费抽」但进入的是错误的详情页?
A:X 坐标偏移量 bounds.left - 200 可能需要根据你的布局调整,适当增减偏移值。

Q:「我要报名」点击无效,弹窗没出现?
A:坐标与你的分辨率不符,按第五节重新标定坐标。

Q:报名成功数一直增加但实际没中?
A:脚本只负责完成点击报名流程,中奖与否由平台抽签决定,成功报名不等于中奖。


八、总结

本脚本的设计思路:

  • 文字定位解决列表页按钮位置不固定的问题,找到就点、找不到就滑,自适应任意滚动位置
  • 绝对坐标处理固定布局的详情页和弹窗,简单直接,响应准确
  • 双线程 + ui.run() 保证 UI 流畅不卡顿,日志实时更新
  • try-catch + 限速 sleep 提高稳定性,单轮出错不崩溃,节奏不过快避免风控

整体代码约 200 行,结构清晰,改动配置区的坐标和阈值即可适配不同设备。

有问题欢迎评论区交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值