Ethscriptions commited on
Commit
c5d5b0d
·
verified ·
1 Parent(s): 6115401

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -182
app.py CHANGED
@@ -1,11 +1,10 @@
1
  import streamlit as st
2
- import pandas as pd
3
  import requests
4
  import time
5
  import json
6
  import os
7
  import threading
8
- from datetime import datetime, timedelta, time as dt_time
9
  from collections import deque
10
 
11
  # --- 1. 配置与常量 ---
@@ -17,46 +16,48 @@ CINEMA_ID = "44001291"
17
  # 页面配置
18
  st.set_page_config(page_title="影城防撤场监控系统 Pro", page_icon="🛡️", layout="wide")
19
 
 
20
 
21
- # --- 2. 辅助工具函数 ---
22
-
23
- def get_business_date():
24
  """
25
- 获取当前的营业日日期
26
- 规则:凌晨 06:00 之前属于前一天。
27
  """
28
- now = datetime.now()
 
 
 
 
 
 
29
  if now.time() < dt_time(6, 0):
30
  return (now - timedelta(days=1)).strftime("%Y-%m-%d")
31
  return now.strftime("%Y-%m-%d")
32
 
33
-
34
  def parse_show_datetime(date_str, time_str):
35
- """
36
- 将营业日期字符串 + 放映时间字符串 转换为 实际的时间对象。
37
- 处理跨天逻辑:如果营业日是 13号,但放映时间是 01:00,则实际时间是 14号 01:00。
38
- """
39
  try:
40
  show_t = datetime.strptime(time_str, "%H:%M").time()
41
  base_date = datetime.strptime(date_str, "%Y-%m-%d")
42
-
43
- # 如果放映时间小于 06:00,说明是次日凌晨场
44
  if show_t < dt_time(6, 0):
45
  base_date += timedelta(days=1)
46
-
47
  return datetime.combine(base_date.date(), show_t)
48
  except:
49
  return None
50
 
51
-
52
  def send_pushover(title, message, priority=0):
 
 
 
 
53
  url = "https://api.pushover.net/1/messages.json"
54
  data = {
55
  "token": PUSHOVER_TOKEN,
56
  "user": PUSHOVER_USER,
57
  "title": title,
58
  "message": message,
59
- "sound": "emergency" if priority == 1 else "pushover",
60
  "priority": priority
61
  }
62
  try:
@@ -64,8 +65,7 @@ def send_pushover(title, message, priority=0):
64
  except:
65
  pass
66
 
67
-
68
- # --- 3. API 模块 (带重试与熔断) ---
69
 
70
  class APIManager:
71
  def __init__(self, logger):
@@ -89,12 +89,11 @@ class APIManager:
89
  return None
90
 
91
  def login(self):
92
- # 检查冷却时间 (10分钟 = 600秒)
93
  if time.time() - self.last_login_fail_time < 600:
94
- self.logger("⏳ 登录处于冷却期 (上次失败于10分钟内),跳过重试。")
95
  return None
96
 
97
- self.logger("🔄 正在尝试登录获取新 Token...")
98
  session = requests.Session()
99
  session.headers.update({'User-Agent': 'Mozilla/5.0'})
100
  login_url = 'https://app.bi.piao51.cn/cinema-app/credential/login.action'
@@ -102,323 +101,235 @@ class APIManager:
102
  'username': 'chenhy', 'password': '123456', 'type': '1',
103
  'resCode': '44001291', 'deviceid': '1517bfd3f6e7fef1333', 'dtype': 'ios',
104
  }
105
-
106
  try:
107
  session.post(login_url, data=login_data, timeout=15)
108
  resp = session.get('https://app.bi.piao51.cn/cinema-app/security/logined.action', timeout=10)
109
  info = resp.json()
110
  if info.get("success") and info.get("data", {}).get("token"):
111
  self.save_token(info['data'])
112
- self.logger("✅ 登录成功,Token 已更新。")
113
  return info['data']['token']
