Files
yu-zhi-ran/platform/backend/app/api/calendar.py
T
Yuzhiran Dev 9c37c9a574 feat: Phase 4 多租户隔离 + 四阶段升级测试 + CSS 统一化
Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列
测试: tests/test_phase_upgrades.py 97项全覆盖
CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式
修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
2026-05-17 06:56:53 +08:00

231 lines
7.5 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime, date
from calendar import monthrange
from ..database import get_db
from ..models import ContentCalendar, Topic
from ..schemas import (
ContentCalendarCreate, ContentCalendarUpdate, ContentCalendarResponse,
ContentCalendarBase
)
from .auth import get_current_user, org_filter
router = APIRouter(prefix="/api/calendar", tags=["calendar"])
PLATFORM_ICONS = {
"zhihu": "",
"wechat": "",
"xiaohongshu": ""
}
def enrich_entry(entry, db):
d = entry.to_dict()
d["platform_icon"] = PLATFORM_ICONS.get(entry.platform, "📝")
d["topic_status"] = None
if entry.topic_id:
topic = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic:
d["topic_status"] = topic.status
return d
@router.get("", response_model=List[ContentCalendarResponse])
def get_calendar(
year: Optional[int] = Query(None),
month: Optional[int] = Query(None),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
today = date.today()
year = year or today.year
month = month or today.month
start = date(year, month, 1)
last_day = monthrange(year, month)[1]
end = date(year, month, last_day)
base = db.query(ContentCalendar).filter(
ContentCalendar.planned_date >= start,
ContentCalendar.planned_date <= end
)
of = org_filter(current_user, Topic)
if of is not True:
base = base.outerjoin(Topic, ContentCalendar.topic_id == Topic.id).filter(
(ContentCalendar.topic_id.is_(None)) | (Topic.org_id == current_user.org_id)
)
entries = base.order_by(ContentCalendar.planned_date).all()
return [enrich_entry(e, db) for e in entries]
@router.get("/entries", response_model=List[ContentCalendarResponse])
def list_entries(
start_date: Optional[date] = None,
end_date: Optional[date] = None,
status: Optional[str] = None,
platform: Optional[str] = None,
limit: int = Query(50, le=200),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
query = db.query(ContentCalendar)
of = org_filter(current_user, Topic)
if of is not True:
query = query.outerjoin(Topic, ContentCalendar.topic_id == Topic.id).filter(
(ContentCalendar.topic_id.is_(None)) | (Topic.org_id == current_user.org_id)
)
if start_date:
query = query.filter(ContentCalendar.planned_date >= start_date)
if end_date:
query = query.filter(ContentCalendar.planned_date <= end_date)
if status:
query = query.filter(ContentCalendar.status == status)
if platform:
query = query.filter(ContentCalendar.platform == platform)
return query.order_by(ContentCalendar.planned_date.desc()).limit(limit).all()
@router.post("/entries", response_model=ContentCalendarResponse)
def create_entry(
data: ContentCalendarCreate,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
if data.topic_id:
topic = db.query(Topic).filter(Topic.id == data.topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="选题不存在")
if current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="选题不存在")
entry = ContentCalendar(**data.model_dump())
db.add(entry)
db.commit()
db.refresh(entry)
if data.topic_id and data.title:
if not topic.title or topic.title != data.title:
topic.title = data.title
db.commit()
return entry
@router.put("/entries/{entry_id}", response_model=ContentCalendarResponse)
def update_entry(
entry_id: int,
data: ContentCalendarUpdate,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="日历条目不存在")
for k, v in data.model_dump(exclude_unset=True).items():
setattr(entry, k, v)
db.commit()
db.refresh(entry)
if entry.topic_id:
topic = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic and entry.status == "published" and not entry.published_date:
entry.published_date = date.today()
if topic.status == "pending" or topic.status == "ready":
topic.status = "published"
topic.published_at = date.today()
topic.updated_at = datetime.now()
db.commit()
return entry
@router.delete("/entries/{entry_id}")
def delete_entry(
entry_id: int,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="日历条目不存在")
db.delete(entry)
db.commit()
return {"ok": True}
@router.post("/entries/bind-topic")
def bind_topic_to_entry(
entry_id: int,
topic_id: str,
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first()
if not entry:
raise HTTPException(status_code=404, detail="日历条目不存在")
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="选题不存在")
if current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="选题不存在")
entry.topic_id = topic_id
entry.title = topic.title
if topic.field_name:
entry.field_id = topic.field_id
db.commit()
db.refresh(entry)
return entry
@router.get("/stats")
def calendar_stats(
year: int = Query(...),
month: int = Query(...),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
start = date(year, month, 1)
last_day = monthrange(year, month)[1]
end = date(year, month, last_day)
q = db.query(ContentCalendar).filter(
ContentCalendar.planned_date >= start,
ContentCalendar.planned_date <= end
)
of = org_filter(current_user, Topic)
if of is not True:
q = q.outerjoin(Topic, ContentCalendar.topic_id == Topic.id).filter(
(ContentCalendar.topic_id.is_(None)) | (Topic.org_id == current_user.org_id)
)
entries = q.all()
stats = {"total": len(entries), "planned": 0, "published": 0, "delayed": 0, "cancelled": 0}
for e in entries:
if e.status in stats:
stats[e.status] += 1
return stats
@router.post("/entries/from-topic")
def create_from_topic(
topic_id: str,
planned_date: date,
platform: str = Query(...),
db: Session = Depends(get_db),
current_user=Depends(get_current_user)
):
topic = db.query(Topic).filter(Topic.id == topic_id).first()
if not topic:
raise HTTPException(status_code=404, detail="选题不存在")
if current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="选题不存在")
entry = ContentCalendar(
topic_id=topic_id,
field_id=topic.field_id,
title=topic.title,
planned_date=planned_date,
platform=platform,
created_by=current_user.username
)
db.add(entry)
db.commit()
db.refresh(entry)
return entry