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:
lt
2026-04-27 10:01:33 +08:00
parent 3cb2df51c8
commit 2bd3c83cf5
14 changed files with 1241 additions and 645 deletions
+151 -7
View File
@@ -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"}