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,239 @@
"""
群組管理 API 端點
"""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import func
from app.db.session import get_db
from app.models import User, Group, Keyword, Subscription
from app.schemas.group import (
GroupCreate, GroupUpdate, GroupResponse, GroupDetailResponse,
GroupListResponse, KeywordCreate, KeywordResponse
)
from app.schemas.user import PaginationResponse
from app.api.v1.endpoints.auth import get_current_user, require_roles
router = APIRouter()
@router.get("", response_model=GroupListResponse)
def list_groups(
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
category: Optional[str] = None,
active_only: bool = True,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""取得群組列表"""
query = db.query(Group)
if category:
query = query.filter(Group.category == category)
if active_only:
query = query.filter(Group.is_active == True)
total = query.count()
groups = query.offset((page - 1) * limit).limit(limit).all()
# 計算關鍵字數和訂閱數
result = []
for g in groups:
keyword_count = db.query(Keyword).filter(Keyword.group_id == g.id).count()
subscriber_count = db.query(Subscription).filter(Subscription.group_id == g.id).count()
group_dict = {
"id": g.id,
"name": g.name,
"description": g.description,
"category": g.category.value,
"is_active": g.is_active,
"keyword_count": keyword_count,
"subscriber_count": subscriber_count
}
result.append(GroupResponse(**group_dict))
return GroupListResponse(
data=result,
pagination=PaginationResponse(
page=page, limit=limit, total=total,
total_pages=(total + limit - 1) // limit
)
)
@router.post("", response_model=GroupResponse, status_code=status.HTTP_201_CREATED)
def create_group(
group_in: GroupCreate,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""新增群組"""
group = Group(
name=group_in.name,
description=group_in.description,
category=group_in.category,
ai_background=group_in.ai_background,
ai_prompt=group_in.ai_prompt,
created_by=current_user.id
)
db.add(group)
db.commit()
db.refresh(group)
# 新增關鍵字
if group_in.keywords:
for kw in group_in.keywords:
keyword = Keyword(group_id=group.id, keyword=kw)
db.add(keyword)
db.commit()
return GroupResponse(
id=group.id,
name=group.name,
description=group.description,
category=group.category.value,
is_active=group.is_active,
keyword_count=len(group_in.keywords) if group_in.keywords else 0,
subscriber_count=0
)
@router.get("/{group_id}", response_model=GroupDetailResponse)
def get_group(
group_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""取得群組詳情"""
group = db.query(Group).filter(Group.id == group_id).first()
if not group:
raise HTTPException(status_code=404, detail="群組不存在")
keywords = db.query(Keyword).filter(Keyword.group_id == group_id).all()
keyword_count = len(keywords)
subscriber_count = db.query(Subscription).filter(Subscription.group_id == group_id).count()
return GroupDetailResponse(
id=group.id,
name=group.name,
description=group.description,
category=group.category.value,
is_active=group.is_active,
ai_background=group.ai_background,
ai_prompt=group.ai_prompt,
keywords=[KeywordResponse.model_validate(k) for k in keywords],
keyword_count=keyword_count,
subscriber_count=subscriber_count,
created_at=group.created_at,
updated_at=group.updated_at
)
@router.put("/{group_id}", response_model=GroupResponse)
def update_group(
group_id: int,
group_in: GroupUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""更新群組"""
group = db.query(Group).filter(Group.id == group_id).first()
if not group:
raise HTTPException(status_code=404, detail="群組不存在")
for field, value in group_in.model_dump(exclude_unset=True).items():
setattr(group, field, value)
db.commit()
db.refresh(group)
keyword_count = db.query(Keyword).filter(Keyword.group_id == group_id).count()
subscriber_count = db.query(Subscription).filter(Subscription.group_id == group_id).count()
return GroupResponse(
id=group.id,
name=group.name,
description=group.description,
category=group.category.value,
is_active=group.is_active,
keyword_count=keyword_count,
subscriber_count=subscriber_count
)
@router.delete("/{group_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_group(
group_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin"))
):
"""刪除群組"""
group = db.query(Group).filter(Group.id == group_id).first()
if not group:
raise HTTPException(status_code=404, detail="群組不存在")
db.delete(group)
db.commit()
# ===== 關鍵字管理 =====
@router.get("/{group_id}/keywords", response_model=list[KeywordResponse])
def list_keywords(
group_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""取得群組關鍵字"""
keywords = db.query(Keyword).filter(Keyword.group_id == group_id).all()
return [KeywordResponse.model_validate(k) for k in keywords]
@router.post("/{group_id}/keywords", response_model=KeywordResponse, status_code=status.HTTP_201_CREATED)
def add_keyword(
group_id: int,
keyword_in: KeywordCreate,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""新增關鍵字"""
group = db.query(Group).filter(Group.id == group_id).first()
if not group:
raise HTTPException(status_code=404, detail="群組不存在")
# 檢查重複
existing = db.query(Keyword).filter(
Keyword.group_id == group_id,
Keyword.keyword == keyword_in.keyword
).first()
if existing:
raise HTTPException(status_code=400, detail="關鍵字已存在")
keyword = Keyword(group_id=group_id, keyword=keyword_in.keyword)
db.add(keyword)
db.commit()
db.refresh(keyword)
return keyword
@router.delete("/{group_id}/keywords/{keyword_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_keyword(
group_id: int,
keyword_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""刪除關鍵字"""
keyword = db.query(Keyword).filter(
Keyword.id == keyword_id,
Keyword.group_id == group_id
).first()
if not keyword:
raise HTTPException(status_code=404, detail="關鍵字不存在")
db.delete(keyword)
db.commit()