Ethscriptions commited on
Commit
e39b6bf
·
verified ·
1 Parent(s): de3cd70

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +264 -29
app.py CHANGED
@@ -33,6 +33,10 @@ load_dotenv() # 加载本地 .env 文件
33
 
34
  # --- 全局配置和常量 ---
35
  TOKEN_FILE = 'token_data.json'
 
 
 
 
36
  # --- 环境变量获取 (替代硬编码) ---
37
  # 使用 os.getenv 获取,如果获取不到默认为空字符串或特定默认值
38
  GAODE_API_KEY = os.getenv("GAODE_API_KEY", "")
@@ -821,12 +825,24 @@ def process_api_data(schedule_list, hall_seat_map, token=None, show_date=None):
821
  df.rename(columns={'movieName': '影片名称', 'showStartTime': '放映时间', 'soldBoxOffice': '总收入',
822
  'soldTicketNum': '总人次', 'hallName': '影厅名称'}, inplace=True)
823
 
824
- # 获取标准电影名列表并进行清洗
825
  canonical_names = []
826
  if token and show_date:
827
  canonical_names = fetch_canonical_movie_names(token, show_date)
828
 
829
- df['影片名称'] = df['影片名称'].apply(lambda x: clean_movie_title(x, canonical_names))
 
 
 
 
 
 
 
 
 
 
 
 
830
 
831
  # 在 required_cols 中增加 '影厅名称'
832
  required_cols = ['影片名称', '放映时间', '座位数', '总收入', '总人次', '影厅名称']
@@ -1006,6 +1022,75 @@ def clean_movie_title(raw_title, canonical_names=None):
1006
  return base_name
1007
 
1008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
  def get_available_movie_name_pool(show_date):
1010
  token_data = load_token()
1011
  token = token_data.get('token') if token_data else None
@@ -1031,7 +1116,11 @@ def get_available_movie_name_pool(show_date):
1031
 
1032
  canonical_names = fetch_canonical_movie_names(token, show_date) if token else []
1033
  cleaned_movie_names = {
1034
- clean_movie_title(item.get('movieName'), canonical_names if canonical_names else None)
 
 
 
 
1035
  for item in available_movies
1036
  if item.get('movieName')
1037
  }
@@ -1469,7 +1558,7 @@ def render_sales_summary_section(df, selected_date, key_prefix):
1469
  return
1470
 
1471
  st.markdown("#### 销售总览 (套餐 Top 10 + 单品 Top 5)")
1472
- st.dataframe(final_summary, use_container_width=True, hide_index=True)
1473
  st.markdown("##### 复制到 Excel")
1474
  st.code(copy_text, language='text')
1475
 
@@ -1775,7 +1864,18 @@ def build_daily_report_from_source_df(source_df, selected_date_str=None, token=N
1775
  if token and display_date_str:
1776
  canonical_names = fetch_canonical_movie_names(token, display_date_str)
1777
 
1778
- df['影片'] = df['影片名称'].apply(lambda x: clean_movie_title(x, canonical_names))
 
 
 
 
 
 
 
 
 
 
 
1779
  df['影厅'] = df['影厅名称']
1780
  df['人数合计'] = df['总人次']
1781
 
@@ -1804,6 +1904,9 @@ def process_and_filter_data_for_report(schedule_list, hall_seat_map, selected_da
1804
  'soldTicketNum': '总人次'
1805
  }, inplace=True)
1806
  df['放映日期'] = selected_date_str
 
 
 
1807
 
1808
  result_df, _ = build_daily_report_from_source_df(df, selected_date_str=selected_date_str, token=token)
1809
  return result_df
@@ -2678,6 +2781,7 @@ def is_time_in_schedule_check_exempt_ranges(ts, ranges):
2678
 
2679
  def load_schedule_check_settings():
2680
  """从 JSON 文件加载场次合理性检查设置"""
 
2681
  if os.path.exists(SCHEDULE_CHECK_SETTINGS_FILE):
2682
  try:
2683
  with open(SCHEDULE_CHECK_SETTINGS_FILE, 'r', encoding='utf-8') as f:
@@ -2768,6 +2872,7 @@ def save_schedule_check_settings(config=None, show_toast=True):
2768
  try:
2769
  with open(SCHEDULE_CHECK_SETTINGS_FILE, 'w', encoding='utf-8') as f:
2770
  json.dump(settings_to_save, f, ensure_ascii=False, indent=4)
 
2771
  if show_toast:
2772
  st.toast("场次合理性检查设置已保存!", icon="💾")
2773
  return True
@@ -2834,7 +2939,7 @@ def render_schedule_check_settings():
2834
  rule1_enabled = st.checkbox("规则一:同影片最小开场间隔", key="schedule_check_rule1_enabled")
2835
  with col2:
2836
  st.number_input(
2837
- "规则一间隔(分钟)", min_value=0, step=5,
2838
  key="schedule_check_rule1_min_interval_minutes",
2839
  disabled=not rule1_enabled
2840
  )
