企業內部新聞彙整與分析系統 - 自動新聞抓取 (Digitimes, 經濟日報, 工商時報) - AI 智慧摘要 (OpenAI/Claude/Ollama) - 群組管理與訂閱通知 - 已清理 Python 快取檔案 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
321 lines
11 KiB
Python
321 lines
11 KiB
Python
"""
|
|
報告管理 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"}
|
|
)
|