114
  else:
115
- raise Exception("API返回登录状态")
116
  except Exception as e:
117
  self.last_login_fail_time = time.time()
118
- err_msg = f"登录失败,10分钟后重试。错误: {e}"
119
- self.logger(f"❌ {err_msg}")
120
- send_pushover("影城监控异常", err_msg, priority=1)
 
121
  return None
122
 
123
  def fetch_schedule(self, date_str):
124
  token = self.load_token()
125
  if not token:
126
  token = self.login()
127
- if not token: return None
128
 
129
  url = 'https://cawapi.yinghezhong.com/showInfo/getHallShowInfo'
130
  params = {'showDate': date_str, 'token': token, '_': int(time.time() * 1000)}
131
  headers = {'Origin': 'https://caw.yinghezhong.com', 'User-Agent': 'Mozilla/5.0'}
132
-
133
  try:
134
  response = requests.get(url, params=params, headers=headers, timeout=15)
135
  data = response.json()
136
  if data.get('code') == 1:
137
  return data.get('data', [])
138
- elif data.get('code') == 500: # Token 失效
139
- self.logger("⚠️ Token失效,尝试新登录...")
140
  token = self.login()
141
  if token:
142
- # 重试一次
143
  params['token'] = token
144
  response = requests.get(url, params=params, headers=headers, timeout=15)
145
  return response.json().get('data', [])
146
  return []
147
  except Exception as e:
148
- self.logger(f"📡 API 请求异常: {e}")
149
  return None
150
 
151
-
152
- # --- 4. 核心监控服务 (智能状态机) ---
153
 
154
  class CinemaMonitor:
155
  def __init__(self):
156
  self.logs = deque(maxlen=50)
157
  self.status_text = "初始化中..."
158
  self.next_wakeup = None
159
- self.monitored_sessions = [] # 前端展示用
160
  self.alert_history = set()
161
  self.api = APIManager(self.log)
162
-
163
  self.current_business_date = None
164
- self.daily_schedule_cache = None # 缓存当天的排片结构
165
-
166
  self.simulation_mode = False
167
  self.simulation_data = []
168
-
169
  self.thread = threading.Thread(target=self._run_loop, daemon=True)
170
  self.thread.start()
171
 
172
  def log(self, msg):
173
- timestamp = datetime.now().strftime("%H:%M:%S")
174
  entry = f"[{timestamp}] {msg}"
175
  print(entry)
176
  self.logs.appendleft(entry)
177
 
178
  def _run_loop(self):
179
- self.log("🚀 监控服务已启动")
180
-
181
  while True:
182
  try:
183
- now = datetime.now()
184
  biz_date = get_business_date()
185
 
186
- # --- 1. 每日 9:30 初始化或跨天重置 ---
187
- # 如果缓存日期不对,或者还没有缓存,并且时间到了 9:30 之后
188
  start_check_time = now.replace(hour=9, minute=30, second=0, microsecond=0)
189
-
190
- # 如果是凌晨 01:00,biz_date是前一天,我们也应该允许运行,只要是当天任务
191
- # 简化逻辑:只要当前时间 >= 今天的 9:30,或者是次日凌晨(属于biz_date的尾巴)都可以运行
192
- # 只有在 06:00 - 09:30 之间我们需要强制等待
193
-
194
- is_early_morning = (dt_time(6, 0) <= now.time() < dt_time(9, 30))
195
  if is_early_morning and not self.simulation_mode:
196
  self.status_text = "非监控时段 (等待 09:30)"
197
  self.next_wakeup = start_check_time
198
  sleep_sec = (start_check_time - now).total_seconds()
199
- self.log(f"💤 未到 09:30,系统休眠中... 预计唤醒: {start_check_time.strftime('%H:%M')}")
200
- time.sleep(min(sleep_sec, 300)) # 最多睡5分钟醒来看一眼
201
  continue
202
 
