From dcfeccc97b7b0908435553c5a4a360499fd12bea Mon Sep 17 00:00:00 2001 From: Yuzhiran Dev Date: Wed, 17 Jun 2026 14:27:27 +0800 Subject: [PATCH] feat(models): add org_id to Article, PublishRecord, ContentMetrics, SearchRanking, GeoReadinessScore - Add org_id column to 5 models with default='default' - Update to_dict() methods for ContentMetrics, SearchRanking, GeoReadinessScore - Add ALTER TABLE migrations in database.py init_db() - Add org_id to 6 Pydantic response schemas - Add require_role() helper factory in auth.py --- .omo/ralph-loop.local.md | 6 +++--- .../ses_12cdfe924ffebqMk2WsK4OQD87.json | 10 ++++++++++ platform/backend/app/api/auth.py | 10 ++++++++++ platform/backend/app/database.py | 5 +++++ platform/backend/app/models.py | 8 ++++++++ platform/backend/app/schemas.py | 6 ++++++ 6 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 .omo/run-continuation/ses_12cdfe924ffebqMk2WsK4OQD87.json diff --git a/.omo/ralph-loop.local.md b/.omo/ralph-loop.local.md index e015bd4..8e0eb9b 100644 --- a/.omo/ralph-loop.local.md +++ b/.omo/ralph-loop.local.md @@ -4,10 +4,10 @@ iteration: 1 max_iterations: 500 completion_promise: "DONE" initial_completion_promise: "DONE" -started_at: "2026-06-16T23:59:41.756Z" +started_at: "2026-06-17T06:11:18.800Z" session_id: "ses_1dede8a7affeG6DxtfPUOviXrp" ultrawork: true strategy: "continue" -message_count_at_start: 8116 +message_count_at_start: 8133 --- -开始全面改造,包括数据隔离与权限工作,改造实施并发数不要超过3 +全部按照你的建议执行启动 diff --git a/.omo/run-continuation/ses_12cdfe924ffebqMk2WsK4OQD87.json b/.omo/run-continuation/ses_12cdfe924ffebqMk2WsK4OQD87.json new file mode 100644 index 0000000..50de5ae --- /dev/null +++ b/.omo/run-continuation/ses_12cdfe924ffebqMk2WsK4OQD87.json @@ -0,0 +1,10 @@ +{ + "sessionID": "ses_12cdfe924ffebqMk2WsK4OQD87", + "updatedAt": "2026-06-17T01:09:35.927Z", + "sources": { + "background-task": { + "state": "idle", + "updatedAt": "2026-06-17T01:09:35.927Z" + } + } +} \ No newline at end of file diff --git a/platform/backend/app/api/auth.py b/platform/backend/app/api/auth.py index 679d08b..e652915 100644 --- a/platform/backend/app/api/auth.py +++ b/platform/backend/app/api/auth.py @@ -160,6 +160,16 @@ def get_current_admin(current_user: User = Depends(get_current_user)) -> User: if current_user.role != "admin": raise HTTPException(status_code=403, detail="需要管理员权限") return current_user + +def require_role(required_role: str): + """依赖项工厂:返回一个 FastAPI 依赖项,要求当前用户拥有指定角色。 + 管理员自动拥有所有角色权限。""" + def role_checker(current_user: User = Depends(get_current_user)): + if current_user.role != required_role and current_user.role != "admin": + raise HTTPException(status_code=403, detail=f"需要{required_role}角色权限") + return current_user + return role_checker + @router.get("/me") def get_me( request: Request, diff --git a/platform/backend/app/database.py b/platform/backend/app/database.py index 7d0e64b..fa2c54b 100644 --- a/platform/backend/app/database.py +++ b/platform/backend/app/database.py @@ -126,6 +126,11 @@ def init_db(): ("menus", "sort_order", "INTEGER DEFAULT 0"), ("menus", "roles", "JSON DEFAULT '[]'::json"), ("menus", "is_active", "BOOLEAN DEFAULT TRUE"), + ("publish_records", "org_id", "VARCHAR DEFAULT 'default'"), + ("content_metrics", "org_id", "VARCHAR DEFAULT 'default'"), + ("search_rankings", "org_id", "VARCHAR DEFAULT 'default'"), + ("geo_readiness_scores", "org_id", "VARCHAR DEFAULT 'default'"), + ("articles", "org_id", "VARCHAR DEFAULT 'default'"), ]: try: conn.execute(text(f"ALTER TABLE {table} ADD COLUMN IF NOT EXISTS {col} {typ}")) diff --git a/platform/backend/app/models.py b/platform/backend/app/models.py index 2ffcd6a..2f071c9 100644 --- a/platform/backend/app/models.py +++ b/platform/backend/app/models.py @@ -280,6 +280,7 @@ class Article(Base): id = Column(String, primary_key=True) topic_id = Column(String, ForeignKey("topics.id"), nullable=False) + org_id = Column(String, default="default", nullable=True) platform = Column(String, nullable=False) file_path = Column(String, nullable=False) title = Column(String, nullable=True) @@ -302,6 +303,7 @@ class PublishRecord(Base): platform = Column(String, nullable=False) action = Column(String, nullable=False) status = Column(String, nullable=False) + org_id = Column(String, default="default", nullable=True) operator = Column(String, nullable=True) description = Column(Text, nullable=True) suggestion = Column(Text, nullable=True) @@ -357,6 +359,7 @@ class ContentMetrics(Base): 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) + org_id = Column(String, default="default", nullable=True) publish_url = Column(String, nullable=True) views = Column(Integer, default=0) likes = Column(Integer, default=0) @@ -382,6 +385,7 @@ class ContentMetrics(Base): "id": self.id, "topic_id": self.topic_id, "platform": self.platform, + "org_id": self.org_id, "publish_url": self.publish_url, "views": self.views, "likes": self.likes, @@ -869,6 +873,7 @@ class SearchRanking(Base): article_id = Column(String, ForeignKey("articles.id"), nullable=True) topic_id = Column(String, ForeignKey("topics.id"), nullable=True) keyword = Column(String, nullable=False, index=True) + org_id = Column(String, default="default", nullable=True) platform = Column(String, nullable=True) search_engine = Column(String, default="bing") # bing / baidu / google position = Column(Integer, nullable=True) # 搜索排名位置(null = 未上榜) @@ -888,6 +893,7 @@ class SearchRanking(Base): "article_id": self.article_id, "topic_id": self.topic_id, "keyword": self.keyword, + "org_id": self.org_id, "platform": self.platform, "search_engine": self.search_engine, "position": self.position, @@ -911,6 +917,7 @@ class GeoReadinessScore(Base): article_id = Column(String, ForeignKey("articles.id"), nullable=True, index=True) topic_id = Column(String, ForeignKey("topics.id"), nullable=True) platform = Column(String, nullable=True) + org_id = Column(String, default="default", nullable=True) total_score = Column(Integer, default=0) # 总分 0-100 has_schema = Column(Boolean, default=False) # 是否有结构化数据 schema_types = Column(String, nullable=True) # 含有的 schema 类型列表 @@ -928,6 +935,7 @@ class GeoReadinessScore(Base): "article_id": self.article_id, "topic_id": self.topic_id, "platform": self.platform, + "org_id": self.org_id, "total_score": self.total_score, "has_schema": self.has_schema, "schema_types": self.schema_types, diff --git a/platform/backend/app/schemas.py b/platform/backend/app/schemas.py index 7d6e322..5b5f2d2 100644 --- a/platform/backend/app/schemas.py +++ b/platform/backend/app/schemas.py @@ -151,6 +151,7 @@ class ArticleBase(BaseModel): 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 @@ -193,6 +194,7 @@ class ContentCalendarUpdate(BaseModel): 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 @@ -230,6 +232,7 @@ class ContentMetricsUpdate(BaseModel): class ContentMetricsResponse(ContentMetricsBase): id: int + org_id: Optional[str] = None engagement_rate: float = 0 last_fetched: Optional[datetime] = None created_at: Optional[datetime] = None @@ -265,6 +268,7 @@ class MediaAssetUpdate(BaseModel): class MediaAssetResponse(MediaAssetBase): id: int + org_id: Optional[str] = None usage_count: int = 0 uploaded_by: Optional[str] = None created_at: Optional[datetime] = None @@ -340,6 +344,7 @@ class ContentTaskCreate(BaseModel): class ContentTaskResponse(ContentTaskBase): id: int + org_id: Optional[str] = None task_id: str topic_id: Optional[str] = None topic_title: Optional[str] = None @@ -454,6 +459,7 @@ class PublishRecordResponse(BaseModel): platform: str action: str status: str + org_id: Optional[str] = None operator: Optional[str] = None description: Optional[str] = None suggestion: Optional[str] = None