Ethscriptions commited on
Commit
f93c204
·
verified ·
1 Parent(s): 933f5da

Delete pages

Browse files
Files changed (1) hide show
  1. pages/monitor.py +0 -398
pages/monitor.py DELETED
@@ -1,398 +0,0 @@
1
- import streamlit as st
2
- import requests
3
- import time
4
- import json
5
- import os
6
- import threading
7
- from datetime import datetime, timedelta, timezone, time as dt_time
8
- from collections import deque
9
- from dotenv import load_dotenv # 引入 dotenv
10
-
11
- # --- 1. 配置与常量 ---
12
- load_dotenv() # 加载根目录下的 .env 文件
13
-
14
- # 从环境变量获取配置 (请确保你的 .env 文件里配置了这些)
15
- PUSHOVER_TOKEN = os.getenv("PUSHOVER_TOKEN", "")
16
- PUSHOVER_USER = os.getenv("PUSHOVER_USER", "")
17
- CINEMA_ID = os.getenv("CINEMA_ID", "44001291")
18
- # 登录信息也建议从环境变量取,这里为了逻辑独立暂时保留 APIManager 里的读取逻辑
19
-
20
- TOKEN_FILE = 'token_data.json' # 与主程序共用同一个 Token 文件
21
-
22
- # 页面配置
23
- st.set_page_config(page_title="影城防撤场监控系统 Pro", page_icon="🛡️", layout="wide")
24
-
25
- # --- 2. 核心时间处理 ---
26
- # ... (以下代码保持你提供的内容不变) ...
27
-
28
- def get_beijing_now():
29
- """获取当前的北京时间 (Naive)"""
30
- utc_now = datetime.now(timezone.utc)
31
- beijing_now = utc_now.astimezone(timezone(timedelta(hours=8)))
32
- return beijing_now.replace(tzinfo=None)
33
-
34
- def get_business_date():
35
- """获取当前的营业日日期"""
36
- now = get_beijing_now()
37
- if now.time() < dt_time(6, 0):
38
- return (now - timedelta(days=1)).strftime("%Y-%m-%d")
39
- return now.strftime("%Y-%m-%d")
40
-
41
- def parse_show_datetime(date_str, time_str):
42
- """解析排片时间"""
43
- try:
44
- show_t = datetime.strptime(time_str, "%H:%M").time()
45
- base_date = datetime.strptime(date_str, "%Y-%m-%d")
46
- if show_t < dt_time(6, 0):
47
- base_date += timedelta(days=1)
48
- return datetime.combine(base_date.date(), show_t)
49
- except:
50
- return None
51
-
52
- def send_pushover(title, message, priority=0):
53
- if not PUSHOVER_TOKEN or not PUSHOVER_USER:
54
- return # 如果没配置 Pushover,直接跳过
55
-
56
- url = "https://api.pushover.net/1/messages.json"
57
- data = {
58
- "token": PUSHOVER_TOKEN,
59
- "user": PUSHOVER_USER,
60
- "title": title,
61
- "message": message,
62
- "sound": "pushover",
63
- "priority": priority
64
- }
65
- try:
66
- requests.post(url, data=data, timeout=5)
67
- except:
68
- pass
69
-
70
- # --- 3. API 管理模块 ---
71
-
72
- class APIManager:
73
- def __init__(self, logger):
74
- self.last_login_fail_time = 0
75
- self.logger = logger
76
-
77
- def save_token(self, token_data):
78
- try:
79
- with open(TOKEN_FILE, 'w', encoding='utf-8') as f:
80
- json.dump(token_data, f, ensure_ascii=False, indent=4)
81
- except:
82
- pass
83
-
84
- def load_token(self):
85
- # 优先读取本地文件,这样能复用 app2.py 登录生成的 Token
86
- if os.path.exists(TOKEN_FILE):
87
- try:
88
- with open(TOKEN_FILE, 'r', encoding='utf-8') as f:
89
- return json.load(f).get('token')
90
- except:
91
- pass
92
- return None
93
-
94
- def login(self):
95
- if time.time() - self.last_login_fail_time < 600:
96
- self.logger("⏳ 登录冷却中,跳过重试。")
97
- return None
98
-
99
- self.logger("🔄 尝试后台自动登录...")
100
-
101
- # 使用环境变量获取账号密码
102
- username = os.getenv("CINEMA_USERNAME")
103
- password = os.getenv("CINEMA_PASSWORD")
104
- res_code = os.getenv("CINEMA_RES_CODE")
105
- device_id = os.getenv("CINEMA_DEVICE_ID")
106
-
107
- if not all([username, password, res_code]):
108
- self.logger("❌ 环境变量缺失,无法自动登录")
109
- return None
110
-
111
- session = requests.Session()
112
- session.headers.update({'User-Agent': 'Mozilla/5.0'})
113
- login_url = 'https://app.bi.piao51.cn/cinema-app/credential/login.action'
114
- login_data = {
115
- 'username': username, 'password': password, 'type': '1',
116
- 'resCode': res_code, 'deviceid': device_id, 'dtype': 'ios',
117
- }
118
-
119
- try:
120
- session.post(login_url, data=login_data, timeout=15)
121
- resp = session.get('https://app.bi.piao51.cn/cinema-app/security/logined.action', timeout=10)
122
- info = resp.json()
123
- if info.get("success") and info.get("data", {}).get("token"):
124
- self.save_token(info['data'])
125
- self.logger("✅ 登录成功。")
126
- return info['data']['token']
127
- else:
128
- raise Exception("未获取到Token")
129
- except Exception as e:
130
- self.last_login_fail_time = time.time()
131
- msg = f"登录失败: {e}"
132
- self.logger(f"❌ {msg}")
133
- send_pushover("监控异常", msg, priority=0)
134
- return None
135
-
136
- def fetch_schedule(self, date_str):
137
- token = self.load_token()
138
- if not token:
139
- token = self.login()
140
- if not token: return None
141
-
142
- url = 'https://cawapi.yinghezhong.com/showInfo/getHallShowInfo'
143
- params = {'showDate': date_str, 'token': token, '_': int(time.time() * 1000)}
144
- headers = {'Origin': 'https://caw.yinghezhong.com', 'User-Agent': 'Mozilla/5.0'}
145
-
146
- try:
147
- response = requests.get(url, params=params, headers=headers, timeout=15)
148
- data = response.json()
149
- if data.get('code') == 1:
150
- return data.get('data', [])
151
- elif data.get('code') == 500:
152
- self.logger("⚠️ Token失效,重试中...")
153
- token = self.login()
154
- if token:
155
- params['token'] = token
156
- response = requests.get(url, params=params, headers=headers, timeout=15)
157
- return response.json().get('data', [])
158
- return None
159
- except Exception as e:
160
- self.logger(f"API请求异常: {e}")
161
- return None
162
-
163
- # --- 4. 监控服务主逻辑 ---
164
- # ... (CinemaMonitor 类及后续代码完全保持不变,直接粘贴即可) ...
165
-
166
- class CinemaMonitor:
167
- def __init__(self):
168
- self.logs = deque(maxlen=50)
169
- self.status_text = "初始化中..."
170
- self.next_wakeup = None
171
- self.monitored_sessions = []
172
-
173
- self.api = APIManager(self.log)
174
- self.current_business_date = None
175
- self.daily_schedule_cache = None
176
-
177
- self.zero_ticket_candidates = set()
178
- self.alerted_sessions = set()
179
-
180
- self.last_daily_report_date = None
181
- self.simulation_mode = False
182
- self.simulation_data = []
183
-
184
- self.thread = threading.Thread(target=self._run_loop, daemon=True)
185
- self.thread.start()
186
-
187
- def log(self, msg):
188
- timestamp = get_beijing_now().strftime("%H:%M:%S")
189
- entry = f"[{timestamp}] {msg}"
190
- print(entry)
191
- self.logs.appendleft(entry)
192
-
193
- def _run_loop(self):
194
- self.log("🚀 监控服务已启动")
195
-
196
- while True:
197
- try:
198
- now = get_beijing_now()
199
- biz_date = get_business_date()
200
- today_str = now.strftime("%Y-%m-%d")
201
-
202
- # --- 0. 每日健康检查 (09:00) ---
203
- if now.time() >= dt_time(9, 0) and self.last_daily_report_date != today_str:
204
- msg = f"日期: {today_str}\n状态: 监控服务正常运行中\nToken: 有效"
205
- self.log(f"🔔 发送每日健康检查通知: {today_str}")
206
- send_pushover("✅ 影城脚本今日运行正常", msg, priority=0)
207
- self.last_daily_report_date = today_str
208
-
209
- # --- 1. 非营业时间休眠 (06:00 - 09:30) ---
210
- start_check_time = now.replace(hour=9, minute=30, second=0, microsecond=0)
211
- is_early_morning = (dt_time(6, 0) <= now.time() < dt_time(9, 30))
212
-
213
- if is_early_morning and not self.simulation_mode:
214
- self.status_text = "非监控时段 (等待 09:30)"
215
- self.next_wakeup = start_check_time
216
- self.zero_ticket_candidates.clear()
217
- self.alerted_sessions.clear()
218
-
219
- sleep_sec = (start_check_time - now).total_seconds()
220
- # 最多睡5分钟醒来一次
221
- self.log(f"💤 系统休眠中... 预计唤醒: {start_check_time.strftime('%H:%M')}")
222
- time.sleep(min(sleep_sec, 300))
223
- continue
224
-
225
- # --- 2. 缓存刷新 ---
226
- if self.daily_schedule_cache is None or self.current_business_date != biz_date:
227
- self.log(f"📅 获取 {biz_date} 全天排片...")
228
- schedule = self.api.fetch_schedule(biz_date)
229
- if schedule is None:
230
- self.log("❌ 初始化获取失败,1分钟后重试")
231
- time.sleep(60)
232
- continue
233
- self.daily_schedule_cache = schedule
234
- self.current_business_date = biz_date
235
- self.zero_ticket_candidates.clear()
236
- self.alerted_sessions.clear()
237
- self.log(f"✅ 数据已更新,共 {len(schedule)} 场")
238
-
239
- # --- 3. 筛选窗口期场次 ---
240
- active_check_needed = False
241
- min_sleep_seconds = 3600
242
- sessions_in_window = []
243
- current_schedule = self.daily_schedule_cache + self.simulation_data
244
-
245
- for item in current_schedule:
246
- start_time_str = item.get('showStartTime')
247
- if not start_time_str: continue
248
- show_dt = parse_show_datetime(biz_date, start_time_str)
249
- if not show_dt: continue
250
-
251
- if now >= show_dt: continue
252
-
253
- monitor_start_dt = show_dt - timedelta(minutes=11)
254
-
255
- if now >= monitor_start_dt:
256
- sessions_in_window.append({'data': item, 'dt': show_dt})
257
- active_check_needed = True
258
- else:
259
- seconds_until_window = (monitor_start_dt - now).total_seconds()
260
- if seconds_until_window < min_sleep_seconds:
261
- min_sleep_seconds = seconds_until_window
262
-
263
- # --- 4. 严格执行逻辑 ---
264
- if active_check_needed:
265
- self.status_text = "🔥 监控进行中 (高频轮询)"
266
- self.next_wakeup = now + timedelta(seconds=60)
267
-
268
- realtime_schedule = self.api.fetch_schedule(biz_date)
269
-
270
- if realtime_schedule is None and not self.simulation_mode:
271
- self.log("⚠️ 网络/API请求失败,跳过本次判定,防止误报!")
272
- time.sleep(60)
273
- continue
274
-
275
- if realtime_schedule is None: realtime_schedule = []
276
- if self.simulation_mode: realtime_schedule += self.simulation_data
277
-
278
- realtime_map = {f"{x.get('hallId')}_{x.get('showStartTime')}": x for x in realtime_schedule}
279
-
280
- display_monitors = []
281
-
282
- for session in sessions_in_window:
283
- unique_id = f"{biz_date}_{session['data'].get('hallId')}_{session['data'].get('showStartTime')}"
284
- key_for_map = f"{session['data'].get('hallId')}_{session['data'].get('showStartTime')}"
285
-
286
- latest = realtime_map.get(key_for_map)
287
- if not latest: continue
288
-
289
- movie_name = latest.get('movieName')
290
- hall_name = latest.get('hallName')
291
- sold = int(latest.get('soldTicketNum', 0))
292
- start_str = latest.get('showStartTime')
293
- mins_left = (session['dt'] - now).total_seconds() / 60
294
-
295
- if sold == 0:
296
- self.zero_ticket_candidates.add(unique_id)
297
- display_monitors.append(f"👁️ {start_str} {movie_name} (0票, 监控中)")
298
- else:
299
- if unique_id in self.zero_ticket_candidates:
300
- if unique_id not in self.alerted_sessions:
301
- self.log(f"🚨 突发购票检测!{hall_name}《{movie_name}》")
302
- send_pushover(
303
- f"禁止撤场:{hall_name}有票!",
304
- f"《{movie_name}》{start_str}\n倒计时 {mins_left:.1f}分\n⚠️ 从0票突增至{sold}张",
305
- priority=0
306
- )
307
- self.alerted_sessions.add(unique_id)
308
- self.zero_ticket_candidates.remove(unique_id)
309
-
310
- display_monitors.append(f"✅ {start_str} {movie_name} (新售出{sold}张, 已报警)")
311
- else:
312
- display_monitors.append(f"🛡️ {start_str} {movie_name} (原有{sold}张, 忽略)")
313
-
314
- self.monitored_sessions = display_monitors
315
- time.sleep(60)
316
-
317
- else:
318
- if 0 < min_sleep_seconds < 86400:
319
- wakeup_dt = now + timedelta(seconds=min_sleep_seconds)
320
- self.status_text = "💤 智能休眠中"
321
- self.next_wakeup = wakeup_dt
322
- self.monitored_sessions = []
323
- self.log(f"休眠 {min_sleep_seconds / 60:.1f} 分钟,直到 {wakeup_dt.strftime('%H:%M')}")
324
- time.sleep(min_sleep_seconds)
325
- else:
326
- self.log("今日无更多监控场次,休眠 5 分钟")
327
- time.sleep(300)
328
-
329
- except Exception as e:
330
- self.log(f"主循环异常: {e}")
331
- time.sleep(60)
332
-
333
-
334
- # --- 5. Streamlit 前端 ---
335
-
336
- @st.cache_resource
337
- def get_monitor():
338
- return CinemaMonitor()
339
-
340
-
341
- def main():
342
- monitor = get_monitor()
343
- st.title("📢 零票影前十分钟内突发购票通知")
344
-
345
- # 计算统计数据:剩余0票场次 / 总场次
346
- total_sessions = 0
347
- zero_remaining = 0
348
-
349
- if monitor.daily_schedule_cache:
350
- total_sessions = len(monitor.daily_schedule_cache)
351
- now = get_beijing_now()
352
- biz_date = monitor.current_business_date or get_business_date()
353
-
354
- for item in monitor.daily_schedule_cache:
355
- start_str = item.get('showStartTime')
356
- if not start_str: continue
357
-
358
- show_dt = parse_show_datetime(biz_date, start_str)
359
- if not show_dt: continue
360
-
361
- # 只统计还没开场的
362
- if show_dt > now:
363
- # 统计票数为0的 (基于缓存数据)
364
- if int(item.get('soldTicketNum', 0)) == 0:
365
- zero_remaining += 1
366
-
367
- c1, c2, c3 = st.columns(3)
368
- with c1:
369
- st.metric("当前状态", monitor.status_text)
370
- with c2:
371
- st.metric("下次唤醒", monitor.next_wakeup.strftime("%H:%M:%S") if monitor.next_wakeup else "--")
372
- with c3:
373
- st.metric("剩余零票场次 / 当日全部场次", f"{zero_remaining} / {total_sessions}")
374
-
375
- st.divider()
376
-
377
- col_logs, col_list = st.columns([3, 2])
378
- with col_logs:
379
- st.subheader("📜 运行日志")
380
- # 移除按钮
381
- st.text_area("Logs", "\n".join(list(monitor.logs)), height=450, disabled=True)
382
- with col_list:
383
- st.subheader("🎯 实时监控列表")
384
- st.markdown("> 说明:当日 0 票场次会被监控中,在 0 票电影开场前 10 分钟内突发购票会触发通知。")
385
- if monitor.monitored_sessions:
386
- for s in monitor.monitored_sessions:
387
- if "✅" in s:
388
- st.success(s)
389
- elif "👁️" in s:
390
- st.error(s)
391
- else:
392
- st.caption(s)
393
- else:
394
- st.info("当前休眠中,无临近场次")
395
-
396
-
397
- if __name__ == "__main__":
398
- main()