加速活動看板時段載入
All checks were successful
CD Pipeline / deploy (push) Successful in 57s

This commit is contained in:
OoO
2026-05-13 11:00:44 +08:00
parent 89c400d53e
commit d410677e5e
3 changed files with 40 additions and 29 deletions

View File

@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.101"
SYSTEM_VERSION = "V10.102"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -62,7 +62,7 @@ def load_scheduler_stats():
return {}
def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order, requested_slot=None):
"""
通用的促銷儀表板數據建構函數
用於 edm 和 festival 兩種頁面類型
@@ -135,20 +135,23 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
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. 計算在架天數與總銷量
all_icodes_in_batch = [item.i_code for item in items_in_batch]
# 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 all_icodes_in_batch:
if visible_icodes:
# 從主商品表查詢分類
main_products = session.query(Product.i_code, Product.category).filter(
Product.i_code.in_(all_icodes_in_batch)
Product.i_code.in_(visible_icodes)
).all()
product_categories = {p.i_code: p.category for p in main_products}
@@ -159,7 +162,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
PromoProduct.i_code,
func.count(func.distinct(func.date(PromoProduct.crawled_at)))
).filter(
PromoProduct.i_code.in_(all_icodes_in_batch),
PromoProduct.i_code.in_(visible_icodes),
PromoProduct.page_type == page_type
).group_by(PromoProduct.i_code).all()
else:
@@ -168,7 +171,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
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.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}
@@ -180,7 +183,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
PromoProduct.i_code,
func.min(PromoProduct.id).label('min_id')
).filter(
PromoProduct.i_code.in_(all_icodes_in_batch),
PromoProduct.i_code.in_(visible_icodes),
PromoProduct.remain_qty.isnot(None),
PromoProduct.page_type == 'edm'
).group_by(PromoProduct.i_code).subquery()
@@ -190,7 +193,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
).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:
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
@@ -204,7 +207,7 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
PromoProduct.remain_qty,
PromoProduct.crawled_at
).filter(
PromoProduct.i_code.in_(all_icodes_in_batch),
PromoProduct.i_code.in_(visible_icodes),
PromoProduct.crawled_at >= today_start
).order_by(PromoProduct.crawled_at).all()
@@ -216,8 +219,8 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
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:
# 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:
@@ -226,9 +229,11 @@ def _build_promo_dashboard_data(session, page_type, page_name, sort_by, order):
item.total_sold = total_sold_map.get(item.i_code, 0)
item.qty_history = history_map.get((item.i_code, item.time_slot), [])
# 9. 排序邏輯
# 9. 排序邏輯:首屏只需要排序目前時段
reverse = (order == 'desc')
for time_slot in sorted_grouped_items:
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':
@@ -307,9 +312,10 @@ def edm_dashboard():
sort_by = request.args.get('sort_by', 'default')
order = request.args.get('order', 'desc')
requested_slot = request.args.get('slot')
try:
data = _build_promo_dashboard_data(session, 'edm', '限時搶購', sort_by, order)
data = _build_promo_dashboard_data(session, 'edm', '限時搶購', sort_by, order, requested_slot)
# 建立儀表板頁籤
promo_pages = [
@@ -362,9 +368,10 @@ def festival_dashboard():
sort_by = request.args.get('sort_by', 'default')
order = request.args.get('order', 'desc')
requested_slot = request.args.get('slot')
try:
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
# 建立儀表板頁籤
promo_pages = [
@@ -414,9 +421,10 @@ def mothers_day_dashboard():
sort_by = request.args.get('sort_by', 'default')
order = request.args.get('order', 'desc')
requested_slot = request.args.get('slot')
try:
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
# 建立儀表板頁籤
promo_pages = [
@@ -466,9 +474,10 @@ def valentine_520_dashboard():
sort_by = request.args.get('sort_by', 'default')
order = request.args.get('order', 'desc')
requested_slot = request.args.get('slot')
try:
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
# 建立儀表板頁籤
promo_pages = [
@@ -518,9 +527,10 @@ def labor_day_dashboard():
sort_by = request.args.get('sort_by', 'default')
order = request.args.get('order', 'desc')
requested_slot = request.args.get('slot')
try:
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order)
data = _build_promo_dashboard_data(session, PAGE_TYPE, PAGE_NAME, sort_by, order, requested_slot)
# 建立儀表板頁籤
promo_pages = [

View File

@@ -333,6 +333,7 @@
border-radius: 6px;
font-size: 12px;
font-weight: 800;
text-decoration: none;
white-space: nowrap;
}
@@ -824,10 +825,10 @@
<div class="campaign-slot-tabs" role="tablist">
{% for slot, stats in slot_stats.items() %}
{% set slot_id = slugify(slot) %}
<button class="campaign-slot-tab {% if slot == active_tab %}active{% endif %}" id="slot-{{ slot_id }}-tab" data-bs-toggle="tab" data-bs-target="#slot-{{ slot_id }}" type="button" role="tab">
<a class="campaign-slot-tab {% if slot == active_tab %}active{% endif %}" id="slot-{{ slot_id }}-tab" href="{{ url_for(current_endpoint, slot=slot, sort_by=current_sort, order=current_order) }}" role="tab" aria-selected="{{ 'true' if slot == active_tab else 'false' }}">
<span class="momo-mono">{{ slot }}</span>
<span class="campaign-slot-count momo-mono">{{ stats.get('on_shelf', 0) }} 件</span>
</button>
</a>
{% endfor %}
</div>
{% else %}
@@ -839,9 +840,10 @@
{% if slot_stats %}
<section class="tab-content">
{% for slot, stats in slot_stats.items() %}
{% set slot_id = slugify(slot) %}
{% set items = grouped_items.get(slot, []) %}
{% set slot = active_tab %}
{% set stats = slot_stats.get(slot, {'new': 0, 'up': 0, 'down': 0, 'delisted_last_run': 0, 'on_shelf': 0}) %}
{% set slot_id = slugify(slot) %}
{% set items = grouped_items.get(slot, []) %}
<div class="tab-pane fade {% if slot == active_tab %}show active{% endif %}" id="slot-{{ slot_id }}" role="tabpanel">
<div class="campaign-table-card">
<div class="campaign-table-head">
@@ -863,16 +865,16 @@
<th>分類 / 狀態</th>
<th>
{% set next_order_name = 'asc' if current_sort == 'name' and current_order == 'desc' else 'desc' %}
<a href="{{ url_for(current_endpoint, sort_by='name', order=next_order_name) }}">商品資訊</a>
<a href="{{ url_for(current_endpoint, slot=active_tab, sort_by='name', order=next_order_name) }}">商品資訊</a>
</th>
<th class="text-end">
{% set next_order_price = 'asc' if current_sort == 'price' and current_order == 'desc' else 'desc' %}
<a href="{{ url_for(current_endpoint, sort_by='price', order=next_order_price) }}">價格</a>
<a href="{{ url_for(current_endpoint, slot=active_tab, sort_by='price', order=next_order_price) }}">價格</a>
</th>
<th class="text-center">
{% if current_promo_page == 'edm' %}
{% set next_order_qty = 'asc' if current_sort == 'remain_qty' and current_order == 'desc' else 'desc' %}
<a href="{{ url_for(current_endpoint, sort_by='remain_qty', order=next_order_qty) }}">銷售 / 庫存</a>
<a href="{{ url_for(current_endpoint, slot=active_tab, sort_by='remain_qty', order=next_order_qty) }}">銷售 / 庫存</a>
{% else %}
狀態
{% endif %}
@@ -1027,7 +1029,6 @@
</div>
</div>
</div>
{% endfor %}
</section>
{% endif %}
</div>