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:
320
app/api/v1/endpoints/reports.py
Normal file
320
app/api/v1/endpoints/reports.py
Normal 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"}
|
||||
)
|
||||
Reference in New Issue
Block a user