@@ -2844,13 +2949,13 @@ def render_schedule_check_settings():
2844
  rule2_enabled = st.checkbox("规则二:X分钟内影片开场超过Y场", key="schedule_check_rule2_enabled")
2845
  with col2:
2846
  st.number_input(
2847
- "规则二阈值(场)", min_value=1, step=1,
2848
  key="schedule_check_rule2_threshold_sessions",
2849
  disabled=not rule2_enabled
2850
  )
2851
  with col3:
2852
  st.number_input(
2853
- "规则二时间窗口(分钟)", min_value=5, step=5,
2854
  key="schedule_check_rule2_window_minutes",
2855
  disabled=not rule2_enabled
2856
  )
@@ -2867,7 +2972,7 @@ def render_schedule_check_settings():
2867
  rule3_enabled = st.checkbox("规则三:场次开场断档", key="schedule_check_rule3_enabled")
2868
  with col2:
2869
  st.number_input(
2870
- "规则三断档阈值(分钟)", min_value=0, step=5,
2871
  key="schedule_check_rule3_gap_threshold_minutes",
2872
  disabled=not rule3_enabled
2873
  )
@@ -2896,7 +3001,7 @@ def render_schedule_check_settings():
2896
  rule6_enabled = st.checkbox("规则六:影厅场次转换时间检查", key="schedule_check_rule6_enabled")
2897
  with col2:
2898
  st.number_input(
2899
- "场次转换基准(分钟)", min_value=0, step=1,
2900
  key="schedule_check_rule6_min_conversion_minutes",
2901
  disabled=not rule6_enabled
2902
  )
@@ -2918,7 +3023,7 @@ def render_schedule_check_settings():
2918
  col1, col2 = st.columns([1, 2])
2919
  with col1:
2920
  st.number_input(
2921
- "规则九热门TopN", min_value=1, step=1,
2922
  key="schedule_check_rule9_hot_top_n",
2923
  disabled=not rule9_enabled
2924
  )
@@ -2942,7 +3047,7 @@ def render_schedule_check_settings():
2942
  rule12_enabled = st.checkbox("规则十二:次日票房前N影片必须有黄金场", key="schedule_check_rule12_enabled")
2943
  with col2:
2944
  st.number_input(
2945
- "规则十二票房TopN", min_value=1, step=1,
2946
  key="schedule_check_rule12_top_n",
2947
  disabled=not rule12_enabled
2948
  )
