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:
donald
2025-12-03 23:53:24 +08:00
commit db0f0bbfe7
50 changed files with 11883 additions and 0 deletions

25
app/models/__init__.py Normal file
View File

@@ -0,0 +1,25 @@
"""
資料模型模組
匯出所有 SQLAlchemy 模型
"""
from app.models.user import User, Role, AuthType
from app.models.news import NewsSource, NewsArticle, CrawlJob, SourceType, CrawlStatus
from app.models.group import Group, Keyword, ArticleGroupMatch, GroupCategory
from app.models.report import Report, ReportArticle, ReportStatus
from app.models.interaction import Subscription, Favorite, Comment, Note
from app.models.system import SystemSetting, AuditLog, NotificationLog, SettingType, NotificationType, NotificationStatus
__all__ = [
# User
"User", "Role", "AuthType",
# News
"NewsSource", "NewsArticle", "CrawlJob", "SourceType", "CrawlStatus",
# Group
"Group", "Keyword", "ArticleGroupMatch", "GroupCategory",
# Report
"Report", "ReportArticle", "ReportStatus",
# Interaction
"Subscription", "Favorite", "Comment", "Note",
# System
"SystemSetting", "AuditLog", "NotificationLog", "SettingType", "NotificationType", "NotificationStatus",
]

82
app/models/group.py Normal file
View File

@@ -0,0 +1,82 @@
"""
群組與關鍵字資料模型
"""
from datetime import datetime
from sqlalchemy import String, Boolean, ForeignKey, Text, JSON, Enum as SQLEnum, UniqueConstraint, Index, DECIMAL
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import Optional, List
import enum
from app.db.session import Base
class GroupCategory(str, enum.Enum):
"""群組分類"""
INDUSTRY = "industry"
TOPIC = "topic"
class Group(Base):
"""群組表"""
__tablename__ = "groups"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="群組名稱")
description: Mapped[Optional[str]] = mapped_column(Text, comment="群組描述")
category: Mapped[GroupCategory] = mapped_column(SQLEnum(GroupCategory), nullable=False, comment="分類")
ai_background: Mapped[Optional[str]] = mapped_column(Text, comment="AI背景資訊設定")
ai_prompt: Mapped[Optional[str]] = mapped_column(Text, comment="AI摘要方向提示")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否啟用")
created_by: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), comment="建立者ID")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
# 關聯
keywords: Mapped[List["Keyword"]] = relationship(back_populates="group", cascade="all, delete-orphan")
article_matches: Mapped[List["ArticleGroupMatch"]] = relationship(back_populates="group", cascade="all, delete-orphan")
reports: Mapped[List["Report"]] = relationship(back_populates="group")
subscriptions: Mapped[List["Subscription"]] = relationship(back_populates="group", cascade="all, delete-orphan")
class Keyword(Base):
"""關鍵字表"""
__tablename__ = "keywords"
__table_args__ = (
UniqueConstraint("group_id", "keyword", name="uk_group_keyword"),
Index("idx_keywords_keyword", "keyword"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
group_id: Mapped[int] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), nullable=False, comment="所屬群組ID")
keyword: Mapped[str] = mapped_column(String(100), nullable=False, comment="關鍵字")
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# 關聯
group: Mapped["Group"] = relationship(back_populates="keywords")
class ArticleGroupMatch(Base):
"""新聞-群組匹配關聯表"""
__tablename__ = "article_group_matches"
__table_args__ = (
UniqueConstraint("article_id", "group_id", name="uk_article_group"),
Index("idx_matches_group", "group_id"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
article_id: Mapped[int] = mapped_column(ForeignKey("news_articles.id", ondelete="CASCADE"), nullable=False)
group_id: Mapped[int] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), nullable=False)
matched_keywords: Mapped[Optional[list]] = mapped_column(JSON, comment="匹配到的關鍵字列表")
match_score: Mapped[Optional[float]] = mapped_column(DECIMAL(5, 2), comment="匹配分數")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# 關聯
article: Mapped["NewsArticle"] = relationship(back_populates="group_matches")
group: Mapped["Group"] = relationship(back_populates="article_matches")
# 避免循環引入
from app.models.news import NewsArticle
from app.models.report import Report
from app.models.interaction import Subscription

