feat: 完成布局优化 - 操作列固定、批量按钮自适应、分类标签带数量
优化内容: 1. 表格布局: - 使用 calc(100vw - 160px) 确保表格不超出视口 - 操作列 fixed='right' 固定在右侧,宽度 300px - 按钮 3 个后自动换行 (max-width: 200px) - 恢复合理列宽,不再过度压缩 2. 批量操作区域: - 容器改为 inline-block,宽度自适应按钮内容 - 背景宽度与按钮总宽度匹配 3. 分类标签: - 显示数量 (如 '待处理 (20)') - 点击切换筛选,去掉误导的 'X' 图标 4. 删除功能: - 操作列增加删除按钮 - 删除前弹出确认对话框 5. 系统日志: - 修复后端日志路径 (parents[4]) - 404 时显示友好提示 6. 其他: - 左侧菜单宽度 160px - 所有功能保留 (登录、用户管理、批量操作等)
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# 宇之然内容创作平台 - 环境变量配置
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/yuzhiran_db
|
||||
|
||||
# JWT安全配置
|
||||
SECRET_KEY=your-secret-key-here-change-in-production
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=10080
|
||||
|
||||
# 应用配置
|
||||
DEBUG=True
|
||||
ENVIRONMENT=development
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# CORS配置(生产环境)
|
||||
ALLOWED_ORIGINS=http://localhost:8080,https://yourdomain.com
|
||||
@@ -0,0 +1,49 @@
|
||||
# 宇之然内容创作平台 - 后端Docker镜像
|
||||
|
||||
FROM python:3.10-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制requirements文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装Python依赖(带缓存优化)
|
||||
RUN pip install --user --no-cache-dir -r requirements.txt
|
||||
|
||||
# 生产阶段
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 从builder阶段复制已安装的依赖
|
||||
COPY --from=builder /root/.local /root/.local
|
||||
COPY . .
|
||||
|
||||
# 确保PATH包含用户本地bin目录
|
||||
ENV PATH=/root/.local/bin:$PATH
|
||||
|
||||
# 创建非root用户
|
||||
RUN groupadd -r appuser && useradd -r -g appuser appuser
|
||||
RUN chown -R appuser:appuser /app
|
||||
|
||||
USER appuser
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8001/health || exit 1
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
# 运行应用
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
|
||||
# 标签信息
|
||||
LABEL maintainer="宇之然团队"
|
||||
LABEL version="1.0.0"
|
||||
LABEL description="企业级内容创作管理系统"
|
||||
@@ -0,0 +1,182 @@
|
||||
# 宇之然内容创作平台 - 管理员API
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from ..core.security import get_current_admin_user
|
||||
from ..database import get_db
|
||||
from ..models import User, AuditLog
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/users", response_model=List[dict])
|
||||
async def get_users(
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取用户列表(管理员功能)"""
|
||||
|
||||
users = db.query(User).all()
|
||||
result = []
|
||||
for user in users:
|
||||
result.append({
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"role": user.role,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None,
|
||||
"last_login": user.last_login.isoformat() if user.last_login else None
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@router.post("/users", response_model=dict)
|
||||
async def create_user(
|
||||
username: str,
|
||||
password: str,
|
||||
role: str = "user",
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建新用户(管理员功能)"""
|
||||
|
||||
# 检查用户名是否已存在
|
||||
existing_user = db.query(User).filter(User.username == username).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户名已存在"
|
||||
)
|
||||
|
||||
# 验证角色
|
||||
if role not in ["admin", "user"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="角色必须是 admin 或 user"
|
||||
)
|
||||
|
||||
# 导入密码哈希函数
|
||||
from ..core.security import get_password_hash
|
||||
|
||||
# 创建新用户
|
||||
new_user = User(
|
||||
username=username,
|
||||
password_hash=get_password_hash(password),
|
||||
role=role
|
||||
)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
||||
# 记录审计日志
|
||||
from ..core.security import create_audit_log
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="create_user",
|
||||
resource_type="user",
|
||||
resource_id=new_user.id,
|
||||
details=f"角色: {role}"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": new_user.id,
|
||||
"username": new_user.username,
|
||||
"role": new_user.role,
|
||||
"created_at": new_user.created_at.isoformat() if new_user.created_at else None
|
||||
}
|
||||
|
||||
@router.put("/users/{user_id}", response_model=dict)
|
||||
async def update_user(
|
||||
user_id: int,
|
||||
username: str = None,
|
||||
role: str = None,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新用户信息(管理员功能)"""
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
updates = {}
|
||||
|
||||
if username is not None:
|
||||
# 检查新用户名是否已被使用(除了当前用户)
|
||||
existing = db.query(User).filter(
|
||||
User.username == username,
|
||||
User.id != user_id
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户名已被使用"
|
||||
)
|
||||
user.username = username
|
||||
updates["username"] = username
|
||||
|
||||
if role is not None:
|
||||
if role not in ["admin", "user"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="角色必须是 admin 或 user"
|
||||
)
|
||||
user.role = role
|
||||
updates["role"] = role
|
||||
|
||||
if updates:
|
||||
db.commit()
|
||||
|
||||
# 记录审计日志
|
||||
from ..core.security import create_audit_log
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="update_user",
|
||||
resource_type="user",
|
||||
resource_id=user_id,
|
||||
details=f"更新字段: {', '.join(updates.keys())}"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"role": user.role,
|
||||
"updated_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
@router.delete("/users/{user_id}")
|
||||
async def delete_user(
|
||||
user_id: int,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除用户(管理员功能)"""
|
||||
|
||||
# 不能删除自己
|
||||
if user_id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="不能删除自己的账户"
|
||||
)
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
# 记录审计日志
|
||||
from ..core.security import create_audit_log
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="delete_user",
|
||||
resource_type="user",
|
||||
resource_id=user_id,
|
||||
details="用户账户已删除"
|
||||
)
|
||||
|
||||
return {"message": "用户已成功删除"}
|
||||
@@ -0,0 +1,63 @@
|
||||
# 宇之然内容创作平台 - 文章预览API
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Topic
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/{topic_id}/preview")
|
||||
async def get_preview(
|
||||
topic_id: int,
|
||||
platform: str = "zhihu",
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取文章预览HTML"""
|
||||
|
||||
# 检查选题是否存在
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="文章不存在")
|
||||
|
||||
# 生成预览HTML(简化实现)
|
||||
preview_html = generate_preview_html(topic.title, platform)
|
||||
|
||||
return {"html": preview_html}
|
||||
|
||||
def generate_preview_html(title: str, platform: str) -> str:
|
||||
"""根据平台和标题生成预览HTML"""
|
||||
|
||||
base_template = f"""
|
||||
<div class="article-preview">
|
||||
<header class="header">
|
||||
<h1>{title}</h1>
|
||||
<div class="meta">
|
||||
<span class="platform">{platform}</span>
|
||||
<span class="date">2026-04-26</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<p>这里是文章的正文内容...</p>
|
||||
<p>文章包含多个段落,展示不同的写作风格和结构。</p>
|
||||
<p>在实际生产环境中,这里应该是完整的文章内容。</p>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="tags">
|
||||
<span class="tag">#人工智能</span>
|
||||
<span class="tag">#科技</span>
|
||||
<span class="tag">#趋势</span>
|
||||
</div>
|
||||
<div class="interaction">
|
||||
<button class="like">👍 点赞</button>
|
||||
<button class="share">🔗 分享</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return base_template
|
||||
@@ -0,0 +1,146 @@
|
||||
# 宇之然内容创作平台 - 认证API
|
||||
|
||||
from datetime import timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
import secrets
|
||||
|
||||
from ..core.security import (
|
||||
verify_password, get_password_hash, create_access_token,
|
||||
create_audit_log
|
||||
)
|
||||
from ..database import get_db
|
||||
from ..models import User
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
token: str
|
||||
role: str
|
||||
user: dict
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
role: str = "user"
|
||||
|
||||
@router.post("/login", response_model=LoginResponse)
|
||||
async def login(
|
||||
login_data: LoginRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""用户登录"""
|
||||
user = db.query(User).filter(User.username == login_data.username).first()
|
||||
|
||||
# 检查用户是否存在和密码是否正确
|
||||
if not user or not verify_password(login_data.password, user.password_hash):
|
||||
# 记录失败的登录尝试
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=0, # 未知用户
|
||||
action="login_failed",
|
||||
resource_type="user",
|
||||
resource_id=None,
|
||||
details=f"用户名: {login_data.username}",
|
||||
ip_address=request.client.host,
|
||||
user_agent=request.headers.get("User-Agent", "")
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误"
|
||||
)
|
||||
|
||||
# 更新最后登录时间
|
||||
user.last_login = __import__('datetime').datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
# 创建访问令牌
|
||||
access_token_expires = timedelta(minutes=10080) # 7天
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username},
|
||||
expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
# 记录成功的登录
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
action="login",
|
||||
resource_type="user",
|
||||
resource_id=user.id,
|
||||
details=f"登录成功",
|
||||
ip_address=request.client.host,
|
||||
user_agent=request.headers.get("User-Agent", "")
|
||||
)
|
||||
|
||||
return {
|
||||
"token": access_token,
|
||||
"role": user.role,
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"role": user.role,
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None,
|
||||
"last_login": user.last_login.isoformat() if user.last_login else None
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/me")
|
||||
async def read_users_me(
|
||||
current_user=Depends(lambda: None), # 占位符,实际由依赖注入
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取当前用户信息"""
|
||||
# 这里应该使用JWT验证中间件获取current_user
|
||||
# 简化实现...
|
||||
raise HTTPException(status_code=501, detail="功能待实现")
|
||||
|
||||
@router.post("/register")
|
||||
async def register(
|
||||
user_data: RegisterRequest,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""用户注册(管理员功能)"""
|
||||
# 检查用户名是否已存在
|
||||
existing_user = db.query(User).filter(User.username == user_data.username).first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="用户名已存在"
|
||||
)
|
||||
|
||||
# 创建新用户
|
||||
hashed_password = get_password_hash(user_data.password)
|
||||
new_user = User(
|
||||
username=user_data.username,
|
||||
password_hash=hashed_password,
|
||||
role=user_data.role
|
||||
)
|
||||
db.add(new_user)
|
||||
db.commit()
|
||||
db.refresh(new_user)
|
||||
|
||||
# 记录审计日志
|
||||
from .auth import create_audit_log
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=new_user.id,
|
||||
action="create_user",
|
||||
resource_type="user",
|
||||
resource_id=new_user.id,
|
||||
details=f"角色: {user_data.role}"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": new_user.id,
|
||||
"username": new_user.username,
|
||||
"role": new_user.role,
|
||||
"created_at": new_user.created_at.isoformat() if new_user.created_at else None
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
# 宇之然内容创作平台 - 文章生成API
|
||||
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import asyncio
|
||||
|
||||
from ..core.security import get_current_user, create_audit_log
|
||||
from ..database import get_db
|
||||
from ..models import Topic, User, GenerateTask
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class BatchGenerateRequest(BaseModel):
|
||||
topic_ids: Optional[List[int]] = None
|
||||
|
||||
class BatchOptimizeRequest(BaseModel):
|
||||
topic_ids: List[int]
|
||||
|
||||
@router.post("/run")
|
||||
async def batch_generate(
|
||||
request: BatchGenerateRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""批量创建选题文章"""
|
||||
|
||||
# 如果没有指定topic_ids,则获取所有待处理的选题
|
||||
if not request.topic_ids:
|
||||
topics = db.query(Topic).filter(Topic.status == "待处理").all()
|
||||
topic_ids = [t.id for t in topics]
|
||||
else:
|
||||
topic_ids = request.topic_ids
|
||||
|
||||
if not topic_ids:
|
||||
return {"result": {"ok": True, "count": 0}}
|
||||
|
||||
# 验证选题是否存在且状态正确
|
||||
valid_topics = db.query(Topic).filter(
|
||||
Topic.id.in_(topic_ids),
|
||||
Topic.status == "待处理"
|
||||
).all()
|
||||
|
||||
if len(valid_topics) != len(topic_ids):
|
||||
invalid_count = len(topic_ids) - len(valid_topics)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"有{invalid_count}个选题状态不正确或不存在"
|
||||
)
|
||||
|
||||
# 创建生成任务记录
|
||||
tasks = []
|
||||
for topic in valid_topics:
|
||||
task = GenerateTask(
|
||||
topic_id=topic.id,
|
||||
status="pending",
|
||||
created_by=current_user.id
|
||||
)
|
||||
db.add(task)
|
||||
tasks.append(task)
|
||||
|
||||
db.commit()
|
||||
|
||||
# 异步执行生成任务(简化实现)
|
||||
# 实际生产环境应使用Celery等任务队列
|
||||
asyncio.create_task(process_generation_tasks(tasks))
|
||||
|
||||
# 更新选题状态为"待审查"
|
||||
for topic in valid_topics:
|
||||
topic.generated_at = datetime.utcnow()
|
||||
topic.status = "待审查"
|
||||
topic.updated_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
|
||||
# 记录审计日志
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="batch_generate",
|
||||
resource_type="topic",
|
||||
resource_id=None,
|
||||
details=f"处理选题数量: {len(tasks)}"
|
||||
)
|
||||
|
||||
return {
|
||||
"result": {
|
||||
"ok": True,
|
||||
"count": len(tasks)
|
||||
}
|
||||
}
|
||||
|
||||
@router.post("/optimize/run")
|
||||
async def batch_optimize(
|
||||
request: BatchOptimizeRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""批量优化选题文章"""
|
||||
|
||||
# 检查选题是否存在且状态正确(必须是待审查)
|
||||
valid_topics = db.query(Topic).filter(
|
||||
Topic.id.in_(request.topic_ids),
|
||||
Topic.status == "待审查"
|
||||
).all()
|
||||
|
||||
if len(valid_topics) != len(request.topic_ids):
|
||||
invalid_count = len(request.topic_ids) - len(valid_topics)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"有{invalid_count}个选题状态不正确或不存在"
|
||||
)
|
||||
|
||||
# 执行优化逻辑(简化实现)
|
||||
auto_passed = 0
|
||||
need_manual = 0
|
||||
|
||||
for topic in valid_topics:
|
||||
# 这里应该调用实际的合规性检查逻辑
|
||||
# 简化实现:随机决定通过或不通过
|
||||
import random
|
||||
if random.choice([True, False]):
|
||||
topic.status = "待发布"
|
||||
auto_passed += 1
|
||||
else:
|
||||
topic.status = "待审查"
|
||||
need_manual += 1
|
||||
|
||||
topic.updated_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
|
||||
# 记录审计日志
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="batch_optimize",
|
||||
resource_type="topic",
|
||||
resource_id=None,
|
||||
details=f"自动通过: {auto_passed}, 需人工: {need_manual}"
|
||||
)
|
||||
|
||||
return {
|
||||
"summary": {
|
||||
"passed_auto": auto_passed,
|
||||
"need_manual": need_manual,
|
||||
"total": len(valid_topics)
|
||||
}
|
||||
}
|
||||
|
||||
async def process_generation_tasks(tasks: List[GenerateTask]):
|
||||
"""处理生成任务(异步函数)"""
|
||||
# 这里是生成文章的异步逻辑
|
||||
# 实际生产环境应使用Celery等专业的任务队列系统
|
||||
|
||||
for task in tasks:
|
||||
try:
|
||||
# 模拟生成过程
|
||||
await asyncio.sleep(2) # 模拟耗时操作
|
||||
|
||||
# 更新任务状态为已完成
|
||||
task.status = "completed"
|
||||
task.result = {"success": True, "message": "文章生成完成"}
|
||||
|
||||
except Exception as e:
|
||||
# 处理失败情况
|
||||
task.status = "failed"
|
||||
task.result = {"success": False, "error": str(e)}
|
||||
|
||||
# 注意:这个函数需要访问数据库,实际实现中可能需要额外的依赖注入
|
||||
@@ -0,0 +1,63 @@
|
||||
# 宇之然内容创作平台 - 日志API
|
||||
|
||||
from datetime import datetime, date
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import AuditLog
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/system/{date}")
|
||||
async def get_system_logs(
|
||||
date: str,
|
||||
log_type: str = "creator", # creator | collector
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取系统日志"""
|
||||
|
||||
try:
|
||||
target_date = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="日期格式不正确,应为 YYYY-MM-DD")
|
||||
|
||||
# 查询指定日期的审计日志(简化实现)
|
||||
# 实际生产环境应从专门的日志系统中查询
|
||||
logs = [
|
||||
f"{target_date} 10:00:00 INFO 创建选题:人工智能发展趋势",
|
||||
f"{target_date} 10:05:00 INFO 选题状态更新为:待审查",
|
||||
f"{target_date} 10:10:00 INFO 批量生成文章任务已启动",
|
||||
f"{target_date} 10:15:00 INFO 文章优化完成,自动通过3篇",
|
||||
f"{target_date} 10:20:00 INFO 发布文章到知乎平台",
|
||||
f"{target_date} 10:25:00 INFO 用户登录成功",
|
||||
f"{target_date} 10:30:00 WARNING 选题合规性检查失败",
|
||||
f"{target_date} 10:35:00 ERROR 文章生成过程中出现异常"
|
||||
]
|
||||
|
||||
return {"content": logs}
|
||||
|
||||
@router.get("/audit")
|
||||
async def get_audit_logs(
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
action: str = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取审计日志(管理员功能)"""
|
||||
|
||||
# 这里应该实现真实的数据库查询
|
||||
# 简化实现返回空列表
|
||||
return []
|
||||
|
||||
@router.post("/clear")
|
||||
async def clear_old_logs(
|
||||
days: int = 30,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""清理旧日志(管理员功能)"""
|
||||
|
||||
# 这里应该实现真实的数据库删除操作
|
||||
# 简化实现
|
||||
return {"message": "日志清理功能待实现"}
|
||||
@@ -0,0 +1,92 @@
|
||||
# 宇之然内容创作平台 - 文章发布API
|
||||
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
import random
|
||||
|
||||
from ..core.security import get_current_user, create_audit_log
|
||||
from ..database import get_db
|
||||
from ..models import Topic, User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class PublishRequest(BaseModel):
|
||||
topic_id: int
|
||||
|
||||
@router.post("/create")
|
||||
async def create_publication(
|
||||
request: PublishRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""发布选题到各平台"""
|
||||
|
||||
# 检查选题是否存在
|
||||
topic = db.query(Topic).filter(Topic.id == request.topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="选题不存在")
|
||||
|
||||
# 检查选题状态是否正确(必须是待发布)
|
||||
if topic.status != "待发布":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"选题状态不正确,当前状态: {topic.status}"
|
||||
)
|
||||
|
||||
# 模拟发布到各个平台(实际生产环境应调用第三方API)
|
||||
urls = {}
|
||||
|
||||
# 知乎发布
|
||||
if publish_to_zhihu(topic.title):
|
||||
urls["zhihu"] = f"https://zhihu.com/article/{random.randint(100000, 999999)}"
|
||||
|
||||
# 微信公众号发布
|
||||
if publish_to_wechat(topic.title):
|
||||
urls["wechat"] = f"https://mp.weixin.qq.com/s/{random.randint(100000, 999999)}"
|
||||
|
||||
# 小红书发布
|
||||
if publish_to_xiaohongshu(topic.title):
|
||||
urls["xiaohongshu"] = f"https://www.xiaohongshu.com/discovery/item/{random.randint(100000, 999999)}"
|
||||
|
||||
# 更新选题状态和发布时间
|
||||
topic.published_at = datetime.utcnow()
|
||||
topic.published_urls = urls
|
||||
topic.status = "已发布"
|
||||
topic.updated_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
|
||||
# 记录审计日志
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="publish",
|
||||
resource_type="topic",
|
||||
resource_id=topic.id,
|
||||
details=f"发布到平台: {list(urls.keys())}"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"urls": urls,
|
||||
"published_at": topic.published_at.isoformat() if topic.published_at else None
|
||||
}
|
||||
|
||||
def publish_to_zhihu(title: str) -> bool:
|
||||
"""模拟发布到知乎"""
|
||||
# 实际实现应调用知乎API
|
||||
import random
|
||||
return random.choice([True, False])
|
||||
|
||||
def publish_to_wechat(title: str) -> bool:
|
||||
"""模拟发布到微信公众号"""
|
||||
# 实际实现应调用微信公众号API
|
||||
import random
|
||||
return random.choice([True, False])
|
||||
|
||||
def publish_to_xiaohongshu(title: str) -> bool:
|
||||
"""模拟发布到小红书"""
|
||||
# 实际实现应调用小红书API
|
||||
import random
|
||||
return random.choice([True, False])
|
||||
@@ -0,0 +1,90 @@
|
||||
# 宇之然内容创作平台 - 系统管理API
|
||||
|
||||
from datetime import datetime, date
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
import os
|
||||
|
||||
from ..database import get_db
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/status")
|
||||
async def get_system_status(db: Session = Depends(get_db)):
|
||||
"""获取系统概览状态"""
|
||||
today = date.today()
|
||||
|
||||
# 统计总数
|
||||
total_topics = db.query(func.count(Topic.id)).scalar()
|
||||
|
||||
# 今日新选题数
|
||||
today_articles = db.query(func.count(Topic.id)).filter(
|
||||
func.date(Topic.created_at) == today
|
||||
).scalar()
|
||||
|
||||
# 各状态选题数量
|
||||
topics_by_status = {}
|
||||
for status in ["待处理", "待审查", "待发布", "已发布"]:
|
||||
count = db.query(func.count(Topic.id)).filter(
|
||||
Topic.status == status
|
||||
).scalar()
|
||||
topics_by_status[status] = count
|
||||
|
||||
return {
|
||||
"total_topics": total_topics,
|
||||
"today_articles": today_articles,
|
||||
"topics_by_status": topics_by_status,
|
||||
"generated_count": db.query(func.count(Topic.id)).filter(
|
||||
Topic.generated_at.isnot(None)
|
||||
).scalar(),
|
||||
"published_count": db.query(func.count(Topic.id)).filter(
|
||||
Topic.published_at.isnot(None)
|
||||
).scalar()
|
||||
}
|
||||
|
||||
@router.get("/pipeline/status")
|
||||
async def get_pipeline_status():
|
||||
"""获取流水线状态"""
|
||||
import subprocess
|
||||
import json
|
||||
|
||||
# 检查各个模块的运行状态
|
||||
pipeline_modules = {
|
||||
"creator": {
|
||||
"exists": os.path.exists("modules/creator"),
|
||||
"has_error": False, # 简化实现,实际应检查日志或进程状态
|
||||
"last_run": get_last_run_time("creator"),
|
||||
"error": None
|
||||
},
|
||||
"collector": {
|
||||
"exists": os.path.exists("modules/collector"),
|
||||
"has_error": False,
|
||||
"last_run": get_last_run_time("collector"),
|
||||
"error": None
|
||||
}
|
||||
}
|
||||
|
||||
# 统计分布(这里应该从数据库查询,简化为静态数据)
|
||||
status_distribution = {
|
||||
"待处理": 0,
|
||||
"待审查": 0,
|
||||
"待发布": 0
|
||||
}
|
||||
|
||||
# 实际实现中应该从数据库查询真实数据
|
||||
# for status in ["待处理", "待审查", "待发布"]:
|
||||
# count = db.query(func.count(Topic.id)).filter(
|
||||
# Topic.status == status
|
||||
# ).scalar()
|
||||
# status_distribution[status] = count
|
||||
|
||||
return {
|
||||
"status_distribution": status_distribution,
|
||||
"pipeline_modules": pipeline_modules,
|
||||
"topics_count": 0 # 简化实现
|
||||
}
|
||||
|
||||
def get_last_run_time(module_name: str) -> str:
|
||||
"""获取模块最后运行时间(简化实现)"""
|
||||
# 实际实现应检查日志文件或数据库记录
|
||||
return "2026-04-26 15:30:00"
|
||||
@@ -0,0 +1,188 @@
|
||||
# 宇之然内容创作平台 - 选题管理API
|
||||
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
import json
|
||||
|
||||
from ..core.security import get_current_admin_user, create_audit_log
|
||||
from ..database import get_db
|
||||
from ..models import Topic, User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class TopicCreateRequest(BaseModel):
|
||||
title: str
|
||||
field: Optional[str] = None
|
||||
priority_score: int = 0
|
||||
|
||||
class TopicUpdateRequest(BaseModel):
|
||||
title: Optional[str] = None
|
||||
field: Optional[str] = None
|
||||
priority_score: Optional[int] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
@router.get("/", response_model=List[dict])
|
||||
async def get_topics(
|
||||
status: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
size: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取选题列表"""
|
||||
query = db.query(Topic)
|
||||
|
||||
# 状态筛选
|
||||
if status:
|
||||
query = query.filter(Topic.status == status)
|
||||
|
||||
# 分页
|
||||
offset = (page - 1) * size
|
||||
topics = query.offset(offset).limit(size).all()
|
||||
|
||||
# 转换为字典格式
|
||||
result = []
|
||||
for topic in topics:
|
||||
result.append({
|
||||
"id": topic.id,
|
||||
"title": topic.title,
|
||||
"field": topic.field,
|
||||
"priority_score": topic.priority_score,
|
||||
"status": topic.status,
|
||||
"compliance_score": topic.compliance_score,
|
||||
"created_at": topic.created_at.isoformat() if topic.created_at else None,
|
||||
"updated_at": topic.updated_at.isoformat() if topic.updated_at else None,
|
||||
"generated_at": topic.generated_at.isoformat() if topic.generated_at else None,
|
||||
"published_at": topic.published_at.isoformat() if topic.published_at else None,
|
||||
"published_urls": topic.published_urls
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@router.post("/", response_model=dict)
|
||||
async def create_topic(
|
||||
topic_data: TopicCreateRequest,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""创建新选题(管理员功能)"""
|
||||
new_topic = Topic(
|
||||
title=topic_data.title,
|
||||
field=topic_data.field,
|
||||
priority_score=topic_data.priority_score,
|
||||
status="待处理"
|
||||
)
|
||||
db.add(new_topic)
|
||||
db.commit()
|
||||
db.refresh(new_topic)
|
||||
|
||||
# 记录审计日志
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="create_topic",
|
||||
resource_type="topic",
|
||||
resource_id=new_topic.id,
|
||||
details=f"标题: {topic_data.title}"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": new_topic.id,
|
||||
"title": new_topic.title,
|
||||
"status": new_topic.status,
|
||||
"created_at": new_topic.created_at.isoformat() if new_topic.created_at else None
|
||||
}
|
||||
|
||||
@router.put("/{topic_id}", response_model=dict)
|
||||
async def update_topic(
|
||||
topic_id: int,
|
||||
topic_data: TopicUpdateRequest,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""更新选题信息(管理员功能)"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="选题不存在")
|
||||
|
||||
# 更新字段
|
||||
if topic_data.title is not None:
|
||||
topic.title = topic_data.title
|
||||
if topic_data.field is not None:
|
||||
topic.field = topic_data.field
|
||||
if topic_data.priority_score is not None:
|
||||
topic.priority_score = topic_data.priority_score
|
||||
if topic_data.status is not None:
|
||||
topic.status = topic_data.status
|
||||
|
||||
topic.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(topic)
|
||||
|
||||
# 记录审计日志
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="update_topic",
|
||||
resource_type="topic",
|
||||
resource_id=topic_id,
|
||||
details=f"状态更新为: {topic_data.status}"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": topic.id,
|
||||
"title": topic.title,
|
||||
"status": topic.status,
|
||||
"updated_at": topic.updated_at.isoformat() if topic.updated_at else None
|
||||
}
|
||||
|
||||
@router.delete("/{topic_id}")
|
||||
async def delete_topic(
|
||||
topic_id: int,
|
||||
current_user: User = Depends(get_current_admin_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""删除选题(管理员功能)"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="选题不存在")
|
||||
|
||||
db.delete(topic)
|
||||
db.commit()
|
||||
|
||||
# 记录审计日志
|
||||
create_audit_log(
|
||||
db=db,
|
||||
user_id=current_user.id,
|
||||
action="delete_topic",
|
||||
resource_type="topic",
|
||||
resource_id=topic_id,
|
||||
details="选题已删除"
|
||||
)
|
||||
|
||||
return {"message": "选题已成功删除"}
|
||||
|
||||
@router.get("/{topic_id}", response_model=dict)
|
||||
async def get_topic(
|
||||
topic_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取单个选题详情"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="选题不存在")
|
||||
|
||||
return {
|
||||
"id": topic.id,
|
||||
"title": topic.title,
|
||||
"field": topic.field,
|
||||
"priority_score": topic.priority_score,
|
||||
"status": topic.status,
|
||||
"compliance_score": topic.compliance_score,
|
||||
"created_at": topic.created_at.isoformat() if topic.created_at else None,
|
||||
"updated_at": topic.updated_at.isoformat() if topic.updated_at else None,
|
||||
"generated_at": topic.generated_at.isoformat() if topic.generated_at else None,
|
||||
"published_at": topic.published_at.isoformat() if topic.published_at else None,
|
||||
"published_urls": topic.published_urls
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from ..database import get_db
|
||||
from ..models import Topic
|
||||
|
||||
router = APIRouter(prefix="/api/publisher", tags=["publisher"])
|
||||
|
||||
# 项目根目录(从 api/publisher.py 上升到 yu-zhi-ran 根目录)
|
||||
import os
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
SCRIPTS_DIR = PROJECT_ROOT / "scripts"
|
||||
|
||||
@router.get("/ready")
|
||||
def get_ready_topics(
|
||||
platform: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取待发布的选题(状态为 ready)"""
|
||||
query = db.query(Topic).filter(Topic.status == "ready")
|
||||
if platform:
|
||||
# 筛选未在该平台发布的选题
|
||||
# platform_urls 是 JSON 字段,需要特殊处理
|
||||
pass # 简化:暂不筛选
|
||||
topics = query.order_by(Topic.ready_at.desc()).all()
|
||||
return topics
|
||||
|
||||
@router.post("/generate/{topic_id}")
|
||||
def generate_publish_package(
|
||||
topic_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""为指定选题生成发布包(所有平台HTML)"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="Topic not found")
|
||||
|
||||
# 调用 publisher.py 脚本
|
||||
script_path = SCRIPTS_DIR / "publisher.py"
|
||||
if not script_path.exists():
|
||||
raise HTTPException(status_code=500, detail="Publisher script not found")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["python3", str(script_path), "--topic-id", topic_id],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300,
|
||||
cwd=str(PROJECT_ROOT)
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Publisher failed: {result.stderr}")
|
||||
|
||||
return {
|
||||
"message": "Publish package generated",
|
||||
"topic_id": topic_id,
|
||||
"output": result.stdout
|
||||
}
|
||||
except subprocess.TimeoutExpired:
|
||||
raise HTTPException(status_code=504, detail="Publisher timeout")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.get("/packages/{topic_id}")
|
||||
def list_platform_packages(topic_id: str):
|
||||
"""列出某个选题的所有平台发布包"""
|
||||
release_dir = PROJECT_ROOT / "automation" / "data" / "releases"
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
packages = []
|
||||
for platform in ["zhihu", "wechat", "xiaohongshu", "bilibili", "toutiao"]:
|
||||
html_file = release_dir / today / platform / f"{platform}_{topic_id}_{platform}.html"
|
||||
if html_file.exists():
|
||||
packages.append({
|
||||
"platform": platform,
|
||||
"file": str(html_file.relative_to(PROJECT_ROOT)),
|
||||
"size": html_file.stat().st_size
|
||||
})
|
||||
|
||||
published_dir = PROJECT_ROOT / "content" / "published" / topic_id / "手动发布"
|
||||
if published_dir.exists():
|
||||
for platform_dir in published_dir.iterdir():
|
||||
if platform_dir.is_dir():
|
||||
html_file = platform_dir / "文章.html"
|
||||
if html_file.exists():
|
||||
packages.append({
|
||||
"platform": platform_dir.name,
|
||||
"file": str(html_file.relative_to(PROJECT_ROOT)),
|
||||
"size": html_file.stat().st_size,
|
||||
"manual": True
|
||||
})
|
||||
|
||||
return {"topic_id": topic_id, "packages": packages}
|
||||
|
||||
@router.get("/package/{topic_id}/{platform}")
|
||||
def get_package_html(topic_id: str, platform: str):
|
||||
"""获取指定平台发布包的HTML内容"""
|
||||
# 优先查找 published 目录(手动发布包)
|
||||
published_html = PROJECT_ROOT / "content" / "published" / topic_id / "手动发布" / platform / "文章.html"
|
||||
if published_html.exists():
|
||||
return {"html": published_html.read_text(encoding='utf-8')}
|
||||
|
||||
# 其次查找 releases 目录(自动生成)
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
release_html = PROJECT_ROOT / "automation" / "data" / "releases" / today / platform / f"{platform}_{topic_id}_{platform}.html"
|
||||
if release_html.exists():
|
||||
return {"html": release_html.read_text(encoding='utf-8')}
|
||||
|
||||
raise HTTPException(status_code=404, detail="Package not found")
|
||||
|
||||
@router.post("/mark/{topic_id}/published")
|
||||
def mark_as_published(
|
||||
topic_id: str,
|
||||
platform_urls: dict,
|
||||
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")
|
||||
|
||||
topic.status = "published"
|
||||
topic.published_at = datetime.now().date()
|
||||
topic.platform_urls = platform_urls
|
||||
db.commit()
|
||||
|
||||
return {"message": "Topic marked as published", "topic_id": topic_id}
|
||||
|
||||
@router.get("/status")
|
||||
def get_publisher_status():
|
||||
"""获取发布统计"""
|
||||
# 统计今日已发布数量等
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
release_dir = PROJECT_ROOT / "automation" / "data" / "releases" / today
|
||||
|
||||
stats = {
|
||||
"today_releases": 0,
|
||||
"platforms": {}
|
||||
}
|
||||
|
||||
if release_dir.exists():
|
||||
for platform_dir in release_dir.iterdir():
|
||||
if platform_dir.is_dir():
|
||||
count = len(list(platform_dir.glob("*.html")))
|
||||
stats["platforms"][platform_dir.name] = count
|
||||
stats["today_releases"] += count
|
||||
|
||||
return stats
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""发布管理 API"""
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Topic, PublishRecord, User
|
||||
from sqlalchemy.orm import Session
|
||||
from .auth import verify_token
|
||||
from ..core.audit_logger import audit_log
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class PublishRequest(BaseModel):
|
||||
topic_id: str
|
||||
|
||||
class PublishResponse(BaseModel):
|
||||
ok: bool
|
||||
topic_id: str
|
||||
message: str
|
||||
|
||||
def get_current_user(request: Request, db: Session = Depends(get_db)) -> User:
|
||||
"""获取当前登录用户(可选,未登录也允许,但记录为 anonymous)"""
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
return None
|
||||
token = auth_header.split(" ")[1]
|
||||
try:
|
||||
from .auth import verify_token
|
||||
return verify_token(token, db)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@router.post("/api/publishing/create", response_model=PublishResponse)
|
||||
async def create_publish_record(
|
||||
req: PublishRequest,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""标记选题为已发布,并创建发布记录"""
|
||||
try:
|
||||
# 查找选题
|
||||
topic = db.query(Topic).filter(Topic.id == req.topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail=f"选题 {req.topic_id} 不存在")
|
||||
|
||||
if topic.status != '待发布':
|
||||
raise HTTPException(status_code=400, detail=f"选题 {req.topic_id} 状态不是待发布")
|
||||
|
||||
# 更新选题状态
|
||||
topic.status = '已发布'
|
||||
topic.updated_at = datetime.now()
|
||||
topic.published_at = datetime.now().date() # 设置发布时间为今天
|
||||
|
||||
# 创建发布记录
|
||||
operator = current_user.username if current_user else 'anonymous'
|
||||
record = PublishRecord(
|
||||
topic_id=req.topic_id,
|
||||
platform='all',
|
||||
action='publish',
|
||||
status='success',
|
||||
operator=operator,
|
||||
description=f"选题 {req.topic_id} 已发布"
|
||||
)
|
||||
db.add(record)
|
||||
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"},
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent", ""),
|
||||
db=db
|
||||
)
|
||||
|
||||
return PublishResponse(
|
||||
ok=True,
|
||||
topic_id=req.topic_id,
|
||||
message=f"选题 {req.topic_id} 已成功发布"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -185,3 +185,14 @@ def generate_packages(topic_id: str):
|
||||
"""
|
||||
# TODO: 实际调用 publisher.py 逻辑,这里先返回模拟响应
|
||||
return {"message": "Package generation triggered", "topic_id": topic_id, "status": "pending"}
|
||||
|
||||
@router.delete("/{topic_id}")
|
||||
def delete_topic(topic_id: str, db: Session = Depends(get_db)):
|
||||
"""删除选题"""
|
||||
topic = db.query(Topic).filter(Topic.id == topic_id).first()
|
||||
if not topic:
|
||||
raise HTTPException(status_code=404, detail="选题不存在")
|
||||
|
||||
db.delete(topic)
|
||||
db.commit()
|
||||
return {"message": "删除成功", "topic_id": topic_id}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
审计日志记录模块
|
||||
|
||||
用法:
|
||||
from .audit_logger import audit_log
|
||||
audit_log(action="create_user", user=current_user, details={...}, request=request, db=db)
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models import AuditLog
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def audit_log(
|
||||
action: str,
|
||||
*,
|
||||
user=None, # User 对象或 None
|
||||
username: Optional[str] = None,
|
||||
resource_type: Optional[str] = None,
|
||||
resource_id: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
db: Session = None
|
||||
) -> None:
|
||||
"""
|
||||
记录审计日志
|
||||
|
||||
参数:
|
||||
action: 操作类型(必填),如 "login", "create_user", "delete_user", "publish"
|
||||
user: 操作用户的 User 对象(可选,如果提供则自动填充 user_id 和 username)
|
||||
username: 直接指定用户名(如果 user 为 None 则必须提供)
|
||||
resource_type: 资源类型,如 "user", "topic", "publish_record"
|
||||
resource_id: 资源ID
|
||||
details: 操作详情字典(如变更前后的值)
|
||||
ip_address: IP 地址
|
||||
user_agent: User-Agent
|
||||
db: 数据库会话(必填)
|
||||
"""
|
||||
if db is None:
|
||||
raise ValueError("db session is required")
|
||||
|
||||
# 确定 user_id 和 username
|
||||
user_id = None
|
||||
if user is not None:
|
||||
user_id = getattr(user, 'id', None)
|
||||
username = getattr(user, 'username', username)
|
||||
if not username:
|
||||
username = "anonymous"
|
||||
|
||||
log = AuditLog(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
details=details or {},
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
# 不抛出异常,避免影响主流程
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 计算项目根目录(从本文件位置上升4层)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
# 允许环境变量覆盖(适合容器部署)
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
@@ -26,7 +26,7 @@ def run_creator(topic_id: str = None):
|
||||
cwd=str(PROJECT_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5分钟超时
|
||||
timeout=1800 # 30分钟超时,避免AI撰写超时
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Creator failed: {result.stderr}")
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
""" ModelScope 专用 LLM 客户端 """
|
||||
import requests
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
class LLMError(Exception):
|
||||
pass
|
||||
|
||||
# 临时使用 NVIDIA 端点(ModelScope Key 已失效)
|
||||
CONFIG = {
|
||||
"base_url": "https://integrate.api.nvidia.com/v1",
|
||||
"api_key": "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",
|
||||
"model": "stepfun-ai/step-3.5-flash",
|
||||
}
|
||||
|
||||
def call_llm(
|
||||
prompt: str,
|
||||
system_prompt: str = "你是一个专业的内容创作助手。",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2000,
|
||||
stream: bool = False,
|
||||
) -> str:
|
||||
"""调用 ModelScope LLM 生成文本"""
|
||||
endpoint = f"{CONFIG['base_url'].rstrip('/')}/chat/completions"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {CONFIG['api_key']}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"model": CONFIG["model"],
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"stream": stream,
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(endpoint, json=payload, headers=headers, timeout=120, stream=stream)
|
||||
if resp.status_code != 200:
|
||||
raise LLMError(f"HTTP {resp.status_code}: {resp.text[:200]}")
|
||||
|
||||
if stream:
|
||||
full = []
|
||||
for line in resp.iter_lines():
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith(b'data: '):
|
||||
data = line[6:]
|
||||
if data == b'[DONE]':
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
delta = chunk['choices'][0]['delta']
|
||||
if 'reasoning_content' in delta and delta['reasoning_content']:
|
||||
full.append(delta['reasoning_content'])
|
||||
if 'content' in delta and delta['content']:
|
||||
full.append(delta['content'])
|
||||
except Exception:
|
||||
continue
|
||||
return "".join(full)
|
||||
else:
|
||||
data = resp.json()
|
||||
msg = data["choices"][0]["message"]
|
||||
content = msg.get('content') or msg.get('reasoning') or msg.get('reasoning_content')
|
||||
return content.strip() if content else ''
|
||||
except requests.RequestException as e:
|
||||
raise LLMError(f"Request failed: {e}")
|
||||
|
||||
def expand_content_with_llm(
|
||||
topic: dict,
|
||||
section_title: str,
|
||||
section_content: str,
|
||||
context: str = ""
|
||||
) -> str:
|
||||
"""扩写大纲章节,返回包含 ## 标题的完整 Markdown"""
|
||||
prompt = f"""你是一个专业的内容创作者。请将以下大纲扩展为完整的文章章节。
|
||||
|
||||
# 选题信息
|
||||
- 标题:{topic.get('title')}
|
||||
- 领域:{topic.get('field')}
|
||||
- 核心观点:{topic.get('core_concept', '')}
|
||||
- 受众痛点:{topic.get('audience_pain', '')}
|
||||
- 独特视角:{topic.get('unique_angle', '')}
|
||||
|
||||
# 当前章节
|
||||
## {section_title}
|
||||
{section_content}
|
||||
|
||||
# 要求
|
||||
- 以 `## {section_title}` 作为章节标题开头
|
||||
- 字数:300-500 字
|
||||
- 风格:客观、专业、易懂
|
||||
- 使用 Markdown 格式
|
||||
- 包含具体数据或案例(如果有)
|
||||
- 保持与整体文章调性一致
|
||||
|
||||
直接输出完整的 Markdown 章节(包括 ## 标题和正文段落)。"""
|
||||
|
||||
if context:
|
||||
prompt = f"# 参考资料\n{context}\n\n{prompt}"
|
||||
|
||||
try:
|
||||
result = call_llm(prompt, temperature=0.8, max_tokens=2000)
|
||||
return result.strip()
|
||||
except Exception as e:
|
||||
return f"## {section_title}\n\n(LLM 调用失败:{e},请手动补充)"
|
||||
|
||||
# 测试
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
print(f"[modelscope_client] 使用模型:{CONFIG['model']}")
|
||||
resp = call_llm("你好,请用一句话介绍你自己。", max_tokens=50)
|
||||
print(f"[modelscope_client] 响应:{resp}")
|
||||
except Exception as e:
|
||||
print(f"[modelscope_client] 错误:{e}")
|
||||
@@ -9,7 +9,7 @@ from typing import List
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 计算项目根目录(从本文件位置上升4层)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
|
||||
|
||||
@@ -3,53 +3,85 @@ import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from .database import SessionLocal, init_db
|
||||
from .models import Topic
|
||||
from .models import Topic, User
|
||||
import bcrypt
|
||||
|
||||
# 计算项目根目录(backend/app/initial_data.py -> 上升3层到 yu-zhi-ran)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
TOPICS_FILE = PROJECT_ROOT / "automation" / "data" / "sustainability_topics.json"
|
||||
|
||||
def import_topics_from_json():
|
||||
# 从环境变量读取管理员配置
|
||||
DEFAULT_ADMIN_USERNAME = os.getenv('DEFAULT_ADMIN_USERNAME', 'admin')
|
||||
DEFAULT_ADMIN_PASSWORD = os.getenv('DEFAULT_ADMIN_PASSWORD', 'admin123')
|
||||
|
||||
def import_initial_data():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
if db.query(Topic).count() > 0:
|
||||
print("数据库已有数据,跳过导入")
|
||||
return
|
||||
if not __import__('os').path.exists(TOPICS_FILE):
|
||||
print(f"选题文件不存在: {TOPICS_FILE}")
|
||||
return
|
||||
topics = json.loads(open(TOPICS_FILE, encoding='utf-8').read())
|
||||
for t in topics:
|
||||
topic = Topic(
|
||||
id=t['id'],
|
||||
title=t['title'],
|
||||
field=t['field'],
|
||||
format=t.get('format'),
|
||||
core_concept=t.get('core_concept'),
|
||||
audience_pain=t.get('audience_pain'),
|
||||
unique_angle=t.get('unique_angle'),
|
||||
priority=t.get('priority'),
|
||||
priority_score=t.get('priority_score', 0),
|
||||
total_score=t.get('total_score'),
|
||||
status=t.get('status', 'pending'),
|
||||
cases=t.get('cases', []),
|
||||
source_file=t.get('source_file'),
|
||||
ready_at=datetime.strptime(t['ready_at'], '%Y-%m-%d').date() if t.get('ready_at') else None,
|
||||
published_at=datetime.strptime(t['published_at'], '%Y-%m-%d').date() if t.get('published_at') else None,
|
||||
compliance_score=t.get('compliance_score'),
|
||||
platform_urls=t.get('platform_urls', {})
|
||||
# 1. 导入选题数据
|
||||
if db.query(Topic).count() == 0:
|
||||
if __import__('os').path.exists(TOPICS_FILE):
|
||||
topics = json.loads(open(TOPICS_FILE, encoding='utf-8').read())
|
||||
# 去重:保留每个 ID 最后出现的记录
|
||||
seen = {}
|
||||
for t in topics:
|
||||
seen[t['id']] = t
|
||||
unique_topics = list(seen.values())
|
||||
for t in unique_topics:
|
||||
topic = Topic(
|
||||
id=t['id'],
|
||||
title=t['title'],
|
||||
field=t['field'],
|
||||
format=t.get('format'),
|
||||
core_concept=t.get('core_concept'),
|
||||
audience_pain=t.get('audience_pain'),
|
||||
unique_angle=t.get('unique_angle'),
|
||||
priority=t.get('priority'),
|
||||
priority_score=t.get('priority_score', 0),
|
||||
total_score=t.get('total_score'),
|
||||
status=t.get('status', 'pending'),
|
||||
cases=t.get('cases', []),
|
||||
source_file=t.get('source_file'),
|
||||
ready_at=datetime.strptime(t['ready_at'], '%Y-%m-%d').date() if t.get('ready_at') else None,
|
||||
published_at=datetime.strptime(t['published_at'], '%Y-%m-%d').date() if t.get('published_at') else None,
|
||||
compliance_score=t.get('compliance_score'),
|
||||
platform_urls=t.get('platform_urls', {})
|
||||
)
|
||||
db.add(topic)
|
||||
db.commit()
|
||||
print(f"✅ 导入 {len(unique_topics)} 个选题到数据库(去重后)")
|
||||
else:
|
||||
print(f"⚠️ 选题文件不存在: {TOPICS_FILE}")
|
||||
else:
|
||||
print("数据库已有选题数据,跳过导入")
|
||||
|
||||
# 2. 创建默认管理员用户(bcrypt 哈希)
|
||||
admin_exists = db.query(User).filter(User.username == DEFAULT_ADMIN_USERNAME).first()
|
||||
if not admin_exists:
|
||||
hashed = bcrypt.hashpw(DEFAULT_ADMIN_PASSWORD.encode('utf-8'), bcrypt.gensalt())
|
||||
admin = User(
|
||||
username=DEFAULT_ADMIN_USERNAME,
|
||||
password_hash=hashed.decode('utf-8'),
|
||||
role="admin"
|
||||
)
|
||||
db.add(topic)
|
||||
db.commit()
|
||||
print(f"✅ 导入 {len(topics)} 个选题到数据库")
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
print(f"✅ 创建默认管理员: {DEFAULT_ADMIN_USERNAME}")
|
||||
else:
|
||||
# 如果管理员已存在但密码为空,更新为默认密码的哈希
|
||||
if not admin_exists.password_hash:
|
||||
hashed = bcrypt.hashpw(DEFAULT_ADMIN_PASSWORD.encode('utf-8'), bcrypt.gensalt())
|
||||
admin_exists.password_hash = hashed.decode('utf-8')
|
||||
db.commit()
|
||||
print(f"✅ 更新管理员密码")
|
||||
print(f"管理员已存在: {DEFAULT_ADMIN_USERNAME}")
|
||||
except Exception as e:
|
||||
print(f"导入失败: {e}")
|
||||
print(f"初始化失败: {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
import_topics_from_json()
|
||||
import_initial_data()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# 宇之然内容创作平台 - 安全模块
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import User
|
||||
from ..database import get_db
|
||||
|
||||
# 密码加密
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY = "your-secret-key-here" # 生产环境应从环境变量读取
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 10080 # 7天
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""生成密码哈希"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
"""创建JWT token"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
async def get_current_user(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
db: Session = Depends(get_db)
|
||||
) -> User:
|
||||
"""获取当前用户"""
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的认证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
user = db.query(User).filter(User.username == username).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
"""获取活跃用户(简单检查)"""
|
||||
# 这里可以添加更多活跃性检查逻辑
|
||||
return current_user
|
||||
|
||||
async def get_current_admin_user(current_user: User = Depends(get_current_user)):
|
||||
"""获取管理员用户"""
|
||||
if current_user.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="权限不足,需要管理员角色"
|
||||
)
|
||||
return current_user
|
||||
|
||||
def create_audit_log(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
action: str,
|
||||
resource_type: str = "",
|
||||
resource_id: int = None,
|
||||
details: str = "",
|
||||
ip_address: str = "",
|
||||
user_agent: str = ""
|
||||
):
|
||||
"""创建审计日志"""
|
||||
from ..models import AuditLog
|
||||
audit_log = AuditLog(
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
details=details,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent
|
||||
)
|
||||
db.add(audit_log)
|
||||
db.commit()
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建 publish_records 表
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from app.database import engine, Base
|
||||
from app.models import PublishRecord
|
||||
|
||||
print("正在创建 publish_records 表...")
|
||||
Base.metadata.create_all(bind=engine, tables=[PublishRecord.__table__])
|
||||
print("✅ publish_records 表创建完成")
|
||||
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
# 宇之然内容创作平台 - 数据库配置
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# 数据库URL(从环境变量读取)
|
||||
SQLALCHEMY_DATABASE_URL = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://user:password@localhost:5432/yuzhiran_db"
|
||||
)
|
||||
|
||||
# 创建数据库引擎
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
pool_size=20,
|
||||
max_overflow=30,
|
||||
pool_pre_ping=True,
|
||||
echo=False # 生产环境设为False
|
||||
)
|
||||
|
||||
# 会话工厂
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# 基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
"""获取数据库会话"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def init_db():
|
||||
"""初始化数据库(创建表)"""
|
||||
from .models import Base
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@@ -0,0 +1,75 @@
|
||||
# 宇之然内容创作平台 - 主应用入口
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import uvicorn
|
||||
|
||||
from .database import init_db
|
||||
from .core.security import SECRET_KEY
|
||||
from .api import auth, topics, system, generate, publishing, articles, logs, admin
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时初始化数据库
|
||||
print("正在初始化数据库...")
|
||||
init_db()
|
||||
print("数据库初始化完成")
|
||||
|
||||
yield
|
||||
|
||||
# 关闭时清理资源
|
||||
print("应用关闭")
|
||||
|
||||
app = FastAPI(
|
||||
title="宇之然内容创作平台 API",
|
||||
description="企业级内容创作管理系统",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS配置(允许前端访问)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # 生产环境应指定具体域名
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 路由注册
|
||||
app.include_router(auth.router, prefix="/api/auth", tags=["认证"])
|
||||
app.include_router(topics.router, prefix="/api/topics", tags=["选题管理"])
|
||||
app.include_router(system.router, prefix="/api/system", tags=["系统状态"])
|
||||
app.include_router(generate.router, prefix="/api/system", tags=["批量操作"])
|
||||
app.include_router(publishing.router, prefix="/api/publishing", tags=["文章发布"])
|
||||
app.include_router(articles.router, prefix="/api/articles", tags=["文章预览"])
|
||||
app.include_router(logs.router, prefix="/api/logs", tags=["日志系统"])
|
||||
app.include_router(admin.router, prefix="/api/admin", tags=["管理员"])
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查接口"""
|
||||
return {"status": "healthy", "timestamp": __import__('datetime').datetime.now().isoformat()}
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
"""全局异常处理器"""
|
||||
import traceback
|
||||
print(f"全局异常: {exc}")
|
||||
print(f"堆栈跟踪:\n{traceback.format_exc()}")
|
||||
return {
|
||||
"error": "服务器内部错误",
|
||||
"message": str(exc),
|
||||
"path": request.url.path
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8001,
|
||||
reload=True, # 开发环境启用热重载
|
||||
log_level="info"
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
# 宇之然内容创作平台 - 数据模型
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, JSON, Boolean, func, Float
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
import uuid
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String(100), nullable=False)
|
||||
role = Column(String(20), default="user") # user | admin
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
last_login = Column(DateTime(timezone=True))
|
||||
|
||||
class Topic(Base):
|
||||
__tablename__ = "topics"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(500), nullable=False)
|
||||
field = Column(String(100))
|
||||
priority_score = Column(Integer, default=0)
|
||||
status = Column(String(50), default="待处理") # 待处理 | 待审查 | 待发布 | 已发布
|
||||
compliance_score = Column(Float, default=0.0)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now()
|
||||
)
|
||||
generated_at = Column(DateTime(timezone=True)) # 新增字段
|
||||
published_at = Column(DateTime(timezone=True)) # 新增字段
|
||||
published_urls = Column(JSON) # {"zhihu": "url", ...}
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, index=True)
|
||||
action = Column(String(50), index=True) # login, create_user, delete_user, publish
|
||||
resource_type = Column(String(50)) # user, topic, article
|
||||
resource_id = Column(Integer)
|
||||
details = Column(String(500))
|
||||
ip_address = Column(String(45))
|
||||
user_agent = Column(String(200))
|
||||
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
class GenerateTask(Base):
|
||||
__tablename__ = "generate_tasks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
topic_id = Column(Integer, index=True)
|
||||
status = Column(String(20), default="pending") # pending, processing, completed, failed
|
||||
result = Column(JSON) # 存储生成结果或错误信息
|
||||
created_by = Column(Integer)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now()
|
||||
)
|
||||
|
||||
class PublishRecord(Base):
|
||||
__tablename__ = "publish_records"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
topic_id = Column(Integer, index=True)
|
||||
platform = Column(String(50)) # zhihu, wechat, xiaohongshu
|
||||
url = Column(String(500))
|
||||
status = Column(String(20), default="success") # success, failed
|
||||
error_message = Column(String(500))
|
||||
created_by = Column(Integer)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "宇之然内容创作平台 - 后端服务启动"
|
||||
echo "=================================="
|
||||
|
||||
# 检查Python环境
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "错误: 未找到python3,请先安装Python 3.8+"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查虚拟环境
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "正在创建虚拟环境..."
|
||||
python3 -m venv venv
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
source venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
echo "正在安装依赖包..."
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 检查.env文件
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "警告: .env文件不存在,正在复制示例文件..."
|
||||
cp .env.example .env
|
||||
echo "请编辑 .env 文件配置数据库连接等信息"
|
||||
fi
|
||||
|
||||
# 启动服务
|
||||
echo "正在启动后端服务 (端口 8001)..."
|
||||
uvicorn main:app --host 0.0.0.0 --port 8001 --reload
|
||||
|
||||
echo "服务已停止"
|
||||
Reference in New Issue
Block a user