Ethscriptions commited on
Commit
fb1e230
·
verified ·
1 Parent(s): ede667a

Delete pages/monitor.py

Browse files
Files changed (1) hide show
  1. pages/monitor.py +0 -496
pages/monitor.py DELETED
@@ -1,496 +0,0 @@
1
- import streamlit as st
2
- import requests
3
- import time
4
- import json
5
- import os
6
- import threading
7
- import re
8
- import urllib3
9
- from datetime import datetime, timedelta, timezone, time as dt_time
10
- from collections import deque
11
- from dotenv import load_dotenv
12
-
13
- # --- 0. 基础配置 ---
14
- st.set_page_config(page_title="突发购票监控", page_icon="🛡️", layout="wide")
15
-
16
- # 屏蔽 HTTPS 证书警告
17
- urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
18
-
19
- # 尝试导入自动刷新组件
20
- try:
21
- from streamlit_autorefresh import st_autorefresh
22
- except ImportError:
23
- st_autorefresh = None
24
-
25
- # --- 1. 配置与常量 ---
26
- load_dotenv()
27
-
28
- # 企业微信机器人配置
29
- WEWORK_BOT_WEBHOOK = os.getenv("WEWORK_BOT_WEBHOOK")
30
-
31
- # 影城配置
32
- CINEMA_ID = os.getenv("CINEMA_ID")
33
-
34
- # 获取 Token 文件路径
35
- CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
36
- ROOT_DIR = os.path.dirname(CURRENT_DIR)
37
- TOKEN_FILE = os.path.join(ROOT_DIR, 'token_data.json')
38
-
39
-
40
- # --- 2. 工具函数 ---
41
- def get_beijing_now():
42
- """获取当前的北京时间"""
43
- utc_now = datetime.now(timezone.utc)
44
- beijing_now = utc_now.astimezone(timezone(timedelta(hours=8)))
45
- return beijing_now.replace(tzinfo=None)
46
-
47
-
48
- def get_business_date():
49
- """获取当前的营业日日期"""
50
- now = get_beijing_now()
51
- if now.time() < dt_time(6, 0):
52
- return (now - timedelta(days=1)).strftime("%Y-%m-%d")
53
- return now.strftime("%Y-%m-%d")
54
-
55
-
56
- def parse_show_datetime(date_str, time_str):
57
- """解析排片时间 (处理跨天)"""
58
- try:
59
- show_t = datetime.strptime(time_str, "%H:%M").time()
60
- base_date = datetime.strptime(date_str, "%Y-%m-%d")
61
- if show_t < dt_time(6, 0):
62
- base_date += timedelta(days=1)
63
- return datetime.combine(base_date.date(), show_t)
64
- except:
65
- return None
66
-
67
-
68
- def simplify_hall_name(raw_name):
69
- """
70
- 影厅名称简化
71
- 例如:'【和成天下1号厅】...' -> '1号厅'
72
- '6号激光厅' -> '6号厅'
73
- """
74
- if not raw_name:
75
- return "未知影厅"
76
- match = re.search(r'(\d+)号', raw_name)
77
- if match:
78
- return f"{match.group(1)}号厅"
79
- return raw_name
80
-
81
-
82
- # --- 3. 企业微信机器人推送模块 ---
83
- class WeWorkBotPusher:
84
- def __init__(self, webhook_url):
85
- self.webhook_url = webhook_url
86
-
87
- def send_text(self, content):
88
- """发送纯文本消息"""
89
- if not self.webhook_url:
90
- return False
91
-
92
- headers = {"Content-Type": "application/json"}
93
- data = {
94
- "msgtype": "text",
95
- "text": {
96
- "content": content
97
- }
98
- }
99
-
100
- try:
101
- resp = requests.post(self.webhook_url, json=data, headers=headers, timeout=10)
102
- result = resp.json()
103
- if result.get("errcode") == 0:
104
- return True
105
- else:
106
- print(f"❌ 企业微信机器人发送失败: {result}")
107
- return False
108
- except Exception as e:
109
- print(f"❌ 企业微信机器人发送异常: {e}")
110
- return False
111
-
112
-
113
- # --- 4. API 管理模块 ---
114
- class APIManager:
115
- def __init__(self, logger):
116
- self.last_login_fail_time = 0
117
- self.logger = logger
118
-
119
- def save_token(self, token_data):
120
- try:
121
- with open(TOKEN_FILE, 'w', encoding='utf-8') as f:
122
- json.dump(token_data, f, ensure_ascii=False, indent=4)
123
- except:
124
- pass
125
-
126
- def load_token(self):
127
- if os.path.exists(TOKEN_FILE):
128
- try:
129
- with open(TOKEN_FILE, 'r', encoding='utf-8') as f:
130
- return json.load(f).get('token')
131
- except:
132
- pass
133
- return None
134
-
135
- def login(self):
136
- if time.time() - self.last_login_fail_time < 600:
137
- self.logger("⏳ 登录冷却中,跳过重试。")
138
- return None
139
-
140
- self.logger("🔄 尝试后台自动登录...")
141
- username = os.getenv("CINEMA_USERNAME")
142
- password = os.getenv("CINEMA_PASSWORD")
143
- res_code = os.getenv("CINEMA_RES_CODE")
144
- device_id = os.getenv("CINEMA_DEVICE_ID")
145
-
146
- if not all([username, password, res_code]):
147
- self.logger("❌ 环境变量缺失,无法自动登录")
148
- return None
149
-
150
- session = requests.Session()
151
- session.headers.update({'User-Agent': 'Mozilla/5.0'})
152
- login_url = 'https://app.bi.piao51.cn/cinema-app/credential/login.action'
153
- login_data = {
154
- 'username': username, 'password': password, 'type': '1',
155
- 'resCode': res_code, 'deviceid': device_id, 'dtype': 'ios',
156
- }
157
-
158
- try:
159
- session.post(login_url, data=login_data, timeout=15)
160
- resp = session.get('https://app.bi.piao51.cn/cinema-app/security/logined.action', timeout=10)
161
- info = resp.json()
162
- if info.get("success") and info.get("data", {}).get("token"):
163
- self.save_token(info['data'])
164
- self.logger("✅ 登录成功。")
165
- return info['data']['token']
166
- else:
167
- raise Exception("未获取到Token")
168
- except Exception as e:
169
- self.last_login_fail_time = time.time()
170
- self.logger(f"❌ 登录失败: {e}")
171
- return None
172
-
173
- def fetch_schedule(self, date_str):
174
- token = self.load_token()
175
- if not token:
176
- token = self.login()
177
- if not token: return None
178
-
179
- url = 'https://cawapi.yinghezhong.com/showInfo/getHallShowInfo'
180
- params = {'showDate': date_str, 'token': token, '_': int(time.time() * 1000)}
181
- headers = {'Origin': 'https://caw.yinghezhong.com', 'User-Agent': 'Mozilla/5.0'}
182
-
183
- try:
184
- response = requests.get(url, params=params, headers=headers, timeout=15)
185
- data = response.json()
186
- if data.get('code') == 1:
187
- return data.get('data', [])
188
- elif data.get('code') == 500:
189
- self.logger("⚠️ Token失效,重试中...")
190
- token = self.login()
191
- if token:
192
- params['token'] = token
193
- response = requests.get(url, params=params, headers=headers, timeout=15)
194
- return response.json().get('data', [])
195
- return None
196
- except Exception as e:
197
- self.logger(f"API请求异常: {e}")
198
- return None
199
-
200
-
201
- # --- 5. 监控服务主逻辑 ---
202
- class CinemaMonitor:
203
- def __init__(self):
204
- self.logs = deque(maxlen=50)
205
- self.status_text = "初始化中..."
206
- self.next_wakeup = None
207
- self.monitored_sessions = []
208
-
209
- self.api = APIManager(self.log)
210
- self.bot = WeWorkBotPusher(WEWORK_BOT_WEBHOOK)
211
-
212
- self.current_business_date = None
213
- self.daily_schedule_cache = None
214
-
215
- self.zero_ticket_candidates = set()
216
- self.alerted_sessions = set()
217
- self.last_daily_report_date = None
218
-
219
- # 统计数据 (用于每日报告)
220
- self.stats = {
221
- "zero_total": 0, # 纳入监控的0票总场次
222
- "alert_count": 0, # 触发通知次数
223
- "api_fails": 0, # API获取失败次数
224
- "notify_fails": 0 # 发送通知失败次数
225
- }
226
-
227
- self.thread = threading.Thread(target=self._run_loop, daemon=True)
228
- self.thread.start()
229
-
230
- def log(self, msg):
231
- timestamp = get_beijing_now().strftime("%H:%M:%S")
232
- entry = f"[{timestamp}] {msg}"
233
- print(entry)
234
- self.logs.appendleft(entry)
235
-
236
- def send_ticket_alert(self, hall_name, movie_name, show_time, ticket_count):
237
- """
238
- 发送突发购票告警 (企业微信机器人)
239
- """
240
- msg = (
241
- f"发现 {hall_name} {show_time} 场次有票!\n\n"
242
- f"{hall_name} {show_time}《{movie_name}》\n"
243
- f"突增至 {ticket_count} 张\n"
244
- f"请尽快处理。"
245
- )
246
-
247
- if not self.bot.send_text(msg):
248
- self.stats["notify_fails"] += 1
249
-
250
- def send_daily_report(self, date_str):
251
- """
252
- 发送每日统计报告 (企业微信机器人)
253
- """
254
- stats_text_full = (
255
- f"0票总场次:{self.stats['zero_total']}\n"
256
- f"触发通知次数:{self.stats['alert_count']}\n"
257
- f"发送通知失败次数:{self.stats['notify_fails']}\n"
258
- f"API获取失败次数:{self.stats['api_fails']}"
259
- )
260
-
261
- msg = (
262
- f"0票场次开场前10分钟内突发购票检查就绪\n\n"
263
- f"{date_str},今日排片数据已加载,开始智能检查。\n\n"
264
- f"昨日情况:\n{stats_text_full}\n"
265
- f"服务正常运行中。"
266
- )
267
- if not self.bot.send_text(msg):
268
- self.stats["notify_fails"] += 1
269
-
270
- def _run_loop(self):
271
- self.log("🚀 突发购票监控服务已启动")
272
- is_first_run = True # 新增:首次运行标志
273
-
274
- while True:
275
- try:
276
- now = get_beijing_now()
277
- biz_date = get_business_date()
278
- today_str = now.strftime("%Y-%m-%d")
279
-
280
- # --- 每日健康检查 (09:00 或 首次运行) ---
281
- if is_first_run or (now.time() >= dt_time(9, 0) and self.last_daily_report_date != today_str):
282
- report_date_str = now.strftime("%Y年%m月%d日")
283
-
284
- if is_first_run:
285
- self.log(f"🔔 首次运行,发送状态报告: {today_str}")
286
- else:
287
- self.log(f"🔔 发送每日统计报告: {today_str}")
288
-
289
- # 发送报告
290
- self.send_daily_report(report_date_str)
291
-
292
- # 重置统计数据 & 更新日期
293
- self.last_daily_report_date = today_str
294
- self.stats = {
295
- "zero_total": 0, "alert_count": 0,
296
- "api_fails": 0, "notify_fails": 0
297
- }
298
- is_first_run = False
299
-
300
- # 休眠逻辑 (06:00 - 09:30)
301
- start_check_time = now.replace(hour=9, minute=30, second=0, microsecond=0)
302
- is_early_morning = (dt_time(6, 0) <= now.time() < dt_time(9, 30))
303
-
304
- if is_early_morning:
305
- self.status_text = "非监控时段 (等待 09:30)"
306
- self.next_wakeup = start_check_time
307
- self.zero_ticket_candidates.clear()
308
- self.alerted_sessions.clear()
309
- time.sleep(60)
310
- continue
311
-
312
- # 缓存刷新
313
- if self.daily_schedule_cache is None or self.current_business_date != biz_date:
314
- self.log(f"📅 获取 {biz_date} 全天排片...")
315
- schedule = self.api.fetch_schedule(biz_date)
316
- if schedule is None:
317
- self.log("❌ 初始化获取失败,1分钟后重试")
318
- self.stats["api_fails"] += 1
319
- time.sleep(60)
320
- continue
321
- self.daily_schedule_cache = schedule
322
- self.current_business_date = biz_date
323
- self.zero_ticket_candidates.clear()
324
- self.alerted_sessions.clear()
325
- self.log(f"✅ 数据已更新,共 {len(schedule)} 场")
326
-
327
- # 筛选窗口期
328
- active_check_needed = False
329
- min_sleep_seconds = 3600
330
- sessions_in_window = []
331
- current_schedule = self.daily_schedule_cache
332
-
333
- for item in current_schedule:
334
- start_time_str = item.get('showStartTime')
335
- if not start_time_str: continue
336
- show_dt = parse_show_datetime(biz_date, start_time_str)
337
- if not show_dt: continue
338
-
339
- if now >= show_dt: continue # 已开场
340
-
341
- monitor_start_dt = show_dt - timedelta(minutes=11)
342
-
343
- if now >= monitor_start_dt:
344
- sessions_in_window.append({'data': item, 'dt': show_dt})
345
- active_check_needed = True
346
- else:
347
- seconds_until_window = (monitor_start_dt - now).total_seconds()
348
- if seconds_until_window < min_sleep_seconds:
349
- min_sleep_seconds = seconds_until_window
350
-
351
- # 执行监控
352
- if active_check_needed:
353
- self.status_text = "🔥 正在监控"
354
- self.next_wakeup = now + timedelta(seconds=60)
355
-
356
- realtime_schedule = self.api.fetch_schedule(biz_date)
357
- if realtime_schedule is None:
358
- self.log("⚠️ API请求失败,跳过本次判定")
359
- self.stats["api_fails"] += 1
360
- time.sleep(60)
361
- continue
362
-
363
- realtime_map = {f"{x.get('hallId')}_{x.get('showStartTime')}": x for x in realtime_schedule}
364
- display_monitors = []
365
-
366
- for session in sessions_in_window:
367
- key = f"{session['data'].get('hallId')}_{session['data'].get('showStartTime')}"
368
- unique_id = f"{biz_date}_{key}"
369
-
370
- latest = realtime_map.get(key)
371
- if not latest: continue
372
-
373
- movie = latest.get('movieName')
374
- hall_raw = latest.get('hallName')
375
- # 影厅名简化
376
- hall_short = simplify_hall_name(hall_raw)
377
-
378
- sold = int(latest.get('soldTicketNum') or 0)
379
- start = latest.get('showStartTime')
380
-
381
- if sold == 0:
382
- # 统计新加入的0票场次
383
- if unique_id not in self.zero_ticket_candidates:
384
- self.stats["zero_total"] += 1
385
-
386
- self.zero_ticket_candidates.add(unique_id)
387
- display_monitors.append(f"👁️ {start} {movie} (0票)")
388
- else:
389
- if unique_id in self.zero_ticket_candidates:
390
- if unique_id not in self.alerted_sessions:
391
- self.log(f"🚨 突发购票!{hall_short}《{movie}》")
392
-
393
- # 统计触发次数
394
- self.stats["alert_count"] += 1
395
-
396
- # 发送通知 (Pushover + WeChat)
397
- self.send_ticket_alert(hall_short, movie, start, sold)
398
-
399
- self.alerted_sessions.add(unique_id)
400
- self.zero_ticket_candidates.remove(unique_id)
401
- display_monitors.append(f"✅ {start} {movie} (新售出)")
402
- else:
403
- display_monitors.append(f"🛡️ {start} {movie} (原有票)")
404
-
405
- self.monitored_sessions = display_monitors
406
- time.sleep(60)
407
-
408
- else:
409
- if 0 < min_sleep_seconds < 86400:
410
- wakeup_dt = now + timedelta(seconds=min_sleep_seconds)
411
- self.status_text = "💤 休眠中"
412
- self.next_wakeup = wakeup_dt
413
- self.monitored_sessions = []
414
- self.log(f"休眠 {min_sleep_seconds / 60:.1f} 分钟,直到 {wakeup_dt.strftime('%H:%M')}")
415
- time.sleep(min_sleep_seconds)
416
- else:
417
- self.log("今日监控结束,长休眠")
418
- time.sleep(300)
419
-
420
- except Exception as e:
421
- self.log(f"主循环异常: {e}")
422
- time.sleep(60)
423
-
424
-
425
- # --- 6. Streamlit 前端 ---
426
-
427
- @st.cache_resource
428
- def get_monitor():
429
- return CinemaMonitor()
430
-
431
-
432
- def main():
433
- monitor = get_monitor()
434
-
435
- # 自动刷新:30秒
436
- if st_autorefresh:
437
- st_autorefresh(interval=30 * 1000, key="monitor_refresh")
438
-
439
- st.title("📢 突发购票监控")
440
-
441
- c1, c2, c3 = st.columns(3)
442
- with c1:
443
- st.metric("运行状态", monitor.status_text)
444
- with c2:
445
- wakeup_str = monitor.next_wakeup.strftime("%H:%M:%S") if monitor.next_wakeup else "--"
446
- st.metric("下次唤醒", wakeup_str)
447
-
448
- # --- 统计逻辑 ---
449
- remaining_zero_count = 0
450
- remaining_total_count = 0
451
-
452
- if monitor.daily_schedule_cache:
453
- now = get_beijing_now()
454
- biz_date = monitor.current_business_date or get_business_date()
455
-
456
- for item in monitor.daily_schedule_cache:
457
- start_str = item.get('showStartTime')
458
- if not start_str: continue
459
-
460
- # 解析时间
461
- show_dt = parse_show_datetime(biz_date, start_str)
462
-
463
- # 只统计“当前时间之后”的场次
464
- if show_dt and show_dt > now:
465
- remaining_total_count += 1
466
- if int(item.get('soldTicketNum') or 0) == 0:
467
- remaining_zero_count += 1
468
-
469
- with c3:
470
- st.metric("剩余 0 票场次 / 剩余总场次", f"{remaining_zero_count} / {remaining_total_count}")
471
-
472
- st.divider()
473
-
474
- col_logs, col_list = st.columns([3, 2])
475
-
476
- with col_logs:
477
- st.subheader("📜 运行日志")
478
- log_text = "\n".join(list(monitor.logs))
479
- st.text_area("Logs", log_text, height=500, disabled=True)
480
-
481
- with col_list:
482
- st.subheader("🎯 实时监控列表")
483
- if monitor.monitored_sessions:
484
- for s in monitor.monitored_sessions:
485
- if "✅" in s:
486
- st.success(s)
487
- elif "👁️" in s:
488
- st.error(s)
489
- else:
490
- st.info(s)
491
- else:
492
- st.caption("暂无监控目标")
493
-
494
-
495
- if __name__ == "__main__":
496
- main()