前言:大众点评的「免费抽」活动每天上线大量免费商品,手速不够快很容易错过。本文介绍一款基于 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="— 点击「开始运行」以启动自动化流程 — — 请确保已授予无障碍服务权限 — — 启动前请先手动停在「免费抽」列表页 —"
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 行,结构清晰,改动配置区的坐标和阈值即可适配不同设备。
有问题欢迎评论区交流!

402

被折叠的 条评论
为什么被折叠?



