源码目录:
mobile/
- Python 3.8+
- UIAutomator2 (u2) 直连 Android 设备,每次操作 ~30-60ms
- 无需额外服务进程,
adb devices能识别设备即可 - 仅支持 Android
| 文件 | 职责 |
|---|---|
damai_app.py |
当前版本:DamaiBot 类,移动端抢票主流程 |
config.py |
配置容器 + load_config() 静态方法 |
config.jsonc |
默认运行配置,普通用户直接维护这个文件 |
config.local.jsonc |
开发者本地覆盖配置,默认不会自动启用 |
config.example.jsonc |
配置样例 |
udid: 设备序列号;真机建议填写adb devices的序列号app_package: 大麦 App 包名app_activity: 大麦启动 Activityitem_url: 大麦详情页链接,脚本会自动提取itemIditem_id: 可直接填数字itemIdkeyword: 搜索关键词;如果item_url可解析,可省略或填nullusers: 观演人姓名列表city: 目标城市date: 目标日期price: 目标票价price_index: 票价在列表中的索引位置if_commit_order: 是否自动提交订单probe_only: 仅做详情页探测,不执行购票点击auto_navigate: 是否从大麦首页/搜索页自动进入目标演出sell_start_time: 开售时间(ISO 格式字符串),null 表示立即购买countdown_lead_ms: 提前轮询时间(毫秒),默认 3000wait_cta_ready_timeout_ms: 当用户已手动停在倒计时详情页时,允许脚本直接等待 CTA 变成立即预定/立即购买的最长时长;默认 0 表示关闭fast_retry_count: 快速重试次数(不重建 driver),默认 8fast_retry_interval_ms: 快速重试间隔(毫秒),默认 80rush_mode: 抢票实战模式;更激进地信任price_index,跳过冗余 UI 读取,热路径耗时可压缩到 2 秒以内;推荐开启
DamaiBot.__init__()
├── Config.load_config() # 默认读取 config.jsonc;开发者可显式覆盖
├── _prepare_runtime_config() # 解析 item_url/item_id,必要时自动补 keyword
└── _setup_driver() # 初始化 u2 设备连接
run_with_retry(max_retries=3)
└── run_ticket_grabbing() # 单次抢票流程
├── dismiss_startup_popups() # 处理首启弹窗
├── check_session_valid() # 登录状态检测
├── probe_current_page() # 探测当前页面状态
│ ├── 非目标演出页 => navigate_to_target_event()
│ └── probe_only => 验证控件后提前结束
├── wait_for_sale_start() # 倒计时等待 / CTA 就绪等待(可选)
├── 0. 选择场次日期
├── 1. 选择城市
├── 2. 点击预约按钮
├── 3. 选择票价(实战模式下优先直接按 `price_index` 点击)
├── 4. 选择数量
├── 5. 确定购买
├── 6. 选择用户
├── 7. 提交订单
└── 8. 验证订单结果
通过 UIAutomator2 直连设备:
import uiautomator2 as u2
d = u2.connect(config.udid) # 直连设备,无需额外服务进程
d.settings["wait_timeout"] = 2.0
d.settings["operation_delay"] = (0, 0)
d.app_start("cn.damai", stop=False) # 不重启 App,保持登录态
self.driver = self.d说明:
- 直连设备,省去中转层,每次操作 ~30-60ms
_find、_click_coordinates等方法统一封装元素查找和手势操作- 热路径使用坐标缓存(
_cached_hot_path_coords),重试时单次 HTTP 调用即可完成点击
激进性能优化:
waitForIdleTimeout: 0 # 不等待页面空闲
actionAcknowledgmentTimeout: 0 # 禁止等待动作确认
keyInjectionDelay: 0 # 禁止输入延迟
waitForSelectorTimeout: 100 # 元素查找超时 100ms
enableNotificationListener: False # 禁用通知监听显式等待: WebDriverWait 超时仅 2 秒(常规为 5-10 秒)
这是 Mobile 端的核心设计。
ultra_fast_click() — 单元素极速点击:
# 不等待 clickable 状态,只等 presence
el = WebDriverWait(driver, 1.5).until(
EC.presence_of_element_located(locator)
)
# 获取坐标,用 gesture 替代 element.click()
rect = el.rect
driver.execute_script("mobile: clickGesture", {
"x": center_x, "y": center_y,
"duration": 50 # 极短点击时间
})为什么用坐标点击?
element.click()需要等待元素 clickable、可见性检查等,开销大clickGesture直接在屏幕坐标执行手势,绕过所有检查
ultra_batch_click() — 批量用户选择优化:
- 先批量收集所有目标元素的坐标(一次遍历)
- 再快速连续点击所有坐标(元素间 delay 仅 0.01s)
- 避免了"找一个点一个"的串行等待
smart_wait_and_click() — 智能备选点击:
- 接受主选择器 + 备用选择器列表
- 依次尝试,第一个成功即返回
dismiss_startup_popups()
- 处理 Android 全屏提示
- 处理大麦隐私协议弹窗
- 处理系统级
Add to home screen取消按钮
probe_current_page()
- 探测
consent_dialog、homepage、search_page、detail_page、order_confirm_page - 同时返回关键控件是否可见:
purchase_buttonprice_containerquantity_pickersubmit_button
- 对
sku_page额外识别reservation_mode,避免误把“抢票预约”当成正式下单 - 同时输出当前 Activity,方便定位卡在首页、搜索页还是订单页
probe_only 模式
- 只验证详情页关键控件是否就绪
- 就绪后停止在真正购票点击前
- 适合首次接设备、校验页面、验证选择器
当配置了 item_url 或 item_id 时,脚本会在启动前先解析演出详情:
- 自动提取
itemId - 读取演出名、城市、场馆、时间范围
- 在
keyword为空时自动生成搜索关键词 - 校验
config.city是否与链接指向城市一致
如果当前页面不在目标演出详情页,且 auto_navigate=true,脚本会:
- 从首页进入搜索页
- 用解析出的
keyword搜索 - 对搜索结果按标题/城市/场馆打分
- 点击最匹配的演出并进入详情页
这一步的目标是把“用户只给链接,其余自动处理”落到项目里,而不是依赖用户先手动点到详情页。
城市选择: 三种选择器备选
UiSelector().text("城市名")— 精确匹配UiSelector().textContains("城市名")— 模糊匹配- XPath
//*[@text="城市名"]— 兜底
预约按钮: 三种选择器备选
- 按资源 ID 定位 (
cn.damai:id/...) - 正则匹配文本 (
.*预约.*|.*购买.*|.*立即.*) - XPath 文本包含
票价选择: 两级策略
- 先尝试
UiSelector().textContains(config.price)文本匹配(1 秒超时) - 文本匹配失败时,回退到
FrameLayout的 index +clickable=true定位- 大麦 APP 的票价元素有时 text 是空的(被隐藏了),此时只能用索引
- 配置中的
price_index就是为此设计的 - 先尝试
find_element直接查找(不等待),失败后走WebDriverWait
rush_mode=true 时的额外优化:
- 详情页“立即购票”按钮会走更短等待的快速二次尝试,但仍保留稳定的选择器路径
- 票档优先直接按
price_index点击,不再先扫描所有可见票档文本 - SKU 页“确定购买”会走连续快速点击,并缩短确认页轮询间隔
- 这是偏实战的模式,核心目标是压缩热路径;前提是你已经通过
probe_only/ 到确认页验证过price_index没配错
手动倒计时页实战模式:
- 用户自己先进入目标演出详情页,并停在倒计时抢票界面
- 配置
auto_navigate=false、rush_mode=true - 如果不想依赖精确开售时间,可把
sell_start_time=null,同时设置wait_cta_ready_timeout_ms - 脚本会先预选日期/城市,然后只等待 CTA 变成
立即预定/立即购买,一旦就绪就直接进入热路径 - 如果本轮失败,快速重试会优先在当前会话里回退到演出详情页再重进,不会先重建 driver 把页面打回首页
- 详细工作流图见 2026-03-30-mobile-hot-path-workflow-design.md
- 可编辑图源: 移动端热路径工作流.drawio
- 静态图片: 移动端热路径工作流.png
flowchart TD
A[用户手动停在目标演出详情页或 SKU 页] --> B[run_ticket_grabbing]
B --> C[probe_current_page]
C -->|detail_page| D[预选日期/城市]
D --> E[等待 CTA 就绪或直接点击 立即购票]
C -->|sku_page| F[直接进入票档选择]
E --> F
F --> G[按 price_index 直点票档]
G --> H[选择数量]
H --> I[点击 确定购买]
I --> J{立即提交是否出现}
J -->|否| K[勾选观演人]
K --> L[_wait_for_submit_ready]
J -->|是| M[订单确认页]
L --> M
M --> N{if_commit_order}
N -->|false| O[停在 立即提交 前]
N -->|true| P[_submit_order_fast]
手动起跑热路径压测脚本:
- 入口:
./mobile/scripts/benchmark_hot_path.sh - 默认读取
mobile/config.jsonc;如果开发者显式设置--config或HATICKETS_CONFIG_PATH,则按指定路径读取;不会写回配置 - 会强制使用安全参数:
if_commit_order=false、auto_navigate=false、rush_mode=true - 报告分两段:第 1 轮标记为
[冷启动](App SKU 数据未缓存,约 2-3s);热重试 avg/min/max 仅统计第 2 轮起(缓存已热,约 1-2s);真实抢票中只有冷启动这一次最关键 - 常用示例:
./mobile/scripts/benchmark_hot_path.sh --runs 5./mobile/scripts/benchmark_hot_path.sh --runs 5 --price 580元 --price-index 2 --city 成都 --date 04.18
数量选择:
- 查找
+按钮(img_jia) - 获取坐标后用
clickGesture点击 (用户数 - 1) 次 - 每次点击间隔仅 0.02s
用户选择: 使用 ultra_batch_click()
- 为每个用户名构造
UiSelector().text("用户名")选择器 - 批量收集坐标后快速连续点击
提交订单: 三种选择器备选
UiSelector().text("立即提交")- 正则匹配
.*提交.*|.*确认.* - XPath 文本包含
run_with_retry(max_retries=3):
- 最多尝试 3 次
- 每次失败后先执行
fast_retry_count次快速重试(不重建 driver) - 第一轮快速重试会立即执行,后续重试间隔由
fast_retry_interval_ms控制(默认 80ms) - 快速重试根据当前页面状态决定策略:detail/sku 页重跑全流程,order_confirm 页直接提交,其他页按返回键后重跑
- 如果处于
if_commit_order=false且已经在确认页,快速重试只验证“立即提交”按钮存在,不会误提交 - 如果识别到
reservation_only、登录失效等不可重试场景,会直接停止后续重试,缩短整体耗时 - 全部快速重试失败后,才重建 driver(
driver.quit()+_setup_driver()) - 全部失败则退出
wait_for_sale_start():
- 当
sell_start_time配置不为 null 时,在开售前countdown_lead_ms毫秒进入休眠等待 - 休眠结束后进入 ~80ms 间隔的紧密轮询循环,同时检测详情页 CTA、SKU 页、确认页等多个开售信号
- 超时 8 秒后自动继续执行,避免死锁
sell_start_time为 null 时立即购买,不等待
verify_order_result(timeout=5):
- 提交订单后轮询检测结果(300ms 间隔)
- 通过 Activity 名称判断支付页面(Pay、Cashier、AlipayClient)
- 通过页面文本检测:支付成功、已售罄、库存不足、验证码、未支付订单等
- 返回值:
success/sold_out/captcha/existing_order/timeout
check_session_valid():
- 在抢票流程开始前检查登录状态
- 检测当前 Activity 是否为登录页(LoginActivity、SignActivity)
- 检测页面是否存在"请先登录"或"登录/注册"文本提示
- 登录失效时立即返回 False,避免无效执行
select_performance_date():
- 在城市选择之前执行,通过
UiSelector().textContains()匹配config.date - 匹配成功则点击对应日期,失败则使用默认场次
- 超时仅 1 秒,不阻塞主流程
巡演 / 音乐节 / 多日演出在 SKU 面板会先出现「请选择场次」/「选择日期」卡片。
page_probe 识别后会进入 session_picker 状态并调用
event_navigator.select_session(),匹配优先级:
date + city 精确 → date 唯一 → fallback_index=0。
匹配失败会抛 SessionNotFoundError 并将运行结果标为
session_not_found,不会盲点。日期格式可写
4月6号 / 04-06 / 04.06 / 2026-04-06,date_utils.normalize_date() 统一归一为
MM.DD 后再比对。
票价选择采用两级策略:
- 文本匹配优先:通过
UiSelector().textContains(config.price)直接匹配票价文本 - 索引回退:文本匹配失败时,回退到原有的
FrameLayoutindex +clickable=true定位方式- 先尝试
find_element直接查找(不等待) - 失败后走
WebDriverWait等待容器出现再查找
- 先尝试
这种两级策略兼容了票价文本可见和不可见两种情况。
运行前需要:
- 用户已手动打开大麦 APP 并登录
- 如果使用
item_url + auto_navigate,可以停留在首页;否则仍需手动进入目标演出详情页 adb devices能识别设备即可,无需额外服务进程
2026-03-29 的真实模拟器测试结论:
- Android 模拟器 + 大麦 App 可以正常启动和探测页面
- 大麦首启弹窗可以通过启动探测层稳定处理
- 目标商品的 deeplink 会短暂进入
ProjectDetailActivity,随后回到首页,不适合作为默认导航方案 - 当前默认方案改为:优先用
item_url + auto_navigate从首页搜索进入目标演出;手动打开详情页仅作为回退方案
- 仅 Android: UIAutomator2 仅支持 Android
- 不支持 iOS: 需要 XCUITest 引擎 + 完全不同的元素定位方式
- 不支持选座: 流程中没有选座步骤
- 用
adb devices确认真机已经连上 - 把设备序列号填进
config.jsonc的udid adb devices确认设备已连接即可,无需额外配置- 如果大麦版本或 ROM 定制导致启动页不同,再覆盖
app_activity
整体设计以速度优先为核心:
- 所有等待时间压缩到最短(0.01s~2s)
- 禁用一切不必要的系统功能(动画、通知、空闲检测)
- 用坐标点击替代元素交互
- 批量操作先收集再执行,减少串行等待
{ "keyword": "张杰 演唱会", "users": ["张三"], "city": "上海", // 巡演 → 同一日期可能多城,city 是去重关键 "date": "04.06", // 统一为 MM.DD 格式(normalize_date 输出) "price": "780元", "price_index": 2, "rush_skip_session": false // 多场次必须 false;alias rush_mode=true 也会强制 false }