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) 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) 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, }