@@ -3065,7 +3170,17 @@ def generate_schedule_check_logs(schedule_list, date_str, today_analysis_df=None
3065
 
3066
  df_original['simpleHallName'] = df_original['Hall'].apply(simplify_hall)
3067
  df_check = df_original.sort_values(by='startTime').reset_index(drop=True)
3068
- df_check['clean_filmName'] = df_check['filmName'].apply(clean_movie_title)
 
 
 
 
 
 
 
 
 
 
3069
 
3070
  next_day_bo_data = fetch_realtime_box_office(date_str)
3071
  movie_box_office = {}
@@ -3855,7 +3970,7 @@ def build_api_gantt_rows(schedule_list, date_str, token, hall_seat_map=None):
3855
  if movie_length <= 0:
3856
  movie_length = int((end_dt - start_dt).total_seconds() / 60)
3857
 
3858
- film_name = clean_movie_title(raw_movie_name, canonical_names if canonical_names else None)
3859
  film_language = item.get('movieLanguage', '') or ''
3860
  imagery = item.get('movieMediaType', '') or ''
3861
  movie_num = item.get('movieNum', '') or ''
@@ -4094,7 +4209,103 @@ def render_nextday_pool_history_table(base_date_str):
4094
 
4095
  st.dataframe(
4096
  summary_df.style.format({'总票房': '{:,.2f}'}),
4097
- use_container_width=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4098
  hide_index=True,
4099
  )
4100
 
@@ -4142,7 +4353,18 @@ def display_analysis_results(df_raw, data_source_name, date_for_display, query_t
4142
  date_str = f"{date_for_display} " if date_for_display else ""
4143
  total_revenue, total_attendance, total_sessions = df_raw['总收入'].sum(), df_raw['总人次'].sum(), len(df_raw)
4144
  st.markdown(
4145
- f"> {date_str}数据总览:总票房 **¥{total_revenue:,.2f}** | 总人次 **{total_attendance:,.0f}** | 总场次 **{total_sessions:,.0f}**")
 
 
 
 
 
 
 
 
 
 
 
4146
 
4147
  format_config = {'座位数': '{:,.0f}', '场次': '{:,.0f}', '人次': '{:,.0f}', '票房': '{:,.2f}', '均价': '{:.2f}',
4148
  '座次比': '{:.2%}', '场次比': '{:.2%}', '票房比': '{:.2%}', '座次效率': '{:.2f}',
@@ -4169,10 +4391,10 @@ def display_analysis_results(df_raw, data_source_name, date_for_display, query_t
4169
 
4170
  st.markdown("#### 全天排片效率分析");
4171
  st.dataframe(full_day_analysis.style.format(format_config).apply(style_efficiency, axis=1),
4172
- use_container_width=True, hide_index=True)
4173
  st.markdown("#### 黄金时段排片效率分析 (14:00-21:00)");
4174
  st.dataframe(prime_time_analysis.style.format(format_config).apply(style_efficiency, axis=1),
4175
- use_container_width=True, hide_index=True)
4176
  if not full_day_analysis.empty:
4177
  st.markdown("### 排片效率汇总")
4178
  full_day_summary = full_day_analysis.rename(
@@ -4189,10 +4411,10 @@ def display_analysis_results(df_raw, data_source_name, date_for_display, query_t
4189
  '全部座次效率': '{:.2f}', '全部场次效率': '{:.2f}', '黄金时段座次效率': '{:.2f}',
4190
  '黄金时段场次效率': '{:.2f}'}
4191
  st.dataframe(summary_df.style.format(summary_format_config).apply(style_summary_efficiency, axis=1),
4192
- use_container_width=True, hide_index=True)
4193
 
4194
 
4195
- st.markdown("## 📈 全国大盘实时票房查询")
4196
 
4197
  # 增加手续费复选框,默认不包含
4198
  include_fee = st.checkbox("包含手续费", value=False)
@@ -4244,7 +4466,7 @@ def display_analysis_results(df_raw, data_source_name, date_for_display, query_t
4244
  '总场次': '{:,.0f}',
4245
  '总人次': '{:,.0f}'
4246
  }
4247
- st.dataframe(today_df.style.format(format_dict), use_container_width=True, hide_index=True)
4248
  else:
4249
  st.info(f"未能获取到 {today_str} 的全国大盘数据。")
4250
 
@@ -4282,11 +4504,13 @@ def display_analysis_results(df_raw, data_source_name, date_for_display, query_t
4282
  '总场次': '{:,.0f}',
4283
  '总人次': '{:,.0f}'
4284
  }
4285
- st.dataframe(tomorrow_df.style.format(format_dict), use_container_width=True, hide_index=True)
4286
  else:
4287
  st.info(f"未能获取到 {tomorrow_str} 的全国大盘预售数据。")
4288
 
4289
- st.markdown(f"## 🎬 {today_str} 排片甘特图")
 
 
4290
  render_today_api_gantt_chart(today_str)
4291
 
4292
 
@@ -4313,7 +4537,18 @@ def fetch_and_process_daily_sessions(date_str, quiet=False):
4313
  if token:
4314
  canonical_names = fetch_canonical_movie_names(token, date_str)
4315
 
4316
- df['影片名称_清理'] = df['movieName'].apply(lambda x: clean_movie_title(x, canonical_names))
 
 
 
 
 
 
 
 
 
 
 
4317
 
4318
  sessions_map = df.groupby('影片名称_清理后').size().to_dict()
4319
  # 返回 (映射表, 总场次, 原始排片列表)
@@ -4634,7 +4869,7 @@ def main():
4634
  '座次效率': '{:.2f}', '场次效率': '{:.2f}', '次日场数': '{:,.0f}'}
4635
  display_df['均价'] = pd.to_numeric(display_df['均价'], errors='coerce').replace([np.inf, -np.inf],
4636
  np.nan)
4637
- st.dataframe(display_df.style.format(report_format, na_rep="#DIV/0!"), use_container_width=True,
4638
  hide_index=True)
4639
 
4640
  n_diff = st.session_state.today_movie_count - st.session_state.previous_day_movie_count
@@ -4824,7 +5059,7 @@ def main():
4824
  st.session_state.daily_report_df.style.format({
4825
  '人数合计': '{:,.0f}', '座位数': '{:,.0f}', '上座率%': '{:.2f}%'
4826
  }),
4827
- use_container_width=True, hide_index=True
4828
  )
4829
  total_attendance = pd.to_numeric(
4830
  st.session_state.daily_report_df.get('人数合计', 0),
@@ -5020,7 +5255,7 @@ def main():
5020
  with tab_views[0]:
5021
  st.markdown(display_pdf(led_output['pdf']), unsafe_allow_html=True)
5022
  if 'png' in led_output:
5023
- with tab_views[1]: st.image(led_output['png'], use_container_width=True)
5024
  else:
5025
  st.error("未能成功生成 '修改 LED 屏排片表'。请检查数据源。")
5026
 
@@ -5087,7 +5322,7 @@ def main():
5087
  '时长(分钟)': format_play_time(item['play_time']),
5088
  '文件名': format_content_name_with_explanation(item['content_name'])}
5089
  for item in movie_list_sorted]
5090
- st.dataframe(pd.DataFrame(view2_data), hide_index=True, use_container_width=True)
5091
  st.markdown("#### 按影厅查看影片内容")
5092
  hall_tabs = st.tabs(list(halls_data.keys()))
5093
  for tab, hall_name in zip(hall_tabs, halls_data.keys()):
@@ -5097,7 +5332,7 @@ def main():
5097
  '时长(分钟)': format_play_time(item['details']['play_time']),
5098
  '文件名': format_content_name_with_explanation(item['content_name'])} for item in
5099
  halls_data[hall_name]]
