简介:这是一套面向i茅台App多账户自动预约场景的完整系统源码,适合具备一定Java与前端基础、希望自建预约工具的技术人员参考使用。项目采用前后端分离架构,后端以Java实现核心预约与账号调度逻辑,前端由Vue配合JavaScript构建操作界面,并附带Dockerfile、nginx.conf、redis.conf等部署配置,便于在服务器或本地环境快速搭建运行。压缩包共553个文件,涵盖209个java、87个vue、84个js、92个svg及xml、yml、sql、bat等类型,整体约201.6MB,内置上千家门店数据,添加账号后即可使用,并配有图文搭建教程,新手也能按图操作。目前已有570人学习浏览。对于想研究自动化预约流程、多账号管理、门店数据维护与容器化部署的读者,可从中获取完整的工程结构、配置模板与排错思路,具备较高的参考与二次开发价值。
1. 多账户预约这件事,难点从来不在“点按钮”
“i茅台app多账户自动预约程序系统源码”这个标题,第一次看到的人多半会以为核心是模拟点击。真做过一轮就知道,点击是最不值钱的一环。真正让人翻车的是三件事:多账户的会话怎么隔离、请求参数怎么保持和 App 一致、以及预约时间点上的并发怎么控。把这三件事想清楚,代码量其实不大;想不清楚,写两千行也是每天手动补号。
这篇面向的是手里有几个到几十个账户、想用一套本地程序把预约动作固定下来的开发者。默认你会一点 Python、能看懂 HTTP 请求、愿意自己抓包。整套思路是:用协议层请求替代 UI 自动化,用配置文件管理账户,用调度器卡时间点,用日志和重试兜住失败。它不解决“一定中签”,只解决“每天不用人守着点”。适合谁?适合把这事当成一个稳定的定时任务来做、而不是当成抽奖外挂的人。
2. 先想清楚:为什么协议层比 UI 自动化更靠谱
2.1 两条技术路线的取舍
做多账户预约,市面上常见两条路。第一条是 UI 自动化,用无障碍服务或者图像识别去点屏幕。第二条是协议层,直接构造 App 发出的 HTTP 请求。两条路我都试过,结论很明确:账户数超过三个,UI 自动化就开始崩。
原因不复杂。UI 自动化依赖一台设备或一个模拟器实例,多账户意味着多开,多开意味着内存、分辨率、渲染时序全都在变。你今天调好的坐标,明天 App 一更新布局就废了。而且 UI 层的操作是串行的,十个账户挨个点,时间点早就过了。
协议层的逻辑是:App 本身也是发 HTTP 请求给服务端,那我把这些请求的参数、签名、头信息复刻出来,用程序直接发。好处是单进程可以并发几十个账户,不依赖设备,跑在服务器上就行。代价是你得先搞清楚请求长什么样,签名怎么算。这个代价是一次性的,抓一次包、逆一次签名,后面长期受益。
提示:协议层方案的前提是你能合法地分析自己账户的请求。只处理自己有权操作的账户,不要碰别人的数据。
2.2 一次预约请求到底包含什么
把一次预约拆开看,核心是四块:身份凭证、业务参数、签名、时间戳。身份凭证通常是登录后拿到的 token,业务参数是门店 ID、商品 ID、数量这类,签名是把前面这些按约定规则算出来的校验值,时间戳用来防重放。
多账户的关键在于:每个账户有独立的 token,但签名算法是同一套。所以程序结构应该是「一套签名逻辑 + N 份账户凭证」。很多人一开始把 token 写死在代码里,加到第五个账户就乱了。正确做法是账户信息全部外置到配置文件,代码只读配置。
# accounts.yaml 结构示例:账户与凭证分离,方便扩展 accounts: - name: "acct_01" token: "eyJhbGciOi..." # 登录后获取的凭证 device_id: "d-8f3a..." # 设备标识,部分接口会校验 enabled: true # 单独控制某个账户是否参与 - name: "acct_02" token: "eyJhbGciOi..." device_id: "d-2b91..." enabled: true这段配置的逻辑是:把易变的凭证和稳定的代码分开。参数说明——token是会话凭证,过期就得重新登录刷新;device_id有些接口会做设备绑定校验,抓包时一并记下来;enabled让你临时停掉某个账户而不用删配置。这样加账户就是复制一段 YAML,不用动一行 Python。
2.3 签名与请求头:最容易被忽略的复刻点
抓包看到的请求,往往不止 body 里有参数,header 里也藏了东西。常见的有User-Agent、X-Device-Id、X-Timestamp、X-Sign。签名一般是对「排序后的参数 + 时间戳 + 密钥」做一次哈希。你复刻的时候,参数顺序、编码方式、时间戳精度(秒还是毫秒)任何一处不一致,服务端都会返回签名错误。
我一般的做法是先写一个最小的请求脚本,只发一个账户、一个固定参数,反复调到返回正常,再去扩展多账户。这一步急不得,签名对不上,后面全是白费。
import hashlib, time, requests def build_sign(params: dict, secret: str) -> str: # 按 key 字典序拼接,注意与抓包结果逐字符比对 raw = "&".join(f"{k}={params[k]}" for k in sorted(params)) raw += f"&ts={params['ts']}&secret={secret}" return hashlib.md5(raw.encode("utf-8")).hexdigest() def make_request(token: str, device_id: str, secret: str): params = { "shop_id": "10086", # 门店 ID,抓包替换 "item_id": "20001", # 商品 ID "count": 1, "ts": int(time.time()), # 秒级时间戳,精度要和抓包一致 } params["sign"] = build_sign(params, secret) headers = { "Authorization": f"Bearer {token}", "X-Device-Id": device_id, "User-Agent": "app/1.0", # 与抓包保持一致 } return requests.post("https://example.invalid/reserve", json=params, headers=headers, timeout=5)逻辑说明:build_sign负责把参数按规则拼成待签名字符串再哈希,make_request组装完整请求。参数说明——secret是签名密钥,通常藏在 App 里,需要静态分析拿到;ts的精度必须和抓包一致,差一个数量级签名就废;timeout一定要设,否则某个账户卡住会拖垮整批。注意这里的域名是占位,实际用你自己抓到的地址。
3. 多账户并发与调度:把时间点卡准
3.1 并发模型怎么选
账户数少(十个以内),用线程池就够,简单直接。账户数上到几十上百,线程切换的开销和 GIL 的限制就明显了,这时候用asyncio配合aiohttp更合适。但要注意,并发不是越高越好。服务端对同一 IP 的请求频率通常有限制,并发太高反而触发风控,全部失败。
我的经验值是:单机并发控制在 5 到 10 之间,账户之间加一个小的随机间隔(比如 50 到 200 毫秒)。这样既能在时间窗口内发完,又不至于像机器一样整齐划一。整齐划一的请求特征,恰恰是风控最容易识别的。
import asyncio, aiohttp, random async def reserve_one(session, account, sem): async with sem: # 信号量限制并发上限 await asyncio.sleep(random.uniform(0.05, 0.2)) # 随机抖动 try: async with session.post(URL, json=payload(account), headers=headers(account), timeout=5) as resp: return account["name"], resp.status, await resp.text() except Exception as e: return account["name"], -1, str(e) async def run_all(accounts): sem = asyncio.Semaphore(8) # 并发上限 8 async with aiohttp.ClientSession() as session: tasks = [reserve_one(session, a, sem) for a in accounts] return await asyncio.gather(*tasks)逻辑说明:用信号量Semaphore(8)把并发压在 8,random.uniform给每个请求加随机延迟,避免请求扎堆。参数说明——并发数根据你实测的服务端容忍度调整,从 5 开始往上试;随机区间不要太长,否则时间窗口内发不完。gather会等所有任务结束,返回每个账户的结果,方便后面统计。
3.2 时间同步:差半秒就出局
预约类场景对时间极其敏感。你本地时钟如果和标准时间差个一两秒,卡点就卡不准。常见做法是启动时先请求一次服务端时间接口,算出本地时钟的偏移量,之后所有定时都基于这个偏移校正。
import time, requests def clock_offset(server_time_url: str) -> float: t0 = time.time() resp = requests.get(server_time_url, timeout=3) t1 = time.time() server_ts = resp.json()["timestamp"] # 服务端返回的秒级时间戳 rtt = t1 - t0 # 往返延迟 return server_ts - (t0 + rtt / 2) # 估算偏移,扣除一半往返 OFFSET = clock_offset("https://example.invalid/time") def now() -> float: return time.time() + OFFSET # 校正后的当前时间逻辑说明:用一次请求的往返延迟估算本地与服务器的偏差,rtt/2是对网络延迟的粗略补偿。参数说明——server_time_url用你抓到的任意返回时间戳的接口;偏移量算一次就够,跑之前刷新一下。注意这个估算有误差,要求更高就多测几次取中位数。
3.3 调度器:提前多少秒发起
卡点不是等到整点才发,而是提前一点点。因为请求从发出到服务端接收有网络延迟,你本地整点发,服务端可能已经过了。我的做法是提前 200 到 500 毫秒发起,具体数值靠实测:连续几天记录请求到达时间,看平均延迟是多少,再定提前量。
调度本身用asyncio的sleep配合校正后的时间就行,不需要引入重型调度框架。算好距离目标时间还有多少秒,睡到那个点,然后触发run_all。注意sleep的精度在毫秒级,够用。
4. 避坑与排查:那些让我重写三遍的细节
4.1 token 过期没处理,整批账户静默失败
现象:程序跑完显示成功,但一个都没约上。原因:token 有有效期,过期后接口返回 401,但代码里没判断状态码,把 401 也当成正常返回记了日志。解决:每个请求都检查状态码,401 单独标记,触发重新登录流程刷新 token。别偷懒只看有没有抛异常,HTTP 错误码不会抛异常。
4.2 参数顺序变了,签名全错
现象:单账户测试通过,多账户一跑全返回签名错误。原因:多账户时参数从字典动态拼装,Python 字典在某些操作后顺序不稳定,导致签名用的顺序和抓包不一致。解决:签名前强制sorted(),并且把待签名字符串打印出来,和抓包结果逐字符比对。这个坑我踩过两次,第二次是因为换了 Python 版本。
4.3 并发太高触发限流,全部返回繁忙
现象:账户一多,成功率反而下降,大量返回“系统繁忙”。原因:同一 IP 短时间请求过多,被服务端限流。解决:降并发、加随机间隔、必要时分散到不同出口。别一上来就拉满并发,先小批量测服务端的容忍度。
4.4 本地时间不准,卡点全偏
现象:明明提前发起了,还是提示已结束。原因:本地时钟没校正,或者校正后没刷新,跑了一周偏移累积。解决:每次运行前重新算一次偏移,别把偏移量写死。虚拟机尤其容易时钟漂移。
4.5 日志只记成功,出问题无从查起
现象:某天全部失败,但日志里只有“完成”,没有任何请求详情。原因:日志级别设太高,或者只记了结果没记请求和响应。解决:每个账户记录请求时间、状态码、响应摘要,失败时把响应体也存下来。排查的时候,响应体里的错误信息往往一句话就点破问题。
5. 让这套东西真正跑得住的一个习惯
前面讲的都是结构和代码,最后说一个决定这套程序能不能长期用的习惯:把每次运行的结果落库,而不是只看当天的日志。我一般用一个简单的 SQLite 表,每次预约记一条:账户、时间、状态码、响应摘要、是否成功。跑上一两周,你就能看出规律——哪个账户的 token 容易过期、哪个时间段成功率高、并发调到多少最稳。这些规律靠脑子记是记不住的。
CREATE TABLE reserve_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, acct_name TEXT NOT NULL, -- 账户标识 run_at DATETIME NOT NULL, -- 发起时间 status_code INTEGER, -- HTTP 状态码 success INTEGER, -- 1 成功 0 失败 detail TEXT -- 响应摘要,失败时存错误信息 );有了这张表,验证方法就很直接:查最近七天的成功率,按账户分组,看有没有哪个账户一直失败。如果某个账户连续失败,多半是 token 或 device_id 的问题,单独处理,不用动其他账户。参数上,detail字段别存太长,截断到几百字符就够,否则库会涨得很快。
再进阶一点,可以把签名逻辑和请求逻辑彻底解耦,签名做成一个纯函数,输入参数输出签名字符串,这样单元测试就好写——拿几条抓包记录当测试用例,签名对不上立刻能发现,不用真发请求。这个习惯帮我省了无数次联调时间。
说个我自己的教训。早期我图省事,把 token 和签名密钥直接写在代码里,结果换账户要改代码、重新部署,一天折腾好几回。后来全部外置到配置,代码一行不动,加账户就是复制粘贴。多账户系统的复杂度不在请求本身,而在管理。把管理做轻,程序才跑得久。希望帮到你。
本文还有配套的精品资源,点击获取