Files
yu-zhi-ran/platform/backend/app/models.py
T
yuzhiran 12e49f2811 feat(models): add external promotion tables (6 models)
Add ExternalProduct, PromotionCampaign, CampaignKeyword, SEOAudit, KeywordRanking, OptimizationTask models and 18 Pydantic schemas for the external promotion module.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-21 16:04:52 +08:00

1154 lines
49 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
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,
}