All checks were successful
CD Pipeline / deploy (push) Successful in 1m12s
- 新增通用促銷活動爬蟲函式 run_promo_event_task() - 更新 crawler_config_loader.py 新增三個活動配置 - 更新 run_scheduler.py 動態註冊促銷活動爬蟲 - 新增 API 端點 /api/run_promo_event_task - 新增三個前端儀表板路由(/edm/mothers_day, /edm/valentine_520, /edm/labor_day) - 更新所有儀表板頁籤列表 - 新增配置檔案 services/data/crawler_config.json - 新增使用文件 docs/guides/promo_event_crawler_guide.md - 更新 agent_actions.py 允許重試列表
542 lines
22 KiB
Python
542 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
EDM 與節慶促銷路由模組
|
|
包含:限時搶購儀表板、節慶活動儀表板
|
|
"""
|
|
|
|
import hashlib
|
|
from datetime import datetime, timezone, timedelta
|
|
from flask import Blueprint, request, render_template, url_for
|
|
from auth import login_required
|
|
from sqlalchemy import func, desc
|
|
|
|
from config import BASE_DIR, public_url, DATABASE_TYPE
|
|
from database.manager import DatabaseManager
|
|
from database.models import Product
|
|
from database.edm_models import PromoProduct
|
|
from services.logger_manager import SystemLogger
|
|
|
|
# 時區設定
|
|
TAIPEI_TZ = timezone(timedelta(hours=8))
|
|
|
|
# Logger
|
|
sys_log = SystemLogger("EDMRoutes").get_logger()
|
|
|
|
# Blueprint 定義
|
|
edm_bp = Blueprint('edm', __name__)
|
|
|
|
|
|
# ==========================================
|
|
# 輔助函數
|
|
# ==========================================
|
|
|
|
def slugify(text):
|
|
"""將文字轉換為 URL 友善格式"""
|
|
if not text:
|
|
return ""
|
|
return str(text).replace(' ', '_').replace(':', '').replace('!', '').replace('?', '').replace('/', '').replace('&', '').replace('(', '').replace(')', '').replace('+', '_').replace('.', '_').replace('%', '').replace("'", "")
|
|
|
|
|
|
def get_color_for_string(s):
|
|
"""為字串生成一個穩定且美觀的 HSL 顏色"""
|
|
if not s:
|
|
return "hsl(0, 0%, 85%)" # 預設灰色
|
|
hash_val = int(hashlib.md5(s.encode('utf-8'), usedforsecurity=False).hexdigest(), 16)
|
|
hue = hash_val % 360
|
|
return f"hsl({hue}, 60%, 88%)"
|
|
|
|
|
|
def load_scheduler_stats():
|
|
"""讀取排程統計資料"""
|
|
import os
|
|
import json
|
|
stats_path = os.path.join(BASE_DIR, 'data', 'scheduler_stats.json')
|
|
if os.path.exists(stats_path):
|
|
try:
|
|
with open(stats_path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
except (IOError, json.JSONDecodeError):
|
|
return {}
|
|
return {}
|
|
|
|
|
|
def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
|
|
"""
|
|
通用的促銷儀表板數據建構函數
|
|
用於 edm 和 festival 兩種頁面類型
|
|
"""
|
|
# 1. 基礎統計
|
|
last_update = session.query(PromoProduct.crawled_at).filter(
|
|
PromoProduct.page_type == page_type
|
|
).order_by(desc(PromoProduct.crawled_at)).first()
|
|
last_update_str = last_update[0].strftime('%Y-%m-%d %H:%M') if last_update else "尚無資料"
|
|
|
|
latest_entry = session.query(PromoProduct).filter(
|
|
PromoProduct.page_type == page_type
|
|
).order_by(desc(PromoProduct.crawled_at)).first()
|
|
activity_time = getattr(latest_entry, 'activity_time_text', page_name) if latest_entry else page_name
|
|
|
|
# 2. 查詢最新批次
|
|
latest_batch = session.query(PromoProduct.batch_id).filter(
|
|
PromoProduct.page_type == page_type
|
|
).order_by(desc(PromoProduct.crawled_at)).first()
|
|
current_batch_id = latest_batch[0] if latest_batch else None
|
|
|
|
# 3. 查詢「全商品的最新狀態快照」
|
|
subq = session.query(
|
|
func.max(PromoProduct.id).label('max_id')
|
|
).filter(PromoProduct.page_type == page_type).group_by(
|
|
PromoProduct.i_code, PromoProduct.time_slot
|
|
).subquery()
|
|
|
|
latest_records = session.query(PromoProduct).join(
|
|
subq, PromoProduct.id == subq.c.max_id
|
|
).all()
|
|
|
|
# 4. 過濾顯示列表
|
|
items_in_batch = []
|
|
today_start = datetime.now(TAIPEI_TZ).replace(
|
|
hour=0, minute=0, second=0, microsecond=0
|
|
) # 保持時區資訊 (台北 +8)
|
|
|
|
for item in latest_records:
|
|
# 確保 crawled_at 有時區資訊,若無則假設為台北時區
|
|
item_crawled_at = item.crawled_at
|
|
if item_crawled_at.tzinfo is None:
|
|
item_crawled_at = item_crawled_at.replace(tzinfo=TAIPEI_TZ)
|
|
|
|
if item.status_change == 'SLOT_END' and item_crawled_at < today_start:
|
|
continue
|
|
if item.status_change == 'DELISTED' and item_crawled_at < today_start:
|
|
continue
|
|
items_in_batch.append(item)
|
|
|
|
# 5. 按時段分組
|
|
grouped_items = {}
|
|
for item in items_in_batch:
|
|
if item.time_slot not in grouped_items:
|
|
grouped_items[item.time_slot] = []
|
|
grouped_items[item.time_slot].append(item)
|
|
|
|
sorted_grouped_items = dict(sorted(grouped_items.items()))
|
|
|
|
# 6. 決定預設顯示的頁籤
|
|
def get_current_time_slot():
|
|
hour = datetime.now(TAIPEI_TZ).hour
|
|
available_slots = sorted([
|
|
int(s.split(':')[0]) for s in sorted_grouped_items.keys() if s and ':' in s
|
|
]) if sorted_grouped_items else [0, 7, 11, 14, 18, 22]
|
|
current_slot_hour = 0
|
|
for s in available_slots:
|
|
if hour >= s:
|
|
current_slot_hour = s
|
|
return f"{current_slot_hour:02d}:00"
|
|
|
|
active_tab = get_current_time_slot()
|
|
if active_tab not in sorted_grouped_items and sorted_grouped_items:
|
|
active_tab = next(iter(sorted_grouped_items))
|
|
|
|
# 7. 計算在架天數與總銷量
|
|
all_icodes_in_batch = [item.i_code for item in items_in_batch]
|
|
product_categories = {}
|
|
days_on_shelf_map = {}
|
|
total_sold_map = {}
|
|
history_map = {}
|
|
|
|
if all_icodes_in_batch:
|
|
# 從主商品表查詢分類
|
|
main_products = session.query(Product.i_code, Product.category).filter(
|
|
Product.i_code.in_(all_icodes_in_batch)
|
|
).all()
|
|
product_categories = {p.i_code: p.category for p in main_products}
|
|
|
|
# 計算上架天數 (兼容 SQLite 和 PostgreSQL)
|
|
if DATABASE_TYPE == 'postgresql':
|
|
# PostgreSQL: 使用 DATE() 函數
|
|
days_on_shelf_q = session.query(
|
|
PromoProduct.i_code,
|
|
func.count(func.distinct(func.date(PromoProduct.crawled_at)))
|
|
).filter(
|
|
PromoProduct.i_code.in_(all_icodes_in_batch),
|
|
PromoProduct.page_type == page_type
|
|
).group_by(PromoProduct.i_code).all()
|
|
else:
|
|
# SQLite: 使用 strftime
|
|
days_on_shelf_q = session.query(
|
|
PromoProduct.i_code,
|
|
func.count(func.distinct(func.strftime('%Y-%m-%d', PromoProduct.crawled_at)))
|
|
).filter(
|
|
PromoProduct.i_code.in_(all_icodes_in_batch),
|
|
PromoProduct.page_type == page_type
|
|
).group_by(PromoProduct.i_code).all()
|
|
days_on_shelf_map = {r[0]: r[1] for r in days_on_shelf_q}
|
|
|
|
# 只有 edm 頁面需要計算總銷量和庫存歷程
|
|
if page_type == 'edm':
|
|
# 計算總銷量
|
|
first_qty_subq = session.query(
|
|
PromoProduct.i_code,
|
|
func.min(PromoProduct.id).label('min_id')
|
|
).filter(
|
|
PromoProduct.i_code.in_(all_icodes_in_batch),
|
|
PromoProduct.remain_qty.isnot(None),
|
|
PromoProduct.page_type == 'edm'
|
|
).group_by(PromoProduct.i_code).subquery()
|
|
|
|
first_qty_records = session.query(
|
|
PromoProduct.i_code, PromoProduct.remain_qty
|
|
).join(first_qty_subq, PromoProduct.id == first_qty_subq.c.min_id).all()
|
|
first_qty_map = {r[0]: r[1] for r in first_qty_records}
|
|
|
|
for item in items_in_batch:
|
|
if item.i_code in first_qty_map and item.remain_qty is not None:
|
|
initial_qty = first_qty_map[item.i_code]
|
|
current_qty = item.remain_qty
|
|
if initial_qty > current_qty:
|
|
total_sold_map[item.i_code] = initial_qty - current_qty
|
|
|
|
# 準備銷售歷程資料
|
|
all_history_records = session.query(
|
|
PromoProduct.i_code,
|
|
PromoProduct.time_slot,
|
|
PromoProduct.remain_qty,
|
|
PromoProduct.crawled_at
|
|
).filter(
|
|
PromoProduct.i_code.in_(all_icodes_in_batch),
|
|
PromoProduct.crawled_at >= today_start
|
|
).order_by(PromoProduct.crawled_at).all()
|
|
|
|
for rec in all_history_records:
|
|
key = (rec.i_code, rec.time_slot)
|
|
if key not in history_map:
|
|
history_map[key] = []
|
|
if rec.remain_qty is not None:
|
|
if not history_map[key] or (history_map[key] and history_map[key][-1]['qty'] != rec.remain_qty):
|
|
history_map[key].append({'time': rec.crawled_at.strftime('%H:%M'), 'qty': rec.remain_qty})
|
|
|
|
# 8. 附加分類資訊到每個 item
|
|
for item in items_in_batch:
|
|
item.main_category = product_categories.get(item.i_code)
|
|
if item.main_category:
|
|
item.category_color = get_color_for_string(item.main_category)
|
|
item.days_on_shelf = days_on_shelf_map.get(item.i_code, 1)
|
|
item.total_sold = total_sold_map.get(item.i_code, 0)
|
|
item.qty_history = history_map.get((item.i_code, item.time_slot), [])
|
|
|
|
# 9. 排序邏輯
|
|
reverse = (order == 'desc')
|
|
for time_slot in sorted_grouped_items:
|
|
if sort_by == 'name':
|
|
sorted_grouped_items[time_slot].sort(key=lambda x: x.name or '', reverse=reverse)
|
|
elif sort_by == 'remain_qty':
|
|
sorted_grouped_items[time_slot].sort(
|
|
key=lambda x: x.remain_qty if x.remain_qty is not None else -1,
|
|
reverse=reverse
|
|
)
|
|
elif sort_by == 'price':
|
|
sorted_grouped_items[time_slot].sort(
|
|
key=lambda x: x.price if x.price is not None else -1,
|
|
reverse=reverse
|
|
)
|
|
else: # 預設排序
|
|
sorted_grouped_items[time_slot].sort(key=lambda x: (
|
|
1 if x.main_category else 0,
|
|
2 if x.status_change in ['NEW', 'PRICE_UP', 'PRICE_DOWN'] else (1 if x.status_change == 'DELISTED' else 0),
|
|
x.price if x.price is not None else -1
|
|
), reverse=True)
|
|
|
|
# 10. 時段統計
|
|
slot_stats = {}
|
|
today_change_records = session.query(PromoProduct).filter(
|
|
PromoProduct.crawled_at >= today_start,
|
|
PromoProduct.page_type == page_type
|
|
).all()
|
|
|
|
slots_from_changes = {rec.time_slot for rec in today_change_records}
|
|
slots_from_display = set(sorted_grouped_items.keys())
|
|
all_relevant_slots = sorted(list(slots_from_changes.union(slots_from_display)))
|
|
|
|
for slot in all_relevant_slots:
|
|
slot_stats[slot] = {
|
|
'new': 0, 'up': 0, 'down': 0,
|
|
'delisted_last_run': 0, 'on_shelf': 0, 'delisted_total': 0
|
|
}
|
|
|
|
for rec in today_change_records:
|
|
if rec.time_slot in slot_stats:
|
|
if rec.status_change == 'NEW':
|
|
slot_stats[rec.time_slot]['new'] += 1
|
|
elif rec.status_change == 'PRICE_UP':
|
|
slot_stats[rec.time_slot]['up'] += 1
|
|
elif rec.status_change == 'PRICE_DOWN':
|
|
slot_stats[rec.time_slot]['down'] += 1
|
|
elif rec.status_change in ['DELISTED', 'SLOT_END']:
|
|
slot_stats[rec.time_slot]['delisted_last_run'] += 1
|
|
|
|
for slot, items in sorted_grouped_items.items():
|
|
if slot in slot_stats:
|
|
on_shelf_count = sum(1 for item in items if item.status_change not in ['DELISTED', 'SLOT_END'])
|
|
delisted_total_count = len(items) - on_shelf_count
|
|
slot_stats[slot]['on_shelf'] = on_shelf_count
|
|
slot_stats[slot]['delisted_total'] = delisted_total_count
|
|
|
|
return {
|
|
'sorted_grouped_items': sorted_grouped_items,
|
|
'slot_stats': slot_stats,
|
|
'items_in_batch': items_in_batch,
|
|
'last_update_str': last_update_str,
|
|
'activity_time': activity_time,
|
|
'active_tab': active_tab,
|
|
'current_batch_id': current_batch_id
|
|
}
|
|
|
|
|
|
# ==========================================
|
|
# EDM 儀表板路由
|
|
# ==========================================
|
|
|
|
@edm_bp.route('/edm')
|
|
@login_required
|
|
def edm_dashboard():
|
|
"""MOMO 限時搶購 (EDM) 專屬儀表板"""
|
|
db = DatabaseManager()
|
|
session = db.get_session()
|
|
|
|
sort_by = request.args.get('sort_by', 'default')
|
|
order = request.args.get('order', 'desc')
|
|
|
|
try:
|
|
data = _build_promo_dashboard_data(session, 'edm', '限時搶購', sort_by, order)
|
|
|
|
# 建立儀表板頁籤
|
|
promo_pages = [
|
|
{'url': url_for('edm.edm_dashboard'), 'name': '限時搶購', 'id': 'edm'},
|
|
{'url': url_for('edm.festival_dashboard'), 'name': '1.1狂歡購物節', 'id': 'festival'},
|
|
{'url': url_for('edm.mothers_day_dashboard'), 'name': '母親節', 'id': 'mothers_day'},
|
|
{'url': url_for('edm.valentine_520_dashboard'), 'name': '520情人節', 'id': 'valentine_520'},
|
|
{'url': url_for('edm.labor_day_dashboard'), 'name': '勞動節', 'id': 'labor_day'}
|
|
]
|
|
|
|
scheduler_stats = load_scheduler_stats()
|
|
now_taipei = datetime.now(TAIPEI_TZ)
|
|
|
|
return render_template('edm_dashboard.html',
|
|
promo_pages=promo_pages,
|
|
current_promo_page='edm',
|
|
page_title='MOMO 限時搶購',
|
|
grouped_items=data['sorted_grouped_items'],
|
|
slot_stats=data['slot_stats'],
|
|
total_edm_products=len(data['items_in_batch']),
|
|
last_update=data['last_update_str'],
|
|
activity_time=data['activity_time'],
|
|
active_tab=data['active_tab'],
|
|
current_batch_id=data['current_batch_id'],
|
|
public_url=public_url,
|
|
scheduler_stats=scheduler_stats,
|
|
current_sort=sort_by,
|
|
current_order=order,
|
|
slugify=slugify,
|
|
datetime_now=now_taipei.strftime('%Y-%m-%d %H:%M:%S'))
|
|
except Exception as e:
|
|
sys_log.error(f"EDM Dashboard 渲染錯誤: {e}")
|
|
return f"系統錯誤: {e}"
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@edm_bp.route('/festival')
|
|
@login_required
|
|
def festival_dashboard():
|
|
"""1.1 狂歡購物節專屬儀表板"""
|
|
db = DatabaseManager()
|
|
session = db.get_session()
|
|
|
|
PAGE_TYPE = "festival"
|
|
PAGE_NAME = "1.1狂歡購物節"
|
|
|
|
sort_by = request.args.get('sort_by', 'default')
|
|
order = request.args.get('order', 'desc')
|
|
|
|
try:
|
|
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
|
|
|
|
# 建立儀表板頁籤
|
|
promo_pages = [
|
|
{'url': url_for('edm.edm_dashboard'), 'name': '限時搶購', 'id': 'edm'},
|
|
{'url': url_for('edm.festival_dashboard'), 'name': '1.1狂歡購物節', 'id': 'festival'},
|
|
{'url': url_for('edm.mothers_day_dashboard'), 'name': '母親節', 'id': 'mothers_day'},
|
|
{'url': url_for('edm.valentine_520_dashboard'), 'name': '520情人節', 'id': 'valentine_520'},
|
|
{'url': url_for('edm.labor_day_dashboard'), 'name': '勞動節', 'id': 'labor_day'}
|
|
]
|
|
|
|
scheduler_stats = load_scheduler_stats()
|
|
|
|
return render_template('edm_dashboard.html',
|
|
promo_pages=promo_pages,
|
|
current_promo_page='festival',
|
|
page_title=PAGE_NAME,
|
|
grouped_items=data['sorted_grouped_items'],
|
|
slot_stats=data['slot_stats'],
|
|
total_edm_products=len(data['items_in_batch']),
|
|
last_update=data['last_update_str'],
|
|
activity_time=data['activity_time'],
|
|
active_tab=data['active_tab'],
|
|
public_url=public_url,
|
|
scheduler_stats=scheduler_stats,
|
|
current_sort=sort_by,
|
|
current_order=order,
|
|
slugify=slugify)
|
|
except Exception as e:
|
|
sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}")
|
|
return f"系統錯誤: {e}"
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@edm_bp.route('/mothers_day')
|
|
@login_required
|
|
def mothers_day_dashboard():
|
|
"""母親節促銷活動專屬儀表板"""
|
|
db = DatabaseManager()
|
|
session = db.get_session()
|
|
|
|
PAGE_TYPE = "mothers_day"
|
|
PAGE_NAME = "母親節超值限時購"
|
|
|
|
sort_by = request.args.get('sort_by', 'default')
|
|
order = request.args.get('order', 'desc')
|
|
|
|
try:
|
|
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
|
|
|
|
# 建立儀表板頁籤
|
|
promo_pages = [
|
|
{'url': url_for('edm.edm_dashboard'), 'name': '限時搶購', 'id': 'edm'},
|
|
{'url': url_for('edm.festival_dashboard'), 'name': '1.1狂歡購物節', 'id': 'festival'},
|
|
{'url': url_for('edm.mothers_day_dashboard'), 'name': '母親節', 'id': 'mothers_day'},
|
|
{'url': url_for('edm.valentine_520_dashboard'), 'name': '520情人節', 'id': 'valentine_520'},
|
|
{'url': url_for('edm.labor_day_dashboard'), 'name': '勞動節', 'id': 'labor_day'}
|
|
]
|
|
|
|
scheduler_stats = load_scheduler_stats()
|
|
|
|
return render_template('edm_dashboard.html',
|
|
promo_pages=promo_pages,
|
|
current_promo_page='mothers_day',
|
|
page_title=PAGE_NAME,
|
|
grouped_items=data['sorted_grouped_items'],
|
|
slot_stats=data['slot_stats'],
|
|
total_edm_products=len(data['items_in_batch']),
|
|
last_update=data['last_update_str'],
|
|
activity_time=data['activity_time'],
|
|
active_tab=data['active_tab'],
|
|
public_url=public_url,
|
|
scheduler_stats=scheduler_stats,
|
|
current_sort=sort_by,
|
|
current_order=order,
|
|
slugify=slugify)
|
|
except Exception as e:
|
|
sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}")
|
|
return f"系統錯誤: {e}"
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@edm_bp.route('/valentine_520')
|
|
@login_required
|
|
def valentine_520_dashboard():
|
|
"""520情人節促銷活動專屬儀表板"""
|
|
db = DatabaseManager()
|
|
session = db.get_session()
|
|
|
|
PAGE_TYPE = "valentine_520"
|
|
PAGE_NAME = "520情人節限定購物"
|
|
|
|
sort_by = request.args.get('sort_by', 'default')
|
|
order = request.args.get('order', 'desc')
|
|
|
|
try:
|
|
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
|
|
|
|
# 建立儀表板頁籤
|
|
promo_pages = [
|
|
{'url': url_for('edm.edm_dashboard'), 'name': '限時搶購', 'id': 'edm'},
|
|
{'url': url_for('edm.festival_dashboard'), 'name': '1.1狂歡購物節', 'id': 'festival'},
|
|
{'url': url_for('edm.mothers_day_dashboard'), 'name': '母親節', 'id': 'mothers_day'},
|
|
{'url': url_for('edm.valentine_520_dashboard'), 'name': '520情人節', 'id': 'valentine_520'},
|
|
{'url': url_for('edm.labor_day_dashboard'), 'name': '勞動節', 'id': 'labor_day'}
|
|
]
|
|
|
|
scheduler_stats = load_scheduler_stats()
|
|
|
|
return render_template('edm_dashboard.html',
|
|
promo_pages=promo_pages,
|
|
current_promo_page='valentine_520',
|
|
page_title=PAGE_NAME,
|
|
grouped_items=data['sorted_grouped_items'],
|
|
slot_stats=data['slot_stats'],
|
|
total_edm_products=len(data['items_in_batch']),
|
|
last_update=data['last_update_str'],
|
|
activity_time=data['activity_time'],
|
|
active_tab=data['active_tab'],
|
|
public_url=public_url,
|
|
scheduler_stats=scheduler_stats,
|
|
current_sort=sort_by,
|
|
current_order=order,
|
|
slugify=slugify)
|
|
except Exception as e:
|
|
sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}")
|
|
return f"系統錯誤: {e}"
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@edm_bp.route('/labor_day')
|
|
@login_required
|
|
def labor_day_dashboard():
|
|
"""勞動節促銷活動專屬儀表板"""
|
|
db = DatabaseManager()
|
|
session = db.get_session()
|
|
|
|
PAGE_TYPE = "labor_day"
|
|
PAGE_NAME = "勞動節購物優惠"
|
|
|
|
sort_by = request.args.get('sort_by', 'default')
|
|
order = request.args.get('order', 'desc')
|
|
|
|
try:
|
|
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
|
|
|
|
# 建立儀表板頁籤
|
|
promo_pages = [
|
|
{'url': url_for('edm.edm_dashboard'), 'name': '限時搶購', 'id': 'edm'},
|
|
{'url': url_for('edm.festival_dashboard'), 'name': '1.1狂歡購物節', 'id': 'festival'},
|
|
{'url': url_for('edm.mothers_day_dashboard'), 'name': '母親節', 'id': 'mothers_day'},
|
|
{'url': url_for('edm.valentine_520_dashboard'), 'name': '520情人節', 'id': 'valentine_520'},
|
|
{'url': url_for('edm.labor_day_dashboard'), 'name': '勞動節', 'id': 'labor_day'}
|
|
]
|
|
|
|
scheduler_stats = load_scheduler_stats()
|
|
|
|
return render_template('edm_dashboard.html',
|
|
promo_pages=promo_pages,
|
|
current_promo_page='labor_day',
|
|
page_title=PAGE_NAME,
|
|
grouped_items=data['sorted_grouped_items'],
|
|
slot_stats=data['slot_stats'],
|
|
total_edm_products=len(data['items_in_batch']),
|
|
last_update=data['last_update_str'],
|
|
activity_time=data['activity_time'],
|
|
active_tab=data['active_tab'],
|
|
public_url=public_url,
|
|
scheduler_stats=scheduler_stats,
|
|
current_sort=sort_by,
|
|
current_order=order,
|
|
slugify=slugify)
|
|
except Exception as e:
|
|
sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}")
|
|
return f"系統錯誤: {e}"
|
|
finally:
|
|
session.close()
|