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
This commit is contained in:
@@ -11,32 +11,46 @@ from ..schemas import (
|
||||
TopicCreate, TopicUpdate, TopicResponse, TopicScoreRequest,
|
||||
PublishRequest, PublishActionRequest, PublishRecordResponse
|
||||
)
|
||||
from .auth import get_current_user
|
||||
from .auth import get_current_user, org_filter
|
||||
|
||||
router = APIRouter(prefix="/api/topics", tags=["topics"], dependencies=[Depends(get_current_user)])
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
|
||||
def _check_org(topic: Topic, current_user, db: Session):
|
||||
"""Verify topic belongs to user's org (unless admin)."""
|
||||
if current_user.role != "admin" and topic.org_id != current_user.org_id:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
return topic
|
||||
|
||||
|
||||
@router.get("", response_model=List[TopicResponse])
|
||||
def list_topics(
|
||||
field_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
today: Optional[bool] = None,
|
||||
tag: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
sort_by: str = Query("priority_score", enum=["priority_score", "created_at", "title", "updated_at"]),
|
||||
order: str = Query("desc", enum=["asc", "desc"]),
|
||||
limit: int = Query(50, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
db.expire_all()
|
||||
query = db.query(Topic).options(joinedload(Topic.field))
|
||||
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
query = query.filter(of)
|
||||
|
||||
if field_id:
|
||||
query = query.filter(Topic.field_id == field_id)
|
||||
if status:
|
||||
query = query.filter(Topic.status == status)
|
||||
if today:
|
||||
query = query.filter(func.date(Topic.created_at) == date.today())
|
||||
if tag:
|
||||
query = query.filter(Topic.tags.contains([tag]))
|
||||
if search:
|
||||
@@ -56,14 +70,19 @@ def topic_stats(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
total = db.query(Topic).count()
|
||||
raw = db.query(Topic.status, func.count()).group_by(Topic.status).all()
|
||||
base_q = db.query(Topic)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
base_q = base_q.filter(of)
|
||||
|
||||
total = base_q.count()
|
||||
raw = base_q.with_entities(Topic.status, func.count()).group_by(Topic.status).all()
|
||||
by_status = {s: c for s, c in raw}
|
||||
|
||||
today = date.today()
|
||||
today_count = db.query(Topic).filter(func.date(Topic.created_at) == today).count()
|
||||
today_count = base_q.filter(func.date(Topic.created_at) == today).count()
|
||||
|
||||
published = db.query(Topic).filter(Topic.status == "published").count()
|
||||
published = base_q.filter(Topic.status.in_(["published", "已发布"])).count()
|
||||
metrics_count = db.query(ContentMetrics).count()
|
||||
|
||||
return {
|
||||
@@ -103,6 +122,7 @@ def create_topic(
|
||||
id=topic_id,
|
||||
field_id=data.field_id,
|
||||
field_name=field_name,
|
||||
org_id=current_user.org_id or "default",
|
||||
title=data.title,
|
||||
format=data.format,
|
||||
core_concept=data.core_concept,
|
||||
@@ -121,10 +141,11 @@ def create_topic(
|
||||
|
||||
|
||||
@router.get("/{topic_id}", response_model=TopicResponse)
|
||||
def get_topic(topic_id: str, db: Session = Depends(get_db)):
|
||||
def get_topic(topic_id: str, db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
topic = db.query(Topic).options(joinedload(Topic.field)).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
return topic
|
||||
|
||||
|
||||
@@ -138,6 +159,7 @@ def update_topic(
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
|
||||
if data.field_id is not None:
|
||||
topic.field_id = data.field_id
|
||||
@@ -167,6 +189,7 @@ def delete_topic(
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
try:
|
||||
db.delete(topic)
|
||||
db.commit()
|
||||
@@ -186,6 +209,7 @@ def score_topic(
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
|
||||
topic.scoring_data = data.scoring_data
|
||||
|
||||
@@ -218,10 +242,11 @@ def score_topic(
|
||||
|
||||
|
||||
@router.post("/{topic_id}/publish")
|
||||
def publish_topic(topic_id: str, req: PublishRequest, db: Session = Depends(get_db)):
|
||||
def publish_topic(topic_id: str, req: PublishRequest, 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="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
if topic.status not in ("pending", "ready", "draft"):
|
||||
raise HTTPException(status_code=400, detail=f"选题状态({topic.status})不允许发布")
|
||||
|
||||
@@ -243,10 +268,11 @@ def publish_topic(topic_id: str, req: PublishRequest, db: Session = Depends(get_
|
||||
|
||||
|
||||
@router.get("/{topic_id}/articles", response_model=List[Dict[str, Any]])
|
||||
def get_topic_articles(topic_id: str, db: Session = Depends(get_db)):
|
||||
def get_topic_articles(topic_id: str, 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="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
articles = db.query(Article).filter(Article.topic_id == topic_id).all()
|
||||
return [a.to_dict() if hasattr(a, 'to_dict') else {
|
||||
"id": a.id, "topic_id": a.topic_id, "platform": a.platform,
|
||||
@@ -255,7 +281,11 @@ def get_topic_articles(topic_id: str, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/{topic_id}/metrics", response_model=List[Dict[str, Any]])
|
||||
def get_topic_metrics(topic_id: str, db: Session = Depends(get_db)):
|
||||
def get_topic_metrics(topic_id: str, 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="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
metrics = db.query(ContentMetrics).filter(ContentMetrics.topic_id == topic_id).all()
|
||||
return [m.to_dict() for m in metrics]
|
||||
|
||||
@@ -265,6 +295,7 @@ def lock_topic(topic_id: str, db: Session = Depends(get_db), current_user=Depend
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
topic.lock_by = current_user.username
|
||||
topic.lock_at = datetime.now()
|
||||
db.commit()
|
||||
@@ -276,6 +307,7 @@ def unlock_topic(topic_id: str, db: Session = Depends(get_db), current_user=Depe
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
_check_org(topic, current_user, db)
|
||||
topic.lock_by = None
|
||||
topic.lock_at = None
|
||||
db.commit()
|
||||
@@ -289,7 +321,11 @@ def batch_update_status(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
updated = db.query(Topic).filter(Topic.id.in_(topic_ids)).update(
|
||||
q = db.query(Topic).filter(Topic.id.in_(topic_ids))
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
q = q.filter(of)
|
||||
updated = q.update(
|
||||
{Topic.status: status, Topic.updated_at: datetime.now()},
|
||||
synchronize_session=False
|
||||
)
|
||||
@@ -302,8 +338,9 @@ def field_distribution(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
rows = db.query(
|
||||
Topic.field_name,
|
||||
func.count(Topic.id).label("count")
|
||||
).group_by(Topic.field_name).all()
|
||||
q = db.query(Topic.field_name, func.count(Topic.id).label("count"))
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
q = q.filter(of)
|
||||
rows = q.group_by(Topic.field_name).all()
|
||||
return [{"field": r.field_name or "未分类", "count": r.count} for r in rows]
|
||||
Reference in New Issue
Block a user