feat: 宇之然平台 v2.0 - 完整重构版
核心功能: - 新增 JWT 认证系统,支持管理员登录/登出 - 前后端合并为单一 FastAPI 应用 (端口 8001) - 系统概览页:6 个统计卡片,点击跳转筛选 - 选题管理页:批量操作 (刷新/创作/优化),时间列展示 - 系统日志页:整合日志查看功能 - 用户管理页:管理员可创建/删除用户 - 移动端适配:响应式布局,底部导航栏 - 标题居中显示 技术改进: - 添加 generated_at 字段支持创作时间记录 - 状态更新时自动同步 updated_at - 所有 API 路由添加 JWT 认证保护 - 前端 authFetch 封装自动附加 Token - 升级 FastAPI 0.136, Pydantic 2.13 等依赖 修复: - 修复 API 500 错误 (数据库列缺失) - 修复 formatRelativeTime 未定义错误 - 修复登录 Token 存储和自动附加逻辑
This commit is contained in:
@@ -1,19 +1,25 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, date
|
||||
from pathlib import Path
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Topic
|
||||
from ..schemas import TopicResponse, PublishRequest
|
||||
from ..models import Topic, PublishRecord
|
||||
from ..schemas import TopicResponse, PublishRequest, PublishActionRequest, PublishRecordResponse
|
||||
from .auth import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/topics", tags=["topics"])
|
||||
router = APIRouter(prefix="/api/topics", tags=["topics"], dependencies=[Depends(get_current_user)])
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
|
||||
@router.get("", response_model=List[TopicResponse])
|
||||
def list_topics(
|
||||
status: str = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# 确保读取最新数据,清除会话缓存
|
||||
db.expire_all()
|
||||
query = db.query(Topic)
|
||||
if status:
|
||||
query = query.filter(Topic.status == status)
|
||||
@@ -35,9 +41,147 @@ def publish_topic(topic_id: str, req: PublishRequest, db: Session = Depends(get_
|
||||
if topic.status != "ready":
|
||||
raise HTTPException(status_code=400, detail="Topic not in ready status")
|
||||
|
||||
# 更新选题状态
|
||||
topic.status = "published"
|
||||
topic.published_at = datetime.now().date()
|
||||
topic.updated_at = datetime.now()
|
||||
topic.platform_urls = req.platform_urls
|
||||
db.commit()
|
||||
|
||||
return {"message": "Topic marked as published", "topic_id": topic_id}
|
||||
# 创建发布记录
|
||||
record = PublishRecord(
|
||||
topic_id=topic_id,
|
||||
platform=req.platform,
|
||||
action="publish",
|
||||
status="success",
|
||||
operator=req.operator,
|
||||
description=req.description,
|
||||
suggestion=req.suggestion,
|
||||
url=req.url,
|
||||
error_msg=req.error_msg
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
|
||||
return {"message": "Topic marked as published", "topic_id": topic_id, "record_id": record.id}
|
||||
|
||||
|
||||
# 获取选题的发布记录
|
||||
@router.get("/{topic_id}/publish-records", response_model=List[PublishRecordResponse])
|
||||
def get_publish_records(topic_id: str, db: Session = Depends(get_db)):
|
||||
records = db.query(PublishRecord).filter(PublishRecord.topic_id == topic_id).order_by(PublishRecord.created_at.desc()).all()
|
||||
return records
|
||||
|
||||
|
||||
# 创建新的发布记录(用于手动记录发布情况)
|
||||
@router.post("/{topic_id}/publish-records")
|
||||
def create_publish_record(topic_id: str, req: PublishActionRequest, db: Session = Depends(get_db)):
|
||||
# 验证选题存在
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
|
||||
record = PublishRecord(
|
||||
topic_id=topic_id,
|
||||
platform=req.platform or "unknown",
|
||||
action=req.action,
|
||||
status=req.status,
|
||||
operator=req.operator,
|
||||
description=req.description,
|
||||
suggestion=req.suggestion,
|
||||
url=req.url,
|
||||
error_msg=req.error_msg
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
|
||||
# 如果操作是发布成功,且platform指定,则更新topic的platform_urls
|
||||
if req.action == "publish" and req.status == "success" and req.platform and req.url:
|
||||
if not topic.platform_urls:
|
||||
topic.platform_urls = {}
|
||||
topic.platform_urls[req.platform] = req.url
|
||||
db.commit()
|
||||
|
||||
return record
|
||||
|
||||
|
||||
# 更新发布记录
|
||||
@router.put("/publish-records/{record_id}")
|
||||
def update_publish_record(record_id: int, req: PublishActionRequest, db: Session = Depends(get_db)):
|
||||
record = db.query(PublishRecord).filter(PublishRecord.id == record_id).first()
|
||||
if not record:
|
||||
raise HTTPException(status_code=404, detail="Record not found")
|
||||
|
||||
# 更新字段
|
||||
for field, value in req.dict(exclude_unset=True).items():
|
||||
setattr(record, field, value)
|
||||
record.updated_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return record
|
||||
|
||||
|
||||
@router.get("/{topic_id}/preview")
|
||||
def preview_topic(topic_id: str, platform: str = Query("zhihu", regex="^(zhihu|wechat|xiaohongshu)$")):
|
||||
"""
|
||||
预览某选题在指定平台的HTML内容。
|
||||
查找最近发布的release文件。
|
||||
"""
|
||||
# 查找最近的发布包
|
||||
releases_dir = PROJECT_ROOT / "automation" / "data" / "releases"
|
||||
if not releases_dir.exists():
|
||||
raise HTTPException(status_code=404, detail="No releases found")
|
||||
|
||||
# 按日期倒序查找
|
||||
dates = sorted([d.name for d in releases_dir.iterdir() if d.is_dir()], reverse=True)
|
||||
found = None
|
||||
for dt in dates:
|
||||
file_path = releases_dir / dt / platform / f"{platform}_{topic_id}_{platform}.html"
|
||||
if file_path.exists():
|
||||
found = file_path
|
||||
break
|
||||
|
||||
if not found:
|
||||
raise HTTPException(status_code=404, detail=f"Preview not found for topic {topic_id} on {platform}")
|
||||
|
||||
content = found.read_text(encoding="utf-8")
|
||||
return {"topic_id": topic_id, "platform": platform, "html": content}
|
||||
|
||||
|
||||
@router.get("/{topic_id}/packages")
|
||||
def list_packages(topic_id: str):
|
||||
"""
|
||||
列出某选题的所有发布包(HTML文件)。
|
||||
"""
|
||||
releases_dir = PROJECT_ROOT / "automation" / "data" / "releases"
|
||||
if not releases_dir.exists():
|
||||
return {"packages": []}
|
||||
|
||||
packages = []
|
||||
dates = sorted([d.name for d in releases_dir.iterdir() if d.is_dir()], reverse=True)
|
||||
for dt in dates:
|
||||
date_dir = releases_dir / dt
|
||||
for platform in ["zhihu", "wechat", "xiaohongshu"]:
|
||||
file_path = date_dir / platform / f"{platform}_{topic_id}_{platform}.html"
|
||||
if file_path.exists():
|
||||
stat = file_path.stat()
|
||||
packages.append({
|
||||
"platform": platform,
|
||||
"path": str(file_path.relative_to(PROJECT_ROOT)),
|
||||
"size": stat.st_size,
|
||||
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat()
|
||||
})
|
||||
|
||||
return {"packages": packages}
|
||||
|
||||
|
||||
@router.post("/{topic_id}/packages/generate")
|
||||
def generate_packages(topic_id: str):
|
||||
"""
|
||||
手动触发单个选题的发布包生成。
|
||||
相当于执行 publisher.py 针对单个选题。
|
||||
"""
|
||||
# TODO: 实际调用 publisher.py 逻辑,这里先返回模拟响应
|
||||
return {"message": "Package generation triggered", "topic_id": topic_id, "status": "pending"}
|
||||
|
||||
Reference in New Issue
Block a user