203
- # --- 2. 获取/刷新 当日排片结构 ---
204
- # 如果这是新的一天,或者是第一次运行,获取全天数据
205
  if self.daily_schedule_cache is None or self.current_business_date != biz_date:
206
- self.log(f"📅 获取 {biz_date} 全天排片数据...")
207
  schedule = self.api.fetch_schedule(biz_date)
208
-
209
  if schedule is None:
210
- self.log("❌ 获取排片失败,1分钟后重试")
211
  time.sleep(60)
212
  continue
213
-
214
  self.daily_schedule_cache = schedule
215
  self.current_business_date = biz_date
216
- self.log(f"✅ 排片数据已获取,共 {len(schedule)} 场")
217
-
218
- # --- 3. 智能决策:下一步做什么? ---
219
- # 我们需要找出当前所有 "值得监控" 的场次
220
- # 值得监控 = (开场时间 > 现在) 且 (在这个时刻,它是 0 票 OR 我们即将进入它的 11分钟窗口)
221
 
222
- active_check_needed = False # 是否需要立即进行高频轮询
223
- min_sleep_seconds = 3600 # 默认睡1小时
224
-
225
- upcoming_sessions = [] # 待处理的场次
226
-
227
- # 注入模拟数据
228
  current_schedule = self.daily_schedule_cache + self.simulation_data
229
 
230
  for item in current_schedule:
231
- # 基础数据解析
232
  start_time_str = item.get('showStartTime')
233
  if not start_time_str: continue
234
-
235
  show_dt = parse_show_datetime(biz_date, start_time_str)
236
  if not show_dt: continue
237
 
238
- # 已经开场了的,跳过
239
  if now >= show_dt: continue
240
 
241
- # 11分钟警戒线时刻
242
  monitor_start_dt = show_dt - timedelta(minutes=11)
243
-
244
- # 判断:我们现在在哪里?
245
  if now >= monitor_start_dt:
246
- # A. 我们已经处于 [T-11, T-0] 的窗口内
247
- # 这就是我们需要高频轮询的目标!
248
- upcoming_sessions.append({
249
- 'data': item,
250
- 'dt': show_dt,
251
- 'status': 'ACTIVE'
252
- })
253
  active_check_needed = True
254
  else:
255
- # B. 还没到时间
256
- # 计算还有多久进入 T-11
257
  seconds_until_window = (monitor_start_dt - now).total_seconds()
258
  if seconds_until_window < min_sleep_seconds:
259
  min_sleep_seconds = seconds_until_window
260
 
261
- # --- 4. 执行动作 ---
262
-
263
  if active_check_needed:
264
- # === 进入高频轮询模式 ===
265
  self.status_text = "🔥 监控进行中 (高频轮询)"
266
  self.next_wakeup = now + timedelta(seconds=60)
267
-
268
- # 这里必须重新请求 API,因为我们要看实时的票数变化
269
- # 注意:为了节省流量,我们其实只需要关心 upcoming_sessions 里的
270
- # 但 API 只能拉全量,所以拉下来后我们只看我们在乎的
271
  realtime_schedule = self.api.fetch_schedule(biz_date)
272
  if self.simulation_mode: realtime_schedule += self.simulation_data
273
-
274
- # 转换为字典方便查找
275
- # Key: hallId_showStartTime (防止同名电影不同厅)
276
  realtime_map = {f"{x.get('hallId')}_{x.get('showStartTime')}": x for x in (realtime_schedule or [])}
277
 
278
  display_monitors = []
279
-
280
  for session in upcoming_sessions:
281
- # 找到最新的实时数据
282
  key = f"{session['data'].get('hallId')}_{session['data'].get('showStartTime')}"
283
- latest_data = realtime_map.get(key, session['data']) # 找不到就用缓存的(不太可能)
284
-
285
- movie_name = latest_data.get('movieName')
286
- hall_name = latest_data.get('hallName')
287
- sold = int(latest_data.get('soldTicketNum', 0))
288
- start_str = latest_data.get('showStartTime')
289
-
290
- # 倒计时
291
  mins_left = (session['dt'] - now).total_seconds() / 60
