feat(models): add external promotion tables (6 models)

Add ExternalProduct, PromotionCampaign, CampaignKeyword, SEOAudit, KeywordRanking, OptimizationTask models and 18 Pydantic schemas for the external promotion module.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
yuzhiran
2026-06-21 16:04:52 +08:00
parent 81f89f6f00
commit 12e49f2811
2 changed files with 365 additions and 0 deletions
+205
View File
@@ -947,3 +947,208 @@ class GeoReadinessScore(Base):
"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,
}
+160
View File
@@ -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)