平台:微信公众号 | 类型:源码级技术拆解 | 关键词:CustomTkinter、mitmproxy、GraphQL、多线程、代理策略、支付链路、桌面端架构

1. 项目概述
本系统是一套基于 Python + CustomTkinter 开发的 桌面端小程序自动化抢购终端。
核心目标只有一个:把只有开发者能跑的脚本,包装成普通用户也能操作的桌面工具。
它解决的不是"能不能发请求"的问题——而是下面这一整条链路能不能跑通:
参数自动捕获 → 商品多关键词检索 → sku/尺码/库存映射 → 多商品任务队列 → 定时/立即抢购 → 订单创建 → 支付参数拉取 → 本地服务生成支付链接 → 手机微信扫码付款
核心技术栈
| GUI 界面 | ||
| HTTP 请求 | ||
| 抓包引擎 | ||
| 代理管理 | ||
| 任务调度 | ||
| 数据持久化 | ||
| 支付桥接 |
2. 整体运行流程
从用户双击桌面图标到完成付款,系统经历了 6 个核心步骤:
Step 1:参数捕获(auto_catcher.py + login.bat)
用户运行 login.bat → Windows 系统代理自动指向 127.0.0.1:8080 → mitmdump 静默启动 → 用户在电脑微信打开 Supreme 小程序随意浏览 → mitmproxy 从请求头/响应体中自动提取 token 和 openId → 写入 config.json → 关闭代理窗口,系统代理自动恢复。
Step 2:环境初始化(main.py 启动)
程序读取 config.json → 自动回填 token、openId、收货地址、代理链接 → GUI 渲染完成,日志区显示"系统初始化完成"。
Step 3:商品检索
用户在"任务大厅"输入关键词(多关键词逗号分隔) → 可选开启代理 → 点击"检索商品" → 后台线程分页请求商品列表 → 关键词匹配标题 → 逐商品拉取 详情(颜色/sku映射) + 库存(尺码/可购数量) → 结果渲染到可滚动列表,每行带勾选框和尺码下拉菜单。
Step 4:任务编排
用户勾选目标商品、选择尺码 → 点击"立即抢购"或输入目标时间后点击"定时抢购" → 程序扫描所有勾选商品,组装成 任务队列。
Step 5:批量下单
后台线程遍历任务队列 → 逐商品调用 GraphQL orderCreate 接口 → 请求体中完整补齐 8 个财务金额字段(含 0 值字段) → 拿到订单号 orderCode。
Step 6:支付链路闭合
下单成功 → 调用 GraphQL pay 接口(走 curl_cffi 模拟 Chrome 指纹绕过 JA3 检测) → 解析微信支付参数(appId / timeStamp / nonceStr / package / signType / paySign) → POST 推送到本地端口 4041 的支付服务 → 服务返回可访问 URL → 用户复制链接到手机微信 → 跳转付款。
3. 核心文件详解
整个项目由 4 个核心文件 组成,职责边界清晰,模块间低耦合:
📌 3.1 主程序入口 —— main.py(约530行)
这是整个系统的 大脑,包含两个核心类:
类一:SupremeAPI —— 接口与业务逻辑层
与 UI 完全解耦,只接收一个 logger 回调用于输出日志。对外暴露的核心方法:
set_proxy(proxy_url) | ||
search_products(keywords, token) | ||
fetch_product_details(spuCode, token) | customSize/Size/size 多种字段名 | |
fetch_sizes(spuCode, sku_size_map, token) | availableCount | |
checkout(product, size_info, config) | ||
fetch_pay(order_code, amount, config) |
最值得关注的设计细节:
① 财务金额字段的"全量补齐"策略
下单接口的 orderCreate mutation 中,即使某些金额为 0,也 一个不漏地显式传入:
"amount": {"amount": price, "currencyCode": "CNY"},
"productAmount": {"amount": price, "currencyCode": "CNY"},
"freight": {"amount": 0, "currencyCode": "CNY"},
"discount": {"amount": 0, "currencyCode": "CNY"},
"coupon": {"amount": 0, "currencyCode": "CNY"},
"giftCard": {"amount": 0, "currencyCode": "CNY"},
"tax": {"amount": 0, "currencyCode": "CNY"},
"additionalServiceFee":{"amount": 0, "currencyCode": "CNY"}
这不是冗余——服务端 GraphQL schema 对这些字段有非空校验,缺任何一个都会直接拒绝请求。很多脚本失败的原因不是业务逻辑错了,而是参数结构不完整。
② 支付环节的 JA3 指纹对抗
fetch_pay 方法没有继续用 requests 库,而是切换到 curl_cffi:
pay_res = cffi_requests.post(
url, headers=..., json=...,
impersonate="chrome110", # 模拟 Chrome 110 的 TLS 指纹
proxies=cffi_proxies
)
原因很明确:支付接口对 TLS 握手特征做了校验。普通 requests 库的 JA3 指纹会被识别为脚本请求,直接拦截。curl_cffi 的 impersonate 参数可以伪装成真实浏览器的 TLS 指纹。
③ 尺码字段的多名兼容
if opt.get('originCode') in ['customSize', 'Size', 'size']:
size_name = opt['value']['displayName']
不同商品的尺码字段命名不一致——有的叫 Size,有的叫 customSize,甚至有大小写差异。写死一个字段名,换个商品就失效。这里的兼容列表是一个 低成本的抗波动方案。
④ 线程安全的日志输出
deflog(self, msg):
defappend():
self.log_box.configure(state="normal")
self.log_box.insert("end", formatted_msg)
...
self.after(0, append) # 回到主线程操作 UI
所有后台线程的日志输出,通过 self.after(0, callback) 投递回 Tkinter 主事件循环执行。这同时解决了两个问题:
网络请求不阻塞 UI(后台线程执行) 子线程不直接操作 GUI 控件(Tkinter 不是线程安全的)
类二:SupremeBotApp(ctk.CTk) —— GUI 界面层
继承自 customtkinter.CTk,采用 左侧导航栏 + Tab 页切换 + 底部日志区 的经典布局:
┌──────┬──────────────────────────────────┐
│ SIDE │ TAB: 任务大厅 | 账号设置 │
│ BAR ├──────────────────────────────────┤
│ │ 检索结果滚动列表(勾选+尺码) │
│ ├──────────────────────────────────┤
│ │ 底部日志区(实时输出) │
└──────┴──────────────────────────────────┘
GUI 层的设计原则:
按钮事件只收参数,不写逻辑。 start_search()只负责读输入框 → 禁按钮 → 启线程。**UI 更新统一走 self.after(0, callback)**。后台线程拿到数据后,不直接操作控件,而是投递回调到主线程。配置读写与 UI 绑定。启动时 load_config()回填所有输入框,保存时save_config()从输入框读值写 JSON。
📌 3.2 抓包模块 —— auto_catcher.py(约92行)
这是降低用户使用门槛的 关键模块。基于 mitmproxy 的 Python API 编写,以 addon(插件) 形式注册到 mitmdump。
核心逻辑
classSupremeCatcher:
defresponse(self, flow: http.HTTPFlow):
# 1. 域名过滤:只拦截 supreme-api.supremecherry.cn
if TARGET_DOMAIN notin flow.request.pretty_url:
return
# 2. Token 提取:兼容 token 和 Authorization 两种头
req_token = (flow.request.headers.get("token", "") or
flow.request.headers.get("Authorization", ""))
# 3. OpenID 提取:递归搜索 JSON 响应体
open_id = self.find_openid(data)
递归查找 OpenID 的设计价值
deffind_openid(self, obj):
if isinstance(obj, dict):
for k, v in obj.items():
if k.lower() == "openid": # 大小写不敏感
return v
elif isinstance(v, (dict, list)):
res = self.find_openid(v) # 递归深入
if res: return res
elif isinstance(obj, list):
for item in obj:
res = self.find_openid(item)
if res: return res
returnNone
为什么要递归?
openId 在接口响应中的位置不固定——可能在 data.openId,可能在 result.user.openId,也可能嵌套在 data.list[0].userInfo.openId。
如果写死路径,接口结构一调整,整个抓包就失效。
递归方案虽然暴力,但抓住了本质:字段语义比字段位置更稳定。只要字段名还叫 openId(或 openid),不管层级多深都能捕获。
增量保存策略
# 初始化时先读已有配置,避免重复抓取
if os.path.exists("config.json"):
data = json.load(f)
self.token = data.get("token")
self.open_id = data.get("openId")
# 只有新值 ≠ 旧值时才写入
if self.token and self.open_id and is_updated:
self.save_to_file()
不是每次拦截都写文件,而是 值变化时才触发写入,减少不必要的 IO。
📌 3.3 一键代理启动器 —— login.bat(约25行)
这是整个项目里 代码最少但用户体验影响最大 的文件。
:: 1. 打开 Windows 系统代理
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
/v ProxyEnable /t REG_DWORD /d 1 /f
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
/v ProxyServer /d "127.0.0.1:8080" /f
:: 2. 启动 mitmdump(静默模式)
mitmdump -s auto_catcher.py -q
:: 3. 退出后恢复代理(关键!)
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings"
/v ProxyEnable /t REG_DWORD /d 0 /f
三行 reg add 命令解决了一个朴素但致命的问题:小白用户用完抓包工具后,电脑上不了网。
它的本质是 环境生命周期管理:
启动前:设置代理环境 运行中:mitmdump 占据前台,用户操作微信小程序 退出后:自动恢复代理设置
工程原则:谁打开的,谁关上;谁修改的,谁恢复。 这个 bat 文件只有 25 行,但它体现的产品化意识,比很多几千行的脚本更有价值。
4. 系统架构与数据流转
┌─────────────────────┐
│ login.bat │
│ ① 开系统代理 │
│ ② 启动 mitmdump │
│ ③ 退出后关代理 │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ auto_catcher.py │
│ mitmproxy 插件 │
│ Token + OpenID ────► config.json
└─────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ main.py (主程序) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ SupremeBotApp│ │ SupremeAPI │ │
│ │ (GUI 界面) │───►│ (业务逻辑) │ │
│ │ │ │ │ │
│ │ 商品检索 ◄──┼────┤ search_products │ │
│ │ 尺码选择 │ │ fetch_sizes │ │
│ │ 任务队列 │ │ checkout │ │
│ │ 日志输出 ◄──┼────┤ fetch_pay │ │
│ └──────────────┘ └───────┬──────────┘ │
│ │ │
└──────────────────────────────┼──────────────────────────┘
│
┌──────────▼──────────┐
│ 外部依赖 │
│ • Supreme API 服务器 │
│ • 代理IP提取API │
│ • 本地支付服务:4041 │
└─────────────────────┘
数据流向总结:
config.json是整个系统的 数据源——token、openId、地址、代理链接全部从此读取SupremeBotApp是 数据消费者——从配置文件读到界面,从界面收集用户选择SupremeAPI是 数据执行者——接收参数,发请求,返回结果支付参数最终 不经过 GUI,直接推送到本地 4041 服务,生成手机端支付链接
5. 核心技术难点与解决方案
难点一:参数自动化获取——从"手把手教用户抓包"到"一键启动"
问题:token 和 openId 是整个链路的地基。手动抓包(Charles/Fiddler → 找请求 → 复制Header → 粘贴)对普通用户是劝退级门槛。
方案:auto_catcher.py + login.bat 组合拳。用户只需双击 bat → 微信打开小程序随便点 → 参数自动写入 config.json。
关键细节:
Token 同时兼容 token和Authorization两种请求头命名OpenID 递归搜索,不依赖固定 JSON 路径 增量保存,值不变不写文件 代理用后自动恢复,不留副作用
难点二:商品/sku/尺码/库存的四层映射
问题:商品列表接口只返回标题和 spuCode。下单需要的 skuCode、尺码名称、库存数量要跨三个接口才能拼完整。
数据流:
商品列表接口 (productsByNavigation)
│
├── spuCode ──► 商品详情接口 (product/info/{spuCode})
│ │
│ ├── attributes[originCode=BrandColor] → 颜色
│ └── skus[].options[originCode] → skuCode ↔ 尺码名
│
└── spuCode ──► 库存接口 (product/spu/inventory)
└── skuList[].availableCount → 库存数量
兼容处理:尺码字段名做多值匹配 ['customSize', 'Size', 'size'],避免因接口字段命名不一致导致的取值为空。
难点三:GraphQL 订单参数的结构完整性
问题:orderCreate 是 GraphQL mutation,参数结构复杂且嵌套深。服务端对金额字段做了 schema 级非空校验——即使值为 0,缺字段也会直接被拒绝。
方案:不依赖"服务端默认值"的假设,所有金额字段显式传入:
amount, productAmount, freight, discount,
coupon, giftCard, tax, additionalServiceFee
8 个字段,值可以为 0,但 字段本身必须存在。
难点四:支付接口的 JA3 指纹检测
问题:支付环节的 GraphQL pay 接口对 TLS 握手特征做了校验。Python requests 库的默认 TLS 指纹会被识别为非浏览器请求,导致支付参数拉取失败。
方案:支付请求切换到 curl_cffi,通过 impersonate="chrome110" 模拟 Chrome 110 的 TLS 握手特征。普通请求(检索、下单)仍走 requests,只在必要环节引入对抗手段。
原则:按需升级对抗,不滥用。 每个接口能跑通的方案就是最简方案。
难点五:桌面应用的线程安全
问题:Tkinter / CustomTkinter 的 GUI 控件 不是线程安全的。如果在子线程中直接调用 log_box.insert() 或 label.configure(),会触发不可预期的异常甚至 segfault。
方案:所有跨线程 UI 操作通过 self.after(0, callback) 投递回主线程:
# 子线程调用 log()
deflog(self, msg):
defappend():
self.log_box.insert("end", formatted_msg) # 主线程执行
self.after(0, append) # 投递到 Tkinter 事件队列
同时,耗时操作(检索、下单)全部放到 threading.Thread(daemon=True),保证 UI 不卡顿。
难点六:代理不是全局开关,是运行时策略
问题:代理需求是动态的——默认不走代理,特定场景才开启。如果把代理写死在每个请求里,代码会又散又乱,切换逻辑难以维护。
方案:SupremeAPI 内部维护 self.proxy_dict 作为运行时策略:
# 切换代理:一个方法调用
self.api.set_proxy(proxy_url) # 从 API 提取 → 解析 → 赋值
self.api.set_proxy(None) # 关闭代理
# 所有请求统一使用 self.proxy_dict
requests.get(url, proxies=self.proxy_dict)
代理从散落在各处的临时代码,变成了 一个可切换的状态位。后期如果要扩展代理池、失败自动切换、按任务分配代理,都可以在这个基础上改,不需要动每一个请求方法。
6. 后期可复用的工程经验
这个项目最有价值的不是某个具体接口的调用方式——接口随时会变。真正能带到下一个项目里去的,是以下几类 工程能力:
6.1 配置化——工具交付的分水岭
原则:把所有硬编码变成可读写的外部配置。
核心思想:**把"改代码"降级为"改参数"**。这是工具能不能交付给非开发者使用的分水岭。
6.2 日志可视化——自动化工具不能是黑盒
没有日志的自动化工具,用户看到的只有两个状态:**"没反应"和"报错了"**。
这套系统的日志输出遵循以下规范:
时间戳:每条日志带 [HH:MM:SS],方便回溯状态标识:用 emoji 标记事件类型(📦 下单 / ✅ 成功 / ❌ 失败 / 💸 支付 / 🎉 完成 / ⚠️ 警告) 关键数据:订单号、商品名、代理IP、支付链接全部打印 线程安全:日志更新统一走 self.after(0)
6.3 接口兼容层——给不稳定数据加"缓冲"
token 和 Authorization 两个 Header | |
openid | |
['customSize', 'Size', 'size'] | |
: 且不含 {,避免将错误信息当IP使用 |
这些兼容处理看起来不起眼,但自动化系统能不能 长期稳定运行,差距就在这里。
6.4 环境生命周期管理——有借有还
login.bat 的代理自动恢复逻辑是一个 可复用的模式:
开始 → 设置环境 → 执行主逻辑 → 恢复环境 → 结束
不管主逻辑是正常退出还是异常中断,恢复动作必须执行。后期凡是涉及环境变更的操作(改 hosts、装证书、改注册表),都应该遵循这个模式。
6.5 跨端协作——把合适的事交给合适的终端
电脑端擅长:自动化请求、批量处理、精确计时。 手机端擅长:微信支付、扫码认证、生物识别。
这套系统的支付链路设计——电脑下单、参数推送到本地服务、手机微信打开链接付款——没有试图在电脑端强行完成一切,而是承认了移动端的不可替代性。
这本身就是一个重要的工程判断:自动化不是替代一切,而是在合适的边界上停下来。
7. 最终产出:从脚本到系统的能力清单
| 参数获取 | ||
| 商品检索 | ||
| 尺码选择 | ||
| 多商品 | ||
| 定时抢购 | ||
| 代理 | ||
| 支付 | ||
| 日志 | ||
| 配置 |
8. 总结
这套 Supreme 抢购终端,表面上看是一个"下单工具"。
但从工程角度审视,它已经具备了一个 小型桌面自动化系统 的全部要素:
GUI 层:CustomTkinter 现代化界面,线程安全 业务层:多商品队列、定时调度、任务编排 能力层:API 请求、代理策略、抓包引擎 桥接层:支付参数转发、跨端协作
它最值得学习的地方,从来不是某个 GraphQL 接口怎么调、某个 Header 怎么拼——
而是 把一个只有开发者能跑的脚本,包装成普通用户双击就能用的系统,中间每一步的设计决策。
别迷信脚本的速度,要重视系统的确定性。
抢购只是场景,架构才是真正可复用的东西。
文中涉及的接口地址、参数结构均为技术分析用途。
夜雨聆风