Files
yu-zhi-ran/platform/backend/app/models.py
T
2026-06-09 17:18:09 +08:00

891 lines
36 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)
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)
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)
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)
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)
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):
"""搜索排名追踪"""
__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)
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 搜索来源名称
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,
"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,
"checked_at": self.checked_at.isoformat() if self.checked_at else None,
}