292
-
293
- # 核心业务规则:
294
- # 1. 如果有人买票 (sold > 0) -> 报警 -> 并不再轮询此场次 (视为安全)
295
- # 2. 如果无人买票 (sold == 0) -> 继续轮询直到开场
296
-
297
  if sold > 0:
298
- # 检查是否已报警
299
  unique_id = f"{biz_date}_{key}"
300
  if unique_id not in self.alert_history:
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已售 {sold}张",
305
- priority=1
306
  )
307
  self.alert_history.add(unique_id)
308
-
309
- # 既然有人买了,就不需要再监控这个场次了,标记为安全
310
- display_monitors.append(f"✅ {start_str} {movie_name} (已售{sold}, 停止监控)")
311
-
312
  else:
313
- # 0 票,危险,继续盯着
314
  display_monitors.append(f"👁️ {start_str} {movie_name} (0票, 倒计时{mins_left:.1f}分)")
315
-
316
  self.monitored_sessions = display_monitors
317
-
318
- # 高频模式下,睡 60 秒
319
  time.sleep(60)
320
-
321
  else:
322
- # === 进入长休眠模式 ===
323
- # 没有任何场次处于 T-11 窗口内
324
- if min_sleep_seconds > 0 and min_sleep_seconds < 86400: # 合理范围
325
  wakeup_dt = now + timedelta(seconds=min_sleep_seconds)
326
  self.status_text = "💤 智能休眠中"
327
  self.next_wakeup = wakeup_dt
328
  self.monitored_sessions = []
329
- self.log(
330
- f"当前无临近场次,休眠 {min_sleep_seconds / 60:.1f} 分钟,直到 {wakeup_dt.strftime('%H:%M')}")
331
  time.sleep(min_sleep_seconds)
332
  else:
333
- # 可能今天没排片了,或者出错
334
  self.log("今日无更多待监控场次,休眠 5 分钟")
335
  time.sleep(300)
336
 
337
  except Exception as e:
338
- self.log(f"循环异常: {e}")
339
  time.sleep(60)
340
 
341
  def trigger_simulation(self):
342
  self.simulation_mode = True
343
- # 构造一个 5分钟后开场的数据
344
- fake_time = (datetime.now() + timedelta(minutes=5)).strftime("%H:%M")
345
  self.simulation_data = [{
346
  'movieName': '测试影片-模拟购票',
347
  'hallName': '测试厅',
348
  'hallId': 'SIM_999',
349
  'showStartTime': fake_time,
350
- 'soldTicketNum': '0' # 初始0票,下一轮 API 获取时如果是实战会自动更新,这里我们模拟逻辑需要配合
351
  }]
352
- # 为了模拟“中途有人买票”,我们在 API 获取不到数据时,让逻辑层认为它变成了1票
353
- # 这里简化处理:直接让下一轮检测认为它有1票
354
  self.log(f"已注入模拟场次: {fake_time} 开场")
355
- # 这里有个小 trick: 因为 fetch_schedule 不会返回 simulation_data,
356
- # 所以我们在 run_loop 里手动拼接了 simulation_data。
357
- # 为了模拟有人买票,我们在几秒后修改 simulation_data 的票数
358
- threading.Timer(5.0, self._update_sim_ticket).start()
359
 
360
- def _update_sim_ticket(self):
361
- if self.simulation_data:
362
- self.simulation_data[0]['soldTicketNum'] = '1'
363
- self.log("⚡ [测试] 模拟观众刚刚购买了一张票!")
364
-
365
-
366
- # --- 5. Streamlit 前端 ---
367
 
368
  @st.cache_resource
369
  def get_monitor():
370
  return CinemaMonitor()
371
 
372
-
373
  def main():
374
  monitor = get_monitor()
375
-
376
- # --- 顶部状态卡片 ---
377
  st.title("📽️ 影城排程防撤场监控终端 Pro")
