| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- import tkinter as tk
- from tkinter import ttk, scrolledtext
- import requests
- import json
- import threading
- import time
- from datetime import datetime
- import winsound
- import os
- import random
- import string
- class TicketMonitor:
- def __init__(self, root):
- self.root = root
- self.root.title("余票监控系统")
- self.root.geometry("1000x850")
-
- # 监控状态
- self.is_monitoring = False
- self.monitor_thread = None
-
- # 默认API配置
- self.api_url = "https://newmanage.mgk.org.cn/campaign/noTokenapi/listCinemaNumByDay?corpCode=0001"
- self.headers_template = {
- 'Connection': 'keep-alive',
- 'content-type': 'application/json',
- 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 26_5_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.75(0x18004b3c) NetType/WIFI Language/zh_CN',
- 'Referer': 'https://servicewechat.com/wx66fdc5f2e062a173/163/page-frame.html'
- }
- self.payload = {
- "corpCode": "0001",
- "orderFlag": "C"
- }
-
- # 生成固定的Token前缀 + 随机6位
- self.token_prefix = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxODU5MTc3NzI4MSIsImNvcnBOYW1lIjoi5rKz5Y2X5bmz5Y-w6YKu5Lu35Yy6IiwiY29ycENvZGUiOiIwMDAxIiwiZXhwIjoxNzU1NDE1MTY3LCJpYXQiOjE3MjM4NzkxNjcsImp0aSI6IjY2Y2NmODVkLTUzMDEtNDdmMS1hZmI4LWM5NDU5YjI3YzdmOSJ9."
-
- # 生成随机6位字符(字母+数字)
- self.random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
- self.token = self.token_prefix + self.random_suffix
-
- # 请求计数器
- self.request_count = 0
- self.auto_refresh_interval = 30 # 每30次请求自动刷新Token
-
- # 用于记录已通知的日期
- self.notified_dates = set()
-
- # 日期选择相关变量
- self.selected_dates = set() # 选中的日期
-
- # 初始化UI
- self.setup_ui()
-
- # 显示生成的Token信息(仅日志)
- self.log(f"Token已自动生成,后缀: {self.random_suffix}", "info")
-
- def setup_ui(self):
- # 创建主框架
- main_frame = ttk.Frame(self.root, padding="10")
- main_frame.pack(fill=tk.BOTH, expand=True)
-
- # ========== 监控设置区域 ==========
- settings_frame = ttk.LabelFrame(main_frame, text="监控设置", padding="10")
- settings_frame.pack(fill=tk.X, pady=(0, 10))
-
- # 第一行:基本设置
- basic_frame = ttk.Frame(settings_frame)
- basic_frame.pack(fill=tk.X, pady=5)
-
- ttk.Label(basic_frame, text="监控间隔(秒):").pack(side=tk.LEFT, padx=5)
- self.interval_var = tk.StringVar(value="10")
- interval_spinbox = ttk.Spinbox(basic_frame, from_=3, to=60, textvariable=self.interval_var, width=5)
- interval_spinbox.pack(side=tk.LEFT, padx=5)
-
- # 应急票监控开关
- ttk.Label(basic_frame, text="应急票监控:").pack(side=tk.LEFT, padx=(20, 5))
- self.monitor_emergency_var = tk.BooleanVar(value=True)
- emergency_switch = ttk.Checkbutton(basic_frame, text="开启", variable=self.monitor_emergency_var)
- emergency_switch.pack(side=tk.LEFT, padx=5)
-
- # 只提醒一次
- self.notify_once_var = tk.BooleanVar(value=True)
- ttk.Checkbutton(basic_frame, text="📌 只提醒一次", variable=self.notify_once_var).pack(side=tk.LEFT, padx=20)
-
- # 声音提醒
- self.sound_var = tk.BooleanVar(value=True)
- ttk.Checkbutton(basic_frame, text="🔊 声音提醒", variable=self.sound_var).pack(side=tk.LEFT, padx=5)
-
- # 第二行:日期选择模式
- date_mode_frame = ttk.Frame(settings_frame)
- date_mode_frame.pack(fill=tk.X, pady=5)
-
- ttk.Label(date_mode_frame, text="监控模式:", width=15).pack(side=tk.LEFT, padx=5)
-
- self.monitor_mode = tk.StringVar(value="all")
- ttk.Radiobutton(date_mode_frame, text="所有日期", variable=self.monitor_mode,
- value="all", command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
- ttk.Radiobutton(date_mode_frame, text="选择日期", variable=self.monitor_mode,
- value="selected", command=self.on_mode_change).pack(side=tk.LEFT, padx=5)
-
- # 快捷选择按钮
- self.select_all_btn = ttk.Button(date_mode_frame, text="全选", command=self.select_all_dates, state=tk.DISABLED)
- self.select_all_btn.pack(side=tk.LEFT, padx=5)
-
- self.deselect_all_btn = ttk.Button(date_mode_frame, text="取消全选", command=self.deselect_all_dates, state=tk.DISABLED)
- self.deselect_all_btn.pack(side=tk.LEFT, padx=5)
-
- # Token状态显示(替代原来的刷新按钮)
- token_status_frame = ttk.Frame(date_mode_frame)
- token_status_frame.pack(side=tk.RIGHT, padx=5)
-
- self.token_status_label = ttk.Label(token_status_frame, text=f"Token: {self.random_suffix} (0次)")
- self.token_status_label.pack()
-
- # 控制按钮
- control_frame = ttk.Frame(settings_frame)
- control_frame.pack(fill=tk.X, pady=10)
-
- self.start_btn = ttk.Button(control_frame, text="▶ 开始监控", command=self.start_monitoring, width=12)
- self.start_btn.pack(side=tk.LEFT, padx=5)
-
- self.stop_btn = ttk.Button(control_frame, text="■ 停止监控", command=self.stop_monitoring, state=tk.DISABLED, width=12)
- self.stop_btn.pack(side=tk.LEFT, padx=5)
-
- ttk.Button(control_frame, text="🔍 立即查询", command=self.manual_check, width=12).pack(side=tk.LEFT, padx=5)
-
- # ========== 状态显示 ==========
- status_frame = ttk.LabelFrame(main_frame, text="状态信息", padding="5")
- status_frame.pack(fill=tk.X, pady=(0, 10))
-
- status_left = ttk.Frame(status_frame)
- status_left.pack(side=tk.LEFT, fill=tk.X, expand=True)
-
- self.status_label = ttk.Label(status_left, text="⏹ 未开始监控", foreground="gray")
- self.status_label.pack(side=tk.LEFT, padx=5)
-
- status_right = ttk.Frame(status_frame)
- status_right.pack(side=tk.RIGHT)
-
- self.last_update_label = ttk.Label(status_right, text="最后更新: --")
- self.last_update_label.pack(side=tk.RIGHT, padx=5)
-
- self.count_label = ttk.Label(status_right, text="发现票数: 0")
- self.count_label.pack(side=tk.RIGHT, padx=20)
-
- # ========== 日期选择区域 ==========
- date_select_frame = ttk.LabelFrame(main_frame, text="日期选择 (点击选择/取消选择日期)", padding="10")
- date_select_frame.pack(fill=tk.X, pady=(0, 10))
-
- # 创建日期选择框架
- date_canvas_frame = ttk.Frame(date_select_frame)
- date_canvas_frame.pack(fill=tk.X, pady=5)
-
- self.date_checkboxes = {} # 存储日期和对应的Checkbutton变量
- self.date_check_vars = {} # 存储BooleanVar
-
- # 创建日期选择容器
- self.date_container = ttk.Frame(date_canvas_frame)
- self.date_container.pack(fill=tk.X)
-
- # 先创建默认的日期占位
- self.init_date_checkboxes()
-
- # ========== 结果显示区域 ==========
- result_frame = ttk.LabelFrame(main_frame, text="余票信息", padding="5")
- result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
-
- # 创建Treeview显示数据
- columns = ("日期", "常规票(A)", "应急票(H)", "状态")
- self.tree = ttk.Treeview(result_frame, columns=columns, show="headings", height=12)
-
- # 设置列标题和宽度
- column_widths = {"日期": 120, "常规票(A)": 100, "应急票(H)": 100, "状态": 100}
- for col in columns:
- self.tree.heading(col, text=col)
- self.tree.column(col, width=column_widths.get(col, 100), anchor="center")
-
- # 添加滚动条
- scrollbar = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=self.tree.yview)
- self.tree.configure(yscrollcommand=scrollbar.set)
-
- self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
- scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
-
- # ========== 日志区域 ==========
- log_frame = ttk.LabelFrame(main_frame, text="日志", padding="5")
- log_frame.pack(fill=tk.BOTH, expand=True)
-
- self.log_text = scrolledtext.ScrolledText(log_frame, height=6, state=tk.DISABLED)
- self.log_text.pack(fill=tk.BOTH, expand=True)
-
- # 配置日志颜色标签
- self.log_text.tag_configure("error", foreground="red")
- self.log_text.tag_configure("success", foreground="green")
- self.log_text.tag_configure("info", foreground="blue")
- self.log_text.tag_configure("warning", foreground="orange")
-
- def init_date_checkboxes(self):
- """初始化日期复选框"""
- # 清空现有复选框
- for widget in self.date_container.winfo_children():
- widget.destroy()
-
- self.date_checkboxes.clear()
- self.date_check_vars.clear()
-
- # 创建示例日期(实际使用时会从API获取真实日期)
- example_dates = []
-
- row_frame = None
- for i, date in enumerate(example_dates):
- if i % 8 == 0:
- row_frame = ttk.Frame(self.date_container)
- row_frame.pack(fill=tk.X, pady=2)
-
- var = tk.BooleanVar(value=True)
- self.date_check_vars[date] = var
- cb = ttk.Checkbutton(row_frame, text=date, variable=var,
- command=lambda d=date: self.on_date_toggle(d))
- cb.pack(side=tk.LEFT, padx=10)
- self.date_checkboxes[date] = cb
-
- # 默认全选
- self.selected_dates.add(date)
-
- def update_date_checkboxes(self, dates):
- """更新日期复选框列表"""
- # 清空现有复选框
- for widget in self.date_container.winfo_children():
- widget.destroy()
-
- self.date_checkboxes.clear()
- self.date_check_vars.clear()
- self.selected_dates.clear()
-
- # 按日期排序
- sorted_dates = sorted(dates)
-
- row_frame = None
- for i, date in enumerate(sorted_dates):
- if i % 8 == 0:
- row_frame = ttk.Frame(self.date_container)
- row_frame.pack(fill=tk.X, pady=2)
-
- var = tk.BooleanVar(value=True) # 默认选中
- self.date_check_vars[date] = var
- cb = ttk.Checkbutton(row_frame, text=date, variable=var,
- command=lambda d=date: self.on_date_toggle(d))
- cb.pack(side=tk.LEFT, padx=10)
- self.date_checkboxes[date] = cb
-
- # 添加到选中集合
- self.selected_dates.add(date)
-
- def on_date_toggle(self, date):
- """日期选择切换"""
- if date in self.date_check_vars:
- if self.date_check_vars[date].get():
- self.selected_dates.add(date)
- self.log(f"已选择日期: {date}", "info")
- else:
- self.selected_dates.discard(date)
- self.log(f"已取消选择日期: {date}", "warning")
-
- def select_all_dates(self):
- """全选日期"""
- for date, var in self.date_check_vars.items():
- var.set(True)
- self.selected_dates.add(date)
- self.log("已全选所有日期", "info")
-
- def deselect_all_dates(self):
- """取消全选"""
- for date, var in self.date_check_vars.items():
- var.set(False)
- self.selected_dates.discard(date)
- self.log("已取消全选所有日期", "warning")
-
- def on_mode_change(self):
- """监控模式切换"""
- if self.monitor_mode.get() == "selected":
- self.select_all_btn.config(state=tk.NORMAL)
- self.deselect_all_btn.config(state=tk.NORMAL)
- self.log("切换到日期选择模式,请选择需要监控的日期", "info")
- else:
- self.select_all_btn.config(state=tk.DISABLED)
- self.deselect_all_btn.config(state=tk.DISABLED)
- self.log("切换到所有日期模式", "info")
-
- def regenerate_token(self):
- """重新生成Token(随机6位后缀)"""
- self.random_suffix = ''.join(random.choices(string.ascii_letters + string.digits, k=6))
- self.token = self.token_prefix + self.random_suffix
- self.log(f"Token已重新生成,新后缀: {self.random_suffix}", "info")
- # 更新Token状态显示
- self.token_status_label.config(text=f"Token: {self.random_suffix} ({self.request_count}次)")
-
- def get_headers(self):
- """获取请求头"""
- headers = self.headers_template.copy()
- headers['access-token'] = self.token
- return headers
-
- def log(self, message, tag=None):
- """添加日志消息"""
- self.log_text.config(state=tk.NORMAL)
- timestamp = datetime.now().strftime("%H:%M:%S")
-
- # 添加时间戳
- self.log_text.insert(tk.END, f"[{timestamp}] ", "info")
-
- # 添加消息(可带颜色标签)
- if tag:
- self.log_text.insert(tk.END, f"{message}\n", tag)
- else:
- self.log_text.insert(tk.END, f"{message}\n")
-
- self.log_text.see(tk.END)
- self.log_text.config(state=tk.DISABLED)
-
- def update_status(self, message, color="black"):
- """更新状态显示"""
- self.status_label.config(text=message, foreground=color)
-
- def fetch_ticket_data(self):
- """获取余票数据"""
- # 增加请求计数并自动刷新Token
- self.request_count += 1
-
- # 每30次请求自动刷新Token
- if self.request_count % self.auto_refresh_interval == 0:
- self.regenerate_token()
- self.log(f"🔄 已自动刷新Token (第{self.request_count}次请求)", "info")
-
- headers = self.get_headers()
-
- try:
- response = requests.post(
- self.api_url,
- headers=headers,
- json=self.payload,
- timeout=10
- )
- if response.status_code == 200:
- data = response.json()
- if data.get('status') == 200:
- return data.get('data', [])
- else:
- self.log(f"API返回错误: {data}", "error")
- return None
- else:
- self.log(f"HTTP请求失败: {response.status_code}", "error")
- return None
- except requests.exceptions.Timeout:
- self.log("请求超时,请检查网络连接", "error")
- return None
- except requests.exceptions.ConnectionError:
- self.log("网络连接失败,请检查网络", "error")
- return None
- except Exception as e:
- self.log(f"请求异常: {str(e)}", "error")
- return None
-
- def should_monitor_date(self, date):
- """检查是否应该监控该日期"""
- if self.monitor_mode.get() == "all":
- return True
- else:
- return date in self.selected_dates
-
- def check_and_notify(self, ticket_data):
- """检查余票并发送通知"""
- new_notified = set()
- total_tickets = 0
-
- # 更新日期复选框(如果有新日期)
- dates_in_data = [item.get('DAY', '') for item in ticket_data if item.get('DAY')]
- if dates_in_data:
- # 获取当前显示的日期列表
- current_dates = set(self.date_check_vars.keys())
- new_dates = set(dates_in_data) - current_dates
-
- if new_dates:
- # 如果有新日期,更新复选框
- self.update_date_checkboxes(dates_in_data)
- self.log(f"更新日期列表,新增 {len(new_dates)} 个日期", "info")
-
- for item in ticket_data:
- day = item.get('DAY', '')
- A_num = int(item.get('A_num', 0))
- H_num = int(item.get('H_num', 0))
-
- # 检查是否应该监控该日期
- if not self.should_monitor_date(day):
- continue
-
- # 检查是否监控应急票
- monitor_emergency = self.monitor_emergency_var.get()
-
- # 统计有票的日期(根据设置决定是否检查应急票)
- has_ticket = False
- if monitor_emergency:
- has_ticket = A_num > 0 or H_num > 0
- else:
- has_ticket = A_num > 0 # 只监控常规票
-
- if has_ticket:
- total_tickets += 1
-
- # 判断是否应该提醒
- should_notify = False
- if self.notify_once_var.get():
- if day not in self.notified_dates:
- should_notify = True
- new_notified.add(day)
- else:
- should_notify = True
- new_notified.add(day)
-
- # 发送提醒
- if should_notify:
- ticket_parts = []
- if A_num > 0:
- ticket_parts.append(f"常规票{A_num}张")
- if monitor_emergency and H_num > 0:
- ticket_parts.append(f"应急票{H_num}张")
-
- msg = f"🎫 {day} 有票了!{' + '.join(ticket_parts)}"
- self.log(msg, "success")
- self.update_status(f"🎉 发现余票!{day}", "green")
-
- # 声音提醒
- if self.sound_var.get():
- self.play_sound()
-
- # 更新已通知日期集合
- if self.notify_once_var.get():
- self.notified_dates.update(new_notified)
- else:
- self.notified_dates = set()
-
- # 更新计数
- self.count_label.config(text=f"发现票数: {total_tickets}")
-
- return total_tickets
-
- def play_sound(self):
- """播放声音提醒"""
- try:
- winsound.MessageBeep(winsound.MB_ICONASTERISK)
- except Exception as e:
- pass
-
- def update_display(self, ticket_data):
- """更新表格显示"""
- # 清空现有数据
- for item in self.tree.get_children():
- self.tree.delete(item)
-
- if not ticket_data:
- self.log("没有获取到数据", "warning")
- return
-
- # 添加新数据
- for item in ticket_data:
- day = item.get('DAY', '')
- A_num = item.get('A_num', '0')
- H_num = item.get('H_num', '0')
- sumNum = item.get('sumNum', '无票')
-
- # 检查是否应该监控该日期
- if not self.should_monitor_date(day):
- continue
-
- values = (day, A_num, H_num, sumNum)
- item_id = self.tree.insert("", tk.END, values=values)
-
- # 修改颜色逻辑:常规票有票时显示绿色
- monitor_emergency = self.monitor_emergency_var.get()
-
- # 检查常规票(A)是否有票
- has_regular_ticket = int(A_num) > 0
-
- # 检查应急票(H)是否有票
- has_emergency_ticket = monitor_emergency and int(H_num) > 0
-
- # 只要有常规票,就显示绿色
- if has_regular_ticket:
- self.tree.tag_configure('has_regular_ticket', background='#90EE90') # 亮绿色
- self.tree.item(item_id, tags=('has_regular_ticket',))
- # 如果只有应急票,显示浅蓝色
- elif has_emergency_ticket:
- self.tree.tag_configure('has_emergency_ticket', background='#B0E0E6') # 浅蓝色
- self.tree.item(item_id, tags=('has_emergency_ticket',))
- # 无票不显示特殊颜色
-
- def manual_check(self):
- """手动查询"""
- self.log("手动查询中...", "info")
- self.update_status("查询中...", "orange")
-
- data = self.fetch_ticket_data()
- if data:
- self.update_display(data)
- total = self.check_and_notify(data)
- self.last_update_label.config(text=f"最后更新: {datetime.now().strftime('%H:%M:%S')}")
- self.update_status(f"查询完成,发现 {total} 天有票", "blue")
- self.log(f"查询完成,共 {len(data)} 天数据", "info")
- else:
- self.update_status("查询失败", "red")
-
- def monitoring_loop(self):
- """监控循环"""
- self.log("开始监控...", "info")
- self.update_status("🔄 监控运行中", "green")
-
- check_count = 0
- while self.is_monitoring:
- try:
- check_count += 1
- self.log(f"第 {check_count} 次检测...", "info")
-
- # 获取数据
- data = self.fetch_ticket_data()
- if data:
- self.update_display(data)
- self.check_and_notify(data)
- self.last_update_label.config(text=f"最后更新: {datetime.now().strftime('%H:%M:%S')}")
- self.update_status(f"🔄 监控运行中 (第{check_count}次)", "green")
-
- # 更新Token状态显示
- self.token_status_label.config(text=f"Token: {self.random_suffix} ({self.request_count}次)")
- else:
- self.update_status("⚠️ 监控运行中 (获取数据失败)", "orange")
-
- # 等待指定的间隔
- interval = int(self.interval_var.get())
- for _ in range(interval):
- if not self.is_monitoring:
- break
- time.sleep(1)
-
- except Exception as e:
- self.log(f"监控循环错误: {str(e)}", "error")
- time.sleep(5)
-
- self.log("监控已停止", "info")
- self.update_status("⏹ 已停止", "gray")
-
- def start_monitoring(self):
- """开始监控"""
- if self.is_monitoring:
- return
-
- self.is_monitoring = True
- self.start_btn.config(state=tk.DISABLED)
- self.stop_btn.config(state=tk.NORMAL)
-
- # 重置计数器
- self.request_count = 0
- self.token_status_label.config(text=f"Token: {self.random_suffix} (0次)")
-
- # 清空已通知列表
- self.notified_dates.clear()
-
- # 在新线程中运行监控
- self.monitor_thread = threading.Thread(target=self.monitoring_loop, daemon=True)
- self.monitor_thread.start()
-
- def stop_monitoring(self):
- """停止监控"""
- self.is_monitoring = False
- self.start_btn.config(state=tk.NORMAL)
- self.stop_btn.config(state=tk.DISABLED)
-
- if self.monitor_thread:
- self.monitor_thread.join(timeout=2)
-
- self.update_status("⏹ 已停止", "gray")
- def main():
- root = tk.Tk()
- app = TicketMonitor(root)
-
- # 窗口关闭处理
- def on_closing():
- app.stop_monitoring()
- root.destroy()
-
- root.protocol("WM_DELETE_WINDOW", on_closing)
-
- # 居中显示
- root.update_idletasks()
- width = root.winfo_width()
- height = root.winfo_height()
- x = (root.winfo_screenwidth() // 2) - (width // 2)
- y = (root.winfo_screenheight() // 2) - (height // 2)
- root.geometry(f'+{x}+{y}')
-
- root.mainloop()
- if __name__ == "__main__":
- main()
|