5100
- st.dataframe(pd.DataFrame(view1_data), hide_index=True, use_container_width=True)
5101
  except Exception as e:
5102
  st.error(f"查询TMS服务器时出错: {e}")
5103
 
 
33
 
34
  # --- 全局配置和常量 ---
35
  TOKEN_FILE = 'token_data.json'
36
+ ROOT_DIR = Path(__file__).resolve().parent
37
+ CINEMA_CACHE_DIR = ROOT_DIR / "cinema_cache"
38
+ NATIONAL_MARKET_MOVIE_FILE = CINEMA_CACHE_DIR / "national_market_movie_daily.csv"
39
+ NATIONAL_MARKET_DAILY_FILE = CINEMA_CACHE_DIR / "national_market_daily_summary.csv"
40
  # --- 环境变量获取 (替代硬编码) ---
41
  # 使用 os.getenv 获取,如果获取不到默认为空字符串或特定默认值
42
  GAODE_API_KEY = os.getenv("GAODE_API_KEY", "")
 
825
  df.rename(columns={'movieName': '影片名称', 'showStartTime': '放映时间', 'soldBoxOffice': '总收入',
826
  'soldTicketNum': '总人次', 'hallName': '影厅名称'}, inplace=True)
827
 
828
+ # 获取标准电影名列表(用于 movieNum 映射未命中时的回退)
829
  canonical_names = []
830
  if token and show_date:
831
  canonical_names = fetch_canonical_movie_names(token, show_date)
832
 
833
+ # 使用 movieNum 映射优先解析影片名(不含制式后缀,仅正式片名)
834
+ movie_num_col = df.get('movieNum')
835
+ if movie_num_col is not None:
836
+ df['影片名称'] = df.apply(
837
+ lambda row: resolve_movie_name_from_schedule_item(
838
+ row['影片名称'],
839
+ movie_num=row.get('movieNum'),
840
+ canonical_names=canonical_names
841
+ ),
842
+ axis=1
843
+ )
844
+ else:
845
+ df['影片名称'] = df['影片名称'].apply(lambda x: clean_movie_title(x, canonical_names))
846
 
847
  # 在 required_cols 中增加 '影厅名称'
848
  required_cols = ['影片名称', '放映时间', '座位数', '总收入', '总人次', '影厅名称']
 
1022
  return base_name
1023
 
1024
 
1025
+ # --- movieNum 影片名映射解析(与 history_sessions_monitor.py 策略一致) ---
1026
+ MOVIE_NUM_NAME_MAP_FILE = CINEMA_CACHE_DIR / "movie_num_name_map.json"
1027
+
1028
+
1029
+ def _normalize_movie_num_key(movie_num):
1030
+ """将 movieNum 标准化为纯大写字母数字"""
1031
+ return re.sub(r'[^A-Z0-9]', '', str(movie_num or '').strip().upper())
1032
+
1033
+
1034
+ @st.cache_data(show_spinner=False, ttl=120)
1035
+ def _load_movie_num_name_map():
1036
+ """加载 movie_num_name_map.json 并返回 {normalized_key: official_name} 字典"""
1037
+ if not MOVIE_NUM_NAME_MAP_FILE.exists():
1038
+ return {}
1039
+ try:
1040
+ with open(MOVIE_NUM_NAME_MAP_FILE, 'r', encoding='utf-8') as f:
1041
+ payload = json.load(f)
1042
+ except Exception:
1043
+ return {}
1044
+ movie_num_map = payload.get('movie_num_map', {})
1045
+ if not isinstance(movie_num_map, dict):
1046
+ return {}
1047
+ result = {}
1048
+ for movie_num, entry in movie_num_map.items():
1049
+ normalized_key = _normalize_movie_num_key(movie_num)
1050
+ if not normalized_key:
1051
+ continue
1052
+ if isinstance(entry, str):
1053
+ official_name = entry.strip()
1054
+ elif isinstance(entry, dict):
1055
+ official_name = str(entry.get('official_name') or '').strip()
1056
+ else:
1057
+ continue
1058
+ if official_name:
1059
+ result[normalized_key] = official_name
1060
+ return result
1061
+
1062
+
1063
+ def resolve_movie_name_from_schedule_item(raw_movie_name, movie_num=None, canonical_names=None):
1064
+ """
1065
+ 使用 movieNum 映射表解析影片正式名称(不含制式后缀,仅用于效率分析展示)。
1066
+ 优先级:movieNum 映射 > canonical_names 匹配 > 空格分割回退。
1067
+ """
1068
+ if not isinstance(raw_movie_name, str):
1069
+ return raw_movie_name
1070
+
1071
+ # 1. 尝试 movieNum 映射
1072
+ if movie_num:
1073
+ num_key = _normalize_movie_num_key(movie_num)
1074
+ if num_key:
1075
+ name_map = _load_movie_num_name_map()
1076
+ official_name = name_map.get(num_key)
1077
+ if official_name:
1078
+ return official_name
1079
+
1080
+ # 2. 回退到旧的 clean_movie_title 逻辑(不含制式后缀)
1081
+ base_name = None
1082
+ if canonical_names:
1083
+ sorted_names = sorted(canonical_names, key=len, reverse=True)
1084
+ for name in sorted_names:
1085
+ if name in raw_movie_name:
1086
+ base_name = name
1087
+ break
1088
+ if not base_name:
1089
+ base_name = raw_movie_name.split(' ', 1)[0]
1090
+
1091
+ return base_name
1092
+
1093
+
1094
  def get_available_movie_name_pool(show_date):