378
-
379
- # 状态指示器
380
- status_color = "green" if "监控" in monitor.status_text else "blue"
381
- if "非监控" in monitor.status_text: status_color = "gray"
382
-
383
  c1, c2, c3 = st.columns(3)
384
- with c1:
385
- st.metric("当前状态", monitor.status_text)
386
- with c2:
387
- wakeup_str = monitor.next_wakeup.strftime("%H:%M:%S") if monitor.next_wakeup else "--"
388
- st.metric("下次唤醒/轮询", wakeup_str)
389
- with c3:
390
- st.metric("营业日期", monitor.current_business_date if monitor.current_business_date else "获取中")
391
-
392
  st.divider()
393
 
394
- # --- 主要内容区 ---
395
  col_logs, col_list = st.columns([3, 2])
396
-
397
  with col_logs:
398
  st.subheader("📜 系统运行日志")
399
  if st.button("刷新日志"): pass
400
- log_txt = "\n".join(list(monitor.logs))
401
- st.text_area("Logs", log_txt, height=450, disabled=True)
402
-
403
  with col_list:
404
  st.subheader("🎯 实时监控列表")
405
- st.caption("仅显示处于开场前 11 分钟窗口内的场次")
406
-
407
  if monitor.monitored_sessions:
408
  for s in monitor.monitored_sessions:
409
- if "停止监控" in s:
410
- st.success(s) # 绿色,有人买票了,安全
411
- else:
412
- st.warning(s) # 黄色,0票,危险
413
  else:
414
- st.info("当前无正在高频监控的场次 (休眠中)")
415
-
416
  st.write("---")
417
- st.write("🛠️ 调试工具")
418
- if st.button("模拟 '撤场前有人买票' 场景"):
419
  monitor.trigger_simulation()
420
- st.toast("已生成模拟数据:5分钟后开场的电影将在5秒后模拟出票。请观察日志")
421
-
422
 
423
  if __name__ == "__main__":
424
  main()
 
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
 
10
  # --- 1. 配置与常量 ---
 
16
  # 页面配置
17
  st.set_page_config(page_title="影城防撤场监控系统 Pro", page_icon="🛡️", layout="wide")
18
 
19
+ # --- 2. 核心时间处理 (北京时间强制转换) ---
20
 
21
+ def get_beijing_now():
 
 
22
  """
23
+ 获取当前的北京时间,并移除时区信息(返回 Naive Datetime)
 
24
  """
25
+ utc_now = datetime.now(timezone.utc)
26
+ beijing_now = utc_now.astimezone(timezone(timedelta(hours=8)))
27
+ return beijing_now.replace(tzinfo=None)
28
+
29
+ def get_business_date():
30
+ """获取当前的营业日日期 (凌晨06:00前算前一天)"""
31
+ now = get_beijing_now()
32
  if now.time() < dt_time(6, 0):
33
  return (now - timedelta(days=1)).strftime("%Y-%m-%d")
34
  return now.strftime("%Y-%m-%d")
35
 
 
36
  def parse_show_datetime(date_str, time_str):
37
+ """解析排片时间为北京时间对象"""
 
 
 
38
  try:
39
  show_t = datetime.strptime(time_str, "%H:%M").time()
40
  base_date = datetime.strptime(date_str, "%Y-%m-%d")
41
+
 
42
  if show_t < dt_time(6, 0):
43
  base_date += timedelta(days=1)
44
+
45
  return datetime.combine(base_date.date(), show_t)
46
  except:
47
  return None
48
 
 
49
  def send_pushover(title, message, priority=0):
50
+ """
51
+ 发送 Pushover 通知
52
+ priority: 0 = 普通 (默认), 1 = 高优先级
53
+ """
54
  url = "https://api.pushover.net/1/messages.json"
55
  data = {
56
  "token": PUSHOVER_TOKEN,
57
  "user": PUSHOVER_USER,
58
  "title": title,
59
  "message": message,
60
+ "sound": "pushover", # 统一使用默认提示音
61
  "priority": priority
62
  }
