9c37c9a574
Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列 测试: tests/test_phase_upgrades.py 97项全覆盖 CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式 修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
597 lines
24 KiB
Python
597 lines
24 KiB
Python
from sqlalchemy import Column, String, Integer, Float, Date, DateTime, Text, Boolean, JSON, ForeignKey, UniqueConstraint
|
|
from sqlalchemy.sql import func
|
|
from sqlalchemy.orm import relationship
|
|
from .database import Base
|
|
from datetime import datetime
|
|
|
|
|
|
class AuditLog(Base):
|
|
__tablename__ = "audit_logs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
user_id = Column(Integer, nullable=True, index=True)
|
|
username = Column(String, nullable=False)
|
|
action = Column(String, nullable=False, index=True)
|
|
resource_type = Column(String, nullable=True, index=True)
|
|
resource_id = Column(String, nullable=True)
|
|
details = Column(JSON, default=dict, nullable=True)
|
|
ip_address = Column(String, nullable=True)
|
|
user_agent = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"user_id": self.user_id,
|
|
"username": self.username,
|
|
"action": self.action,
|
|
"resource_type": self.resource_type,
|
|
"resource_id": self.resource_id,
|
|
"details": self.details or {},
|
|
"ip_address": self.ip_address,
|
|
"user_agent": self.user_agent,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None
|
|
}
|
|
|
|
|
|
class User(Base):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
username = Column(String, unique=True, nullable=False, index=True)
|
|
password_hash = Column(String, nullable=False)
|
|
role = Column(String, default="user", nullable=False)
|
|
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 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)
|
|
ready_at = Column(Date)
|
|
published_at = Column(Date)
|
|
compliance_score = Column(Integer)
|
|
platform_urls = Column(JSON, default=dict)
|
|
lock_by = Column(String, nullable=True)
|
|
lock_at = Column(DateTime, nullable=True)
|
|
|
|
field = relationship("TopicField", backref="topics")
|
|
|
|
@property
|
|
def field_info(self):
|
|
if self.field:
|
|
return self.field.to_dict()
|
|
return None
|
|
|
|
|
|
class Article(Base):
|
|
__tablename__ = "articles"
|
|
|
|
id = Column(String, primary_key=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=False)
|
|
platform = Column(String, nullable=False)
|
|
file_path = Column(String, nullable=False)
|
|
status = Column(String, default="draft")
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
compliance_score = Column(Integer)
|
|
html_content = Column(Text)
|
|
word_count = Column(Integer, nullable=True)
|
|
outline = Column(Text, nullable=True)
|
|
images = Column(JSON, default=dict) # {"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)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"filename": self.filename,
|
|
"file_path": self.file_path,
|
|
"file_url": self.file_url,
|
|
"file_type": self.file_type,
|
|
"mime_type": self.mime_type,
|
|
"size": self.size,
|
|
"width": self.width,
|
|
"height": self.height,
|
|
"thumbnail_path": self.thumbnail_path,
|
|
"alt_text": self.alt_text,
|
|
"tags": self.tags or [],
|
|
"topic_ids": self.topic_ids or [],
|
|
"usage_count": self.usage_count,
|
|
"uploaded_by": self.uploaded_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class PlatformConfig(Base):
|
|
__tablename__ = "platform_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
platform = Column(String, unique=True, nullable=False) # zhihu/wechat/xiaohongshu
|
|
name = Column(String, nullable=False)
|
|
icon = Column(String, nullable=True)
|
|
api_endpoint = Column(String, nullable=True)
|
|
auth_config = Column(JSON, default=dict)
|
|
format_template = Column(JSON, default=dict)
|
|
compliance_rules = Column(JSON, default=dict)
|
|
default_format = Column(Text, nullable=True)
|
|
is_active = Column(Boolean, default=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"platform": self.platform,
|
|
"name": self.name,
|
|
"icon": self.icon,
|
|
"api_endpoint": self.api_endpoint,
|
|
"format_template": self.format_template or {},
|
|
"compliance_rules": self.compliance_rules or {},
|
|
"default_format": self.default_format,
|
|
"is_active": self.is_active,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class ContentTask(Base):
|
|
__tablename__ = "content_tasks"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
topic_id = Column(String, ForeignKey("topics.id"), nullable=True, index=True)
|
|
task_id = Column(String, unique=True, nullable=False)
|
|
stage = Column(String, nullable=False) # research/outline/writer/optimizer/format/publish
|
|
status = Column(String, default="pending") # pending/running/completed/failed/cancelled
|
|
progress = Column(Integer, default=0)
|
|
message = Column(Text, nullable=True)
|
|
result_data = Column(JSON, default=dict)
|
|
error_msg = Column(Text, nullable=True)
|
|
started_at = Column(DateTime(timezone=True), nullable=True)
|
|
finished_at = Column(DateTime(timezone=True), nullable=True)
|
|
duration = Column(Integer, nullable=True)
|
|
created_by = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
|
|
topic = relationship("Topic")
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"topic_id": self.topic_id,
|
|
"task_id": self.task_id,
|
|
"stage": self.stage,
|
|
"status": self.status,
|
|
"progress": self.progress,
|
|
"message": self.message,
|
|
"error_msg": self.error_msg,
|
|
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
|
"duration": self.duration,
|
|
"created_by": self.created_by,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
}
|
|
|
|
|
|
class Case(Base):
|
|
__tablename__ = "cases"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
title = Column(String, nullable=False)
|
|
field = Column(String, nullable=False)
|
|
summary = Column(Text)
|
|
key_metrics = Column(Text, nullable=True)
|
|
date = Column(String, nullable=True)
|
|
source = Column(String, nullable=True)
|
|
credibility_rating = Column(String, nullable=True)
|
|
china_applicability = Column(String, nullable=True)
|
|
source_url = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"title": self.title,
|
|
"field": self.field,
|
|
"summary": self.summary,
|
|
"key_metrics": self.key_metrics,
|
|
"date": self.date,
|
|
"source": self.source,
|
|
"credibility_rating": self.credibility_rating,
|
|
"china_applicability": self.china_applicability,
|
|
"source_url": self.source_url,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class TaskLog(Base):
|
|
__tablename__ = "task_logs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
task_name = Column(String, nullable=False)
|
|
topic_id = Column(String, nullable=True)
|
|
status = Column(String, nullable=False)
|
|
message = Column(Text, nullable=True)
|
|
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
finished_at = Column(DateTime(timezone=True), nullable=True)
|
|
duration = Column(Integer, nullable=True)
|
|
|
|
def to_dict(self):
|
|
return {
|
|
"id": self.id,
|
|
"task_name": self.task_name,
|
|
"topic_id": self.topic_id,
|
|
"status": self.status,
|
|
"message": self.message,
|
|
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
|
"duration": self.duration,
|
|
}
|
|
|
|
|
|
class LLMConfig(Base):
|
|
__tablename__ = "llm_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
name = Column(String, unique=True, nullable=False)
|
|
system_prompt = Column(Text, nullable=True)
|
|
user_prompt_template = Column(Text, nullable=False)
|
|
temperature = Column(Float, default=0.7)
|
|
max_tokens = Column(Integer, default=2000)
|
|
model = Column(String, nullable=True)
|
|
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)
|
|
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,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
class SystemConfig(Base):
|
|
__tablename__ = "system_configs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
key = Column(String, unique=True, nullable=False)
|
|
value = Column(Text, nullable=True)
|
|
description = Column(String, nullable=True)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
|
|
|
def to_dict(self):
|
|
import json
|
|
return {
|
|
"id": self.id,
|
|
"key": self.key,
|
|
"value": json.loads(self.value) if self.value else None,
|
|
"description": self.description,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
|
|
|
|
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)
|
|
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,
|
|
"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 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,
|
|
} |