1095
  token_data = load_token()
1096
  token = token_data.get('token') if token_data else None
 
1116
 
1117
  canonical_names = fetch_canonical_movie_names(token, show_date) if token else []
1118
  cleaned_movie_names = {
1119
+ resolve_movie_name_from_schedule_item(
1120
+ item.get('movieName'),
1121
+ movie_num=item.get('movieNum'),
1122
+ canonical_names=canonical_names if canonical_names else None
1123
+ )
1124
  for item in available_movies
1125
  if item.get('movieName')
1126
  }
 
1558
  return
1559
 
1560
  st.markdown("#### 销售总览 (套餐 Top 10 + 单品 Top 5)")
1561
+ st.dataframe(final_summary, width="stretch", hide_index=True)
1562
  st.markdown("##### 复制到 Excel")
1563
  st.code(copy_text, language='text')
1564
 
 
1864
  if token and display_date_str:
1865
  canonical_names = fetch_canonical_movie_names(token, display_date_str)
1866
 
1867
+ # 使用 movieNum 映射优先解析影片名(不含制式后缀)
1868
+ if 'movieNum' in df.columns:
1869
+ df['影片'] = df.apply(
1870
+ lambda row: resolve_movie_name_from_schedule_item(
1871
+ row['影片名称'],
1872
+ movie_num=row.get('movieNum'),
1873
+ canonical_names=canonical_names
1874
+ ),
1875
+ axis=1
1876
+ )
1877
+ else:
1878
+ df['影片'] = df['影片名称'].apply(lambda x: clean_movie_title(x, canonical_names))
1879
  df['影厅'] = df['影厅名称']
1880
  df['人数合计'] = df['总人次']
1881
 
 
1904
  'soldTicketNum': '总人次'
1905
  }, inplace=True)
1906
  df['放映日期'] = selected_date_str
1907
+ # 保留 movieNum 列供 build_daily_report_from_source_df 使用
1908
+ if 'movieNum' not in df.columns:
1909
+ df['movieNum'] = ''
1910
 
1911
  result_df, _ = build_daily_report_from_source_df(df, selected_date_str=selected_date_str, token=token)
1912
  return result_df
 
2781
 
2782
  def load_schedule_check_settings():
2783
  """从 JSON 文件加载场次合理性检查设置"""
2784
+ ensure_settings_file_synced(SCHEDULE_CHECK_SETTINGS_FILE)
2785
  if os.path.exists(SCHEDULE_CHECK_SETTINGS_FILE):
2786
  try:
2787
  with open(SCHEDULE_CHECK_SETTINGS_FILE, 'r', encoding='utf-8') as f:
 
2872
  try:
2873
  with open(SCHEDULE_CHECK_SETTINGS_FILE, 'w', encoding='utf-8') as f:
2874
  json.dump(settings_to_save, f, ensure_ascii=False, indent=4)
2875
+ upload_settings_file_to_r2(SCHEDULE_CHECK_SETTINGS_FILE)
2876
  if show_toast:
2877
  st.toast("场次合理性检查设置已保存!", icon="💾")
2878
  return True
 
2939
  rule1_enabled = st.checkbox("规则一:同影片最小开场间隔", key="schedule_check_rule1_enabled")
2940
  with col2:
2941
  st.number_input(
2942
+ "规则一间隔(分钟)", min_value=30, step=5,
2943
  key="schedule_check_rule1_min_interval_minutes",
2944
  disabled=not rule1_enabled
2945
  )
 
2949
  rule2_enabled = st.checkbox("规则二:X分钟内影片开场超过Y场", key="schedule_check_rule2_enabled")
2950
  with col2:
2951
  st.number_input(
2952
+ "规则二阈值(场)", min_value=4, step=1,
2953
  key="schedule_check_rule2_threshold_sessions",
2954
  disabled=not rule2_enabled
2955
  )
