refactor(notifications): unify email sending via _send_email; standardize crawler notifications\n\n- Extract _send_email and have send_email/send_custom_email share it\n- BaseCrawler: centralize _send_notifications and add _build_email hook\n- BarronsCrawler: override _build_email to keep original subject/body\n- OpenInsiderCrawler: remove custom _send_notifications, add _build_email\n- /notify_test: use crawler _build_email + send_custom_email for emails

This commit is contained in:
2025-09-04 22:59:51 +08:00
parent e89567643b
commit b2c58c0560
5 changed files with 51 additions and 51 deletions

View File

@@ -8,11 +8,13 @@ import requests
from bs4 import BeautifulSoup
from app.crawlers.base import BaseCrawler
from app.services import notifications as notif
class BarronsCrawler(BaseCrawler):
def __init__(self, config, logger):
super().__init__(name="Barron's 股票推薦", config=config, logger=logger, data_filename='barrons_data.json')
# Name used in generic notifications; include emoji to match previous subject
super().__init__(name="📈 Barron's 新股票推薦", config=config, logger=logger, data_filename='barrons_data.json')
self.url = "https://www.barrons.com/market-data/stocks/stock-picks?mod=BOL_TOPNAV"
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
@@ -68,3 +70,8 @@ class BarronsCrawler(BaseCrawler):
self.stats['errors'] += 1
return []
# Keep Barron's specific email formatting (subject + body)
def _build_email(self, items: List[Dict]):
subject = f"📈 Barron's 新股票推薦 ({len(items)}條)"
body = notif.format_email_body(items)
return subject, body

View File

@@ -95,9 +95,11 @@ class BaseCrawler(ABC):
def _send_notifications(self, items: List[Dict]) -> None:
sent = False
# Build subject/body via hook for consistency across crawlers
subject, body = self._build_email(items)
if self.config.email:
try:
notif.send_email(items, self.config.email)
notif.send_custom_email(subject, body, self.config.email)
sent = True
except Exception as e:
self.logger.error(f"電子郵件通知失敗: {e}")
@@ -116,6 +118,23 @@ class BaseCrawler(ABC):
if sent:
self.stats['last_notification'] = datetime.now().isoformat()
def _build_email(self, items: List[Dict]):
"""Construct a generic email subject and body.
Subclasses can override to customize content/format.
"""
subject = f"{self.name} ({len(items)}條)"
lines = []
for pick in items:
line = f"📊 {pick.get('title','').strip()}\n"
if pick.get('link'):
line += f"🔗 {pick['link']}\n"
line += f"🕒 {pick.get('scraped_at', datetime.now().isoformat())}\n"
line += "-" * 60 + "\n"
lines.append(line)
body = f"發現 {len(items)} 條新內容:\n\n" + "".join(lines)
return subject, body
# --- Run loop ---
def _signal_handler(self, signum, frame):
self.logger.info("收到停止信號,正在關閉...")

View File

@@ -8,7 +8,6 @@ import requests
from bs4 import BeautifulSoup
from app.crawlers.base import BaseCrawler
from app.services import notifications as notif
class OpenInsiderCrawler(BaseCrawler):
@@ -128,35 +127,15 @@ class OpenInsiderCrawler(BaseCrawler):
self.logger.info(f"OpenInsider解析完成擷取 {len(items)} 筆交易")
return items
def _send_notifications(self, items: List[Dict]) -> None:
# Use BaseCrawler._send_notifications for unified flow
def _build_email(self, items: List[Dict]):
subject = f"OpenInsider 內部人交易異動 - {self.symbol} ({len(items)}筆)"
lines = []
for it in items[:10]:
lines.append(f"{it['title']}")
lines.append(f"{it.get('title','')}")
body = (
f"發現 {len(items)} 筆新的內部人交易異動OpenInsider\n\n" + "\n".join(lines) + "\n\n"
f"抓取時間:{datetime.now().isoformat()}\n來源:{self.url}"
)
sent = False
if self.config.email:
try:
notif.send_custom_email(subject, body, self.config.email)
sent = True
except Exception as e:
self.logger.error(f"電子郵件通知失敗: {e}")
if self.config.webhook_url:
try:
notif.send_text_webhook(subject + "\n\n" + body, self.config.webhook_url)
sent = True
except Exception as e:
self.logger.error(f"Webhook 通知失敗: {e}")
if self.config.discord_webhook:
try:
notif.send_text_discord(title=subject, description=f"{self.symbol} 內部人交易更新OpenInsider", lines=lines[:10], webhook=self.config.discord_webhook)
sent = True
except Exception as e:
self.logger.error(f"Discord 通知失敗: {e}")
if sent:
self.stats['last_notification'] = datetime.now().isoformat()
return subject, body