#!/usr/bin/env python3 """发布管理 API""" from fastapi import APIRouter, HTTPException, Depends, Request from pydantic import BaseModel from datetime import datetime, timezone, timedelta, date 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_user, org_filter from ..core.audit_logger import audit_log router = APIRouter(prefix="/api/publishing", tags=["publishing"]) PLATFORM_LABELS = { "zhihu": "知乎", "wechat": "微信公众号", "xiaohongshu": "小红书" } 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=MultiPublishResponse) async def create_publish_record( req: MultiPublishRequest, request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """多平台发布选题(运营/管理员,按组织隔离)""" try: 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='stage', status='pending_manual', operator=operator, description=f"选题 {req.topic_id} 已生成 {PLATFORM_LABELS.get(platform, platform)} 发布稿,待人工校验后发布", org_id=topic.org_id ) db.add(record) results.append(PublishResult( platform=platform, platform_label=PLATFORM_LABELS.get(platform, platform), status='pending_manual' )) except Exception as e: results.append(PublishResult( platform=platform, platform_label=PLATFORM_LABELS.get(platform, platform), status='failed', error_msg=str(e) )) # 诚实状态流转:本项目无真实自动发布器(且按《AI生成内容标识办法》AI 稿需真人把关), # 故仅将选题置为「待人工发布」,待运营在对应平台校验声明后手动发布。 topic.status = 'pending_publish' _now = datetime.now(timezone(timedelta(hours=8))) topic.updated_at = _now db.commit() db.expire_all() db.refresh(topic) audit_log( action="publish_stage", user=current_user, resource_type="topic", resource_id=req.topic_id, 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 ) staged = sum(1 for r in results if r.status == 'pending_manual') return MultiPublishResponse( ok=staged == len(results), topic_id=req.topic_id, results=results, message=( f"选题 {req.topic_id} 已生成 {staged}/{len(results)} 个平台的发布稿并置为「待人工发布」。" f"AI 生成内容须由真人校验声明后手动发布,系统不代发。" ) ) except HTTPException: raise except Exception as e: db.rollback() raise HTTPException(status_code=500, detail=str(e)) @router.get("/records") def list_publish_records( request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """列出发布记录(按组织隔离),用于运营查看「待人工发布 / 已发布」状态""" q = db.query(PublishRecord) of = org_filter(current_user, PublishRecord) if of is not True: q = q.filter(of) recs = q.order_by(PublishRecord.created_at.desc()).all() result = [] for r in recs: topic = db.query(Topic).filter(Topic.id == r.topic_id).first() result.append({ "id": r.id, "topic_id": r.topic_id, "topic_title": topic.title if topic else None, "platform": r.platform, "action": r.action, "status": r.status, "operator": r.operator, "description": r.description, "url": r.url, "created_at": r.created_at.isoformat() if r.created_at else None, "updated_at": r.updated_at.isoformat() if r.updated_at else None, }) return {"records": result} class MarkPublishedResponse(BaseModel): ok: bool topic_id: str message: str @router.post("/{topic_id}/mark-published", response_model=MarkPublishedResponse) def mark_published( topic_id: str, request: Request, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """运营在对应平台手动发布后,回填「已发布」闭环状态""" q = db.query(Topic).filter(Topic.id == 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"选题 {topic_id} 不存在") if topic.status != 'pending_publish': raise HTTPException(status_code=400, detail=f"选题 {topic_id} 当前状态为「{topic.status}」,无需标记发布") _now = datetime.now(timezone(timedelta(hours=8))) topic.status = 'published' topic.published_at = date.today() topic.updated_at = _now recs = db.query(PublishRecord).filter( PublishRecord.topic_id == topic_id, PublishRecord.status == 'pending_manual' ) for r in recs: r.status = 'published' r.updated_at = _now db.commit() audit_log( action="publish_confirm", user=current_user, resource_type="topic", resource_id=topic_id, details={"operator": current_user.username}, ip_address=request.client.host if request.client else None, user_agent=request.headers.get("user-agent", ""), db=db ) return MarkPublishedResponse( ok=True, topic_id=topic_id, message=f"选题 {topic_id} 已标记为「已发布」,发布记录同步更新。" )