2956
  with col3:
2957
  st.number_input(
2958
+ "规则二时间窗口(分钟)", min_value=20, step=5,
2959
  key="schedule_check_rule2_window_minutes",
2960
  disabled=not rule2_enabled
2961
  )
 
2972
  rule3_enabled = st.checkbox("规则三:场次开场断档", key="schedule_check_rule3_enabled")
2973
  with col2:
2974
  st.number_input(
2975
+ "规则三断档阈值(分钟)", min_value=30, step=5,
2976
  key="schedule_check_rule3_gap_threshold_minutes",
2977
  disabled=not rule3_enabled
2978
  )
 
3001
  rule6_enabled = st.checkbox("规则六:影厅场次转换时间检查", key="schedule_check_rule6_enabled")
3002
  with col2:
3003
  st.number_input(
3004
+ "场次转换基准(分钟)", min_value=10, step=1,
3005
  key="schedule_check_rule6_min_conversion_minutes",
3006
  disabled=not rule6_enabled
3007
  )
 
3023
  col1, col2 = st.columns([1, 2])
3024
  with col1:
3025
  st.number_input(
3026
+ "规则九热门TopN", min_value=3, step=1,
3027
  key="schedule_check_rule9_hot_top_n",
3028
  disabled=not rule9_enabled
3029
  )
 
3047
  rule12_enabled = st.checkbox("规则十二:次日票房前N影片必须有黄金场", key="schedule_check_rule12_enabled")
3048
  with col2:
3049
  st.number_input(
3050
+ "规则十二票房TopN", min_value=5, step=1,
3051
  key="schedule_check_rule12_top_n",
3052
  disabled=not rule12_enabled
3053
  )
 
3170
 
3171
  df_original['simpleHallName'] = df_original['Hall'].apply(simplify_hall)
3172
  df_check = df_original.sort_values(by='startTime').reset_index(drop=True)
3173
+ # 使用 movieNum 映射优先解析影片名(不含制式后缀)
3174
+ if 'movieNum' in df_check.columns:
3175
+ df_check['clean_filmName'] = df_check.apply(
3176
+ lambda row: resolve_movie_name_from_schedule_item(
3177
+ row['filmName'],
3178
+ movie_num=row.get('movieNum'),
3179
+ ),
3180
+ axis=1
3181
+ )
3182
+ else:
3183
+ df_check['clean_filmName'] = df_check['filmName'].apply(clean_movie_title)
3184
 
3185
  next_day_bo_data = fetch_realtime_box_office(date_str)
3186
  movie_box_office = {}
 
3970
  if movie_length <= 0:
3971
  movie_length = int((end_dt - start_dt).total_seconds() / 60)
3972
 
3973
+ film_name = resolve_movie_name_from_schedule_item(raw_movie_name, movie_num=item.get('movieNum'), canonical_names=canonical_names if canonical_names else None)
3974
  film_language = item.get('movieLanguage', '') or ''
3975
  imagery = item.get('movieMediaType', '') or ''
3976
  movie_num = item.get('movieNum', '') or ''
 
4209
 
