
简介这是一套面向茅台抢购需求用户的i茅台自动预约工具源码包基于Java后端与Vue前端构建支持Docker一键部署可每日定时自动完成预约流程省去手动操作。资源共542个文件以209个java业务代码、87个vue页面组件、84个js脚本为主辅以xml配置、yml部署文件、bat启动脚本及sql初始化脚本压缩包约2.99MB目录结构完整涵盖前后端与容器化部署所需模块。目前已有1484人学习下载。通过该源码读者可了解自动预约任务的接口调用与定时调度实现思路掌握Docker容器化打包与一键启动的配置方法并参考项目分层结构进行二次开发或本地调试。需注意自动化预约可能涉及平台使用条款使用前请自行确认合规性。1. i茅台自动预约这件事到底卡在哪一步每天九点整i茅台 App 的申购按钮亮起几万人同时点下去中签率却低得让人怀疑人生。手动抢了三个月一瓶没中这不是运气问题是流程问题。所谓 i茅台自动预约本质上是把「定时打开 App → 定位门店 → 选择酒品 → 提交申购」这套固定动作交给一个跑在容器里的定时任务去完成。它解决的不是中签率而是「你不需要每天记着这件事」。适合谁适合每天忙到忘记申购、又不想错过每一次机会的人。Docker 一键部署的意义在于你不需要懂 Python 环境、不需要装依赖、不需要配定时器一条命令拉起来就能跑。但这里面有几个关键点——登录态怎么保持、定时怎么触发、请求怎么构造——才是决定这套方案能不能长期稳定跑下去的核心。2. 自动预约的底层逻辑从登录态到定时触发2.1 为什么不能直接模拟登录而要复用 Tokeni茅台 App 的登录流程涉及短信验证码、设备指纹、加密签名等多层校验。想用纯代码从零模拟登录成本极高且容易触发风控。常见做法是手动在手机上完成一次登录把返回的 Token通常是MT-Token或类似的鉴权字段提取出来交给自动化脚本复用。这个 Token 有有效期短则几小时长则几天过期后需要重新获取。我一般会这样做先用抓包工具比如 Charles 或 Fiddler在手机上抓一次完整的申购请求把请求头里的关键字段全部记录下来。这些字段通常包括字段名作用是否必须MT-Token用户身份鉴权必须MT-Device-ID设备唯一标识必须MT-APP-VersionApp 版本号建议保留MT-Request-ID请求唯一 ID必须每次随机生成MT-Network-Type网络类型可选MT-R请求签名参数必须部分版本需要Token 的获取方式决定了整个方案的稳定性。如果 Token 有效期只有几小时那每天自动预约之前都得先手动更新一次自动化就失去了意义。所以选型时要优先考虑那些 Token 有效期较长的方案或者支持自动刷新 Token 的实现。2.2 定时任务的三种触发方式与选择定时触发是自动预约的骨架。没有定时脚本就只是一次性工具。常见的触发方式有三种第一种宿主机 crontab。在 Linux 宿主机上直接写 crontab 规则到点执行docker exec进入容器跑脚本。优点是简单直接缺点是容器和宿主机的耦合度高迁移不方便。第二种容器内 cron。在 Docker 镜像里内置 cron 服务容器启动后自动加载定时规则。优点是自包含缺点是容器内 cron 的日志排查比较麻烦而且如果容器重启cron 需要重新加载。第三种Python 调度库。用schedule或APScheduler在脚本内部实现定时循环。优点是灵活可以精确控制到秒级缺点是脚本必须常驻运行对容器的资源占用有要求。我一般会推荐第三种因为 i茅台的申购时间窗口很窄通常是 9:00:00 到 9:00:30 之间秒级精度很重要。用APScheduler可以设置CronTrigger(hour9, minute0, second0)误差控制在毫秒级。from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger def reserve(): # 这里放申购逻辑构造请求、发送、解析结果 print(开始执行申购...) scheduler BlockingScheduler() # 每天 9:00:00 触发秒级精度 scheduler.add_job(reserve, CronTrigger(hour9, minute0, second0)) scheduler.start()这段代码的逻辑很直白创建一个阻塞式调度器注册一个 cron 触发器每天九点整执行reserve函数。BlockingScheduler会让主线程挂起适合容器里只有一个任务的情况。如果需要在同一容器里跑多个任务可以换成BackgroundScheduler。参数说明hour9是 24 小时制minute0是整点second0是零秒。如果你的服务器时间不是北京时间需要在容器启动时设置TZAsia/Shanghai环境变量否则触发时间会偏移。2.3 请求构造签名参数和随机 ID 的处理申购请求的构造是整个方案里最容易翻车的环节。i茅台的接口有签名校验签名算法会随 App 版本更新而变化。常见做法是从抓包结果里提取签名逻辑用 Python 复现。如果签名算法太复杂也可以直接用抓到的原始请求体只替换其中的随机字段。import requests import uuid import time def build_headers(token, device_id): return { MT-Token: token, MT-Device-ID: device_id, MT-APP-Version: 1.3.7, # 根据实际抓包结果填写 MT-Request-ID: str(uuid.uuid4()), # 每次请求必须不同 MT-Network-Type: WiFi, User-Agent: Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36, Content-Type: application/json } def submit_reservation(token, device_id, shop_id, item_id): url https://app.moutai519.com.cn/xhr/front/mall/reservation/add headers build_headers(token, device_id) payload { shopId: shop_id, itemId: item_id, count: 1, sessionId: int(time.time() * 1000) } resp requests.post(url, headersheaders, jsonpayload, timeout10) return resp.json()逻辑说明build_headers负责组装请求头其中MT-Request-ID用 UUID 保证每次唯一MT-Token和MT-Device-ID从配置中读取。submit_reservation发送 POST 请求payload里的sessionId用毫秒时间戳shopId和itemId需要提前从 App 里查到。参数说明shop_id是你想预约的门店编号不同城市不同门店的 ID 不一样需要在 App 里切换门店时抓包获取。item_id是酒品编号比如飞天茅台 500ml 的 ID 是固定的。count通常填 1部分活动允许填 2。注意请求频率不要太高同一账号在短时间内多次请求会触发风控导致 Token 失效甚至账号被限制。3. Docker 一键部署从镜像拉取到容器跑通3.1 镜像从哪来自建还是用现成的Docker 一键部署的前提是有一个可用的镜像。来源通常有两种一是自己写 Dockerfile 构建二是用别人已经构建好的镜像。自己构建的好处是可控坏处是需要懂 Dockerfile 和依赖管理。用现成镜像的好处是省事坏处是不知道里面装了什么有安全风险。我一般会建议自己构建因为 i茅台的接口会变脚本需要经常更新。自己构建的话改完代码重新docker build就行不用等别人更新镜像。FROM python:3.11-slim WORKDIR /app # 安装依赖 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制脚本 COPY . . # 设置时区 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezone # 启动调度器 CMD [python, main.py]逻辑说明基础镜像用python:3.11-slim体积小、启动快。WORKDIR设置工作目录COPY把依赖文件和脚本复制进去pip install安装依赖。时区设置很关键不设置的话容器默认是 UTC定时任务会偏移 8 小时。参数说明requirements.txt里通常包含requests、apscheduler、pycryptodome如果需要签名。CMD是容器启动命令main.py是入口脚本。3.2 docker-compose 编排环境变量与持久化单容器用docker run就够了但如果要挂载配置文件、设置环境变量、配置重启策略用docker-compose更清晰。version: 3.8 services: moutai: build: . container_name: moutai-reserve restart: unless-stopped environment: - TZAsia/Shanghai - MT_TOKEN${MT_TOKEN} - MT_DEVICE_ID${MT_DEVICE_ID} - SHOP_ID${SHOP_ID} - ITEM_ID${ITEM_ID} volumes: - ./logs:/app/logs - ./config.yaml:/app/config.yaml logging: driver: json-file options: max-size: 10m max-file: 3逻辑说明build: .表示用当前目录的 Dockerfile 构建镜像。restart: unless-stopped保证容器挂了自动重启。environment里的变量从.env文件读取避免把敏感信息写死在代码里。volumes把日志目录和配置文件挂载到宿主机方便查看和修改。logging限制日志大小防止磁盘被写满。参数说明MT_TOKEN和MT_DEVICE_ID是必填项从抓包结果里获取。SHOP_ID和ITEM_ID根据你要预约的门店和酒品填写。.env文件不要提交到 Git避免泄露。3.3 启动与验证三条命令确认容器在跑部署完成后用三条命令确认状态# 启动容器 docker-compose up -d # 查看容器状态 docker-compose ps # 查看实时日志 docker-compose logs -f逻辑说明up -d后台启动容器ps查看容器是否在运行logs -f实时跟踪日志输出。如果日志里出现「开始执行申购」并且没有报错说明调度器已经正常工作。参数说明-d是 detached 模式容器在后台运行。-f是 follow 模式持续输出日志。如果只想看最近 100 行可以用docker-compose logs --tail100。提示第一次部署后建议手动把系统时间调到 8:59:50观察容器是否在 9:00:00 准时触发。这样可以提前发现时区或调度器配置的问题。4. 避坑指南Token 失效、风控拦截、时间偏移4.1 Token 过期了但脚本还在跑现象日志里显示请求返回 401 或「登录已过期」但脚本没有停止继续每天发送无效请求。原因Token 有有效期过期后接口会拒绝请求。脚本没有做 Token 有效性检查也没有告警机制。解决在每次申购前先发一个轻量级的验证请求比如查询用户信息如果返回 401就发送通知邮件、Server 酱、钉钉机器人提醒手动更新 Token。也可以在配置文件里设置 Token 过期时间到期前自动提醒。4.2 请求频率过高触发风控现象刚开始几天能正常申购后来突然所有请求都返回「操作过于频繁」或直接封号。原因i茅台的风控系统会检测请求频率、设备指纹、IP 地址等。如果脚本在短时间内发送大量请求或者多个账号用同一个 IP很容易被识别。解决控制请求频率每天只发一次申购请求不要重试。如果跑多个账号每个账号之间间隔至少 5 秒。IP 方面尽量用家庭宽带不要用机房 IP。4.3 容器时间不对导致错过申购窗口现象日志显示申购时间是 1:00 而不是 9:00或者 9:00 过了几分钟才触发。原因容器默认时区是 UTC比北京时间晚 8 小时。如果没有设置TZ环境变量调度器会按 UTC 时间触发。解决在docker-compose.yml里加上TZAsia/Shanghai并且在 Dockerfile 里也设置时区。启动后用docker exec moutai-reserve date确认容器内时间是否正确。4.4 配置文件挂载后权限不对现象容器启动时报「Permission denied」无法读取config.yaml。原因宿主机上的文件权限是root:root容器内的进程以非 root 用户运行没有读取权限。解决在宿主机上执行chmod 644 config.yaml或者把容器内的用户改成 root不推荐。更好的做法是在 Dockerfile 里创建一个非 root 用户并把文件所有权赋给该用户。4.5 日志文件把磁盘写满现象容器运行一段时间后宿主机磁盘空间不足容器被系统杀掉。原因日志没有轮转每天的输出都追加到同一个文件里时间长了文件会变得很大。解决在docker-compose.yml里配置logging选项限制单个日志文件的大小和数量。也可以在脚本里用logging.handlers.RotatingFileHandler实现日志轮转。5. 进阶技巧多账号管理与申购结果通知5.1 用配置文件管理多个账号如果你有多个账号需要预约不要把 Token 写死在代码里。用一个 YAML 文件管理所有账号脚本启动时读取。accounts: - name: 账号A token: xxxxx device_id: yyyyy shop_id: 12345 item_id: 10001 - name: 账号B token: zzzzz device_id: wwwww shop_id: 12345 item_id: 10001逻辑说明每个账号独立配置 Token、设备 ID、门店和酒品。脚本遍历accounts列表依次执行申购。账号之间加time.sleep(5)避免触发风控。参数说明name只是标识方便日志里区分。token和device_id必须成对出现不能混用。5.2 申购结果推送到手机申购完成后把结果推送到手机这样不用每天去翻日志。常见做法是用 Server 酱或钉钉机器人。import requests def notify(title, content): url https://sctapi.ftqq.com/你的KEY.send data {title: title, desp: content} requests.post(url, datadata) # 在申购逻辑结束后调用 notify(i茅台申购结果, f账号A{成功 if result[code] 200 else 失败})逻辑说明notify函数发送 HTTP POST 请求到 Server 酱的接口把标题和内容推送到微信。result[code]是申购接口的返回码200 表示成功。参数说明你的KEY是 Server 酱分配的 SendKey需要自己去申请。钉钉机器人的用法类似只是 URL 和 payload 格式不同。5.3 验证方案是否长期可用的三个指标部署完成后怎么判断这套方案能不能长期跑我一般会看三个指标指标合格标准检查方式Token 有效期大于 24 小时记录每次更新 Token 的时间申购成功率大于 0%连续跑 7 天看是否有中签记录容器稳定性无异常重启docker-compose ps查看运行时长如果 Token 有效期太短说明方案需要频繁手动干预不适合长期跑。如果连续 7 天申购成功率都是 0%可能是请求构造有问题或者账号被风控了。容器稳定性方面如果每天都会重启说明资源限制或代码有 bug。5.4 我踩过的一个坑别把申购当成抢购最后说一个血泪经验。i茅台的申购是「预约制」不是「先到先得」。你在 9:00:00 提交和 9:00:30 提交中签概率是一样的。所以不要为了追求毫秒级精度去写多线程并发请求那样只会增加风控风险。我一开始就是没搞懂这个逻辑写了 10 个线程同时发请求结果账号直接被限制登录 7 天。后来改成单线程、每天一次反而稳定跑了半年。希望帮到你。本文还有配套的精品资源点击获取