Files
Yuzhiran Dev 5caf7bc52e feat: Wave 2 - API org isolation + factory.html + insights.html
- Add org_id auto-population to articles.py, publishing.py, metrics.py
- Add org_id filtering to search_rankings.py geo endpoints
- Add org_id to calendar.py create/update/delete endpoints
- Add org_id to tasks.py create endpoints
- Create factory.html content pipeline page (3-tab: 待创作/进行中/待审查)
- Create insights.html analytics page (4-tab: 概览/搜索排名/GEO/平台对比)
2026-06-17 16:13:48 +08:00

250 lines
8.8 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(), org_id=current_user.org_id or "default")
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="日历条目不存在")
# Verify ownership via topic org_id
if entry.topic_id:
topic_check = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic_check and current_user.role != "admin" and topic_check.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="日历条目不存在")
if not entry.topic_id and current_user.role != "admin" and entry.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="日历条目不存在")
if entry.topic_id:
topic = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
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="日历条目不存在")
if entry.topic_id:
topic = db.query(Topic).filter(Topic.id == entry.topic_id).first()
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
raise HTTPException(status_code=404, detail="日历条目不存在")
if not entry.topic_id and current_user.role != "admin" and entry.org_id != current_user.org_id:
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,
org_id=current_user.org_id or "default"
)
db.add(entry)
db.commit()
db.refresh(entry)
return entry