4210
  st.dataframe(
4211
  summary_df.style.format({'总票房': '{:,.2f}'}),
4212
+ width="stretch",
4213
+ hide_index=True,
4214
+ )
4215
+
4216
+
4217
+
4218
+ def load_nextday_pool_cache_for_app():
4219
+ """加载次日电影池统计缓存 CSV 和 JSON 元数据"""
4220
+ nextday_pool_csv = CINEMA_CACHE_DIR / "history_sessions_monitor_nextday_pool_stats.csv"
4221
+ nextday_pool_json = CINEMA_CACHE_DIR / "history_sessions_monitor_nextday_pool_stats.json"
4222
+ if not nextday_pool_csv.exists():
4223
+ return pd.DataFrame(), {}
4224
+ try:
4225
+ df = pd.read_csv(nextday_pool_csv)
4226
+ except Exception:
4227
+ return pd.DataFrame(), {}
4228
+ meta = {}
4229
+ if nextday_pool_json.exists():
4230
+ try:
4231
+ with open(nextday_pool_json, 'r', encoding='utf-8') as f:
4232
+ meta = json.load(f)
4233
+ except Exception:
4234
+ pass
4235
+ return df, meta
4236
+
4237
+
4238
+ def render_nextday_pool_cache_table():
4239
+ """渲染次日电影池统计缓存表"""
4240
+ pool_df, meta = load_nextday_pool_cache_for_app()
4241
+ if pool_df.empty:
4242
+ return
4243
+
4244
+ nextday_date = meta.get('nextday_date', '')
4245
+ reference_date = meta.get('reference_date', '')
4246
+ generated_at = meta.get('generated_at', '')
4247
+ status = meta.get('status', '')
4248
+
4249
+ title_parts = ["### 📊 次日电影池统计数据"]
4250
+ if nextday_date:
4251
+ title_parts[0] += f"(目标日期:{nextday_date})"
4252
+ st.markdown(title_parts[0])
4253
+
4254
+ caption_parts = []
4255
+ if reference_date:
4256
+ caption_parts.append(f"统计截止:{reference_date}")
4257
+ if generated_at:
4258
+ caption_parts.append(f"生成时间:{generated_at}")
4259
+ if status:
4260
+ caption_parts.append(status)
4261
+ if caption_parts:
4262
+ st.caption(" | ".join(caption_parts))
4263
+
4264
+ # 定义次日电影池的列顺序
4265
+ nextday_pool_windows = [
4266
+ ("最近一天", 1),
4267
+ ("最近三天", 3),
4268
+ ("最近一周", 7),
4269
+ ("最近一个月", 30),
4270
+ ]
4271
+ display_columns = ["影片", "历史票房", "历史场次", "历史效率"]
4272
+ for label, _days in nextday_pool_windows:
4273
+ display_columns.extend([f"{label}票房", f"{label}场次", f"{label}效率"])
4274
+
4275
+ available_columns = [col for col in display_columns if col in pool_df.columns]
4276
+ remaining_columns = [col for col in pool_df.columns if col not in available_columns]
4277
+ display_df = pool_df[available_columns + remaining_columns].copy()
4278
+
4279
+ def _format_nextday_metric(value, pattern):
4280
+ if value is None:
4281
+ return 'n/a'
4282
+ if isinstance(value, str):
4283
+ if value.strip().lower() in {'', 'n/a', 'na', 'nan', '<na>'}:
4284
+ return 'n/a'
4285
+ try:
4286
+ if pd.isna(value):
4287
+ return 'n/a'
4288
+ except Exception:
4289
+ pass
4290
+ try:
4291
+ return pattern.format(float(value))
4292
+ except Exception:
4293
+ return str(value)
4294
+
4295
+ format_config = {
4296
+ '历史票房': lambda v: _format_nextday_metric(v, '{:,.2f}'),
4297
+ '历史场次': lambda v: _format_nextday_metric(v, '{:,.0f}'),
4298
+ '历史效率': lambda v: _format_nextday_metric(v, '{:.2f}'),
4299
+ }
4300
+ for label, _days in nextday_pool_windows:
4301
+ format_config[f'{label}票房'] = lambda v: _format_nextday_metric(v, '{:,.2f}')
4302
+ format_config[f'{label}场次'] = lambda v: _format_nextday_metric(v, '{:,.0f}')
4303
+ format_config[f'{label}效率'] = lambda v: _format_nextday_metric(v, '{:.2f}')
4304
+
4305
+ format_config = {k: v for k, v in format_config.items() if k in display_df.columns}
4306
+ st.dataframe(
4307
+ display_df.style.format(format_config),
4308
+ width="stretch",
4309
  hide_index=True,
4310
  )
4311
 
 
4353
  date_str = f"{date_for_display} " if date_for_display else ""
4354
  total_revenue, total_attendance, total_sessions = df_raw['总收入'].sum(), df_raw['总人次'].sum(), len(df_raw)
4355
  st.markdown(
4356
+ f"""
4357
+ <div style="margin: 12px 0 18px; padding: 14px 18px; border-left: 6px solid #D83B01; background: #FFF4ED;">
4358
+ <span style="font-size: 18px; font-weight: 700; color: #5C1F00;">{date_str}总票房:</span>
4359
+ <span style="font-size: 26px; font-weight: 800; color: #D83B01;">¥{total_revenue:,.2f}</span>
4360
+ <span style="font-size: 18px; font-weight: 700; color: #5C1F00;"> | 总人次:</span>
4361
+ <span style="font-size: 26px; font-weight: 800; color: #D83B01;">{total_attendance:,.0f}</span>
4362
+ <span style="font-size: 18px; font-weight: 700; color: #5C1F00;"> | 总场次:</span>
4363
+ <span style="font-size: 26px; font-weight: 800; color: #D83B01;">{total_sessions:,.0f}</span>
4364
+ </div>
4365
+ """,
4366
+ unsafe_allow_html=True,
4367
+ )
4368
 
