feat: Phase 4 多租户隔离 + 四阶段升级测试 + CSS 统一化

Phase 4: org_id 注入 JWT/API 过滤/组织管理 CRUD/前端组织列
测试: tests/test_phase_upgrades.py 97项全覆盖
CSS: theme-modern.css 共享 mobile-card-list/status-dot/search-bar 等模式
修复: initial_data.py LLM配置 NOT NULL 约束, TopicResponse 含 org_id
This commit is contained in:
Yuzhiran Dev
2026-05-17 06:56:53 +08:00
parent 301dc3e438
commit 9c37c9a574
45 changed files with 3707 additions and 1366 deletions
+74
View File
@@ -41,6 +41,7 @@ class User(Base):
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())
@@ -50,6 +51,7 @@ class User(Base):
"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
}
@@ -153,6 +155,7 @@ class Topic(Base):
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)
@@ -199,6 +202,7 @@ class Article(Base):
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):
@@ -482,6 +486,9 @@ class LLMConfig(Base):
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())
@@ -495,6 +502,9 @@ class LLMConfig(Base):
"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,
@@ -520,4 +530,68 @@ class SystemConfig(Base):
"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,
}