diff --git a/platform/backend/app/models.py b/platform/backend/app/models.py index 2f071c9..d353372 100644 --- a/platform/backend/app/models.py +++ b/platform/backend/app/models.py @@ -946,4 +946,209 @@ class GeoReadinessScore(Base): "readability_score": self.readability_score, "heading_structure_score": self.heading_structure_score, "checked_at": self.checked_at.isoformat() if self.checked_at else None, + } + + +class ExternalProduct(Base): + """外部推广产品""" + __tablename__ = "external_products" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String, nullable=False, comment="产品名称") + type = Column(String, nullable=False, comment="类型: website/article/wechat_account/miniprogram") + url = Column(String, nullable=True, comment="主URL") + domain = Column(String, nullable=True, comment="域名") + app_id = Column(String, nullable=True, comment="小程序AppID") + account_id = Column(String, nullable=True, comment="公众号ID") + description = Column(Text, nullable=True, comment="产品描述") + org_id = Column(String, default="default", nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "type": self.type, + "url": self.url, + "domain": self.domain, + "app_id": self.app_id, + "account_id": self.account_id, + "description": self.description, + "org_id": self.org_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class PromotionCampaign(Base): + """推广活动""" + __tablename__ = "promotion_campaigns" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + name = Column(String, nullable=False, comment="活动名称") + product_id = Column(Integer, ForeignKey("external_products.id"), nullable=False) + status = Column(String, default="active", comment="状态: active/paused/completed") + keywords = Column(JSON, default=list, comment="目标关键词列表") + target_engines = Column(JSON, default=list, comment="目标搜索引擎列表") + notes = Column(Text, nullable=True, comment="备注") + org_id = Column(String, default="default", nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + product = relationship("ExternalProduct", backref="campaigns") + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "product_id": self.product_id, + "status": self.status, + "keywords": self.keywords or [], + "target_engines": self.target_engines or [], + "notes": self.notes, + "org_id": self.org_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class CampaignKeyword(Base): + """推广关键词""" + __tablename__ = "campaign_keywords" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + campaign_id = Column(Integer, ForeignKey("promotion_campaigns.id"), nullable=False) + keyword = Column(String, nullable=False, comment="关键词") + search_volume = Column(Integer, nullable=True, comment="搜索量") + difficulty = Column(Float, nullable=True, comment="竞争难度 0-1") + current_rank = Column(Integer, nullable=True, comment="当前排名") + target_rank = Column(Integer, nullable=True, comment="目标排名") + best_rank = Column(Integer, nullable=True, comment="历史最佳排名") + last_checked = Column(DateTime(timezone=True), nullable=True, comment="最后检查时间") + org_id = Column(String, default="default", nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + campaign = relationship("PromotionCampaign", backref="campaign_keywords") + + def to_dict(self): + return { + "id": self.id, + "campaign_id": self.campaign_id, + "keyword": self.keyword, + "search_volume": self.search_volume, + "difficulty": self.difficulty, + "current_rank": self.current_rank, + "target_rank": self.target_rank, + "best_rank": self.best_rank, + "last_checked": self.last_checked.isoformat() if self.last_checked else None, + "org_id": self.org_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class SEOAudit(Base): + """SEO审计报告""" + __tablename__ = "seo_audits" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + product_id = Column(Integer, ForeignKey("external_products.id"), nullable=False) + audit_type = Column(String, default="full", comment="审计类型: full/quick") + overall_score = Column(Float, nullable=True, comment="总分 0-100") + meta_score = Column(Float, nullable=True, comment="Meta标签评分") + heading_score = Column(Float, nullable=True, comment="标题结构评分") + content_score = Column(Float, nullable=True, comment="内容评分") + perf_score = Column(Float, nullable=True, comment="性能评分") + links_score = Column(Float, nullable=True, comment="链接评分") + mobile_score = Column(Float, nullable=True, comment="移动端评分") + raw_data = Column(JSON, default=dict, comment="审计详情JSON") + page_count = Column(Integer, nullable=True, comment="审计页面数") + issues_found = Column(Integer, nullable=True, comment="发现问题数") + org_id = Column(String, default="default", nullable=True) + checked_at = Column(DateTime(timezone=True), server_default=func.now()) + + product = relationship("ExternalProduct", backref="audits") + + def to_dict(self): + return { + "id": self.id, + "product_id": self.product_id, + "audit_type": self.audit_type, + "overall_score": self.overall_score, + "meta_score": self.meta_score, + "heading_score": self.heading_score, + "content_score": self.content_score, + "perf_score": self.perf_score, + "links_score": self.links_score, + "mobile_score": self.mobile_score, + "page_count": self.page_count, + "issues_found": self.issues_found, + "org_id": self.org_id, + "checked_at": self.checked_at.isoformat() if self.checked_at else None, + } + + +class KeywordRanking(Base): + """外部关键词多引擎排名""" + __tablename__ = "keyword_rankings" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + campaign_id = Column(Integer, ForeignKey("promotion_campaigns.id"), nullable=True) + keyword_id = Column(Integer, ForeignKey("campaign_keywords.id"), nullable=True) + product_id = Column(Integer, ForeignKey("external_products.id"), nullable=True) + keyword = Column(String, nullable=False, comment="关键词") + search_engine = Column(String, nullable=False, comment="搜索引擎: baidu/360/sogou/wechat/bing/google") + rank = Column(Integer, nullable=True, comment="排名位置") + url_found = Column(String, nullable=True, comment="排名URL") + org_id = Column(String, default="default", nullable=True) + checked_at = Column(DateTime(timezone=True), server_default=func.now()) + + def to_dict(self): + return { + "id": self.id, + "campaign_id": self.campaign_id, + "keyword_id": self.keyword_id, + "product_id": self.product_id, + "keyword": self.keyword, + "search_engine": self.search_engine, + "rank": self.rank, + "url_found": self.url_found, + "org_id": self.org_id, + "checked_at": self.checked_at.isoformat() if self.checked_at else None, + } + + +class OptimizationTask(Base): + """优化建议任务""" + __tablename__ = "optimization_tasks" + + id = Column(Integer, primary_key=True, index=True, autoincrement=True) + audit_id = Column(Integer, ForeignKey("seo_audits.id"), nullable=False) + product_id = Column(Integer, ForeignKey("external_products.id"), nullable=False) + category = Column(String, nullable=False, comment="分类: meta/heading/content/performance/links/mobile/other") + severity = Column(String, default="medium", comment="严重度: high/medium/low") + issue = Column(Text, nullable=False, comment="问题描述") + recommendation = Column(Text, nullable=True, comment="优化建议") + status = Column(String, default="open", comment="状态: open/resolved/ignored") + org_id = Column(String, default="default", nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + audit = relationship("SEOAudit", backref="optimization_tasks") + + def to_dict(self): + return { + "id": self.id, + "audit_id": self.audit_id, + "product_id": self.product_id, + "category": self.category, + "severity": self.severity, + "issue": self.issue, + "recommendation": self.recommendation, + "status": self.status, + "org_id": self.org_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, } \ No newline at end of file diff --git a/platform/backend/app/schemas.py b/platform/backend/app/schemas.py index 5b5f2d2..5c7a403 100644 --- a/platform/backend/app/schemas.py +++ b/platform/backend/app/schemas.py @@ -636,3 +636,163 @@ class MenuResponse(MenuBase): 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) + +