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:
25
app/models/__init__.py
Normal file
25
app/models/__init__.py
Normal 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
82
app/models/group.py
Normal 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
90
app/models/interaction.py
Normal 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
100
app/models/news.py
Normal 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
79
app/models/report.py
Normal 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
103
app/models/system.py
Normal 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
59
app/models/user.py
Normal 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
|
||||
Reference in New Issue
Block a user