233e23016c
- 文章 HTML 存储从文件系统迁移至 articles 表,删除 releases 目录 - 合规审查从 DB 读取 HTML,审查结果写回 DB,通过后自动推进至待发布 - 新增 todayCount 筛选按钮,与系统概览统计数据一致 - 全屏预览修复:提升 z-index 超过侧边栏,添加退出全屏/关闭按钮 - 统一 '优化' → '审查' 命名,消除前后端术语不一致 - 调度器创作完成后自动触发审查(生成 → 审查 → 待发布) - 清理旧备份/调试文件、过期大纲和研究笔记
523 lines
20 KiB
Python
523 lines
20 KiB
Python
from sqlalchemy import Column, String, Integer, Float, Date, DateTime, Text, Boolean, JSON, ForeignKey, UniqueConstraint
|
|
from sqlalchemy.sql import func
|
|
from sqlalchemy.orm import relationship
|
|
from .database import Base
|
|
from datetime import datetime
|
|
|
|
|
|
class AuditLog(Base):
|
|
__tablename__ = "audit_logs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
user_id = Column(Integer, nullable=True, index=True)
|
|
username = Column(String, nullable=False)
|
|
action = Column(String, nullable=False, index=True)
|
|
resource_type = Column(String, nullable=True, index=True)
|
|
resource_id = Column(String, nullable=True)
|
|
details = Column(JSON, default=dict, nullable=True)
|
|
ip_address = Column(String, nullable=True)
|
|
user_agent = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"user_id": self.user_id,
|
|
"username": self.username,
|
|
"action": self.action,
|
|
"resource_type": self.resource_type,
|
|
"resource_id": self.resource_id,
|
|
"details": self.details or {},
|
|
"ip_address": self.ip_address,
|
|
"user_agent": self.user_agent,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
username = Column(String, unique=True, nullable=False, index=True)
|
|
password_hash = Column(String, nullable=False)
|
|
role = Column(String, default="user", nullable=False)
|
|
last_login = Column(DateTime(timezone=True), nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"username": self.username,
|
|
"role": self.role,
|
|
"last_login": self.last_login.isoformat() if self.last_login else None,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
|
|
class TopicField(Base):
|
|
__tablename__ = "topic_fields"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
name = Column(String, nullable=False)
|
|
icon = Column(String, nullable=True)
|
|
color = Column(String, nullable=True)
|
|
description = Column(Text, nullable=True)
|
|
parent_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=True)
|
|
sort_order = Column(Integer, default=0)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
parent = relationship("TopicField", remote_side=[id], backref="children")
|
|
scoring_fields = relationship("TopicConfigField", back_populates="field", cascade="all, delete-orphan")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"icon": self.icon,
|
|
"color": self.color,
|
|
"description": self.description,
|
|
"parent_id": self.parent_id,
|
|
"sort_order": self.sort_order,
|
|
"is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class TopicConfigField(Base):
|
|
__tablename__ = "topic_config_fields"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=False)
|
|
name = Column(String, nullable=False)
|
|
key = Column(String, nullable=False)
|
|
field_type = Column(String, default="number") # number/select/multi_select/text
|
|
weight = Column(Float, default=1.0)
|
|
options = Column(JSON, default=list) # for select/multi_select
|
|
min_value = Column(Float, nullable=True)
|
|
max_value = Column(Float, nullable=True)
|
|
is_required = Column(Boolean, default=False)
|
|
sort_order = Column(Integer, default=0)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
field = relationship("TopicField", back_populates="scoring_fields")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"field_id": self.field_id,
|
|
"name": self.name,
|
|
"key": self.key,
|
|
"field_type": self.field_type,
|
|
"weight": self.weight,
|
|
"options": self.options or [],
|
|
"min_value": self.min_value,
|
|
"max_value": self.max_value,
|
|
"is_required": self.is_required,
|
|
"sort_order": self.sort_order,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class TopicStatusConfig(Base):
|
|
__tablename__ = "topic_status_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
status = Column(String, unique=True, nullable=False)
|
|
label = Column(String, nullable=False)
|
|
color = Column(String, nullable=True)
|
|
icon = Column(String, nullable=True)
|
|
sort_order = Column(Integer, default=0)
|
|
is_default = Column(Boolean, default=False)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"status": self.status,
|
|
"label": self.label,
|
|
"color": self.color,
|
|
"icon": self.icon,
|
|
"sort_order": self.sort_order,
|
|
"is_default": self.is_default,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class Topic(Base):
|
|
__tablename__ = "topics"
|
|
|
|
id = Column(String, primary_key=True, index=True)
|
|
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=True)
|
|
field_name = Column(String, nullable=True)
|
|
title = Column(String, nullable=False)
|
|
format = Column(String)
|
|
core_concept = Column(Text)
|
|
audience_pain = Column(Text)
|
|
unique_angle = Column(Text)
|
|
priority = Column(String)
|
|
priority_score = Column(Integer, default=0)
|
|
total_score = Column(Float)
|
|
status = Column(String, default="pending")
|
|
cases = Column(JSON, default=list)
|
|
source_file = Column(String)
|
|
tags = Column(JSON, default=list)
|
|
custom_data = Column(JSON, default=dict)
|
|
scoring_data = Column(JSON, default=dict)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
generated_at = Column(DateTime(timezone=True), nullable=True)
|
|
ready_at = Column(Date)
|
|
published_at = Column(Date)
|
|
compliance_score = Column(Integer)
|
|
platform_urls = Column(JSON, default=dict)
|
|
lock_by = Column(String, nullable=True)
|
|
lock_at = Column(DateTime, nullable=True)
|
|
|
|
field = relationship("TopicField", backref="topics")
|
|
|
|
@property
|
|
def field_info(self):
|
|
if self.field:
|
|
return self.field.to_dict()
|
|
return None
|
|
|
|
|
|
class Article(Base):
|
|
__tablename__ = "articles"
|
|
|
|
id = Column(String, primary_key=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=False)
|
|
platform = Column(String, nullable=False)
|
|
file_path = Column(String, nullable=False)
|
|
status = Column(String, default="draft")
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
compliance_score = Column(Integer)
|
|
html_content = Column(Text)
|
|
word_count = Column(Integer, nullable=True)
|
|
outline = Column(Text, nullable=True)
|
|
|
|
|
|
class PublishRecord(Base):
|
|
__tablename__ = "publish_records"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=False)
|
|
platform = Column(String, nullable=False)
|
|
action = Column(String, nullable=False)
|
|
status = Column(String, nullable=False)
|
|
operator = Column(String, nullable=True)
|
|
description = Column(Text, nullable=True)
|
|
suggestion = Column(Text, nullable=True)
|
|
url = Column(String, nullable=True)
|
|
error_msg = Column(Text, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
|
|
class ContentCalendar(Base):
|
|
__tablename__ = "content_calendar"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=True)
|
|
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=True)
|
|
title = Column(String, nullable=False)
|
|
planned_date = Column(Date, nullable=False, index=True)
|
|
published_date = Column(Date, nullable=True)
|
|
platform = Column(String, nullable=True)
|
|
status = Column(String, default="planned") # planned/published/delayed/cancelled
|
|
reminder_time = Column(DateTime, nullable=True)
|
|
notes = Column(Text, nullable=True)
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"topic_id": self.topic_id,
|
|
"field_id": self.field_id,
|
|
"title": self.title,
|
|
"planned_date": self.planned_date.isoformat() if self.planned_date else None,
|
|
"published_date": self.published_date.isoformat() if self.published_date else None,
|
|
"platform": self.platform,
|
|
"status": self.status,
|
|
"reminder_time": self.reminder_time.isoformat() if self.reminder_time else None,
|
|
"notes": self.notes,
|
|
"created_by": self.created_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class ContentMetrics(Base):
|
|
__tablename__ = "content_metrics"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=False, index=True)
|
|
platform = Column(String, nullable=False)
|
|
publish_url = Column(String, nullable=True)
|
|
views = Column(Integer, default=0)
|
|
likes = Column(Integer, default=0)
|
|
favorites = Column(Integer, default=0)
|
|
comments = Column(Integer, default=0)
|
|
shares = Column(Integer, default=0)
|
|
last_fetched = Column(DateTime(timezone=True), nullable=True)
|
|
data_snapshot = Column(JSON, default=dict)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
@property
|
|
def engagement_rate(self):
|
|
total = self.views or 0
|
|
if total == 0:
|
|
return 0
|
|
return round((self.likes or 0) / total * 100, 2)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"topic_id": self.topic_id,
|
|
"platform": self.platform,
|
|
"publish_url": self.publish_url,
|
|
"views": self.views,
|
|
"likes": self.likes,
|
|
"favorites": self.favorites,
|
|
"comments": self.comments,
|
|
"shares": self.shares,
|
|
"engagement_rate": self.engagement_rate,
|
|
"last_fetched": self.last_fetched.isoformat() if self.last_fetched else None,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class MediaAsset(Base):
|
|
__tablename__ = "media_assets"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
filename = Column(String, nullable=False)
|
|
file_path = Column(String, nullable=False)
|
|
file_url = Column(String, nullable=True)
|
|
file_type = Column(String, nullable=False) # image/video/document
|
|
mime_type = Column(String, nullable=True)
|
|
size = Column(Integer, nullable=True)
|
|
width = Column(Integer, nullable=True)
|
|
height = Column(Integer, nullable=True)
|
|
thumbnail_path = Column(String, nullable=True)
|
|
alt_text = Column(String, nullable=True)
|
|
tags = Column(JSON, default=list)
|
|
topic_ids = Column(JSON, default=list)
|
|
usage_count = Column(Integer, default=0)
|
|
uploaded_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"filename": self.filename,
|
|
"file_path": self.file_path,
|
|
"file_url": self.file_url,
|
|
"file_type": self.file_type,
|
|
"mime_type": self.mime_type,
|
|
"size": self.size,
|
|
"width": self.width,
|
|
"height": self.height,
|
|
"thumbnail_path": self.thumbnail_path,
|
|
"alt_text": self.alt_text,
|
|
"tags": self.tags or [],
|
|
"topic_ids": self.topic_ids or [],
|
|
"usage_count": self.usage_count,
|
|
"uploaded_by": self.uploaded_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class PlatformConfig(Base):
|
|
__tablename__ = "platform_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
platform = Column(String, unique=True, nullable=False) # zhihu/wechat/xiaohongshu
|
|
name = Column(String, nullable=False)
|
|
icon = Column(String, nullable=True)
|
|
api_endpoint = Column(String, nullable=True)
|
|
auth_config = Column(JSON, default=dict)
|
|
format_template = Column(JSON, default=dict)
|
|
compliance_rules = Column(JSON, default=dict)
|
|
default_format = Column(Text, nullable=True)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"platform": self.platform,
|
|
"name": self.name,
|
|
"icon": self.icon,
|
|
"api_endpoint": self.api_endpoint,
|
|
"format_template": self.format_template or {},
|
|
"compliance_rules": self.compliance_rules or {},
|
|
"default_format": self.default_format,
|
|
"is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class ContentTask(Base):
|
|
__tablename__ = "content_tasks"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=True, index=True)
|
|
task_id = Column(String, unique=True, nullable=False)
|
|
stage = Column(String, nullable=False) # research/outline/writer/optimizer/format/publish
|
|
status = Column(String, default="pending") # pending/running/completed/failed/cancelled
|
|
progress = Column(Integer, default=0)
|
|
message = Column(Text, nullable=True)
|
|
result_data = Column(JSON, default=dict)
|
|
error_msg = Column(Text, nullable=True)
|
|
started_at = Column(DateTime(timezone=True), nullable=True)
|
|
finished_at = Column(DateTime(timezone=True), nullable=True)
|
|
duration = Column(Integer, nullable=True)
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"topic_id": self.topic_id,
|
|
"task_id": self.task_id,
|
|
"stage": self.stage,
|
|
"status": self.status,
|
|
"progress": self.progress,
|
|
"message": self.message,
|
|
"error_msg": self.error_msg,
|
|
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
|
"duration": self.duration,
|
|
"created_by": self.created_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class Case(Base):
|
|
__tablename__ = "cases"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
title = Column(String, nullable=False)
|
|
field = Column(String, nullable=False)
|
|
summary = Column(Text)
|
|
key_metrics = Column(Text, nullable=True)
|
|
date = Column(String, nullable=True)
|
|
source = Column(String, nullable=True)
|
|
credibility_rating = Column(String, nullable=True)
|
|
china_applicability = Column(String, nullable=True)
|
|
source_url = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"title": self.title,
|
|
"field": self.field,
|
|
"summary": self.summary,
|
|
"key_metrics": self.key_metrics,
|
|
"date": self.date,
|
|
"source": self.source,
|
|
"credibility_rating": self.credibility_rating,
|
|
"china_applicability": self.china_applicability,
|
|
"source_url": self.source_url,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class TaskLog(Base):
|
|
__tablename__ = "task_logs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
task_name = Column(String, nullable=False)
|
|
topic_id = Column(String, nullable=True)
|
|
status = Column(String, nullable=False)
|
|
message = Column(Text, nullable=True)
|
|
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
finished_at = Column(DateTime(timezone=True), nullable=True)
|
|
duration = Column(Integer, nullable=True)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"task_name": self.task_name,
|
|
"topic_id": self.topic_id,
|
|
"status": self.status,
|
|
"message": self.message,
|
|
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
|
"duration": self.duration,
|
|
}
|
|
|
|
|
|
class LLMConfig(Base):
|
|
__tablename__ = "llm_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
name = Column(String, unique=True, nullable=False)
|
|
system_prompt = Column(Text, nullable=True)
|
|
user_prompt_template = Column(Text, nullable=False)
|
|
temperature = Column(Float, default=0.7)
|
|
max_tokens = Column(Integer, default=2000)
|
|
model = Column(String, nullable=True)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"system_prompt": self.system_prompt,
|
|
"user_prompt_template": self.user_prompt_template,
|
|
"temperature": self.temperature,
|
|
"max_tokens": self.max_tokens,
|
|
"model": self.model,
|
|
"is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class SystemConfig(Base):
|
|
__tablename__ = "system_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
key = Column(String, unique=True, nullable=False)
|
|
value = Column(Text, nullable=True)
|
|
description = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
import json
|
|
return {
|
|
"id": self.id,
|
|
"key": self.key,
|
|
"value": json.loads(self.value) if self.value else None,
|
|
"description": self.description,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
} |