63
  try:
 
65
  except:
66
  pass
67
 
68
+ # --- 3. API 模块 ---
 
69
 
70
  class APIManager:
71
  def __init__(self, logger):
 
89
  return None
90
 
91
  def login(self):
 
92
  if time.time() - self.last_login_fail_time < 600:
93
+ self.logger("⏳ 登录冷却,跳过重试。")
94
  return None
95
 
96
+ self.logger("🔄 尝试后台自动登录...")
97
  session = requests.Session()
98
  session.headers.update({'User-Agent': 'Mozilla/5.0'})
99
  login_url = 'https://app.bi.piao51.cn/cinema-app/credential/login.action'
 
101
  'username': 'chenhy', 'password': '123456', 'type': '1',
102
  'resCode': '44001291', 'deviceid': '1517bfd3f6e7fef1333', 'dtype': 'ios',
103
  }
104
+
105
  try:
106
  session.post(login_url, data=login_data, timeout=15)
107
  resp = session.get('https://app.bi.piao51.cn/cinema-app/security/logined.action', timeout=10)
108
  info = resp.json()
109
  if info.get("success") and info.get("data", {}).get("token"):
110
  self.save_token(info['data'])
111
+ self.logger("✅ 登录成功。")
112
  return info['data']['token']
113
  else:
114
+ raise Exception("未获取到Token")
115
  except Exception as e:
116
  self.last_login_fail_time = time.time()
117
+ msg = f"登录失败: {e}"
118
+ self.logger(f"❌ {msg}")
119
+ # 修改点:登录失败改为 0 级通知
120
+ send_pushover("监控异常", msg, priority=0)
121
  return None
122
 
123
  def fetch_schedule(self, date_str):
124
  token = self.load_token()
125
  if not token:
126
  token = self.login()
127
+ if not token: return None
128
 
129
  url = 'https://cawapi.yinghezhong.com/showInfo/getHallShowInfo'
130
  params = {'showDate': date_str, 'token': token, '_': int(time.time() * 1000)}
131
  headers = {'Origin': 'https://caw.yinghezhong.com', 'User-Agent': 'Mozilla/5.0'}
132
+
133
  try:
134
  response = requests.get(url, params=params, headers=headers, timeout=15)
135
  data = response.json()
136
  if data.get('code') == 1:
137
  return data.get('data', [])
138
+ elif data.get('code') == 500:
139
+ self.logger("⚠️ Token失效,重试中...")
140
  token = self.login()
141
  if token:
 
142
  params['token'] = token
143
  response = requests.get(url, params=params, headers=headers, timeout=15)
144
  return response.json().get('data', [])
145
  return []
146
  except Exception as e:
147
+ self.logger(f"API请求异常: {e}")
148
  return None
149
 
150
+ # --- 4. 监控服务 ---
 
151
 
152
  class CinemaMonitor:
153
  def __init__(self):
154
  self.logs = deque(maxlen=50)
155
  self.status_text = "初始化中..."
156
  self.next_wakeup = None
157
+ self.monitored_sessions = []
158
  self.alert_history = set()
159
  self.api = APIManager(self.log)
 
160
  self.current_business_date = None
161
+ self.daily_schedule_cache = None
 
162
  self.simulation_mode = False
163
  self.simulation_data = []
164
+
165
  self.thread = threading.Thread(target=self._run_loop, daemon=True)
166
  self.thread.start()
167
 
168
  def log(self, msg):
169
+ timestamp = get_beijing_now().strftime("%H:%M:%S")
170
  entry = f"[{timestamp}] {msg}"
171
  print(entry)
172
  self.logs.appendleft(entry)
173
 
174
  def _run_loop(self):
175
+ self.log("🚀 监控服务已启动 (BJ时间/普通通知)")
176
+
177
  while True:
178
  try:
179
+ now = get_beijing_now()
180
  biz_date = get_business_date()