4369
  format_config = {'座位数': '{:,.0f}', '场次': '{:,.0f}', '人次': '{:,.0f}', '票房': '{:,.2f}', '均价': '{:.2f}',
4370
  '座次比': '{:.2%}', '场次比': '{:.2%}', '票房比': '{:.2%}', '座次效率': '{:.2f}',
 
4391
 
4392
  st.markdown("#### 全天排片效率分析");
4393
  st.dataframe(full_day_analysis.style.format(format_config).apply(style_efficiency, axis=1),
4394
+ width="stretch", hide_index=True)
4395
  st.markdown("#### 黄金时段排片效率分析 (14:00-21:00)");
4396
  st.dataframe(prime_time_analysis.style.format(format_config).apply(style_efficiency, axis=1),
4397
+ width="stretch", hide_index=True)
4398
  if not full_day_analysis.empty:
4399
  st.markdown("### 排片效率汇总")
4400
  full_day_summary = full_day_analysis.rename(
 
4411
  '全部座次效率': '{:.2f}', '全部场次效率': '{:.2f}', '黄金时段座次效率': '{:.2f}',
4412
  '黄金时段场次效率': '{:.2f}'}
4413
  st.dataframe(summary_df.style.format(summary_format_config).apply(style_summary_efficiency, axis=1),
4414
+ width="stretch", hide_index=True)
4415
 
4416
 
4417
+ st.markdown("### 📈 全国大盘实时票房查询")
4418
 
4419
  # 增加手续费复选框,默认不包含
4420
  include_fee = st.checkbox("包含手续费", value=False)
 
4466
  '总场次': '{:,.0f}',
4467
  '总人次': '{:,.0f}'
4468
  }
4469
+ st.dataframe(today_df.style.format(format_dict), width="stretch", hide_index=True)
4470
  else:
4471
  st.info(f"未能获取到 {today_str} 的全国大盘数据。")
4472
 
 
4504
  '总场次': '{:,.0f}',
4505
  '总人次': '{:,.0f}'
4506
  }
4507
+ st.dataframe(tomorrow_df.style.format(format_dict), width="stretch", hide_index=True)
4508
  else:
4509
  st.info(f"未能获取到 {tomorrow_str} 的全国大盘预售数据。")
4510
 
4511
+ render_nextday_pool_cache_table()
4512
+
4513
+ st.markdown(f"### 🎬 {today_str} 排片甘特图")
4514
  render_today_api_gantt_chart(today_str)
4515
 
4516
 
 
4537
  if token:
4538
  canonical_names = fetch_canonical_movie_names(token, date_str)
4539
 
4540
+ # 使用 movieNum 映射优先解析影片名(不含制式缀)
4541
+ if 'movieNum' in df.columns:
4542
+ df['影片名称_清理后'] = df.apply(
4543
+ lambda row: resolve_movie_name_from_schedule_item(
4544
+ row.get('movieName', ''),
4545
+ movie_num=row.get('movieNum'),
4546
+ canonical_names=canonical_names
4547
+ ),
4548
+ axis=1
4549
+ )
4550
+ else:
4551
+ df['影片名称_清理后'] = df['movieName'].apply(lambda x: clean_movie_title(x, canonical_names))
4552
 
4553
  sessions_map = df.groupby('影片名称_清理后').size().to_dict()
4554
  # 返回 (映射表, 总场次, 原始排片列表)
 
4869
  '座次效率': '{:.2f}', '场次效率': '{:.2f}', '次日场数': '{:,.0f}'}
4870
  display_df['均价'] = pd.to_numeric(display_df['均价'], errors='coerce').replace([np.inf, -np.inf],
4871
  np.nan)
4872
+ st.dataframe(display_df.style.format(report_format, na_rep="#DIV/0!"), width="stretch",
4873
  hide_index=True)
4874
 
4875
  n_diff = st.session_state.today_movie_count - st.session_state.previous_day_movie_count
 
5059
  st.session_state.daily_report_df.style.format({
5060
  '人数合计': '{:,.0f}', '座位数': '{:,.0f}', '上座率%': '{:.2f}%'
5061
  }),
5062
+ width="stretch", hide_index=True
5063
  )
5064
  total_attendance = pd.to_numeric(
5065
  st.session_state.daily_report_df.get('人数合计', 0),
 
5255
  with tab_views[0]:
5256
  st.markdown(display_pdf(led_output['pdf']), unsafe_allow_html=True)
5257
  if 'png' in led_output:
5258
+ with tab_views[1]: st.image(led_output['png'], width="stretch")
5259
  else:
5260
  st.error("未能成功生成 '修改 LED 屏排片表'。请检查数据源。")
5261
 
 
5322
  '时长(分钟)': format_play_time(item['play_time']),
5323
  '文件名': format_content_name_with_explanation(item['content_name'])}
5324
  for item in movie_list_sorted]
5325
+ st.dataframe(pd.DataFrame(view2_data), hide_index=True, width="stretch")
5326
  st.markdown("#### 按影厅查看影片内容")
5327
  hall_tabs = st.tabs(list(halls_data.keys()))
5328
  for tab, hall_name in zip(hall_tabs, halls_data.keys()):
 
5332
  '时长(分钟)': format_play_time(item['details']['play_time']),
5333
  '文件名': format_content_name_with_explanation(item['content_name'])} for item in
5334
  halls_data[hall_name]]
5335
+ st.dataframe(pd.DataFrame(view1_data), hide_index=True, width="stretch")
5336
  except Exception as e:
5337
  st.error(f"查询TMS服务器时出错: {e}")
5338