""" 報告管理 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"} )