181
 
182
+ # --- 06:00 - 09:30 强制休眠 ---
 
183
  start_check_time = now.replace(hour=9, minute=30, second=0, microsecond=0)
184
+ is_early_morning = (dt_time(6,0) <= now.time() < dt_time(9,30))
185
+
 
 
 
 
186
  if is_early_morning and not self.simulation_mode:
187
  self.status_text = "非监控时段 (等待 09:30)"
188
  self.next_wakeup = start_check_time
189
  sleep_sec = (start_check_time - now).total_seconds()
190
+ self.log(f"💤 系统休眠中... 预计唤醒: {start_check_time.strftime('%H:%M')}")
191
+ time.sleep(min(sleep_sec, 300))
192
  continue
193
 
194
+ # --- 获取排片 ---
 
195
  if self.daily_schedule_cache is None or self.current_business_date != biz_date:
196
+ self.log(f"📅 获取 {biz_date} 全天排片...")
197
  schedule = self.api.fetch_schedule(biz_date)
 
198
  if schedule is None:
199
+ self.log("❌ 获取失败,1分钟后重试")
200
  time.sleep(60)
201
  continue
 
202
  self.daily_schedule_cache = schedule
203
  self.current_business_date = biz_date
204
+ self.log(f"✅ 数据已更新,共 {len(schedule)} 场")
 
 
 
 
205
 
206
+ # --- 筛选监控目标 ---
207
+ active_check_needed = False
208
+ min_sleep_seconds = 3600
209
+ upcoming_sessions = []
 
 
210
  current_schedule = self.daily_schedule_cache + self.simulation_data
211
 
212
  for item in current_schedule:
 
213
  start_time_str = item.get('showStartTime')
214
  if not start_time_str: continue
 
215
  show_dt = parse_show_datetime(biz_date, start_time_str)
216
  if not show_dt: continue
217
 
 
218
  if now >= show_dt: continue
219
 
 
220
  monitor_start_dt = show_dt - timedelta(minutes=11)
221
+
 
222
  if now >= monitor_start_dt:
223
+ upcoming_sessions.append({'data': item, 'dt': show_dt})
 
 
 
 
 
 
224
  active_check_needed = True
225
  else:
 
 
226
  seconds_until_window = (monitor_start_dt - now).total_seconds()
227
  if seconds_until_window < min_sleep_seconds:
228
  min_sleep_seconds = seconds_until_window
229
 
230
+ # --- 执行监控 ---
 
231
  if active_check_needed:
 
232
  self.status_text = "🔥 监控进行中 (高频轮询)"
233
  self.next_wakeup = now + timedelta(seconds=60)
234
+
 
 
 
235
  realtime_schedule = self.api.fetch_schedule(biz_date)
236
  if self.simulation_mode: realtime_schedule += self.simulation_data
 
 
 
237
  realtime_map = {f"{x.get('hallId')}_{x.get('showStartTime')}": x for x in (realtime_schedule or [])}
238
 
239
  display_monitors = []
 
240
  for session in upcoming_sessions:
 
241
  key = f"{session['data'].get('hallId')}_{session['data'].get('showStartTime')}"
242
+ latest = realtime_map.get(key, session['data'])
243
+
244
+ movie_name = latest.get('movieName')
245
+ hall_name = latest.get('hallName')
246
+ sold = int(latest.get('soldTicketNum', 0))
247
+ start_str = latest.get('showStartTime')
 
 
248
  mins_left = (session['dt'] - now).total_seconds() / 60
249
+
 
 
 
 
250
  if sold > 0:
 
251
  unique_id = f"{biz_date}_{key}"
252
  if unique_id not in self.alert_history:
253
  self.log(f"🚨 发现购票!{hall_name}《{movie_name}》")
254
+ # 修改点:发现有票,发送通知,优先级改为 0
255
  send_pushover(
256
+ f"禁止撤场:{hall_name}有票!",
257
+ f"《{movie_name}》{start_str}\n倒计时 {mins_left:.1f}分\n已售 {sold}张",
258
+ priority=0
259
  )
