Initial commit: Daily News App
企業內部新聞彙整與分析系統 - 自動新聞抓取 (Digitimes, 經濟日報, 工商時報) - AI 智慧摘要 (OpenAI/Claude/Ollama) - 群組管理與訂閱通知 - 已清理 Python 快取檔案 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
203
app/services/notification_service.py
Normal file
203
app/services/notification_service.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
通知服務模組
|
||||
處理 Email 發送
|
||||
"""
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from typing import Optional
|
||||
from html import escape
|
||||
from sqlalchemy.orm import Session
|
||||
import logging
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models import Report, Subscription, User, NotificationLog, NotificationStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_email(to_email: str, subject: str, html_content: str) -> bool:
|
||||
"""
|
||||
發送 Email
|
||||
|
||||
Returns:
|
||||
是否發送成功
|
||||
"""
|
||||
if not settings.smtp_host:
|
||||
logger.warning("SMTP 未設定,跳過發送")
|
||||
return False
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = f"{settings.smtp_from_name} <{settings.smtp_from_email}>"
|
||||
msg["To"] = to_email
|
||||
|
||||
html_part = MIMEText(html_content, "html", "utf-8")
|
||||
msg.attach(html_part)
|
||||
|
||||
with smtplib.SMTP(settings.smtp_host, settings.smtp_port) as server:
|
||||
server.starttls()
|
||||
if settings.smtp_username and settings.smtp_password:
|
||||
server.login(settings.smtp_username, settings.smtp_password)
|
||||
server.sendmail(settings.smtp_from_email, to_email, msg.as_string())
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Email 發送失敗", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
def create_report_email_content(report: Report, base_url: str = "") -> str:
|
||||
"""建立報告通知 Email 內容"""
|
||||
summary = report.edited_summary or report.ai_summary or "無摘要內容"
|
||||
|
||||
# 截取摘要前 500 字
|
||||
if len(summary) > 500:
|
||||
summary = summary[:500] + "..."
|
||||
|
||||
# 轉義 HTML 特殊字元,防止 XSS
|
||||
safe_title = escape(report.title)
|
||||
safe_group_name = escape(report.group.name)
|
||||
safe_summary = escape(summary)
|
||||
safe_base_url = escape(base_url)
|
||||
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
|
||||
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
|
||||
.header {{ background: #4a6fa5; color: white; padding: 20px; text-align: center; }}
|
||||
.content {{ padding: 20px; background: #f9f9f9; }}
|
||||
.summary {{ background: white; padding: 15px; border-left: 4px solid #4a6fa5; margin: 15px 0; }}
|
||||
.button {{ display: inline-block; padding: 12px 24px; background: #4a6fa5; color: white; text-decoration: none; border-radius: 4px; }}
|
||||
.footer {{ text-align: center; padding: 20px; color: #666; font-size: 12px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 style="margin:0;">每日報導</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>{safe_title}</h2>
|
||||
<p>
|
||||
<strong>群組:</strong>{safe_group_name}<br>
|
||||
<strong>日期:</strong>{report.report_date}
|
||||
</p>
|
||||
<div class="summary">
|
||||
<h3>摘要</h3>
|
||||
<p>{safe_summary}</p>
|
||||
</div>
|
||||
<p style="text-align: center; margin-top: 30px;">
|
||||
<a href="{safe_base_url}/reports/{report.id}" class="button">閱讀完整報告</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>此郵件由每日報導系統自動發送</p>
|
||||
<p>如不想收到通知,請至系統調整訂閱設定</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def send_report_notifications(db: Session, report: Report) -> int:
|
||||
"""
|
||||
發送報告通知給訂閱者
|
||||
|
||||
Returns:
|
||||
發送成功數量
|
||||
"""
|
||||
# 取得訂閱此群組的用戶
|
||||
subscriptions = db.query(Subscription).filter(
|
||||
Subscription.group_id == report.group_id,
|
||||
Subscription.email_notify == True
|
||||
).all()
|
||||
|
||||
sent_count = 0
|
||||
|
||||
for sub in subscriptions:
|
||||
user = db.query(User).filter(User.id == sub.user_id).first()
|
||||
if not user or not user.email or not user.is_active:
|
||||
continue
|
||||
|
||||
# 建立通知記錄
|
||||
notification = NotificationLog(
|
||||
user_id=user.id,
|
||||
report_id=report.id,
|
||||
notification_type="email",
|
||||
subject=f"【每日報導】{report.title}",
|
||||
content=report.edited_summary or report.ai_summary
|
||||
)
|
||||
db.add(notification)
|
||||
|
||||
# 發送 Email
|
||||
html_content = create_report_email_content(report)
|
||||
success = send_email(
|
||||
user.email,
|
||||
f"【每日報導】{report.title}",
|
||||
html_content
|
||||
)
|
||||
|
||||
if success:
|
||||
notification.status = NotificationStatus.SENT
|
||||
from datetime import datetime
|
||||
notification.sent_at = datetime.utcnow()
|
||||
sent_count += 1
|
||||
else:
|
||||
notification.status = NotificationStatus.FAILED
|
||||
notification.error_message = "發送失敗"
|
||||
|
||||
db.commit()
|
||||
return sent_count
|
||||
|
||||
|
||||
def send_delay_notification(db: Session, report: Report) -> int:
|
||||
"""
|
||||
發送延遲發布通知
|
||||
|
||||
Returns:
|
||||
發送成功數量
|
||||
"""
|
||||
subscriptions = db.query(Subscription).filter(
|
||||
Subscription.group_id == report.group_id,
|
||||
Subscription.email_notify == True
|
||||
).all()
|
||||
|
||||
sent_count = 0
|
||||
|
||||
for sub in subscriptions:
|
||||
user = db.query(User).filter(User.id == sub.user_id).first()
|
||||
if not user or not user.email or not user.is_active:
|
||||
continue
|
||||
|
||||
# 轉義 HTML 特殊字元,防止 XSS
|
||||
safe_group_name = escape(report.group.name)
|
||||
html_content = f"""
|
||||
<html>
|
||||
<body>
|
||||
<h2>報告延遲通知</h2>
|
||||
<p>您訂閱的「{safe_group_name}」今日報告延遲發布,敬請稍後。</p>
|
||||
<p>造成不便,敬請見諒。</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
success = send_email(
|
||||
user.email,
|
||||
f"【每日報導】{report.group.name} 報告延遲通知",
|
||||
html_content
|
||||
)
|
||||
|
||||
if success:
|
||||
sent_count += 1
|
||||
|
||||
return sent_count
|
||||
Reference in New Issue
Block a user