90
app/models/interaction.py Normal file
View File

@@ -0,0 +1,90 @@
"""
讀者互動資料模型(訂閱、收藏、留言、筆記)
"""
from datetime import datetime
from sqlalchemy import String, Boolean, ForeignKey, Text, UniqueConstraint, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import Optional, List, TYPE_CHECKING
from app.db.session import Base
if TYPE_CHECKING:
from app.models.user import User
from app.models.group import Group
from app.models.report import Report
class Subscription(Base):
"""訂閱表"""
__tablename__ = "subscriptions"
__table_args__ = (
UniqueConstraint("user_id", "group_id", name="uk_user_group"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
group_id: Mapped[int] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), nullable=False)
email_notify: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否Email通知")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# 關聯
user: Mapped["User"] = relationship(back_populates="subscriptions")
group: Mapped["Group"] = relationship(back_populates="subscriptions")
class Favorite(Base):
"""收藏表"""
__tablename__ = "favorites"
__table_args__ = (
UniqueConstraint("user_id", "report_id", name="uk_user_report"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
report_id: Mapped[int] = mapped_column(ForeignKey("reports.id", ondelete="CASCADE"), nullable=False)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# 關聯
user: Mapped["User"] = relationship(back_populates="favorites")
report: Mapped["Report"] = relationship(back_populates="favorites")
class Comment(Base):
"""留言表"""
__tablename__ = "comments"
__table_args__ = (
Index("idx_comments_report", "report_id"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
report_id: Mapped[int] = mapped_column(ForeignKey("reports.id", ondelete="CASCADE"), nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False, comment="留言內容")
parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("comments.id"), comment="父留言ID")
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
# 關聯
report: Mapped["Report"] = relationship(back_populates="comments")
user: Mapped["User"] = relationship(back_populates="comments")
parent: Mapped[Optional["Comment"]] = relationship(remote_side=[id], backref="replies")
class Note(Base):
"""個人筆記表"""
__tablename__ = "notes"
__table_args__ = (
Index("idx_notes_user_report", "user_id", "report_id"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
report_id: Mapped[int] = mapped_column(ForeignKey("reports.id", ondelete="CASCADE"), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False, comment="筆記內容")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
# 關聯
user: Mapped["User"] = relationship(back_populates="notes")
report: Mapped["Report"] = relationship(back_populates="notes")

100
app/models/news.py Normal file
View File

@@ -0,0 +1,100 @@
"""
新聞來源與文章資料模型
"""
from datetime import datetime
from sqlalchemy import String, Boolean, ForeignKey, Text, JSON, Enum as SQLEnum, UniqueConstraint, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import Optional, List
import enum
from app.db.session import Base
class SourceType(str, enum.Enum):
"""來源類型"""
SUBSCRIPTION = "subscription"
PUBLIC = "public"
class CrawlStatus(str, enum.Enum):
"""抓取任務狀態"""
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class NewsSource(Base):
"""新聞來源表"""
__tablename__ = "news_sources"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(30), unique=True, nullable=False, comment="來源代碼")
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="來源名稱")
base_url: Mapped[str] = mapped_column(String(255), nullable=False, comment="網站基礎URL")
source_type: Mapped[SourceType] = mapped_column(SQLEnum(SourceType), nullable=False, comment="來源類型")
login_username: Mapped[Optional[str]] = mapped_column(String(100), comment="登入帳號")
login_password_encrypted: Mapped[Optional[str]] = mapped_column(String(255), comment="加密後密碼")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否啟用")
crawl_config: Mapped[Optional[dict]] = mapped_column(JSON, comment="爬蟲設定")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
# 關聯
articles: Mapped[List["NewsArticle"]] = relationship(back_populates="source")
crawl_jobs: Mapped[List["CrawlJob"]] = relationship(back_populates="source")
class NewsArticle(Base):
"""新聞文章表"""
__tablename__ = "news_articles"
__table_args__ = (
UniqueConstraint("source_id", "external_id", name="uk_source_external"),
Index("idx_articles_published", "published_at"),
Index("idx_articles_crawled", "crawled_at"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
source_id: Mapped[int] = mapped_column(ForeignKey("news_sources.id"), nullable=False, comment="來源ID")
external_id: Mapped[Optional[str]] = mapped_column(String(100), comment="外部文章ID")
title: Mapped[str] = mapped_column(String(500), nullable=False, comment="文章標題")
content: Mapped[Optional[str]] = mapped_column(Text, comment="文章全文")
summary: Mapped[Optional[str]] = mapped_column(Text, comment="原文摘要")
url: Mapped[str] = mapped_column(String(500), nullable=False, comment="原文連結")
author: Mapped[Optional[str]] = mapped_column(String(100), comment="作者")
published_at: Mapped[Optional[datetime]] = mapped_column(comment="發布時間")
crawled_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, comment="抓取時間")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# 關聯
source: Mapped["NewsSource"] = relationship(back_populates="articles")
group_matches: Mapped[List["ArticleGroupMatch"]] = relationship(back_populates="article", cascade="all, delete-orphan")
report_articles: Mapped[List["ReportArticle"]] = relationship(back_populates="article")
class CrawlJob(Base):
"""抓取任務記錄表"""
__tablename__ = "crawl_jobs"
__table_args__ = (
Index("idx_crawl_jobs_status", "status"),
Index("idx_crawl_jobs_scheduled", "scheduled_at"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
source_id: Mapped[int] = mapped_column(ForeignKey("news_sources.id"), nullable=False, comment="來源ID")
status: Mapped[CrawlStatus] = mapped_column(SQLEnum(CrawlStatus), default=CrawlStatus.PENDING)
scheduled_at: Mapped[datetime] = mapped_column(nullable=False, comment="排程時間")
started_at: Mapped[Optional[datetime]] = mapped_column(comment="開始時間")
completed_at: Mapped[Optional[datetime]] = mapped_column(comment="完成時間")
articles_count: Mapped[int] = mapped_column(default=0, comment="抓取文章數")
error_message: Mapped[Optional[str]] = mapped_column(Text, comment="錯誤訊息")
retry_count: Mapped[int] = mapped_column(default=0, comment="重試次數")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# 關聯
source: Mapped["NewsSource"] = relationship(back_populates="crawl_jobs")
# 避免循環引入
from app.models.group import ArticleGroupMatch
from app.models.report import ReportArticle

79
app/models/report.py Normal file
View File

@@ -0,0 +1,79 @@
"""
報告資料模型
"""
from datetime import datetime, date
from sqlalchemy import String, Boolean, ForeignKey, Text, Date, Enum as SQLEnum, UniqueConstraint, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import Optional, List
import enum
from app.db.session import Base
class ReportStatus(str, enum.Enum):
"""報告狀態"""
DRAFT = "draft"
PENDING = "pending"
PUBLISHED = "published"
DELAYED = "delayed"
class Report(Base):
"""報告表"""
__tablename__ = "reports"
__table_args__ = (
UniqueConstraint("group_id", "report_date", name="uk_group_date"),
Index("idx_reports_status", "status"),
Index("idx_reports_date", "report_date"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
group_id: Mapped[int] = mapped_column(ForeignKey("groups.id"), nullable=False, comment="所屬群組ID")
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="報告標題")
report_date: Mapped[date] = mapped_column(Date, nullable=False, comment="報告日期")
ai_summary: Mapped[Optional[str]] = mapped_column(Text, comment="AI綜合摘要")
edited_summary: Mapped[Optional[str]] = mapped_column(Text, comment="編輯後摘要")
status: Mapped[ReportStatus] = mapped_column(SQLEnum(ReportStatus), default=ReportStatus.DRAFT, comment="狀態")
published_at: Mapped[Optional[datetime]] = mapped_column(comment="發布時間")
published_by: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), comment="發布者ID")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
# 關聯
group: Mapped["Group"] = relationship(back_populates="reports")
report_articles: Mapped[List["ReportArticle"]] = relationship(back_populates="report", cascade="all, delete-orphan")
favorites: Mapped[List["Favorite"]] = relationship(back_populates="report", cascade="all, delete-orphan")
comments: Mapped[List["Comment"]] = relationship(back_populates="report", cascade="all, delete-orphan")
notes: Mapped[List["Note"]] = relationship(back_populates="report", cascade="all, delete-orphan")
notifications: Mapped[List["NotificationLog"]] = relationship(back_populates="report")
@property
def final_summary(self) -> str:
"""取得最終摘要(優先使用編輯後版本)"""
return self.edited_summary or self.ai_summary or ""
class ReportArticle(Base):
"""報告-新聞關聯表"""
__tablename__ = "report_articles"
__table_args__ = (
UniqueConstraint("report_id", "article_id", name="uk_report_article"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
report_id: Mapped[int] = mapped_column(ForeignKey("reports.id", ondelete="CASCADE"), nullable=False)
article_id: Mapped[int] = mapped_column(ForeignKey("news_articles.id"), nullable=False)
is_included: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否納入報告")
display_order: Mapped[int] = mapped_column(default=0, comment="顯示順序")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# 關聯
report: Mapped["Report"] = relationship(back_populates="report_articles")
article: Mapped["NewsArticle"] = relationship(back_populates="report_articles")
# 避免循環引入
from app.models.group import Group
from app.models.news import NewsArticle
from app.models.interaction import Favorite, Comment, Note
from app.models.system import NotificationLog

103
app/models/system.py Normal file
View File

@@ -0,0 +1,103 @@
"""
系統設定與日誌資料模型
"""
from datetime import datetime
from sqlalchemy import String, ForeignKey, Text, JSON, Enum as SQLEnum, Index
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import Optional, TYPE_CHECKING
import enum
from app.db.session import Base
if TYPE_CHECKING:
from app.models.user import User
from app.models.report import Report
class SettingType(str, enum.Enum):
"""設定值類型"""
STRING = "string"
NUMBER = "number"
BOOLEAN = "boolean"
JSON = "json"
class NotificationType(str, enum.Enum):
"""通知類型"""
EMAIL = "email"
SYSTEM = "system"
class NotificationStatus(str, enum.Enum):
"""通知狀態"""
PENDING = "pending"
SENT = "sent"
FAILED = "failed"
class SystemSetting(Base):
"""系統設定表"""
__tablename__ = "system_settings"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
setting_key: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="設定鍵")
setting_value: Mapped[Optional[str]] = mapped_column(Text, comment="設定值")
setting_type: Mapped[SettingType] = mapped_column(SQLEnum(SettingType), default=SettingType.STRING)
description: Mapped[Optional[str]] = mapped_column(String(200), comment="設定描述")
updated_by: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), comment="更新者ID")
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
def get_value(self):
"""取得轉換後的設定值"""
if self.setting_value is None:
return None
if self.setting_type == SettingType.NUMBER:
return float(self.setting_value) if '.' in self.setting_value else int(self.setting_value)
if self.setting_type == SettingType.BOOLEAN:
return self.setting_value.lower() in ('true', '1', 'yes')
if self.setting_type == SettingType.JSON:
import json
return json.loads(self.setting_value)
return self.setting_value
class AuditLog(Base):
"""操作日誌表"""
__tablename__ = "audit_logs"
__table_args__ = (
Index("idx_audit_user", "user_id"),
Index("idx_audit_action", "action"),
Index("idx_audit_created", "created_at"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[Optional[int]] = mapped_column(ForeignKey("users.id"), comment="操作用戶ID")
action: Mapped[str] = mapped_column(String(50), nullable=False, comment="操作類型")
target_type: Mapped[Optional[str]] = mapped_column(String(50), comment="目標類型")
target_id: Mapped[Optional[str]] = mapped_column(String(50), comment="目標ID")
details: Mapped[Optional[dict]] = mapped_column(JSON, comment="操作詳情")
ip_address: Mapped[Optional[str]] = mapped_column(String(45), comment="IP地址")
user_agent: Mapped[Optional[str]] = mapped_column(String(500), comment="User Agent")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
class NotificationLog(Base):
"""通知記錄表"""
__tablename__ = "notification_logs"
__table_args__ = (
Index("idx_notification_status", "status"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
report_id: Mapped[Optional[int]] = mapped_column(ForeignKey("reports.id"), comment="關聯報告ID")
notification_type: Mapped[NotificationType] = mapped_column(SQLEnum(NotificationType), default=NotificationType.EMAIL)
subject: Mapped[Optional[str]] = mapped_column(String(200), comment="通知標題")
content: Mapped[Optional[str]] = mapped_column(Text, comment="通知內容")
status: Mapped[NotificationStatus] = mapped_column(SQLEnum(NotificationStatus), default=NotificationStatus.PENDING)
sent_at: Mapped[Optional[datetime]] = mapped_column()
error_message: Mapped[Optional[str]] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# 關聯
report: Mapped[Optional["Report"]] = relationship(back_populates="notifications")

59
app/models/user.py Normal file
View File

@@ -0,0 +1,59 @@
"""
用戶與角色資料模型
"""
from datetime import datetime
from sqlalchemy import String, Boolean, ForeignKey, Text, Enum as SQLEnum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import Optional, List
import enum
from app.db.session import Base
class AuthType(str, enum.Enum):
"""認證類型"""
AD = "ad"
LOCAL = "local"
class Role(Base):
"""角色表"""
__tablename__ = "roles"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(20), unique=True, nullable=False, comment="角色代碼")
name: Mapped[str] = mapped_column(String(50), nullable=False, comment="角色名稱")
description: Mapped[Optional[str]] = mapped_column(String(200), comment="角色描述")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
# 關聯
users: Mapped[List["User"]] = relationship(back_populates="role")
class User(Base):
"""用戶表"""
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True, comment="用戶帳號")
password_hash: Mapped[Optional[str]] = mapped_column(String(255), comment="密碼雜湊")
display_name: Mapped[str] = mapped_column(String(100), nullable=False, comment="顯示名稱")
email: Mapped[Optional[str]] = mapped_column(String(100), comment="電子郵件")
auth_type: Mapped[AuthType] = mapped_column(SQLEnum(AuthType), default=AuthType.LOCAL, nullable=False, comment="認證類型")
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"), nullable=False, comment="角色ID")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否啟用")
last_login_at: Mapped[Optional[datetime]] = mapped_column(comment="最後登入時間")
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(default=datetime.utcnow, onupdate=datetime.utcnow)
# 關聯
role: Mapped["Role"] = relationship(back_populates="users")
subscriptions: Mapped[List["Subscription"]] = relationship(back_populates="user", cascade="all, delete-orphan")
favorites: Mapped[List["Favorite"]] = relationship(back_populates="user", cascade="all, delete-orphan")
comments: Mapped[List["Comment"]] = relationship(back_populates="user")
notes: Mapped[List["Note"]] = relationship(back_populates="user", cascade="all, delete-orphan")
# 避免循環引入
from app.models.interaction import Subscription, Favorite, Comment, Note