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

View File

@@ -0,0 +1,320 @@
"""
報告管理 API 端點
"""
from datetime import date, datetime
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
import io
from app.db.session import get_db
from app.models import User, Report, ReportArticle, Group, NewsArticle, Favorite, Comment
from app.schemas.report import (
ReportUpdate, ReportResponse, ReportDetailResponse, ReportReviewResponse,
ReportListResponse, PublishResponse, RegenerateSummaryResponse,
ArticleInReport, GroupBrief
)
from app.schemas.user import PaginationResponse
from app.api.v1.endpoints.auth import get_current_user, require_roles
from app.services.llm_service import generate_summary
from app.services.notification_service import send_report_notifications
router = APIRouter()
@router.get("", response_model=ReportListResponse)
def list_reports(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
group_id: Optional[int] = None,
status: Optional[str] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""取得報告列表"""
query = db.query(Report).join(Group)
if group_id:
query = query.filter(Report.group_id == group_id)
if status:
query = query.filter(Report.status == status)
if date_from:
query = query.filter(Report.report_date >= date_from)
if date_to:
query = query.filter(Report.report_date <= date_to)
# 讀者只能看到已發布的報告
if current_user.role.code == "reader":
query = query.filter(Report.status == "published")
total = query.count()
reports = query.order_by(Report.report_date.desc()).offset((page - 1) * limit).limit(limit).all()
result = []
for r in reports:
article_count = db.query(ReportArticle).filter(
ReportArticle.report_id == r.id,
ReportArticle.is_included == True
).count()
result.append(ReportResponse(
id=r.id,
title=r.title,
report_date=r.report_date,
status=r.status.value,
group=GroupBrief(id=r.group.id, name=r.group.name, category=r.group.category.value),
article_count=article_count,
published_at=r.published_at
))
return ReportListResponse(
data=result,
pagination=PaginationResponse(
page=page, limit=limit, total=total,
total_pages=(total + limit - 1) // limit
)
)
@router.get("/today", response_model=list[ReportReviewResponse])
def get_today_reports(
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""取得今日報告(專員審核用)"""
today = date.today()
reports = db.query(Report).filter(Report.report_date == today).all()
result = []
for r in reports:
report_articles = db.query(ReportArticle).filter(ReportArticle.report_id == r.id).all()
articles = []
for ra in report_articles:
article = db.query(NewsArticle).filter(NewsArticle.id == ra.article_id).first()
if article:
articles.append(ArticleInReport(
id=article.id,
title=article.title,
source_name=article.source.name,
url=article.url,
published_at=article.published_at,
is_included=ra.is_included
))
result.append(ReportReviewResponse(
id=r.id,
title=r.title,
report_date=r.report_date,
status=r.status.value,
group=GroupBrief(id=r.group.id, name=r.group.name, category=r.group.category.value),
article_count=len([a for a in articles if a.is_included]),
published_at=r.published_at,
ai_summary=r.ai_summary,
edited_summary=r.edited_summary,
articles=articles
))
return result
@router.get("/{report_id}", response_model=ReportDetailResponse)
def get_report(
report_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""取得報告詳情"""
report = db.query(Report).filter(Report.id == report_id).first()
if not report:
raise HTTPException(status_code=404, detail="報告不存在")
# 讀者只能看已發布的報告
if current_user.role.code == "reader" and report.status.value != "published":
raise HTTPException(status_code=403, detail="無權限查看此報告")
# 取得文章
report_articles = db.query(ReportArticle).filter(ReportArticle.report_id == report_id).all()
articles = []
for ra in report_articles:
article = db.query(NewsArticle).filter(NewsArticle.id == ra.article_id).first()
if article:
articles.append(ArticleInReport(
id=article.id,
title=article.title,
source_name=article.source.name,
url=article.url,
published_at=article.published_at,
is_included=ra.is_included
))
# 檢查是否已收藏
is_favorited = db.query(Favorite).filter(
Favorite.user_id == current_user.id,
Favorite.report_id == report_id
).first() is not None
# 留言數
comment_count = db.query(Comment).filter(
Comment.report_id == report_id,
Comment.is_deleted == False
).count()
return ReportDetailResponse(
id=report.id,
title=report.title,
report_date=report.report_date,
status=report.status.value,
group=GroupBrief(id=report.group.id, name=report.group.name, category=report.group.category.value),
article_count=len([a for a in articles if a.is_included]),
published_at=report.published_at,
ai_summary=report.ai_summary,
edited_summary=report.edited_summary,
articles=articles,
is_favorited=is_favorited,
comment_count=comment_count,
created_at=report.created_at,
updated_at=report.updated_at
)
@router.put("/{report_id}", response_model=ReportResponse)
def update_report(
report_id: int,
report_in: ReportUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""更新報告"""
report = db.query(Report).filter(Report.id == report_id).first()
if not report:
raise HTTPException(status_code=404, detail="報告不存在")
if report_in.title:
report.title = report_in.title
if report_in.edited_summary is not None:
report.edited_summary = report_in.edited_summary
# 更新文章篩選
if report_in.article_selections:
for sel in report_in.article_selections:
ra = db.query(ReportArticle).filter(
ReportArticle.report_id == report_id,
ReportArticle.article_id == sel["article_id"]
).first()
if ra:
ra.is_included = sel["is_included"]
db.commit()
db.refresh(report)
article_count = db.query(ReportArticle).filter(
ReportArticle.report_id == report_id,
ReportArticle.is_included == True
).count()
return ReportResponse(
id=report.id,
title=report.title,
report_date=report.report_date,
status=report.status.value,
group=GroupBrief(id=report.group.id, name=report.group.name, category=report.group.category.value),
article_count=article_count,
published_at=report.published_at
)
@router.post("/{report_id}/publish", response_model=PublishResponse)
def publish_report(
report_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""發布報告"""
report = db.query(Report).filter(Report.id == report_id).first()
if not report:
raise HTTPException(status_code=404, detail="報告不存在")
if report.status.value == "published":
raise HTTPException(status_code=400, detail="報告已發布")
report.status = "published"
report.published_at = datetime.utcnow()
report.published_by = current_user.id
db.commit()
# 發送通知
notifications_sent = send_report_notifications(db, report)
return PublishResponse(
published_at=report.published_at,
notifications_sent=notifications_sent
)
@router.post("/{report_id}/regenerate-summary", response_model=RegenerateSummaryResponse)
def regenerate_summary(
report_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""重新產生 AI 摘要"""
report = db.query(Report).filter(Report.id == report_id).first()
if not report:
raise HTTPException(status_code=404, detail="報告不存在")
# 取得納入的文章
report_articles = db.query(ReportArticle).filter(
ReportArticle.report_id == report_id,
ReportArticle.is_included == True
).all()
articles = []
for ra in report_articles:
article = db.query(NewsArticle).filter(NewsArticle.id == ra.article_id).first()
if article:
articles.append(article)
# 產生摘要
summary = generate_summary(report.group, articles)
report.ai_summary = summary
db.commit()
return RegenerateSummaryResponse(ai_summary=summary)
@router.get("/{report_id}/export")
def export_report_pdf(
report_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""匯出報告 PDF"""
report = db.query(Report).filter(Report.id == report_id).first()
if not report:
raise HTTPException(status_code=404, detail="報告不存在")
# 讀者只能匯出已發布的報告
if current_user.role.code == "reader" and report.status.value != "published":
raise HTTPException(status_code=403, detail="無權限匯出此報告")
# TODO: 實作 PDF 生成
# 暫時返回簡單文字
content = f"""
{report.title}
日期:{report.report_date}
群組:{report.group.name}
{report.edited_summary or report.ai_summary or '無摘要內容'}
"""
buffer = io.BytesIO(content.encode('utf-8'))
return StreamingResponse(
buffer,
media_type="text/plain",
headers={"Content-Disposition": f"attachment; filename=report_{report_id}.txt"}
)