Files
ewoooc/routes/edm_routes.py
OoO 1205ce87fb
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
預熱活動看板 worker 快取
2026-05-19 13:34:06 +08:00

744 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
EDM 與節慶促銷路由模組
包含:限時搶購儀表板、節慶活動儀表板
"""
import hashlib
import math
import os
import pickle
from datetime import datetime, timezone, timedelta
from pathlib import Path
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, SYSTEM_VERSION
from database.manager import DatabaseManager
from database.models import Product
from database.edm_models import PromoProduct
from services.logger_manager import SystemLogger
from utils.momo_url_utils import build_momo_product_url, normalize_momo_product_url
# 時區設定
TAIPEI_TZ = timezone(timedelta(hours=8))
# Logger
sys_log = SystemLogger("EDMRoutes").get_logger()
# Blueprint 定義
edm_bp = Blueprint('edm', __name__)
_PROMO_DASHBOARD_CACHE = {}
_PROMO_DASHBOARD_CACHE_MAX = 32
_PROMO_PAGE_SIZE_DEFAULT = 24
_PROMO_PAGE_SIZE_MAX = 200
_PROMO_SHARED_CACHE_FILE = Path(BASE_DIR) / 'data' / 'promo_dashboard_cache.pkl'
# ==========================================
# 輔助函數
# ==========================================
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 _get_promo_dashboard_fingerprint(session, page_type):
"""取得促銷資料指紋,用於判斷快取是否仍可復用。"""
max_id, max_crawled_at, row_count = session.query(
func.max(PromoProduct.id),
func.max(PromoProduct.crawled_at),
func.count(PromoProduct.id)
).filter(PromoProduct.page_type == page_type).one()
return (
max_id or 0,
max_crawled_at.isoformat() if max_crawled_at else '',
row_count or 0
)
def _get_promo_dashboard_cache_key(session, page_type, sort_by, order, requested_slot):
"""建立 dashboard data 快取鍵;日期/小時避免自動時段跨時段後仍命中舊首屏。"""
now_taipei = datetime.now(TAIPEI_TZ)
auto_slot_bucket = now_taipei.strftime('%Y-%m-%d-%H') if not requested_slot else ''
return (
page_type,
sort_by,
order,
requested_slot or '',
auto_slot_bucket,
_get_promo_dashboard_fingerprint(session, page_type)
)
def _remember_promo_dashboard_data(cache_key, data):
"""保留少量活動 dashboard data避免常用時段每次重算。"""
if len(_PROMO_DASHBOARD_CACHE) >= _PROMO_DASHBOARD_CACHE_MAX:
_PROMO_DASHBOARD_CACHE.pop(next(iter(_PROMO_DASHBOARD_CACHE)))
_PROMO_DASHBOARD_CACHE[cache_key] = data
def _load_shared_promo_dashboard_cache(cache_key):
"""讀取跨 worker 促銷 dashboard 快取,讓冷 worker 不必重算首屏資料。"""
try:
if not os.path.exists(_PROMO_SHARED_CACHE_FILE):
return None
with open(_PROMO_SHARED_CACHE_FILE, 'rb') as f:
payload = pickle.load(f)
if payload.get('version') != SYSTEM_VERSION:
return None
entries = payload.get('entries')
if not isinstance(entries, dict):
return None
return entries.get(cache_key)
except Exception:
sys_log.debug("promo dashboard shared cache load failed", exc_info=True)
return None
def _write_shared_promo_dashboard_cache(cache_key, data):
"""原子寫入促銷 dashboard 共享快取;失敗不阻斷頁面回應。"""
cache_file = str(_PROMO_SHARED_CACHE_FILE)
tmp_file = f"{cache_file}.{os.getpid()}.tmp"
entries = {}
try:
if os.path.exists(_PROMO_SHARED_CACHE_FILE):
with open(_PROMO_SHARED_CACHE_FILE, 'rb') as f:
payload = pickle.load(f)
if payload.get('version') == SYSTEM_VERSION and isinstance(payload.get('entries'), dict):
entries = payload['entries']
entries[cache_key] = data
while len(entries) > _PROMO_DASHBOARD_CACHE_MAX:
entries.pop(next(iter(entries)))
os.makedirs(os.path.dirname(cache_file), exist_ok=True)
with open(tmp_file, 'wb') as f:
pickle.dump({'version': SYSTEM_VERSION, 'entries': entries}, f, protocol=pickle.HIGHEST_PROTOCOL)
os.replace(tmp_file, cache_file)
except Exception:
try:
if os.path.exists(tmp_file):
os.remove(tmp_file)
except OSError:
pass
sys_log.debug("promo dashboard shared cache write failed", exc_info=True)
def _get_promo_page_window_args():
"""讀取促銷商品清單分頁參數,限制首屏 HTML 重量。"""
page = request.args.get('page', 1, type=int) or 1
per_page = request.args.get('per_page', _PROMO_PAGE_SIZE_DEFAULT, type=int) or _PROMO_PAGE_SIZE_DEFAULT
return max(1, page), max(20, min(per_page, _PROMO_PAGE_SIZE_MAX))
def _paginate_active_slot(data, page, per_page):
"""只裁切目前顯示時段;其他統計仍保留完整資料。"""
active_tab = data.get('active_tab')
grouped_items = dict(data.get('sorted_grouped_items') or {})
active_items = list(grouped_items.get(active_tab, []))
total_items = len(active_items)
total_pages = max(1, math.ceil(total_items / per_page)) if total_items else 1
page = min(max(1, page), total_pages)
start_idx = (page - 1) * per_page
end_idx = min(start_idx + per_page, total_items)
grouped_items[active_tab] = active_items[start_idx:end_idx]
return grouped_items, {
'page': page,
'per_page': per_page,
'total_pages': total_pages,
'total_items': total_items,
'start_item': start_idx + 1 if total_items else 0,
'end_item': end_idx,
'has_prev': page > 1,
'has_next': page < total_pages,
}
def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order, requested_slot=None):
"""
通用的促銷儀表板數據建構函數
用於 edm 和 festival 兩種頁面類型
"""
cache_key = _get_promo_dashboard_cache_key(session, page_type, sort_by, order, requested_slot)
cached_data = _PROMO_DASHBOARD_CACHE.get(cache_key)
if cached_data is not None:
return cached_data
cached_data = _load_shared_promo_dashboard_cache(cache_key)
if cached_data is not None:
_remember_promo_dashboard_data(cache_key, cached_data)
return cached_data
# 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 requested_slot in sorted_grouped_items:
active_tab = requested_slot
if active_tab not in sorted_grouped_items and sorted_grouped_items:
active_tab = next(iter(sorted_grouped_items))
# 7. 僅為首屏目前時段補齊列級資訊;其他時段以連結切換後再載入。
visible_items = sorted_grouped_items.get(active_tab, [])
visible_icodes = [item.i_code for item in visible_items]
product_categories = {}
days_on_shelf_map = {}
total_sold_map = {}
history_map = {}
if visible_icodes:
# 從主商品表查詢分類
main_products = session.query(Product.i_code, Product.category).filter(
Product.i_code.in_(visible_icodes)
).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_(visible_icodes),
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_(visible_icodes),
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_(visible_icodes),
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 visible_items:
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_(visible_icodes),
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 visible_items:
item.safe_product_url = normalize_momo_product_url(item.url, item.i_code) or build_momo_product_url(item.i_code)
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 [active_tab]:
if time_slot not in sorted_grouped_items:
continue
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
data = {
'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
}
_remember_promo_dashboard_data(cache_key, data)
_write_shared_promo_dashboard_cache(cache_key, data)
return data
def warm_promo_dashboard_cache(reason='manual'):
"""預熱常用活動看板 shared cache避免部署後第一個使用者承擔冷 worker。"""
db = DatabaseManager()
session = db.get_session()
page_configs = [
('edm', '限時搶購'),
('festival', '1.1狂歡購物節'),
('mothers_day', '母親節超值限時購'),
('valentine_520', '520情人節限定購物'),
('labor_day', '勞動節購物優惠'),
]
started = datetime.now(TAIPEI_TZ)
warmed = 0
try:
for page_type, page_name in page_configs:
data = _build_promo_dashboard_data(session, page_type, page_name, 'default', 'desc', None)
if data:
warmed += 1
elapsed_ms = (datetime.now(TAIPEI_TZ) - started).total_seconds() * 1000
sys_log.info(
f"[EDM] [Cache] ✅ 活動看板預熱完成 | reason={reason} | pages={warmed} | 耗時={elapsed_ms:.0f}ms"
)
return warmed
finally:
session.close()
# ==========================================
# 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')
requested_slot = request.args.get('slot')
page, per_page = _get_promo_page_window_args()
try:
data = _build_promo_dashboard_data(session, 'edm', '限時搶購', sort_by, order, requested_slot)
grouped_items, page_window = _paginate_active_slot(data, page, per_page)
# 建立儀表板頁籤
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)
template_name = 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
current_promo_page='edm',
page_title='MOMO 限時搶購',
grouped_items=grouped_items,
slot_stats=data['slot_stats'],
page_window=page_window,
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'),
active_page='edm')
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')
requested_slot = request.args.get('slot')
page, per_page = _get_promo_page_window_args()
try:
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
grouped_items, page_window = _paginate_active_slot(data, page, per_page)
# 建立儀表板頁籤
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()
template_name = 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
current_promo_page='festival',
page_title=PAGE_NAME,
grouped_items=grouped_items,
slot_stats=data['slot_stats'],
page_window=page_window,
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,
active_page='edm')
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')
requested_slot = request.args.get('slot')
page, per_page = _get_promo_page_window_args()
try:
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
grouped_items, page_window = _paginate_active_slot(data, page, per_page)
# 建立儀表板頁籤
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()
template_name = 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
current_promo_page='mothers_day',
page_title=PAGE_NAME,
grouped_items=grouped_items,
slot_stats=data['slot_stats'],
page_window=page_window,
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,
active_page='edm')
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')
requested_slot = request.args.get('slot')
page, per_page = _get_promo_page_window_args()
try:
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
grouped_items, page_window = _paginate_active_slot(data, page, per_page)
# 建立儀表板頁籤
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()
template_name = 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
current_promo_page='valentine_520',
page_title=PAGE_NAME,
grouped_items=grouped_items,
slot_stats=data['slot_stats'],
page_window=page_window,
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,
active_page='edm')
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')
requested_slot = request.args.get('slot')
page, per_page = _get_promo_page_window_args()
try:
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
grouped_items, page_window = _paginate_active_slot(data, page, per_page)
# 建立儀表板頁籤
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()
template_name = 'edm_dashboard_v2.html'
return render_template(template_name,
promo_pages=promo_pages,
current_promo_page='labor_day',
page_title=PAGE_NAME,
grouped_items=grouped_items,
slot_stats=data['slot_stats'],
page_window=page_window,
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,
active_page='edm')
except Exception as e:
sys_log.error(f"{PAGE_NAME} Dashboard 渲染錯誤: {e}")
return f"系統錯誤: {e}"
finally:
session.close()