from pydantic import BaseModel, ConfigDict, Field from datetime import datetime, date from typing import Optional, List, Dict, Any class TopicFieldBase(BaseModel): name: str icon: Optional[str] = None color: Optional[str] = None description: Optional[str] = None parent_id: Optional[int] = None sort_order: int = 0 is_active: bool = True class TopicFieldResponse(TopicFieldBase): id: int created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class TopicConfigFieldBase(BaseModel): name: str key: str field_type: str = "number" weight: float = 1.0 options: List[Any] = [] min_value: Optional[float] = None max_value: Optional[float] = None is_required: bool = False sort_order: int = 0 class TopicConfigFieldResponse(TopicConfigFieldBase): id: int field_id: int created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class TopicStatusConfigBase(BaseModel): status: str label: str color: Optional[str] = None icon: Optional[str] = None sort_order: int = 0 is_default: bool = False class TopicStatusConfigResponse(TopicStatusConfigBase): id: int created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class TopicBase(BaseModel): id: str field_id: Optional[int] = None field_name: Optional[str] = None org_id: Optional[str] = None title: str format: Optional[str] = None core_concept: Optional[str] = None audience_pain: Optional[str] = None unique_angle: Optional[str] = None priority: Optional[str] = None priority_score: int | float = 0 total_score: Optional[float] = None status: str = "pending" tags: List[str] = [] custom_data: Dict[str, Any] = {} scoring_data: Dict[str, Any] = {} lock_by: Optional[str] = None lock_at: Optional[datetime] = None series: Optional[str] = None class TopicCreate(BaseModel): id: Optional[str] = None field_id: Optional[int] = None title: str = Field(..., min_length=1) format: Optional[str] = None core_concept: Optional[str] = None audience_pain: Optional[str] = None unique_angle: Optional[str] = None priority: Optional[str] = None tags: List[str] = [] custom_data: Dict[str, Any] = {} scoring_data: Dict[str, Any] = {} series: Optional[str] = None class TopicAnalyzeRequest(BaseModel): raw_input: str = Field(..., min_length=1, description="用户输入的选题思路/内容描述") reference_links: List[str] = Field(default=[], description="参考链接列表(可选)") field_id: Optional[int] = Field(default=None, description="所属领域ID(可选)") class TopicAnalyzeResponse(BaseModel): title: str format: str core_concept: str audience_pain: str unique_angle: str field_name: Optional[str] = None tags: List[str] = [] priority: str = "中" class TopicUpdate(BaseModel): field_id: Optional[int] = None title: Optional[str] = None format: Optional[str] = None core_concept: Optional[str] = None audience_pain: Optional[str] = None unique_angle: Optional[str] = None priority: Optional[str] = None status: Optional[str] = None tags: Optional[List[str]] = None custom_data: Optional[Dict[str, Any]] = None scoring_data: Optional[Dict[str, Any]] = None priority_score: Optional[int] = None total_score: Optional[float] = None series: Optional[str] = None class TopicResponse(TopicBase): created_at: Optional[datetime] = None updated_at: Optional[datetime] = None generated_at: Optional[datetime] = None reviewed_at: Optional[datetime] = None ready_at: Optional[date] = None published_at: Optional[date] = None compliance_score: Optional[int] = None platform_urls: Dict[str, str] = {} cases: List[Any] = [] model_config = ConfigDict(from_attributes=True) class TopicScoreRequest(BaseModel): scoring_data: Dict[str, Any] class ArticleBase(BaseModel): id: str topic_id: str platform: str file_path: str title: Optional[str] = None content: Optional[str] = None status: str = "draft" org_id: Optional[str] = None compliance_score: Optional[int] = None word_count: Optional[int] = None outline: Optional[str] = None created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class ArticleResponse(ArticleBase): html_content: Optional[str] = None model_config = ConfigDict(from_attributes=True) class ContentCalendarBase(BaseModel): topic_id: Optional[str] = None field_id: Optional[int] = None title: str planned_date: date platform: Optional[str] = None status: str = "planned" reminder_time: Optional[datetime] = None notes: Optional[str] = None class ContentCalendarCreate(ContentCalendarBase): created_by: Optional[str] = None class ContentCalendarUpdate(BaseModel): topic_id: Optional[str] = None title: Optional[str] = None planned_date: Optional[date] = None published_date: Optional[date] = None platform: Optional[str] = None status: Optional[str] = None reminder_time: Optional[datetime] = None notes: Optional[str] = None class ContentCalendarResponse(ContentCalendarBase): id: int org_id: Optional[str] = None published_date: Optional[date] = None created_by: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None topic_status: Optional[str] = None platform_icon: Optional[str] = None model_config = ConfigDict(from_attributes=True) class ContentMetricsBase(BaseModel): topic_id: str platform: str publish_url: Optional[str] = None views: int = 0 likes: int = 0 favorites: int = 0 comments: int = 0 shares: int = 0 class ContentMetricsCreate(ContentMetricsBase): data_snapshot: Optional[Dict[str, Any]] = None class ContentMetricsUpdate(BaseModel): publish_url: Optional[str] = None views: Optional[int] = None likes: Optional[int] = None favorites: Optional[int] = None comments: Optional[int] = None shares: Optional[int] = None data_snapshot: Optional[Dict[str, Any]] = None class ContentMetricsResponse(ContentMetricsBase): id: int org_id: Optional[str] = None engagement_rate: float = 0 last_fetched: Optional[datetime] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class MediaAssetBase(BaseModel): filename: str file_path: str file_url: Optional[str] = None file_type: str mime_type: Optional[str] = None size: Optional[int] = None width: Optional[int] = None height: Optional[int] = None thumbnail_path: Optional[str] = None alt_text: Optional[str] = None tags: List[str] = [] topic_ids: List[str] = [] class MediaAssetCreate(MediaAssetBase): uploaded_by: Optional[str] = None class MediaAssetUpdate(BaseModel): alt_text: Optional[str] = None tags: Optional[List[str]] = None topic_ids: Optional[List[str]] = None class MediaAssetResponse(MediaAssetBase): id: int org_id: Optional[str] = None usage_count: int = 0 uploaded_by: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class PlatformConfigBase(BaseModel): platform: str name: str icon: Optional[str] = None website_url: Optional[str] = None api_endpoint: Optional[str] = None auth_config: Dict[str, Any] = {} format_template: Dict[str, Any] = {} compliance_rules: Dict[str, Any] = {} default_format: Optional[str] = None is_active: bool = True requires_image: bool = False image_count_min: int = 0 image_count_max: int = 0 image_width: int = 0 image_height: int = 0 min_words: int = 0 max_words: int = 0 class PlatformConfigCreate(PlatformConfigBase): pass class PlatformConfigUpdate(BaseModel): name: Optional[str] = None icon: Optional[str] = None website_url: Optional[str] = None api_endpoint: Optional[str] = None auth_config: Optional[Dict[str, Any]] = None format_template: Optional[Dict[str, Any]] = None compliance_rules: Optional[Dict[str, Any]] = None default_format: Optional[str] = None is_active: Optional[bool] = None requires_image: Optional[bool] = None image_count_min: Optional[int] = None image_count_max: Optional[int] = None image_width: Optional[int] = None image_height: Optional[int] = None min_words: Optional[int] = None max_words: Optional[int] = None class PlatformConfigResponse(PlatformConfigBase): id: int created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class ContentTaskBase(BaseModel): topic_id: Optional[str] = None stage: str status: str = "pending" progress: int = 0 message: Optional[str] = None class ContentTaskCreate(BaseModel): topic_id: Optional[str] = None stage: str created_by: Optional[str] = None class ContentTaskResponse(ContentTaskBase): id: int org_id: Optional[str] = None task_id: str topic_id: Optional[str] = None topic_title: Optional[str] = None result_data: Dict[str, Any] = {} error_msg: Optional[str] = None started_at: Optional[datetime] = None finished_at: Optional[datetime] = None duration: Optional[int] = None created_by: Optional[str] = None created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class MetricsDashboard(BaseModel): total_topics: int topics_by_status: Dict[str, int] total_published: int total_views: int total_likes: int avg_engagement_rate: float top_topics: List[Dict[str, Any]] recent_metrics: List[ContentMetricsResponse] class CalendarMonthResponse(BaseModel): year: int month: int entries: List[ContentCalendarResponse] stats: Dict[str, int] class AuditLogBase(BaseModel): user_id: Optional[int] = None username: str action: str resource_type: Optional[str] = None resource_id: Optional[str] = None details: Optional[Dict[str, Any]] = None ip_address: Optional[str] = None user_agent: Optional[str] = None created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class AuditLogResponse(AuditLogBase): id: int class UserBase(BaseModel): username: str role: str = "user" class UserCreate(UserBase): password: str org_id: Optional[str] = None class UserUpdate(BaseModel): username: Optional[str] = None password: Optional[str] = None role: Optional[str] = None class UserResponse(UserBase): id: int org_id: Optional[str] = None last_login: Optional[datetime] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class ChangePasswordRequest(BaseModel): old_password: str new_password: str class LoginRequest(BaseModel): username: str password: str class TokenResponse(BaseModel): token: str role: str user: UserResponse class PublishRequest(BaseModel): topic_id: str platform_urls: Dict[str, str] class PublishActionRequest(BaseModel): action: str platform: Optional[str] = None status: str operator: Optional[str] = None description: Optional[str] = None suggestion: Optional[str] = None url: Optional[str] = None error_msg: Optional[str] = None class PublishRecordResponse(BaseModel): id: int topic_id: str platform: str action: str status: str org_id: Optional[str] = None operator: Optional[str] = None description: Optional[str] = None suggestion: Optional[str] = None url: Optional[str] = None error_msg: Optional[str] = None created_at: datetime model_config = ConfigDict(from_attributes=True) class CaseBase(BaseModel): title: str field: str summary: str key_metrics: Optional[str] = None date: Optional[str] = None source: str source_url: Optional[str] = None credibility_rating: Optional[str] = None china_applicability: Optional[str] = None class CaseResponse(CaseBase): id: int created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class TaskLogBase(BaseModel): module_id: str task_name: str topic_id: Optional[str] = None status: str message: Optional[str] = None error_trace: Optional[str] = None triggered_by: str = "scheduler" result_data: Dict[str, Any] = {} started_at: Optional[datetime] = None finished_at: Optional[datetime] = None duration: Optional[int] = None next_run_time: Optional[datetime] = None class TaskLogResponse(TaskLogBase): id: int model_config = ConfigDict(from_attributes=True) class TaskConfigBase(BaseModel): module_id: str enabled: bool = True params: Dict[str, Any] = {} schedule: Optional[str] = None class TaskConfigUpdate(BaseModel): enabled: Optional[bool] = None params: Optional[Dict[str, Any]] = None schedule: Optional[str] = None last_modified_by: Optional[str] = None class TaskConfigResponse(TaskConfigBase): id: int last_modified_by: Optional[str] = None last_run_at: Optional[datetime] = None result_data: Optional[Dict[str, Any]] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class LLMConfigBase(BaseModel): name: str system_prompt: Optional[str] = None user_prompt_template: str temperature: float = 0.7 max_tokens: int = 131072 model: Optional[str] = None provider: str = "opencode-go" base_url: Optional[str] = None api_key: Optional[str] = None is_active: bool = True is_default: bool = False rate_limit: int = 0 rate_limit_window_minutes: int = 300 class LLMConfigResponse(LLMConfigBase): id: int created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class SystemConfigBase(BaseModel): key: str value: Optional[str] = None description: Optional[str] = None class SystemConfigResponse(SystemConfigBase): value: Optional[Any] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class SystemStatus(BaseModel): total_topics: int topics_by_status: Dict[str, int] ready_topics: List[TopicResponse] today_articles: int compliance_rate: float last_optimization: Optional[datetime] = None execution_time: Optional[float] = None class RoleBase(BaseModel): name: str description: str = "" is_system: bool = False class RoleCreate(RoleBase): pass class RoleUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None class RoleResponse(RoleBase): id: int created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class MenuBase(BaseModel): parent_id: Optional[int] = None name: str path: str icon: str = "" sort_order: int = 0 roles: List[str] = [] is_active: bool = True class MenuCreate(MenuBase): pass class MenuUpdate(BaseModel): parent_id: Optional[int] = None name: Optional[str] = None path: Optional[str] = None icon: Optional[str] = None sort_order: Optional[int] = None roles: Optional[List[str]] = None is_active: Optional[bool] = None class MenuResponse(MenuBase): id: int created_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) # ========== 外部推广模块 ========== class ExternalProductBase(BaseModel): name: str type: str = "website" url: Optional[str] = None domain: Optional[str] = None app_id: Optional[str] = None account_id: Optional[str] = None description: Optional[str] = None class ExternalProductCreate(ExternalProductBase): pass class ExternalProductUpdate(BaseModel): name: Optional[str] = None type: Optional[str] = None url: Optional[str] = None domain: Optional[str] = None app_id: Optional[str] = None account_id: Optional[str] = None description: Optional[str] = None class ExternalProductResponse(ExternalProductBase): id: int org_id: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class PromotionCampaignBase(BaseModel): name: str product_id: int status: str = "active" keywords: List[str] = [] target_engines: List[str] = [] notes: Optional[str] = None class PromotionCampaignCreate(PromotionCampaignBase): pass class PromotionCampaignUpdate(BaseModel): name: Optional[str] = None status: Optional[str] = None keywords: Optional[List[str]] = None target_engines: Optional[List[str]] = None notes: Optional[str] = None class PromotionCampaignResponse(PromotionCampaignBase): id: int org_id: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class CampaignKeywordBase(BaseModel): campaign_id: int keyword: str search_volume: Optional[int] = None difficulty: Optional[float] = None current_rank: Optional[int] = None target_rank: Optional[int] = None class CampaignKeywordCreate(CampaignKeywordBase): pass class CampaignKeywordUpdate(BaseModel): search_volume: Optional[int] = None difficulty: Optional[float] = None current_rank: Optional[int] = None target_rank: Optional[int] = None class CampaignKeywordResponse(CampaignKeywordBase): id: int best_rank: Optional[int] = None last_checked: Optional[datetime] = None org_id: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class SEOAuditResponse(BaseModel): id: int product_id: int audit_type: str overall_score: Optional[float] = None meta_score: Optional[float] = None heading_score: Optional[float] = None content_score: Optional[float] = None perf_score: Optional[float] = None links_score: Optional[float] = None mobile_score: Optional[float] = None page_count: Optional[int] = None issues_found: Optional[int] = None org_id: Optional[str] = None checked_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class KeywordRankingResponse(BaseModel): id: int campaign_id: Optional[int] = None keyword_id: Optional[int] = None product_id: Optional[int] = None keyword: str search_engine: str rank: Optional[int] = None url_found: Optional[str] = None org_id: Optional[str] = None checked_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) class OptimizationTaskBase(BaseModel): category: str severity: str = "medium" issue: str recommendation: Optional[str] = None class OptimizationTaskCreate(OptimizationTaskBase): audit_id: int product_id: int class OptimizationTaskUpdate(BaseModel): status: Optional[str] = None severity: Optional[str] = None recommendation: Optional[str] = None class OptimizationTaskResponse(OptimizationTaskBase): id: int audit_id: int product_id: int status: str org_id: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True)