import os import csv import pandas as pd from datetime import datetime from config import EXCEL_EXPORT_DIR from utils.momo_url_utils import build_momo_product_url, normalize_momo_product_url class Exporter: """ 報表匯出管理器 負責將數據轉換為 CSV/Excel 格式並儲存至 exports 目錄 """ def __init__(self): self.export_dir = EXCEL_EXPORT_DIR if not os.path.exists(self.export_dir): try: os.makedirs(self.export_dir) except OSError: pass def _write_csv(self, filename, headers, rows): """內部方法:寫入 CSV 檔案 (含 BOM 以支援 Excel 中文顯示)""" filepath = os.path.join(self.export_dir, filename) try: with open(filepath, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerow(headers) writer.writerows(rows) return filepath except Exception as e: print(f"❌ Export error: {e}") return None def _safe_product_url(self, product): sku = str(getattr(product, 'i_code', '') or '') return normalize_momo_product_url(getattr(product, 'url', None), sku) or build_momo_product_url(sku) def generate_all_categories_report(self): """匯出所有分類商品快照""" # 由於 app.py 呼叫此方法時未傳入數據,需自行查詢資料庫 from database.manager import DatabaseManager from database.models import PriceRecord from sqlalchemy import func db = DatabaseManager() session = db.get_session() try: # 查詢每個商品最新價格 subq = session.query(func.max(PriceRecord.id).label('max_id')).group_by(PriceRecord.product_id).subquery() records = session.query(PriceRecord).join(subq, PriceRecord.id == subq.c.max_id).all() rows = [] for r in records: rows.append([r.product.category, r.product.name, r.price, self._safe_product_url(r.product), r.timestamp.strftime('%Y-%m-%d %H:%M')]) filename = f"All_Products_{datetime.now().strftime('%Y%m%d_%H%M')}.csv" return self._write_csv(filename, ['Category', 'Name', 'Price', 'URL', 'Last Update'], rows) finally: session.close() def generate_price_change_report(self, items): """匯出價格異動商品""" rows = [] for item in items: rec = item['record'] diff = item['yesterday_diff'] rows.append([rec.product.category, rec.product.name, rec.price, diff, self._safe_product_url(rec.product)]) filename = f"Price_Changes_{datetime.now().strftime('%Y%m%d_%H%M')}.csv" return self._write_csv(filename, ['Category', 'Name', 'Price', 'Change', 'URL'], rows) def generate_low_price_report(self): return self._write_csv(f"Low_Price_{datetime.now().strftime('%Y%m%d')}.csv", ['Message'], [['Feature not fully implemented']]) def generate_custom_report(self, items, title): rows = [] for item in items: rec = item['record'] rows.append([rec.product.category, rec.product.name, rec.price, self._safe_product_url(rec.product)]) filename = f"{title}_{datetime.now().strftime('%Y%m%d_%H%M')}.csv" return self._write_csv(filename, ['Category', 'Name', 'Price', 'URL'], rows) def generate_delisted_report(self, items, title): rows = [] for item in items: p = item['product'] price = item['last_price'] rows.append([p.category, p.name, price, self._safe_product_url(p), "DELISTED"]) filename = f"{title}_{datetime.now().strftime('%Y%m%d_%H%M')}.csv" return self._write_csv(filename, ['Category', 'Name', 'Last Price', 'URL', 'Status'], rows) def generate_all_products_excel(self, items): """匯出所有商品至 Excel (依分類分頁)""" data = [] for item in items: rec = item['record'] data.append({ '分類': rec.product.category, '商品名稱': rec.product.name, '價格': rec.price, '連結': self._safe_product_url(rec.product), '更新時間': rec.timestamp.strftime('%Y-%m-%d %H:%M') }) filename = f"MOMO_All_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" filepath = os.path.join(self.export_dir, filename) df = pd.DataFrame(data) with pd.ExcelWriter(filepath, engine='openpyxl') as writer: for cat, group in df.groupby('分類'): sheet_name = str(cat)[:30].replace('/', '_').replace(':', '') # 處理 Excel 工作表名稱限制 group.to_excel(writer, sheet_name=sheet_name, index=False) return filepath def generate_changes_excel(self, increase, decrease): """匯出漲跌商品至 Excel (兩個分頁)""" filename = f"MOMO_Changes_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" filepath = os.path.join(self.export_dir, filename) with pd.ExcelWriter(filepath, engine='openpyxl') as writer: # 漲價 data_inc = [{'分類': i['record'].product.category, '商品名稱': i['record'].product.name, '價格': i['record'].price, '漲幅': i['yesterday_diff'], '連結': self._safe_product_url(i['record'].product)} for i in increase] pd.DataFrame(data_inc).to_excel(writer, sheet_name='漲價商品', index=False) # 跌價 data_dec = [{'分類': i['record'].product.category, '商品名稱': i['record'].product.name, '價格': i['record'].price, '跌幅': i['yesterday_diff'], '連結': self._safe_product_url(i['record'].product)} for i in decrease] pd.DataFrame(data_dec).to_excel(writer, sheet_name='跌價商品', index=False) return filepath def generate_delisted_excel(self, delisted_items): """匯出下架商品至 Excel""" filename = f"MOMO_Delisted_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx" filepath = os.path.join(self.export_dir, filename) data = [{'分類': i['product'].category, '商品名稱': i['product'].name, '最後價格': i['last_price'], '連結': self._safe_product_url(i['product']), '狀態': '下架'} for i in delisted_items] with pd.ExcelWriter(filepath, engine='openpyxl') as writer: pd.DataFrame(data).to_excel(writer, sheet_name='下架商品', index=False) return filepath