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:
@@ -3,16 +3,19 @@ from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
import bcrypt
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
from ..models import User, Topic, SystemConfig
|
||||
from ..schemas import UserCreate, UserUpdate, UserResponse
|
||||
from ..core.audit_logger import audit_log
|
||||
import json
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
|
||||
def get_current_admin(request: Request, db: Session = Depends(get_db)):
|
||||
"""依赖项:验证管理员权限"""
|
||||
from .auth import verify_token
|
||||
from .auth import verify_token, org_filter
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(status_code=401, detail="未提供认证令牌")
|
||||
@@ -29,7 +32,11 @@ def list_users(
|
||||
admin_user: User = Depends(get_current_admin)
|
||||
):
|
||||
"""获取用户列表(管理员)"""
|
||||
users = db.query(User).all()
|
||||
q = db.query(User)
|
||||
of = org_filter(admin_user, User)
|
||||
if of is not True:
|
||||
q = q.filter(of)
|
||||
users = q.all()
|
||||
return [UserResponse.from_orm(u) for u in users]
|
||||
|
||||
@router.post("/users", response_model=UserResponse)
|
||||
@@ -51,7 +58,8 @@ def create_user(
|
||||
user = User(
|
||||
username=user_data.username,
|
||||
password_hash=hashed_password,
|
||||
role=user_data.role or "user"
|
||||
role=user_data.role or "user",
|
||||
org_id=user_data.org_id or admin_user.org_id or "default"
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
@@ -144,3 +152,115 @@ def delete_user(
|
||||
db=db
|
||||
)
|
||||
return {"message": "删除成功"}
|
||||
|
||||
# ---------- Org Management ----------
|
||||
|
||||
class OrgCreate(BaseModel):
|
||||
org_id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class OrgUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
@router.get("/orgs")
|
||||
def list_orgs(
|
||||
db: Session = Depends(get_db),
|
||||
admin_user: User = Depends(get_current_admin)
|
||||
):
|
||||
distinct_orgs = db.query(User.org_id).distinct().all()
|
||||
result = []
|
||||
for (org_id,) in distinct_orgs:
|
||||
if not org_id:
|
||||
continue
|
||||
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
|
||||
meta = json.loads(cfg.value) if cfg and cfg.value else {}
|
||||
user_count = db.query(User).filter(User.org_id == org_id).count()
|
||||
topic_count = db.query(Topic).filter(Topic.org_id == org_id).count()
|
||||
result.append({
|
||||
"org_id": org_id,
|
||||
"name": meta.get("name", org_id),
|
||||
"description": meta.get("description", ""),
|
||||
"user_count": user_count,
|
||||
"topic_count": topic_count,
|
||||
"is_default": org_id == "default"
|
||||
})
|
||||
return result
|
||||
|
||||
@router.post("/orgs")
|
||||
def create_org(
|
||||
data: OrgCreate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
admin_user: User = Depends(get_current_admin)
|
||||
):
|
||||
existing = db.query(SystemConfig).filter(SystemConfig.key == f"org:{data.org_id}").first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="组织已存在")
|
||||
cfg = SystemConfig(
|
||||
key=f"org:{data.org_id}",
|
||||
value=json.dumps({"name": data.name, "description": data.description or ""}, ensure_ascii=False),
|
||||
description=f"Organization: {data.org_id}"
|
||||
)
|
||||
db.add(cfg)
|
||||
db.commit()
|
||||
audit_log(
|
||||
action="create_org", user=admin_user,
|
||||
resource_type="org", resource_id=data.org_id,
|
||||
details=data.model_dump(),
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent", ""),
|
||||
db=db
|
||||
)
|
||||
return {"org_id": data.org_id, "name": data.name, "description": data.description or ""}
|
||||
|
||||
@router.put("/orgs/{org_id}")
|
||||
def update_org(
|
||||
org_id: str,
|
||||
data: OrgUpdate,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
admin_user: User = Depends(get_current_admin)
|
||||
):
|
||||
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
|
||||
if not cfg:
|
||||
raise HTTPException(status_code=404, detail="组织不存在")
|
||||
meta = json.loads(cfg.value) if cfg.value else {}
|
||||
if data.name is not None:
|
||||
meta["name"] = data.name
|
||||
if data.description is not None:
|
||||
meta["description"] = data.description
|
||||
cfg.value = json.dumps(meta, ensure_ascii=False)
|
||||
db.commit()
|
||||
audit_log(
|
||||
action="update_org", user=admin_user,
|
||||
resource_type="org", resource_id=org_id,
|
||||
details=data.model_dump(exclude_unset=True),
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent", ""),
|
||||
db=db
|
||||
)
|
||||
return {"org_id": org_id, **meta}
|
||||
|
||||
@router.delete("/orgs/{org_id}")
|
||||
def delete_org(
|
||||
org_id: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
admin_user: User = Depends(get_current_admin)
|
||||
):
|
||||
if org_id == "default":
|
||||
raise HTTPException(status_code=400, detail="不能删除默认组织")
|
||||
cfg = db.query(SystemConfig).filter(SystemConfig.key == f"org:{org_id}").first()
|
||||
if cfg:
|
||||
db.delete(cfg)
|
||||
db.commit()
|
||||
audit_log(
|
||||
action="delete_org", user=admin_user,
|
||||
resource_type="org", resource_id=org_id,
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent", ""),
|
||||
db=db
|
||||
)
|
||||
return {"message": "删除成功"}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
from fastapi import APIRouter, HTTPException, Query, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from pathlib import Path
|
||||
import os
|
||||
from datetime import datetime, date
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import User, Article
|
||||
from .auth import get_current_user
|
||||
from ..models import User, Article, Topic
|
||||
from .auth import get_current_user, org_filter
|
||||
|
||||
router = APIRouter(prefix="/api/articles", tags=["articles"])
|
||||
|
||||
@router.get("/drafts")
|
||||
def list_drafts(topic_id: str = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""从 articles 表列出草稿"""
|
||||
query = db.query(Article)
|
||||
query = db.query(Article).join(Topic, Article.topic_id == Topic.id)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
query = query.filter(of)
|
||||
if topic_id:
|
||||
query = query.filter(Article.topic_id == topic_id)
|
||||
articles = query.order_by(Article.created_at.desc()).all()
|
||||
@@ -22,9 +26,114 @@ def list_drafts(topic_id: str = None, current_user: User = Depends(get_current_u
|
||||
result.setdefault(a.platform, []).append({"id": a.id, "topic_id": a.topic_id, "status": a.status})
|
||||
return {"articles": result}
|
||||
|
||||
@router.get("/list")
|
||||
def list_articles(
|
||||
platform: str = None,
|
||||
status: str = None,
|
||||
search: str = None,
|
||||
topic_id: str = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""列出所有文章(含选题标题)"""
|
||||
query = db.query(Article, Topic.title.label("topic_title")).join(Topic, Article.topic_id == Topic.id)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
query = query.filter(of)
|
||||
if platform:
|
||||
query = query.filter(Article.platform == platform)
|
||||
if status:
|
||||
query = query.filter(Article.status == status)
|
||||
if topic_id:
|
||||
query = query.filter(Article.topic_id == topic_id)
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(Article.id.ilike(f"%{search}%"), Topic.title.ilike(f"%{search}%"))
|
||||
)
|
||||
rows = query.order_by(Article.created_at.desc()).all()
|
||||
return {
|
||||
"articles": [
|
||||
{
|
||||
"id": r.Article.id,
|
||||
"topic_id": r.Article.topic_id,
|
||||
"topic_title": r.topic_title,
|
||||
"platform": r.Article.platform,
|
||||
"status": r.Article.status,
|
||||
"compliance_score": r.Article.compliance_score,
|
||||
"word_count": r.Article.word_count,
|
||||
"created_at": r.Article.created_at.isoformat() if r.Article.created_at else None,
|
||||
"images": r.Article.images or {},
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
}
|
||||
|
||||
@router.get("/detail/{article_id}")
|
||||
def get_article_detail(article_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""获取文章详情"""
|
||||
article = db.query(Article).filter(Article.id == article_id).first()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Article not found")
|
||||
topic = db.query(Topic).filter(Topic.id == article.topic_id).first()
|
||||
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
|
||||
raise HTTPException(status_code=404, detail="Article not found")
|
||||
return {
|
||||
"id": article.id,
|
||||
"topic_id": article.topic_id,
|
||||
"topic_title": topic.title if topic else None,
|
||||
"platform": article.platform,
|
||||
"file_path": article.file_path,
|
||||
"status": article.status,
|
||||
"compliance_score": article.compliance_score,
|
||||
"word_count": article.word_count,
|
||||
"outline": article.outline,
|
||||
"html_content": article.html_content,
|
||||
"images": article.images or {},
|
||||
"created_at": article.created_at.isoformat() if article.created_at else None,
|
||||
}
|
||||
|
||||
@router.delete("/{article_id}")
|
||||
def delete_article(article_id: str, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""删除文章"""
|
||||
article = db.query(Article).filter(Article.id == article_id).first()
|
||||
if not article:
|
||||
raise HTTPException(status_code=404, detail="Article not found")
|
||||
topic = db.query(Topic).filter(Topic.id == article.topic_id).first()
|
||||
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
|
||||
raise HTTPException(status_code=404, detail="Article not found")
|
||||
file_path = Path(article.file_path) if article.file_path else None
|
||||
db.delete(article)
|
||||
db.commit()
|
||||
if file_path and file_path.exists():
|
||||
try:
|
||||
file_path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
return {"detail": "deleted"}
|
||||
|
||||
@router.get("/{topic_id}/images")
|
||||
def get_article_images(topic_id: str, platform: str = None, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""获取某选题在各平台的配图路径"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
query = db.query(Article).filter(Article.topic_id == topic_id)
|
||||
if platform:
|
||||
query = query.filter(Article.platform == platform)
|
||||
articles = query.all()
|
||||
return {
|
||||
topic_id: {
|
||||
a.platform: (a.images or {})
|
||||
for a in articles
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/{topic_id}/preview")
|
||||
def preview_article(topic_id: str, platform: str = "zhihu", current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
"""从 articles 表预览某选题的 HTML 内容"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if topic and current_user.role != "admin" and topic.org_id != current_user.org_id:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
article_id = f"{platform}_{topic_id}"
|
||||
article = db.query(Article).filter(Article.id == article_id).first()
|
||||
if not article or not article.html_content:
|
||||
|
||||
@@ -32,6 +32,7 @@ def create_token(user: User) -> str:
|
||||
"sub": str(user.id),
|
||||
"username": user.username,
|
||||
"role": user.role,
|
||||
"org_id": user.org_id,
|
||||
"exp": expire
|
||||
}
|
||||
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
|
||||
@@ -66,7 +67,8 @@ def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_
|
||||
user = User(
|
||||
username=DEFAULT_ADMIN_USERNAME,
|
||||
password_hash=hashed.decode('utf-8'),
|
||||
role="admin"
|
||||
role="admin",
|
||||
org_id="default"
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
@@ -147,6 +149,12 @@ def get_current_user(request: Request, db: Session = Depends(get_db)) -> User:
|
||||
user = verify_token(token, db)
|
||||
return user
|
||||
|
||||
def org_filter(current_user: User, model):
|
||||
"""返回 org_id 过滤条件。管理员看到全部数据,普通用户仅限本组织。"""
|
||||
if current_user.role == "admin":
|
||||
return True # no filter
|
||||
return model.org_id == current_user.org_id
|
||||
|
||||
def get_current_admin(current_user: User = Depends(get_current_user)) -> User:
|
||||
"""依赖项:验证管理员权限"""
|
||||
if current_user.role != "admin":
|
||||
|
||||
@@ -10,11 +10,27 @@ from ..schemas import (
|
||||
ContentCalendarCreate, ContentCalendarUpdate, ContentCalendarResponse,
|
||||
ContentCalendarBase
|
||||
)
|
||||
from .auth import get_current_user
|
||||
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),
|
||||
@@ -28,10 +44,17 @@ def get_calendar(
|
||||
start = date(year, month, 1)
|
||||
last_day = monthrange(year, month)[1]
|
||||
end = date(year, month, last_day)
|
||||
return db.query(ContentCalendar).filter(
|
||||
base = db.query(ContentCalendar).filter(
|
||||
ContentCalendar.planned_date >= start,
|
||||
ContentCalendar.planned_date <= end
|
||||
).order_by(ContentCalendar.planned_date).all()
|
||||
)
|
||||
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])
|
||||
@@ -45,6 +68,11 @@ def list_entries(
|
||||
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:
|
||||
@@ -66,6 +94,8 @@ def create_entry(
|
||||
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)
|
||||
@@ -135,6 +165,8 @@ def bind_topic_to_entry(
|
||||
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:
|
||||
@@ -154,10 +186,16 @@ def calendar_stats(
|
||||
start = date(year, month, 1)
|
||||
last_day = monthrange(year, month)[1]
|
||||
end = date(year, month, last_day)
|
||||
entries = db.query(ContentCalendar).filter(
|
||||
q = db.query(ContentCalendar).filter(
|
||||
ContentCalendar.planned_date >= start,
|
||||
ContentCalendar.planned_date <= end
|
||||
).all()
|
||||
)
|
||||
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:
|
||||
@@ -177,6 +215,8 @@ def create_from_topic(
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""采集管理 API:类别与信息源的 CRUD"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import CollectorCategory, CollectorSource
|
||||
from .auth import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/api/admin/collector", tags=["collector_mgmt"], dependencies=[Depends(get_current_admin)])
|
||||
|
||||
# ---------- Schemas ----------
|
||||
class CategoryCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
search_query: Optional[str] = None
|
||||
sort_order: int = 0
|
||||
is_active: bool = True
|
||||
|
||||
class CategoryUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
search_query: Optional[str] = None
|
||||
sort_order: Optional[int] = None
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
class SourceCreate(BaseModel):
|
||||
category_id: Optional[int] = None
|
||||
name: str
|
||||
source_type: str
|
||||
url: Optional[str] = None
|
||||
query: Optional[str] = None
|
||||
credibility: str = "medium"
|
||||
focus: Optional[str] = None
|
||||
is_active: bool = True
|
||||
sort_order: int = 0
|
||||
|
||||
class SourceUpdate(BaseModel):
|
||||
category_id: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
source_type: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
query: Optional[str] = None
|
||||
credibility: Optional[str] = None
|
||||
focus: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
sort_order: Optional[int] = None
|
||||
|
||||
# ---------- Categories ----------
|
||||
@router.get("/categories")
|
||||
def list_categories(db: Session = Depends(get_db)):
|
||||
cats = db.query(CollectorCategory).order_by(CollectorCategory.sort_order).all()
|
||||
result = []
|
||||
for c in cats:
|
||||
d = c.to_dict()
|
||||
d["source_count"] = db.query(CollectorSource).filter(CollectorSource.category_id == c.id).count()
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
@router.post("/categories", status_code=201)
|
||||
def create_category(data: CategoryCreate, db: Session = Depends(get_db)):
|
||||
existing = db.query(CollectorCategory).filter(CollectorCategory.name == data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(400, "类别名已存在")
|
||||
cat = CollectorCategory(**data.model_dump())
|
||||
db.add(cat)
|
||||
db.commit()
|
||||
db.refresh(cat)
|
||||
return cat.to_dict()
|
||||
|
||||
@router.put("/categories/{cat_id}")
|
||||
def update_category(cat_id: int, data: CategoryUpdate, db: Session = Depends(get_db)):
|
||||
cat = db.query(CollectorCategory).filter(CollectorCategory.id == cat_id).first()
|
||||
if not cat:
|
||||
raise HTTPException(404, "类别不存在")
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(cat, k, v)
|
||||
db.commit()
|
||||
db.refresh(cat)
|
||||
return cat.to_dict()
|
||||
|
||||
@router.delete("/categories/{cat_id}")
|
||||
def delete_category(cat_id: int, db: Session = Depends(get_db)):
|
||||
cat = db.query(CollectorCategory).filter(CollectorCategory.id == cat_id).first()
|
||||
if not cat:
|
||||
raise HTTPException(404, "类别不存在")
|
||||
db.delete(cat)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
# ---------- Sources ----------
|
||||
@router.get("/sources")
|
||||
def list_sources(category_id: Optional[int] = None, db: Session = Depends(get_db)):
|
||||
q = db.query(CollectorSource).order_by(CollectorSource.sort_order)
|
||||
if category_id is not None:
|
||||
q = q.filter(CollectorSource.category_id == category_id)
|
||||
return [s.to_dict() for s in q.all()]
|
||||
|
||||
@router.post("/sources", status_code=201)
|
||||
def create_source(data: SourceCreate, db: Session = Depends(get_db)):
|
||||
src = CollectorSource(**data.model_dump())
|
||||
db.add(src)
|
||||
db.commit()
|
||||
db.refresh(src)
|
||||
return src.to_dict()
|
||||
|
||||
@router.put("/sources/{src_id}")
|
||||
def update_source(src_id: int, data: SourceUpdate, db: Session = Depends(get_db)):
|
||||
src = db.query(CollectorSource).filter(CollectorSource.id == src_id).first()
|
||||
if not src:
|
||||
raise HTTPException(404, "信息源不存在")
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(src, k, v)
|
||||
db.commit()
|
||||
db.refresh(src)
|
||||
return src.to_dict()
|
||||
|
||||
@router.delete("/sources/{src_id}")
|
||||
def delete_source(src_id: int, db: Session = Depends(get_db)):
|
||||
src = db.query(CollectorSource).filter(CollectorSource.id == src_id).first()
|
||||
if not src:
|
||||
raise HTTPException(404, "信息源不存在")
|
||||
db.delete(src)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
@@ -11,7 +11,7 @@ from ..schemas import (
|
||||
ContentMetricsCreate, ContentMetricsUpdate, ContentMetricsResponse,
|
||||
MetricsDashboard
|
||||
)
|
||||
from .auth import get_current_user
|
||||
from .auth import get_current_user, org_filter
|
||||
|
||||
router = APIRouter(prefix="/api/metrics", tags=["metrics"])
|
||||
|
||||
@@ -24,18 +24,24 @@ def get_dashboard(
|
||||
):
|
||||
since = datetime.now() - timedelta(days=days)
|
||||
|
||||
total_topics = db.query(Topic).count()
|
||||
topic_base = db.query(Topic)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
topic_base = topic_base.filter(of)
|
||||
|
||||
raw_status = db.query(Topic.status, func.count()).group_by(Topic.status).all()
|
||||
total_topics = topic_base.count()
|
||||
|
||||
raw_status = topic_base.with_entities(Topic.status, func.count()).group_by(Topic.status).all()
|
||||
topics_by_status = {}
|
||||
for s, cnt in raw_status:
|
||||
topics_by_status[s] = cnt
|
||||
|
||||
total_published = db.query(ContentMetrics).filter(
|
||||
ContentMetrics.views > 0
|
||||
).count()
|
||||
metrics_q = db.query(ContentMetrics).join(Topic, ContentMetrics.topic_id == Topic.id)
|
||||
if of is not True:
|
||||
metrics_q = metrics_q.filter(of)
|
||||
total_published = metrics_q.filter(ContentMetrics.views > 0).count()
|
||||
|
||||
all_metrics = db.query(ContentMetrics).filter(
|
||||
all_metrics = metrics_q.filter(
|
||||
ContentMetrics.created_at >= since
|
||||
).all()
|
||||
|
||||
@@ -45,11 +51,14 @@ def get_dashboard(
|
||||
engagement_rates = [m.engagement_rate for m in all_metrics if m.views > 0]
|
||||
avg_engagement = sum(engagement_rates) / len(engagement_rates) if engagement_rates else 0
|
||||
|
||||
top_topics_data = db.query(
|
||||
top_q = db.query(
|
||||
ContentMetrics.topic_id,
|
||||
func.sum(ContentMetrics.views).label("total_views"),
|
||||
func.sum(ContentMetrics.likes).label("total_likes")
|
||||
).join(Topic).filter(
|
||||
).join(Topic, ContentMetrics.topic_id == Topic.id)
|
||||
if of is not True:
|
||||
top_q = top_q.filter(of)
|
||||
top_topics_data = top_q.filter(
|
||||
ContentMetrics.created_at >= since
|
||||
).group_by(ContentMetrics.topic_id).order_by(desc("total_views")).limit(10).all()
|
||||
|
||||
@@ -63,7 +72,10 @@ def get_dashboard(
|
||||
"total_likes": row.total_likes or 0,
|
||||
})
|
||||
|
||||
recent_metrics = db.query(ContentMetrics).order_by(
|
||||
recent_q = db.query(ContentMetrics).join(Topic, ContentMetrics.topic_id == Topic.id)
|
||||
if of is not True:
|
||||
recent_q = recent_q.filter(of)
|
||||
recent_metrics = recent_q.order_by(
|
||||
ContentMetrics.created_at.desc()
|
||||
).limit(10).all()
|
||||
|
||||
@@ -93,13 +105,17 @@ def get_trend(
|
||||
else:
|
||||
date_format = func.date_trunc(group_by, ContentMetrics.created_at)
|
||||
|
||||
rows = db.query(
|
||||
q = db.query(
|
||||
date_format.label("period"),
|
||||
func.sum(ContentMetrics.views).label("views"),
|
||||
func.sum(ContentMetrics.likes).label("likes"),
|
||||
func.sum(ContentMetrics.comments).label("comments"),
|
||||
func.count(ContentMetrics.id).label("count")
|
||||
).filter(
|
||||
).join(Topic, ContentMetrics.topic_id == Topic.id)
|
||||
of_m = org_filter(current_user, Topic)
|
||||
if of_m is not True:
|
||||
q = q.filter(of_m)
|
||||
rows = q.filter(
|
||||
ContentMetrics.created_at >= since
|
||||
).group_by(date_format).order_by(date_format).all()
|
||||
|
||||
@@ -123,7 +139,10 @@ def list_metrics(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
query = db.query(ContentMetrics)
|
||||
query = db.query(ContentMetrics).join(Topic, ContentMetrics.topic_id == Topic.id)
|
||||
of_m = org_filter(current_user, Topic)
|
||||
if of_m is not True:
|
||||
query = query.filter(of_m)
|
||||
if topic_id:
|
||||
query = query.filter(ContentMetrics.topic_id == topic_id)
|
||||
if platform:
|
||||
@@ -140,6 +159,8 @@ def create_metric(
|
||||
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="选题不存在")
|
||||
|
||||
existing = db.query(ContentMetrics).filter(
|
||||
ContentMetrics.topic_id == data.topic_id,
|
||||
@@ -204,6 +225,8 @@ def get_topic_metrics(
|
||||
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="选题不存在")
|
||||
return db.query(ContentMetrics).filter(
|
||||
ContentMetrics.topic_id == topic_id
|
||||
).order_by(ContentMetrics.created_at.desc()).all()
|
||||
@@ -214,13 +237,17 @@ def get_metrics_by_platform(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
rows = db.query(
|
||||
q = db.query(
|
||||
ContentMetrics.platform,
|
||||
func.sum(ContentMetrics.views).label("total_views"),
|
||||
func.sum(ContentMetrics.likes).label("total_likes"),
|
||||
func.sum(ContentMetrics.comments).label("total_comments"),
|
||||
func.count(ContentMetrics.id).label("count")
|
||||
).group_by(ContentMetrics.platform).all()
|
||||
).join(Topic, ContentMetrics.topic_id == Topic.id)
|
||||
of_m = org_filter(current_user, Topic)
|
||||
if of_m is not True:
|
||||
q = q.filter(of_m)
|
||||
rows = q.group_by(ContentMetrics.platform).all()
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -243,11 +270,15 @@ def recommend_topics_from_metrics(
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
try:
|
||||
high_performing = db.query(
|
||||
hp_q = db.query(
|
||||
ContentMetrics.topic_id,
|
||||
func.avg(ContentMetrics.engagement_rate).label("avg_engagement"),
|
||||
func.max(ContentMetrics.views).label("max_views")
|
||||
).group_by(ContentMetrics.topic_id).order_by(desc("avg_engagement")).limit(20).all()
|
||||
).join(Topic, ContentMetrics.topic_id == Topic.id)
|
||||
of_m = org_filter(current_user, Topic)
|
||||
if of_m is not True:
|
||||
hp_q = hp_q.filter(of_m)
|
||||
high_performing = hp_q.group_by(ContentMetrics.topic_id).order_by(desc("avg_engagement")).limit(20).all()
|
||||
|
||||
recommendations = []
|
||||
for row in high_performing:
|
||||
|
||||
@@ -3,78 +3,112 @@
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Topic, PublishRecord, User
|
||||
from sqlalchemy.orm import Session
|
||||
from .auth import get_current_admin
|
||||
from .auth import get_current_admin, org_filter
|
||||
from ..core.audit_logger import audit_log
|
||||
|
||||
router = APIRouter(prefix="/api/publishing", tags=["publishing"])
|
||||
|
||||
class PublishRequest(BaseModel):
|
||||
topic_id: str
|
||||
PLATFORM_LABELS = {
|
||||
"zhihu": "知乎",
|
||||
"wechat": "微信公众号",
|
||||
"xiaohongshu": "小红书"
|
||||
}
|
||||
|
||||
class PublishResponse(BaseModel):
|
||||
class MultiPublishRequest(BaseModel):
|
||||
topic_id: str
|
||||
platforms: List[str] = ["zhihu", "wechat", "xiaohongshu"]
|
||||
|
||||
class PublishResult(BaseModel):
|
||||
platform: str
|
||||
platform_label: str
|
||||
status: str
|
||||
error_msg: Optional[str] = None
|
||||
|
||||
class MultiPublishResponse(BaseModel):
|
||||
ok: bool
|
||||
topic_id: str
|
||||
results: List[PublishResult]
|
||||
message: str
|
||||
|
||||
@router.post("/create", response_model=PublishResponse)
|
||||
@router.post("/create", response_model=MultiPublishResponse)
|
||||
async def create_publish_record(
|
||||
req: PublishRequest,
|
||||
req: MultiPublishRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_admin)
|
||||
):
|
||||
"""标记选题为已发布,并创建发布记录(管理员)"""
|
||||
"""多平台发布选题(管理员)"""
|
||||
try:
|
||||
# 查找选题
|
||||
topic = db.query(Topic).filter(Topic.id == req.topic_id).first()
|
||||
q = db.query(Topic).filter(Topic.id == req.topic_id)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
q = q.filter(of)
|
||||
topic = q.first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail=f"选题 {req.topic_id} 不存在")
|
||||
|
||||
if topic.status not in ('ready', '待发布'):
|
||||
raise HTTPException(status_code=400, detail=f"选题 {req.topic_id} 状态不是待发布(当前: {topic.status})")
|
||||
|
||||
# 更新选题状态
|
||||
if not req.platforms:
|
||||
raise HTTPException(status_code=400, detail="至少选择一个发布平台")
|
||||
|
||||
operator = current_user.username
|
||||
results = []
|
||||
|
||||
for platform in req.platforms:
|
||||
try:
|
||||
record = PublishRecord(
|
||||
topic_id=req.topic_id,
|
||||
platform=platform,
|
||||
action='publish',
|
||||
status='success',
|
||||
operator=operator,
|
||||
description=f"选题 {req.topic_id} 发布到 {PLATFORM_LABELS.get(platform, platform)}"
|
||||
)
|
||||
db.add(record)
|
||||
results.append(PublishResult(
|
||||
platform=platform,
|
||||
platform_label=PLATFORM_LABELS.get(platform, platform),
|
||||
status='success'
|
||||
))
|
||||
except Exception as e:
|
||||
results.append(PublishResult(
|
||||
platform=platform,
|
||||
platform_label=PLATFORM_LABELS.get(platform, platform),
|
||||
status='failed',
|
||||
error_msg=str(e)
|
||||
))
|
||||
|
||||
topic.status = '已发布'
|
||||
topic.updated_at = datetime.now()
|
||||
topic.published_at = datetime.now().date() # 设置发布时间为今天
|
||||
|
||||
# 创建发布记录
|
||||
operator = current_user.username
|
||||
record = PublishRecord(
|
||||
topic_id=req.topic_id,
|
||||
platform='all',
|
||||
action='publish',
|
||||
status='success',
|
||||
operator=operator,
|
||||
description=f"选题 {req.topic_id} 已发布"
|
||||
)
|
||||
db.add(record)
|
||||
topic.published_at = datetime.now().date()
|
||||
db.commit()
|
||||
# 强制刷新会话缓存,确保后续读取最新数据
|
||||
db.expire_all()
|
||||
db.refresh(topic)
|
||||
|
||||
# 审计日志
|
||||
audit_log(
|
||||
action="publish",
|
||||
user=current_user,
|
||||
resource_type="topic",
|
||||
resource_id=req.topic_id,
|
||||
details={"operator": operator, "status": "success"},
|
||||
details={"operator": operator, "platforms": req.platforms, "results": [r.model_dump() for r in results]},
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent", ""),
|
||||
db=db
|
||||
)
|
||||
|
||||
return PublishResponse(
|
||||
ok=True,
|
||||
success_count = sum(1 for r in results if r.status == 'success')
|
||||
return MultiPublishResponse(
|
||||
ok=success_count > 0,
|
||||
topic_id=req.topic_id,
|
||||
message=f"选题 {req.topic_id} 已成功发布"
|
||||
results=results,
|
||||
message=f"选题 {req.topic_id} 发布完成({success_count}/{len(results)} 平台成功)"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
@@ -13,7 +13,7 @@ from ..core.generator import run_creator
|
||||
from ..core.optimizer import run_optimizer
|
||||
from ..core.sync import sync_all_topics
|
||||
from ..core.scheduler import scheduler
|
||||
from .auth import get_current_user
|
||||
from .auth import get_current_user, org_filter
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
@@ -23,9 +23,9 @@ LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/system", tags=["system"])
|
||||
|
||||
def _aggregate_status_counts(db: Session):
|
||||
def _aggregate_status_counts(q):
|
||||
"""聚合状态计数,兼容中英文状态值"""
|
||||
raw = db.query(Topic.status, func.count()).group_by(Topic.status).all()
|
||||
raw = q.with_entities(Topic.status, func.count()).group_by(Topic.status).all()
|
||||
mapping = {
|
||||
'pending': ['pending', '待处理'],
|
||||
'review': ['review', '待审查'],
|
||||
@@ -43,7 +43,7 @@ def _aggregate_status_counts(db: Session):
|
||||
@router.get("/status")
|
||||
def get_status(db: Session = Depends(get_db)):
|
||||
total = db.query(Topic).count()
|
||||
counts = _aggregate_status_counts(db)
|
||||
counts = _aggregate_status_counts(db.query(Topic))
|
||||
today = date.today()
|
||||
today_count = db.query(Topic).filter(func.date(Topic.created_at) == today).count()
|
||||
return {
|
||||
@@ -58,7 +58,7 @@ def get_status(db: Session = Depends(get_db)):
|
||||
}
|
||||
|
||||
@router.post("/generate/run", dependencies=[Depends(get_current_user)])
|
||||
def trigger_generation(topic_id: str = Body(None, embed=True), db: Session = Depends(get_db)):
|
||||
def trigger_generation(topic_id: str = Body(None, embed=True), db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
logger.info(f"Received topic_id={topic_id}")
|
||||
try:
|
||||
result = run_creator(topic_id)
|
||||
@@ -70,7 +70,7 @@ def trigger_generation(topic_id: str = Body(None, embed=True), db: Session = Dep
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/review/run", dependencies=[Depends(get_current_user)])
|
||||
def trigger_review(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db)):
|
||||
def trigger_review(topic_ids: List[str] = Body(None, embed=True), db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
try:
|
||||
result = run_optimizer(topic_ids)
|
||||
if not result["ok"]:
|
||||
@@ -86,7 +86,11 @@ def trigger_review(topic_ids: List[str] = Body(None, embed=True), db: Session =
|
||||
if topic_ids:
|
||||
updated = 0
|
||||
for tid in topic_ids:
|
||||
topic = db.query(Topic).filter(Topic.id == tid).first()
|
||||
q = db.query(Topic).filter(Topic.id == tid)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
q = q.filter(of)
|
||||
topic = q.first()
|
||||
if topic and topic.status in ('review', '待审查'):
|
||||
topic.status = 'ready'
|
||||
if not topic.generated_at:
|
||||
@@ -109,9 +113,13 @@ def get_logs(log_date: str, log_type: str = "creator"):
|
||||
return {"log_date": log_date, "log_type": log_type, "content": lines}
|
||||
|
||||
@router.get("/pipeline/status", dependencies=[Depends(get_current_user)])
|
||||
def get_pipeline_status(db: Session = Depends(get_db)):
|
||||
total = db.query(Topic).count()
|
||||
counts = _aggregate_status_counts(db)
|
||||
def get_pipeline_status(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
topic_base = db.query(Topic)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
topic_base = topic_base.filter(of)
|
||||
total = topic_base.count()
|
||||
counts = _aggregate_status_counts(topic_base)
|
||||
log_files = {
|
||||
"collector": LOGS_DIR / f"collector_{date.today().isoformat()}.log",
|
||||
"creator": LOGS_DIR / f"creator_{date.today().isoformat()}.log",
|
||||
@@ -135,9 +143,13 @@ def run_sync():
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/automation/topics")
|
||||
def list_automation_topics(db: Session = Depends(get_db)):
|
||||
def list_automation_topics(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
try:
|
||||
topics = db.query(Topic).order_by(Topic.created_at.desc()).limit(100).all()
|
||||
topic_base = db.query(Topic)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
topic_base = topic_base.filter(of)
|
||||
topics = topic_base.order_by(Topic.created_at.desc()).limit(100).all()
|
||||
result = []
|
||||
for t in topics:
|
||||
result.append({
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import List, Optional
|
||||
from ..database import get_db
|
||||
from ..models import ContentTask, Topic
|
||||
from ..schemas import ContentTaskCreate, ContentTaskResponse
|
||||
from .auth import get_current_user
|
||||
from .auth import get_current_user, org_filter
|
||||
|
||||
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
|
||||
|
||||
@@ -20,7 +20,10 @@ def list_tasks(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
query = db.query(ContentTask)
|
||||
query = db.query(ContentTask).join(Topic, ContentTask.topic_id == Topic.id, isouter=True)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
query = query.filter((ContentTask.topic_id.is_(None)) | (Topic.org_id == current_user.org_id))
|
||||
if status:
|
||||
query = query.filter(ContentTask.status == status)
|
||||
if topic_id:
|
||||
@@ -35,7 +38,11 @@ def get_active_tasks(
|
||||
db: Session = Depends(get_db),
|
||||
current_user=Depends(get_current_user)
|
||||
):
|
||||
return db.query(ContentTask).filter(
|
||||
q = db.query(ContentTask).join(Topic, ContentTask.topic_id == Topic.id, isouter=True)
|
||||
of = org_filter(current_user, Topic)
|
||||
if of is not True:
|
||||
q = q.filter((ContentTask.topic_id.is_(None)) | (Topic.org_id == current_user.org_id))
|
||||
return q.filter(
|
||||
ContentTask.status == "running"
|
||||
).order_by(ContentTask.started_at.desc()).all()
|
||||
|
||||
@@ -50,6 +57,8 @@ def create_task(
|
||||
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="选题不存在")
|
||||
|
||||
task_id = f"task_{uuid.uuid4().hex[:16]}"
|
||||
|
||||
|
||||
@@ -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