f1e6c11505
Topic model: new series VARCHAR field (optional, indexed) Schema: series in TopicBase/TopicCreate/TopicUpdate API: GET /api/topics supports ?series= query param DB migration: ALTER TABLE topics ADD COLUMN series Ultraworked with Sisyphus Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1155 lines
49 KiB
Python
1155 lines
49 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, timezone
|
||
|
||
|
||
class SearchProvider(Base):
|
||
__tablename__ = "search_providers"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
name = Column(String, nullable=False, comment="显示名称")
|
||
provider_type = Column(String, nullable=False, comment="baidu / qiniu / tinyfish / bing / mcp / 360 / sogou / wechat")
|
||
api_key = Column(String, nullable=True, comment="API密钥")
|
||
api_url = Column(String, nullable=True, comment="API地址")
|
||
priority = Column(Integer, default=1, comment="优先级,越小越优先")
|
||
enabled = Column(Boolean, default=True)
|
||
daily_limit = Column(Integer, default=1500, comment="每日调用上限")
|
||
usage_today = Column(Integer, default=0, comment="当日已用次数")
|
||
console_url = Column(String, nullable=True, comment="官网控制台地址")
|
||
extra_config = Column(JSON, default=dict, comment="额外配置")
|
||
last_used_at = 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,
|
||
"name": self.name,
|
||
"provider_type": self.provider_type,
|
||
"api_key": self.api_key,
|
||
"api_url": self.api_url,
|
||
"console_url": self.console_url,
|
||
"priority": self.priority,
|
||
"enabled": self.enabled,
|
||
"daily_limit": self.daily_limit,
|
||
"usage_today": self.usage_today,
|
||
"extra_config": self.extra_config or {},
|
||
"last_used_at": self.last_used_at.isoformat() if self.last_used_at 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 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)
|
||
org_id = Column(String, default="default", nullable=True)
|
||
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,
|
||
"org_id": self.org_id,
|
||
"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 Role(Base):
|
||
__tablename__ = "roles"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
name = Column(String, unique=True, nullable=False)
|
||
description = Column(String, default="")
|
||
is_system = Column(Boolean, default=False)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"name": self.name,
|
||
"description": self.description,
|
||
"is_system": self.is_system,
|
||
"created_at": self.created_at.isoformat() if self.created_at else None
|
||
}
|
||
|
||
|
||
class Menu(Base):
|
||
__tablename__ = "menus"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
parent_id = Column(Integer, ForeignKey("menus.id"), nullable=True)
|
||
name = Column(String, nullable=False)
|
||
path = Column(String, nullable=False)
|
||
icon = Column(String, default="")
|
||
sort_order = Column(Integer, default=0)
|
||
roles = Column(JSON, default=list)
|
||
is_active = Column(Boolean, default=True)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
parent = relationship("Menu", remote_side=[id], backref="children")
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"parent_id": self.parent_id,
|
||
"name": self.name,
|
||
"path": self.path,
|
||
"icon": self.icon,
|
||
"sort_order": self.sort_order,
|
||
"roles": self.roles or [],
|
||
"is_active": self.is_active,
|
||
"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)
|
||
org_id = Column(String, default="default", 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)
|
||
reviewed_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)
|
||
series = Column(String, nullable=True, index=True, comment="所属栏目,如:AI工具实测周报、科技人文思辨")
|
||
|
||
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)
|
||
org_id = Column(String, default="default", nullable=True)
|
||
platform = Column(String, nullable=False)
|
||
file_path = Column(String, nullable=False)
|
||
title = Column(String, nullable=True)
|
||
content = Column(Text, nullable=True)
|
||
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)
|
||
images = Column(JSON, default=dict)
|
||
updated_at = Column(DateTime(timezone=True), onupdate=func.now()) # {"cover": "/path/to/cover.png", "chart": "/path/to/chart.png"}
|
||
|
||
|
||
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)
|
||
org_id = Column(String, default="default", nullable=True)
|
||
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)
|
||
org_id = Column(String, default="default", 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)
|
||
org_id = Column(String, default="default", nullable=True)
|
||
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,
|
||
"org_id": self.org_id,
|
||
"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)
|
||
org_id = Column(String, default="default", 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)
|
||
website_url = 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)
|
||
requires_image = Column(Boolean, default=False)
|
||
image_count_min = Column(Integer, default=0)
|
||
image_count_max = Column(Integer, default=0)
|
||
image_width = Column(Integer, default=0)
|
||
image_height = Column(Integer, default=0)
|
||
min_words = Column(Integer, default=0)
|
||
max_words = Column(Integer, default=0)
|
||
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,
|
||
"website_url": self.website_url,
|
||
"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,
|
||
"requires_image": self.requires_image,
|
||
"image_count_min": self.image_count_min,
|
||
"image_count_max": self.image_count_max,
|
||
"image_width": self.image_width,
|
||
"image_height": self.image_height,
|
||
"min_words": self.min_words,
|
||
"max_words": self.max_words,
|
||
"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)
|
||
org_id = Column(String, default="default", 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)
|
||
module_id = Column(String, nullable=False, index=True) # scheduled_collect / scheduled_generate 等
|
||
task_name = Column(String, nullable=False)
|
||
topic_id = Column(String, nullable=True, index=True)
|
||
status = Column(String, nullable=False) # pending / running / success / failed / cancelled
|
||
message = Column(Text, nullable=True)
|
||
error_trace = Column(Text, nullable=True)
|
||
triggered_by = Column(String, default="scheduler") # scheduler / manual / api
|
||
result_data = Column(JSON, default=dict) # 产出摘要:{topics_found, articles_created, issues_fixed, ...}
|
||
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
finished_at = Column(DateTime(timezone=True), nullable=True)
|
||
duration = Column(Integer, nullable=True) # seconds
|
||
next_run_time = Column(DateTime(timezone=True), nullable=True)
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"module_id": self.module_id,
|
||
"task_name": self.task_name,
|
||
"topic_id": self.topic_id,
|
||
"status": self.status,
|
||
"message": self.message,
|
||
"error_trace": self.error_trace,
|
||
"triggered_by": self.triggered_by,
|
||
"result_data": self.result_data or {},
|
||
"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,
|
||
"next_run_time": self.next_run_time.isoformat() if self.next_run_time else None,
|
||
}
|
||
|
||
|
||
class TaskConfig(Base):
|
||
__tablename__ = "task_configs"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
module_id = Column(String, unique=True, nullable=False)
|
||
enabled = Column(Boolean, default=True)
|
||
params = Column(JSON, default=dict) # 各任务自定义参数,JSON 格式
|
||
schedule = Column(String, nullable=True) # cron 表达式,覆盖默认
|
||
last_modified_by = Column(String, nullable=True)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"module_id": self.module_id,
|
||
"enabled": self.enabled,
|
||
"params": self.params or {},
|
||
"schedule": self.schedule,
|
||
"last_modified_by": self.last_modified_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 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)
|
||
provider = Column(String, default="opencode-go") # opencode-go / nvidia
|
||
base_url = Column(String, nullable=True)
|
||
api_key = Column(String, nullable=True)
|
||
is_active = Column(Boolean, default=True)
|
||
is_default = Column(Boolean, default=False)
|
||
rate_limit = Column(Integer, default=0, comment="每个时间窗口的调用上限,0=不限")
|
||
rate_limit_window_minutes = Column(Integer, default=300, comment="时间窗口(分钟),默认5小时")
|
||
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,
|
||
"provider": self.provider,
|
||
"base_url": self.base_url,
|
||
"api_key": f"{self.api_key[:8]}..." if self.api_key else None,
|
||
"is_active": self.is_active,
|
||
"is_default": self.is_default,
|
||
"rate_limit": self.rate_limit,
|
||
"rate_limit_window_minutes": self.rate_limit_window_minutes,
|
||
"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 PromptConfig(Base):
|
||
__tablename__ = "prompt_configs"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
key = Column(String, unique=True, nullable=False, index=True)
|
||
module_id = Column(String, nullable=True, index=True)
|
||
category = Column(String, default="prompt") # prompt / rule / template
|
||
version = Column(String, default="v1")
|
||
content = Column(Text, nullable=False)
|
||
variables = Column(JSON, default=[]) # [{name, description, default_value}]
|
||
description = Column(String, nullable=True)
|
||
enabled = Column(Boolean, default=True)
|
||
temperature = Column(Float, nullable=True)
|
||
max_tokens = Column(Integer, nullable=True)
|
||
created_by = Column(String, nullable=True)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"key": self.key,
|
||
"module_id": self.module_id,
|
||
"category": self.category,
|
||
"version": self.version,
|
||
"content": self.content,
|
||
"variables": self.variables or [],
|
||
"description": self.description,
|
||
"enabled": self.enabled,
|
||
"temperature": self.temperature,
|
||
"max_tokens": self.max_tokens,
|
||
"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 KeywordDomainMap(Base):
|
||
__tablename__ = "keyword_domain_map"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
pattern = Column(String, nullable=False)
|
||
domain = Column(String, nullable=False)
|
||
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())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id, "pattern": self.pattern, "domain": self.domain,
|
||
"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 SensitiveWord(Base):
|
||
__tablename__ = "sensitive_words"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
word = Column(String, nullable=False)
|
||
category = Column(String, default="general")
|
||
is_active = Column(Boolean, default=True)
|
||
added_by = Column(String, nullable=True)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id, "word": self.word, "category": self.category,
|
||
"is_active": self.is_active, "added_by": self.added_by,
|
||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||
}
|
||
|
||
|
||
class ContentCleanRule(Base):
|
||
__tablename__ = "content_clean_rules"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
rule_type = Column(String, nullable=False) # thinking / preface / verbosity / html_thinking
|
||
pattern = Column(Text, nullable=False)
|
||
description = Column(String, nullable=True)
|
||
is_active = Column(Boolean, default=True)
|
||
sort_order = Column(Integer, default=0)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id, "rule_type": self.rule_type, "pattern": self.pattern,
|
||
"description": self.description, "is_active": self.is_active,
|
||
"sort_order": self.sort_order,
|
||
"created_at": self.created_at.isoformat() if self.created_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,
|
||
}
|
||
|
||
|
||
class CollectorCategory(Base):
|
||
"""采集类别(可在运营管理中动态编辑)"""
|
||
__tablename__ = "collector_categories"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
name = Column(String, unique=True, nullable=False)
|
||
description = Column(Text, nullable=True)
|
||
search_query = Column(String, nullable=True)
|
||
pain_template = Column(Text, 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())
|
||
|
||
sources = relationship("CollectorSource", back_populates="category", cascade="all, delete-orphan")
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"name": self.name,
|
||
"description": self.description,
|
||
"search_query": self.search_query,
|
||
"pain_template": self.pain_template,
|
||
"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 TrendFieldMapping(Base):
|
||
__tablename__ = "trend_field_mappings"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
trend_keyword = Column(String, nullable=False)
|
||
field_name = Column(String, nullable=False)
|
||
sort_order = Column(Integer, default=0)
|
||
is_active = Column(Boolean, default=True)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id, "trend_keyword": self.trend_keyword, "field_name": self.field_name,
|
||
"sort_order": self.sort_order, "is_active": self.is_active,
|
||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||
}
|
||
|
||
|
||
class CollectorSource(Base):
|
||
"""采集信息源(可在运营管理中动态编辑)"""
|
||
__tablename__ = "collector_sources"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
category_id = Column(Integer, ForeignKey("collector_categories.id"), nullable=True)
|
||
name = Column(String, nullable=False)
|
||
source_type = Column(String, nullable=False) # rss / web_search / local
|
||
url = Column(Text, nullable=True)
|
||
query = Column(String, nullable=True)
|
||
credibility = Column(String, default="medium")
|
||
focus = Column(String, nullable=True)
|
||
is_active = Column(Boolean, default=True)
|
||
sort_order = Column(Integer, default=0)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||
|
||
category = relationship("CollectorCategory", back_populates="sources")
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"category_id": self.category_id,
|
||
"name": self.name,
|
||
"source_type": self.source_type,
|
||
"url": self.url,
|
||
"query": self.query,
|
||
"credibility": self.credibility,
|
||
"focus": self.focus,
|
||
"is_active": self.is_active,
|
||
"sort_order": self.sort_order,
|
||
"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 SearchRanking(Base):
|
||
"""搜索排名追踪 + AI 搜索引用追踪"""
|
||
__tablename__ = "search_rankings"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
article_id = Column(String, ForeignKey("articles.id"), nullable=True)
|
||
topic_id = Column(String, ForeignKey("topics.id"), nullable=True)
|
||
keyword = Column(String, nullable=False, index=True)
|
||
org_id = Column(String, default="default", nullable=True)
|
||
platform = Column(String, nullable=True)
|
||
search_engine = Column(String, default="bing") # bing / baidu / google
|
||
position = Column(Integer, nullable=True) # 搜索排名位置(null = 未上榜)
|
||
url_found = Column(String, nullable=True) # 被找到的具体 URL
|
||
ai_cited = Column(Boolean, default=False) # 是否被 AI 搜索引用
|
||
ai_source = Column(String, nullable=True) # AI 搜索来源(chatgpt/perplexity/deepseek)
|
||
ai_search_engine = Column(String, nullable=True) # AI 搜索具体引擎模型名
|
||
citation_snippet = Column(String, nullable=True) # 被引用的文本片段
|
||
citation_url = Column(String, nullable=True) # 引用来源链接
|
||
geo_score = Column(Integer, nullable=True) # GEO 就绪度评分 0-100
|
||
content_type = Column(String, nullable=True) # 内容类型: article/listicle/howto/faq/review
|
||
checked_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"article_id": self.article_id,
|
||
"topic_id": self.topic_id,
|
||
"keyword": self.keyword,
|
||
"org_id": self.org_id,
|
||
"platform": self.platform,
|
||
"search_engine": self.search_engine,
|
||
"position": self.position,
|
||
"url_found": self.url_found,
|
||
"ai_cited": self.ai_cited,
|
||
"ai_source": self.ai_source,
|
||
"ai_search_engine": self.ai_search_engine,
|
||
"citation_snippet": self.citation_snippet,
|
||
"citation_url": self.citation_url,
|
||
"geo_score": self.geo_score,
|
||
"content_type": self.content_type,
|
||
"checked_at": self.checked_at.isoformat() if self.checked_at else None,
|
||
}
|
||
|
||
|
||
class GeoReadinessScore(Base):
|
||
"""GEO 就绪度评分 — 按文章评估被 AI 搜索引用的概率"""
|
||
__tablename__ = "geo_readiness_scores"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
article_id = Column(String, ForeignKey("articles.id"), nullable=True, index=True)
|
||
topic_id = Column(String, ForeignKey("topics.id"), nullable=True)
|
||
platform = Column(String, nullable=True)
|
||
org_id = Column(String, default="default", nullable=True)
|
||
total_score = Column(Integer, default=0) # 总分 0-100
|
||
has_schema = Column(Boolean, default=False) # 是否有结构化数据
|
||
schema_types = Column(String, nullable=True) # 含有的 schema 类型列表
|
||
has_faq_format = Column(Boolean, default=False) # 是否含 FAQ 格式
|
||
has_howto_format = Column(Boolean, default=False) # 是否含 HowTo 格式
|
||
has_citations = Column(Boolean, default=False) # 是否引用数据源
|
||
word_count = Column(Integer, default=0)
|
||
readability_score = Column(Integer, default=0) # 可读性评分 0-100
|
||
heading_structure_score = Column(Integer, default=0) # 标题结构评分 0-100
|
||
checked_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"article_id": self.article_id,
|
||
"topic_id": self.topic_id,
|
||
"platform": self.platform,
|
||
"org_id": self.org_id,
|
||
"total_score": self.total_score,
|
||
"has_schema": self.has_schema,
|
||
"schema_types": self.schema_types,
|
||
"has_faq_format": self.has_faq_format,
|
||
"has_howto_format": self.has_howto_format,
|
||
"has_citations": self.has_citations,
|
||
"word_count": self.word_count,
|
||
"readability_score": self.readability_score,
|
||
"heading_structure_score": self.heading_structure_score,
|
||
"checked_at": self.checked_at.isoformat() if self.checked_at else None,
|
||
}
|
||
|
||
|
||
class ExternalProduct(Base):
|
||
"""外部推广产品"""
|
||
__tablename__ = "external_products"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
name = Column(String, nullable=False, comment="产品名称")
|
||
type = Column(String, nullable=False, comment="类型: website/article/wechat_account/miniprogram")
|
||
url = Column(String, nullable=True, comment="主URL")
|
||
domain = Column(String, nullable=True, comment="域名")
|
||
app_id = Column(String, nullable=True, comment="小程序AppID")
|
||
account_id = Column(String, nullable=True, comment="公众号ID")
|
||
description = Column(Text, nullable=True, comment="产品描述")
|
||
org_id = Column(String, default="default", 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,
|
||
"name": self.name,
|
||
"type": self.type,
|
||
"url": self.url,
|
||
"domain": self.domain,
|
||
"app_id": self.app_id,
|
||
"account_id": self.account_id,
|
||
"description": self.description,
|
||
"org_id": self.org_id,
|
||
"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 PromotionCampaign(Base):
|
||
"""推广活动"""
|
||
__tablename__ = "promotion_campaigns"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
name = Column(String, nullable=False, comment="活动名称")
|
||
product_id = Column(Integer, ForeignKey("external_products.id"), nullable=False)
|
||
status = Column(String, default="active", comment="状态: active/paused/completed")
|
||
keywords = Column(JSON, default=list, comment="目标关键词列表")
|
||
target_engines = Column(JSON, default=list, comment="目标搜索引擎列表")
|
||
notes = Column(Text, nullable=True, comment="备注")
|
||
org_id = Column(String, default="default", nullable=True)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||
|
||
product = relationship("ExternalProduct", backref="campaigns")
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"name": self.name,
|
||
"product_id": self.product_id,
|
||
"status": self.status,
|
||
"keywords": self.keywords or [],
|
||
"target_engines": self.target_engines or [],
|
||
"notes": self.notes,
|
||
"org_id": self.org_id,
|
||
"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 CampaignKeyword(Base):
|
||
"""推广关键词"""
|
||
__tablename__ = "campaign_keywords"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
campaign_id = Column(Integer, ForeignKey("promotion_campaigns.id"), nullable=False)
|
||
keyword = Column(String, nullable=False, comment="关键词")
|
||
search_volume = Column(Integer, nullable=True, comment="搜索量")
|
||
difficulty = Column(Float, nullable=True, comment="竞争难度 0-1")
|
||
current_rank = Column(Integer, nullable=True, comment="当前排名")
|
||
target_rank = Column(Integer, nullable=True, comment="目标排名")
|
||
best_rank = Column(Integer, nullable=True, comment="历史最佳排名")
|
||
last_checked = Column(DateTime(timezone=True), nullable=True, comment="最后检查时间")
|
||
org_id = Column(String, default="default", nullable=True)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||
|
||
campaign = relationship("PromotionCampaign", backref="campaign_keywords")
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"campaign_id": self.campaign_id,
|
||
"keyword": self.keyword,
|
||
"search_volume": self.search_volume,
|
||
"difficulty": self.difficulty,
|
||
"current_rank": self.current_rank,
|
||
"target_rank": self.target_rank,
|
||
"best_rank": self.best_rank,
|
||
"last_checked": self.last_checked.isoformat() if self.last_checked else None,
|
||
"org_id": self.org_id,
|
||
"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 SEOAudit(Base):
|
||
"""SEO审计报告"""
|
||
__tablename__ = "seo_audits"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
product_id = Column(Integer, ForeignKey("external_products.id"), nullable=False)
|
||
audit_type = Column(String, default="full", comment="审计类型: full/quick")
|
||
overall_score = Column(Float, nullable=True, comment="总分 0-100")
|
||
meta_score = Column(Float, nullable=True, comment="Meta标签评分")
|
||
heading_score = Column(Float, nullable=True, comment="标题结构评分")
|
||
content_score = Column(Float, nullable=True, comment="内容评分")
|
||
perf_score = Column(Float, nullable=True, comment="性能评分")
|
||
links_score = Column(Float, nullable=True, comment="链接评分")
|
||
mobile_score = Column(Float, nullable=True, comment="移动端评分")
|
||
raw_data = Column(JSON, default=dict, comment="审计详情JSON")
|
||
page_count = Column(Integer, nullable=True, comment="审计页面数")
|
||
issues_found = Column(Integer, nullable=True, comment="发现问题数")
|
||
org_id = Column(String, default="default", nullable=True)
|
||
checked_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
product = relationship("ExternalProduct", backref="audits")
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"product_id": self.product_id,
|
||
"audit_type": self.audit_type,
|
||
"overall_score": self.overall_score,
|
||
"meta_score": self.meta_score,
|
||
"heading_score": self.heading_score,
|
||
"content_score": self.content_score,
|
||
"perf_score": self.perf_score,
|
||
"links_score": self.links_score,
|
||
"mobile_score": self.mobile_score,
|
||
"page_count": self.page_count,
|
||
"issues_found": self.issues_found,
|
||
"org_id": self.org_id,
|
||
"checked_at": self.checked_at.isoformat() if self.checked_at else None,
|
||
}
|
||
|
||
|
||
class KeywordRanking(Base):
|
||
"""外部关键词多引擎排名"""
|
||
__tablename__ = "keyword_rankings"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
campaign_id = Column(Integer, ForeignKey("promotion_campaigns.id"), nullable=True)
|
||
keyword_id = Column(Integer, ForeignKey("campaign_keywords.id"), nullable=True)
|
||
product_id = Column(Integer, ForeignKey("external_products.id"), nullable=True)
|
||
keyword = Column(String, nullable=False, comment="关键词")
|
||
search_engine = Column(String, nullable=False, comment="搜索引擎: baidu/360/sogou/wechat/bing/google")
|
||
rank = Column(Integer, nullable=True, comment="排名位置")
|
||
url_found = Column(String, nullable=True, comment="排名URL")
|
||
org_id = Column(String, default="default", nullable=True)
|
||
checked_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"campaign_id": self.campaign_id,
|
||
"keyword_id": self.keyword_id,
|
||
"product_id": self.product_id,
|
||
"keyword": self.keyword,
|
||
"search_engine": self.search_engine,
|
||
"rank": self.rank,
|
||
"url_found": self.url_found,
|
||
"org_id": self.org_id,
|
||
"checked_at": self.checked_at.isoformat() if self.checked_at else None,
|
||
}
|
||
|
||
|
||
class OptimizationTask(Base):
|
||
"""优化建议任务"""
|
||
__tablename__ = "optimization_tasks"
|
||
|
||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||
audit_id = Column(Integer, ForeignKey("seo_audits.id"), nullable=False)
|
||
product_id = Column(Integer, ForeignKey("external_products.id"), nullable=False)
|
||
category = Column(String, nullable=False, comment="分类: meta/heading/content/performance/links/mobile/other")
|
||
severity = Column(String, default="medium", comment="严重度: high/medium/low")
|
||
issue = Column(Text, nullable=False, comment="问题描述")
|
||
recommendation = Column(Text, nullable=True, comment="优化建议")
|
||
status = Column(String, default="open", comment="状态: open/resolved/ignored")
|
||
org_id = Column(String, default="default", nullable=True)
|
||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||
|
||
audit = relationship("SEOAudit", backref="optimization_tasks")
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"audit_id": self.audit_id,
|
||
"product_id": self.product_id,
|
||
"category": self.category,
|
||
"severity": self.severity,
|
||
"issue": self.issue,
|
||
"recommendation": self.recommendation,
|
||
"status": self.status,
|
||
"org_id": self.org_id,
|
||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||
} |