feat: 全面升级项目架构 - PostgreSQL迁移 + 配置化改造
主要变更: - 数据库: SQLite → PostgreSQL (yzr_nr) - 选题系统: 硬编码字段 → 配置化 (TopicField/TopicConfigField/TopicStatusConfig) - 新增模型: ContentCalendar, ContentMetrics, MediaAsset, PlatformConfig, ContentTask - 新增 API: topic-config, calendar, metrics, assets, tasks, platform-config - 数据迁移: 现有选题数据迁移到新 schema (field_id/tags/custom_data/scoring_data) - 初始化数据: 10个领域, 5种状态, 3个平台配置 服务运行: http://localhost:8001 默认账号: admin / admin123
This commit is contained in:
+347
-48
@@ -1,5 +1,6 @@
|
||||
from sqlalchemy import Column, String, Integer, Float, Date, DateTime, Text, Boolean, JSON
|
||||
from sqlalchemy import Column, String, Integer, Float, Date, DateTime, Text, Boolean, JSON, ForeignKey, UniqueConstraint
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from .database import Base
|
||||
from datetime import datetime
|
||||
|
||||
@@ -8,14 +9,14 @@ class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
user_id = Column(Integer, nullable=True, index=True) # 操作用户ID(未登录/匿名可为空)
|
||||
username = Column(String, nullable=False) # 操作用户名(冗余存储)
|
||||
action = Column(String, nullable=False, index=True) # 操作类型: login/logout/create_user/update_user/delete_user/publish/etc.
|
||||
resource_type = Column(String, nullable=True, index=True) # 资源类型: user/topic/publish_record/etc.
|
||||
resource_id = Column(String, nullable=True) # 资源ID
|
||||
details = Column(JSON, default=dict, nullable=True) # 操作详情(变更前后、额外信息等)
|
||||
ip_address = Column(String, nullable=True) # IP 地址
|
||||
user_agent = Column(String, nullable=True) # User-Agent
|
||||
user_id = Column(Integer, nullable=True, index=True)
|
||||
username = Column(String, nullable=False)
|
||||
action = Column(String, nullable=False, index=True)
|
||||
resource_type = Column(String, nullable=True, index=True)
|
||||
resource_id = Column(String, nullable=True)
|
||||
details = Column(JSON, default=dict, nullable=True)
|
||||
ip_address = Column(String, nullable=True)
|
||||
user_agent = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
def to_dict(self):
|
||||
@@ -38,8 +39,8 @@ class User(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
username = Column(String, unique=True, nullable=False, index=True)
|
||||
password_hash = Column(String, nullable=False) # bcrypt 哈希
|
||||
role = Column(String, default="user", nullable=False) # admin/user
|
||||
password_hash = Column(String, nullable=False)
|
||||
role = Column(String, default="user", nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
@@ -52,62 +53,363 @@ class User(Base):
|
||||
}
|
||||
|
||||
|
||||
class TopicField(Base):
|
||||
__tablename__ = "topic_fields"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
icon = Column(String, nullable=True)
|
||||
color = Column(String, nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
parent_id = Column(Integer, ForeignKey("topic_fields.id"), 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())
|
||||
|
||||
parent = relationship("TopicField", remote_side=[id], backref="children")
|
||||
scoring_fields = relationship("TopicConfigField", back_populates="field", cascade="all, delete-orphan")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"icon": self.icon,
|
||||
"color": self.color,
|
||||
"description": self.description,
|
||||
"parent_id": self.parent_id,
|
||||
"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 TopicConfigField(Base):
|
||||
__tablename__ = "topic_config_fields"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
key = Column(String, nullable=False)
|
||||
field_type = Column(String, default="number") # number/select/multi_select/text
|
||||
weight = Column(Float, default=1.0)
|
||||
options = Column(JSON, default=list) # for select/multi_select
|
||||
min_value = Column(Float, nullable=True)
|
||||
max_value = Column(Float, nullable=True)
|
||||
is_required = Column(Boolean, default=False)
|
||||
sort_order = Column(Integer, default=0)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
field = relationship("TopicField", back_populates="scoring_fields")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"field_id": self.field_id,
|
||||
"name": self.name,
|
||||
"key": self.key,
|
||||
"field_type": self.field_type,
|
||||
"weight": self.weight,
|
||||
"options": self.options or [],
|
||||
"min_value": self.min_value,
|
||||
"max_value": self.max_value,
|
||||
"is_required": self.is_required,
|
||||
"sort_order": self.sort_order,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class TopicStatusConfig(Base):
|
||||
__tablename__ = "topic_status_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
status = Column(String, unique=True, nullable=False)
|
||||
label = Column(String, nullable=False)
|
||||
color = Column(String, nullable=True)
|
||||
icon = Column(String, nullable=True)
|
||||
sort_order = Column(Integer, default=0)
|
||||
is_default = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"status": self.status,
|
||||
"label": self.label,
|
||||
"color": self.color,
|
||||
"icon": self.icon,
|
||||
"sort_order": self.sort_order,
|
||||
"is_default": self.is_default,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class Topic(Base):
|
||||
__tablename__ = "topics"
|
||||
|
||||
id = Column(String, primary_key=True, index=True)
|
||||
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=True)
|
||||
field_name = Column(String, nullable=True)
|
||||
title = Column(String, nullable=False)
|
||||
field = Column(String, nullable=False)
|
||||
format = Column(String)
|
||||
core_concept = Column(Text)
|
||||
audience_pain = Column(Text)
|
||||
unique_angle = Column(Text)
|
||||
priority = Column(String) # 高/中
|
||||
priority = Column(String)
|
||||
priority_score = Column(Integer, default=0)
|
||||
total_score = Column(Float)
|
||||
status = Column(String, default="pending") # pending/draft/ready/published
|
||||
status = Column(String, default="pending")
|
||||
cases = Column(JSON, default=list)
|
||||
source_file = Column(String)
|
||||
tags = Column(JSON, default=list)
|
||||
custom_data = Column(JSON, default=dict)
|
||||
scoring_data = Column(JSON, default=dict)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
generated_at = Column(DateTime(timezone=True), nullable=True) # 选题创作完成时间
|
||||
generated_at = Column(DateTime(timezone=True), nullable=True)
|
||||
ready_at = Column(Date)
|
||||
published_at = Column(Date)
|
||||
compliance_score = Column(Integer)
|
||||
platform_urls = Column(JSON, default=dict) # {"zhihu": "...", "wechat": "...", "xiaohongshu": "..."}
|
||||
platform_urls = Column(JSON, default=dict)
|
||||
lock_by = Column(String, nullable=True)
|
||||
lock_at = Column(DateTime, nullable=True)
|
||||
|
||||
field = relationship("TopicField", backref="topics")
|
||||
|
||||
@property
|
||||
def field_info(self):
|
||||
if self.field:
|
||||
return self.field.to_dict()
|
||||
return None
|
||||
|
||||
|
||||
class Article(Base):
|
||||
__tablename__ = "articles"
|
||||
|
||||
id = Column(String, primary_key=True) # e.g., A01_zhihu
|
||||
topic_id = Column(String, nullable=False)
|
||||
id = Column(String, primary_key=True)
|
||||
topic_id = Column(String, ForeignKey("topics.id"), nullable=False)
|
||||
platform = Column(String, nullable=False)
|
||||
file_path = Column(String, nullable=False)
|
||||
status = Column(String, default="draft") # draft/optimized/published
|
||||
status = Column(String, default="draft")
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
compliance_score = Column(Integer)
|
||||
html_content = Column(Text) # 可缓存HTML内容以便预览
|
||||
html_content = Column(Text)
|
||||
word_count = Column(Integer, nullable=True)
|
||||
outline = Column(Text, nullable=True)
|
||||
|
||||
|
||||
class PublishRecord(Base):
|
||||
__tablename__ = "publish_records"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
topic_id = Column(String, nullable=False)
|
||||
platform = Column(String, nullable=False) # 发布平台:zhihu/wechat/xiaohongshu
|
||||
action = Column(String, nullable=False) # 操作:publish/update/delete/invalid
|
||||
status = Column(String, nullable=False) # 状态:success/failed/partial/cancelled
|
||||
operator = Column(String, nullable=True) # 操作人
|
||||
description = Column(Text, nullable=True) # 发布说明
|
||||
suggestion = Column(Text, nullable=True) # 建议内容
|
||||
url = Column(String, nullable=True) # 发布后链接
|
||||
error_msg = Column(Text, nullable=True) # 错误信息
|
||||
topic_id = Column(String, ForeignKey("topics.id"), nullable=False)
|
||||
platform = Column(String, nullable=False)
|
||||
action = Column(String, nullable=False)
|
||||
status = Column(String, nullable=False)
|
||||
operator = Column(String, nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
suggestion = Column(Text, nullable=True)
|
||||
url = Column(String, nullable=True)
|
||||
error_msg = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# 关联查询
|
||||
# 可以添加外键关联到 Topic, 但这里保持简单
|
||||
topic = relationship("Topic")
|
||||
|
||||
|
||||
class ContentCalendar(Base):
|
||||
__tablename__ = "content_calendar"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
topic_id = Column(String, ForeignKey("topics.id"), nullable=True)
|
||||
field_id = Column(Integer, ForeignKey("topic_fields.id"), nullable=True)
|
||||
title = Column(String, nullable=False)
|
||||
planned_date = Column(Date, nullable=False, index=True)
|
||||
published_date = Column(Date, nullable=True)
|
||||
platform = Column(String, nullable=True)
|
||||
status = Column(String, default="planned") # planned/published/delayed/cancelled
|
||||
reminder_time = Column(DateTime, nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_by = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
topic = relationship("Topic")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"topic_id": self.topic_id,
|
||||
"field_id": self.field_id,
|
||||
"title": self.title,
|
||||
"planned_date": self.planned_date.isoformat() if self.planned_date else None,
|
||||
"published_date": self.published_date.isoformat() if self.published_date else None,
|
||||
"platform": self.platform,
|
||||
"status": self.status,
|
||||
"reminder_time": self.reminder_time.isoformat() if self.reminder_time else None,
|
||||
"notes": self.notes,
|
||||
"created_by": self.created_by,
|
||||
"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 ContentMetrics(Base):
|
||||
__tablename__ = "content_metrics"
|
||||
|
||||
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)
|
||||
publish_url = Column(String, nullable=True)
|
||||
views = Column(Integer, default=0)
|
||||
likes = Column(Integer, default=0)
|
||||
favorites = Column(Integer, default=0)
|
||||
comments = Column(Integer, default=0)
|
||||
shares = Column(Integer, default=0)
|
||||
last_fetched = Column(DateTime(timezone=True), nullable=True)
|
||||
data_snapshot = Column(JSON, default=dict)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
topic = relationship("Topic")
|
||||
|
||||
@property
|
||||
def engagement_rate(self):
|
||||
total = self.views or 0
|
||||
if total == 0:
|
||||
return 0
|
||||
return round((self.likes or 0) / total * 100, 2)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"topic_id": self.topic_id,
|
||||
"platform": self.platform,
|
||||
"publish_url": self.publish_url,
|
||||
"views": self.views,
|
||||
"likes": self.likes,
|
||||
"favorites": self.favorites,
|
||||
"comments": self.comments,
|
||||
"shares": self.shares,
|
||||
"engagement_rate": self.engagement_rate,
|
||||
"last_fetched": self.last_fetched.isoformat() if self.last_fetched else None,
|
||||
"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 MediaAsset(Base):
|
||||
__tablename__ = "media_assets"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
filename = Column(String, nullable=False)
|
||||
file_path = Column(String, nullable=False)
|
||||
file_url = Column(String, nullable=True)
|
||||
file_type = Column(String, nullable=False) # image/video/document
|
||||
mime_type = Column(String, nullable=True)
|
||||
size = Column(Integer, nullable=True)
|
||||
width = Column(Integer, nullable=True)
|
||||
height = Column(Integer, nullable=True)
|
||||
thumbnail_path = Column(String, nullable=True)
|
||||
alt_text = Column(String, nullable=True)
|
||||
tags = Column(JSON, default=list)
|
||||
topic_ids = Column(JSON, default=list)
|
||||
usage_count = Column(Integer, default=0)
|
||||
uploaded_by = Column(String, 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,
|
||||
"filename": self.filename,
|
||||
"file_path": self.file_path,
|
||||
"file_url": self.file_url,
|
||||
"file_type": self.file_type,
|
||||
"mime_type": self.mime_type,
|
||||
"size": self.size,
|
||||
"width": self.width,
|
||||
"height": self.height,
|
||||
"thumbnail_path": self.thumbnail_path,
|
||||
"alt_text": self.alt_text,
|
||||
"tags": self.tags or [],
|
||||
"topic_ids": self.topic_ids or [],
|
||||
"usage_count": self.usage_count,
|
||||
"uploaded_by": self.uploaded_by,
|
||||
"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 PlatformConfig(Base):
|
||||
__tablename__ = "platform_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
platform = Column(String, unique=True, nullable=False) # zhihu/wechat/xiaohongshu
|
||||
name = Column(String, nullable=False)
|
||||
icon = Column(String, nullable=True)
|
||||
api_endpoint = Column(String, nullable=True)
|
||||
auth_config = Column(JSON, default=dict)
|
||||
format_template = Column(JSON, default=dict)
|
||||
compliance_rules = Column(JSON, default=dict)
|
||||
default_format = Column(Text, 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())
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"platform": self.platform,
|
||||
"name": self.name,
|
||||
"icon": self.icon,
|
||||
"api_endpoint": self.api_endpoint,
|
||||
"format_template": self.format_template or {},
|
||||
"compliance_rules": self.compliance_rules or {},
|
||||
"default_format": self.default_format,
|
||||
"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 ContentTask(Base):
|
||||
__tablename__ = "content_tasks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
topic_id = Column(String, ForeignKey("topics.id"), nullable=True, index=True)
|
||||
task_id = Column(String, unique=True, nullable=False)
|
||||
stage = Column(String, nullable=False) # research/outline/writer/optimizer/format/publish
|
||||
status = Column(String, default="pending") # pending/running/completed/failed/cancelled
|
||||
progress = Column(Integer, default=0)
|
||||
message = Column(Text, nullable=True)
|
||||
result_data = Column(JSON, default=dict)
|
||||
error_msg = Column(Text, nullable=True)
|
||||
started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
finished_at = Column(DateTime(timezone=True), nullable=True)
|
||||
duration = Column(Integer, nullable=True)
|
||||
created_by = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
topic = relationship("Topic")
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"topic_id": self.topic_id,
|
||||
"task_id": self.task_id,
|
||||
"stage": self.stage,
|
||||
"status": self.status,
|
||||
"progress": self.progress,
|
||||
"message": self.message,
|
||||
"error_msg": self.error_msg,
|
||||
"started_at": self.started_at.isoformat() if self.started_at else None,
|
||||
"finished_at": self.finished_at.isoformat() if self.finished_at else None,
|
||||
"duration": self.duration,
|
||||
"created_by": self.created_by,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class Case(Base):
|
||||
@@ -115,13 +417,13 @@ class Case(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
title = Column(String, nullable=False)
|
||||
field = Column(String, nullable=False) # 对应四大支柱或其子领域
|
||||
field = Column(String, nullable=False)
|
||||
summary = Column(Text)
|
||||
key_metrics = Column(Text, nullable=True) # 关键数据
|
||||
date = Column(String, nullable=True) # 日期或年份字符串
|
||||
key_metrics = Column(Text, nullable=True)
|
||||
date = Column(String, nullable=True)
|
||||
source = Column(String, nullable=True)
|
||||
credibility_rating = Column(String, nullable=True) # 如 "⭐⭐⭐"
|
||||
china_applicability = Column(String, nullable=True) # 如 "⭐⭐⭐⭐"
|
||||
credibility_rating = Column(String, nullable=True)
|
||||
china_applicability = Column(String, nullable=True)
|
||||
source_url = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
@@ -147,13 +449,13 @@ class TaskLog(Base):
|
||||
__tablename__ = "task_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
task_name = Column(String, nullable=False) # collector, creator, research, outline, writer, optimizer
|
||||
topic_id = Column(String, nullable=True) # 关联选题ID
|
||||
status = Column(String, nullable=False) # started, completed, failed
|
||||
task_name = Column(String, nullable=False)
|
||||
topic_id = Column(String, nullable=True)
|
||||
status = Column(String, nullable=False)
|
||||
message = Column(Text, nullable=True)
|
||||
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
finished_at = Column(DateTime(timezone=True), nullable=True)
|
||||
duration = Column(Integer, nullable=True) # 秒数
|
||||
duration = Column(Integer, nullable=True)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
@@ -172,12 +474,12 @@ class LLMConfig(Base):
|
||||
__tablename__ = "llm_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
name = Column(String, unique=True, nullable=False) # 如 default_expand
|
||||
name = Column(String, unique=True, nullable=False)
|
||||
system_prompt = Column(Text, nullable=True)
|
||||
user_prompt_template = Column(Text, nullable=False) # 含占位符 {topic.get('title')} 等
|
||||
user_prompt_template = Column(Text, nullable=False)
|
||||
temperature = Column(Float, default=0.7)
|
||||
max_tokens = Column(Integer, default=2000)
|
||||
model = Column(String, nullable=True) # 后端模型名
|
||||
model = 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())
|
||||
@@ -202,7 +504,7 @@ class SystemConfig(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
key = Column(String, unique=True, nullable=False)
|
||||
value = Column(Text, nullable=True) # 可存储 JSON 字符串
|
||||
value = Column(Text, nullable=True)
|
||||
description = Column(String, nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
@@ -216,7 +518,4 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
# --- 新增模型:案例库 ---
|
||||
}
|
||||
Reference in New Issue
Block a user