260
  self.alert_history.add(unique_id)
261
+ display_monitors.append(f"✅ {start_str} {movie_name} (已售{sold}, 安全)")
 
 
 
262
  else:
 
263
  display_monitors.append(f"👁️ {start_str} {movie_name} (0票, 倒计时{mins_left:.1f}分)")
264
+
265
  self.monitored_sessions = display_monitors
 
 
266
  time.sleep(60)
 
267
  else:
268
+ if 0 < min_sleep_seconds < 86400:
 
 
269
  wakeup_dt = now + timedelta(seconds=min_sleep_seconds)
270
  self.status_text = "💤 智能休眠中"
271
  self.next_wakeup = wakeup_dt
272
  self.monitored_sessions = []
273
+ self.log(f"休眠 {min_sleep_seconds/60:.1f} 分钟,直到 {wakeup_dt.strftime('%H:%M')}")
 
274
  time.sleep(min_sleep_seconds)
275
  else:
 
276
  self.log("今日无更多待监控场次,休眠 5 分钟")
277
  time.sleep(300)
278
 
279
  except Exception as e:
280
+ self.log(f"循环异常: {e}")
281
  time.sleep(60)
282
 
283
  def trigger_simulation(self):
284
  self.simulation_mode = True
285
+ fake_time = (get_beijing_now() + timedelta(minutes=5)).strftime("%H:%M")
 
286
  self.simulation_data = [{
287
  'movieName': '测试影片-模拟购票',
288
  'hallName': '测试厅',
289
  'hallId': 'SIM_999',
290
  'showStartTime': fake_time,
291
+ 'soldTicketNum': '0'
292
  }]
 
 
293
  self.log(f"已注入模拟场次: {fake_time} 开场")
294
+ threading.Timer(5.0, lambda: self.simulation_data[0].update({'soldTicketNum': '1'}) or self.log("⚡ [测试] 模拟出票成功!")).start()
 
 
 
295
 
296
+ # --- 5. 前端显示 ---
 
 
 
 
 
 
297
 
298
  @st.cache_resource
299
  def get_monitor():
300
  return CinemaMonitor()
301
 
 
302
  def main():
303
  monitor = get_monitor()
 
 
304
  st.title("📽️ 影城排程防撤场监控终端 Pro")
305
+
306
+ utc_now = datetime.now(timezone.utc).strftime("%H:%M:%S")
307
+ bj_now = get_beijing_now().strftime("%H:%M:%S")
308
+
 
309
  c1, c2, c3 = st.columns(3)
310
+ with c1: st.metric("当前状态", monitor.status_text)
311
+ with c2: st.metric("下次唤醒 (北京时间)", monitor.next_wakeup.strftime("%H:%M:%S") if monitor.next_wakeup else "--")
312
+ with c3: st.metric("系统时间 (UTC / BJ)", f"{utc_now} / {bj_now}")
313
+
 
 
 
 
314
  st.divider()
315
 
 
316
  col_logs, col_list = st.columns([3, 2])
 
317
  with col_logs:
318
  st.subheader("📜 系统运行日志")
319
  if st.button("刷新日志"): pass
320
+ st.text_area("Logs", "\n".join(list(monitor.logs)), height=450, disabled=True)
 
 
321
  with col_list:
322
  st.subheader("🎯 实时监控列表")
 
 
323
  if monitor.monitored_sessions:
324
  for s in monitor.monitored_sessions:
325
+ if "安全" in s: st.success(s)
326
+ else: st.warning(s)
 
 
327
  else:
328
+ st.info("当前休眠中")
 
329
  st.write("---")
330
+ if st.button("模拟 '撤场前有人买票'"):
 
331
  monitor.trigger_simulation()
332
+ st.toast("测试触发,请观察日志")
 
333
 
334
  if __name__ == "__main__":
335
  main()