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:
@@ -3,85 +3,36 @@ import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from .database import SessionLocal, init_db
|
||||
from .models import Topic, User, Case, LLMConfig, SystemConfig
|
||||
from .models import (
|
||||
Topic, TopicField, TopicConfigField, TopicStatusConfig,
|
||||
User, Case, LLMConfig, SystemConfig, PlatformConfig
|
||||
)
|
||||
import bcrypt
|
||||
|
||||
# 计算项目根目录(backend/app/initial_data.py -> 上升3层到 yu-zhi-ran)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
TOPICS_FILE = PROJECT_ROOT / "automation" / "data" / "sustainability_topics.json"
|
||||
CASES_FILE = PROJECT_ROOT / "automation" / "data" / "initial_cases.json"
|
||||
|
||||
# 从环境变量读取管理员配置
|
||||
DEFAULT_ADMIN_USERNAME = os.getenv('DEFAULT_ADMIN_USERNAME', 'admin')
|
||||
DEFAULT_ADMIN_PASSWORD = os.getenv('DEFAULT_ADMIN_PASSWORD', 'admin123')
|
||||
|
||||
|
||||
def import_initial_data():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# 1. 导入选题数据
|
||||
if db.query(Topic).count() == 0:
|
||||
if __import__('os').path.exists(TOPICS_FILE):
|
||||
topics = json.loads(open(TOPICS_FILE, encoding='utf-8').read())
|
||||
# 去重:保留每个 ID 最后出现的记录
|
||||
seen = {}
|
||||
for t in topics:
|
||||
seen[t['id']] = t
|
||||
unique_topics = list(seen.values())
|
||||
for t in unique_topics:
|
||||
topic = Topic(
|
||||
id=t['id'],
|
||||
title=t['title'],
|
||||
field=t['field'],
|
||||
format=t.get('format'),
|
||||
core_concept=t.get('core_concept'),
|
||||
audience_pain=t.get('audience_pain'),
|
||||
unique_angle=t.get('unique_angle'),
|
||||
priority=t.get('priority'),
|
||||
priority_score=t.get('priority_score', 0),
|
||||
total_score=t.get('total_score'),
|
||||
status=t.get('status', 'pending'),
|
||||
cases=t.get('cases', []),
|
||||
source_file=t.get('source_file'),
|
||||
ready_at=datetime.strptime(t['ready_at'], '%Y-%m-%d').date() if t.get('ready_at') else None,
|
||||
published_at=datetime.strptime(t['published_at'], '%Y-%m-%d').date() if t.get('published_at') else None,
|
||||
compliance_score=t.get('compliance_score'),
|
||||
platform_urls=t.get('platform_urls', {})
|
||||
)
|
||||
db.add(topic)
|
||||
db.commit()
|
||||
print(f"✅ 导入 {len(unique_topics)} 个选题到数据库(去重后)")
|
||||
else:
|
||||
print(f"⚠️ 选题文件不存在: {TOPICS_FILE}")
|
||||
else:
|
||||
print("数据库已有选题数据,跳过导入")
|
||||
if db.query(User).filter(User.username == DEFAULT_ADMIN_USERNAME).first() is None:
|
||||
hashed = bcrypt.hashpw(DEFAULT_ADMIN_PASSWORD.encode('utf-8'), bcrypt.gensalt())
|
||||
admin = User(
|
||||
username=DEFAULT_ADMIN_USERNAME,
|
||||
password_hash=hashed.decode('utf-8'),
|
||||
role="admin"
|
||||
)
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
print(f"✅ 创建默认管理员: {DEFAULT_ADMIN_USERNAME}")
|
||||
|
||||
|
||||
# 3. 导入案例数据(如果为空)
|
||||
if db.query(Case).count() == 0:
|
||||
if __import__('os').path.exists(CASES_FILE):
|
||||
cases_data = json.loads(open(CASES_FILE, encoding='utf-8').read())
|
||||
for c in cases_data:
|
||||
case = Case(
|
||||
id=c['id'],
|
||||
title=c['title'],
|
||||
field=c['field'],
|
||||
summary=c['summary'],
|
||||
key_metrics=c.get('key_metrics'),
|
||||
date=c.get('date'),
|
||||
source=c['source'],
|
||||
source_url=c.get('source_url'),
|
||||
credibility_rating=c.get('credibility_rating'),
|
||||
china_applicability=c.get('china_applicability')
|
||||
)
|
||||
db.add(case)
|
||||
db.commit()
|
||||
print(f"✅ 导入 {len(cases_data)} 条案例")
|
||||
else:
|
||||
print(f"⚠️ 案例文件不存在: {CASES_FILE}")
|
||||
|
||||
# 4. 插入 LLM 默认配置
|
||||
if db.query(LLMConfig).count() == 0:
|
||||
default_llm = LLMConfig(
|
||||
name="default_expand",
|
||||
@@ -117,7 +68,6 @@ def import_initial_data():
|
||||
db.commit()
|
||||
print("✅ 插入默认 LLM 配置")
|
||||
|
||||
# 5. 插入系统配置默认值
|
||||
default_system_configs = [
|
||||
{"key": "collector_enabled", "value": "false", "description": "是否启用采集器"},
|
||||
{"key": "scheduler_interval", "value": "daily", "description": "调度间隔:daily/hourly/weekly"},
|
||||
@@ -128,32 +78,148 @@ def import_initial_data():
|
||||
db.commit()
|
||||
print("✅ 插入默认系统配置")
|
||||
|
||||
# 2. 创建默认管理员用户(bcrypt 哈希)
|
||||
admin_exists = db.query(User).filter(User.username == DEFAULT_ADMIN_USERNAME).first()
|
||||
if not admin_exists:
|
||||
hashed = bcrypt.hashpw(DEFAULT_ADMIN_PASSWORD.encode('utf-8'), bcrypt.gensalt())
|
||||
admin = User(
|
||||
username=DEFAULT_ADMIN_USERNAME,
|
||||
password_hash=hashed.decode('utf-8'),
|
||||
role="admin"
|
||||
)
|
||||
db.add(admin)
|
||||
if db.query(PlatformConfig).count() == 0:
|
||||
platforms = [
|
||||
{
|
||||
"platform": "zhihu",
|
||||
"name": "知乎",
|
||||
"icon": "🔍",
|
||||
"default_format": "长文深度分析,1500-3000字,有数据支撑",
|
||||
"compliance_rules": {
|
||||
"max_length": 50000,
|
||||
"requires_authentication": False,
|
||||
"sensitive_words": ["敏感词示例1", "敏感词示例2"]
|
||||
},
|
||||
"is_active": True
|
||||
},
|
||||
{
|
||||
"platform": "wechat",
|
||||
"name": "微信公众号",
|
||||
"icon": "💚",
|
||||
"default_format": "公众号图文,800-1500字,亲切口语化",
|
||||
"compliance_rules": {
|
||||
"max_length": 20000,
|
||||
"requires_authentication": True
|
||||
},
|
||||
"is_active": True
|
||||
},
|
||||
{
|
||||
"platform": "xiaohongshu",
|
||||
"name": "小红书",
|
||||
"icon": "📕",
|
||||
"default_format": "图文笔记,300-800字,emoji+标签",
|
||||
"compliance_rules": {
|
||||
"max_length": 1000,
|
||||
"requires_tags": True,
|
||||
"max_tags": 10
|
||||
},
|
||||
"is_active": True
|
||||
}
|
||||
]
|
||||
for p in platforms:
|
||||
db.add(PlatformConfig(**p))
|
||||
db.commit()
|
||||
print(f"✅ 创建默认管理员: {DEFAULT_ADMIN_USERNAME}")
|
||||
else:
|
||||
# 如果管理员已存在但密码为空,更新为默认密码的哈希
|
||||
if not admin_exists.password_hash:
|
||||
hashed = bcrypt.hashpw(DEFAULT_ADMIN_PASSWORD.encode('utf-8'), bcrypt.gensalt())
|
||||
admin_exists.password_hash = hashed.decode('utf-8')
|
||||
print("✅ 插入平台配置")
|
||||
|
||||
if db.query(TopicField).count() == 0:
|
||||
fields = [
|
||||
{"name": "未来工作方式", "icon": "💼", "color": "#667eea", "description": "远程工作、零工经济、职业转型", "sort_order": 1},
|
||||
{"name": "AI与效率", "icon": "🤖", "color": "#764ba2", "description": "AI工具、数字助手、效率方法", "sort_order": 2},
|
||||
{"name": "可持续生活", "icon": "🌿", "color": "#67c23a", "description": "环保、低碳、自然生活方式", "sort_order": 3},
|
||||
{"name": "数字游民", "icon": "🌍", "color": "#409eff", "description": "旅行、地理自由、海外生活", "sort_order": 4},
|
||||
{"name": "个人成长", "icon": "📚", "color": "#e6a23c", "description": "学习、技能、认知升级", "sort_order": 5},
|
||||
{"name": "科技人文", "icon": "🔬", "color": "#f56c6c", "description": "科技伦理、数字生活反思", "sort_order": 6},
|
||||
]
|
||||
for f in fields:
|
||||
db.add(TopicField(**f))
|
||||
db.commit()
|
||||
print("✅ 插入默认领域配置")
|
||||
|
||||
if db.query(TopicStatusConfig).count() == 0:
|
||||
statuses = [
|
||||
{"status": "pending", "label": "待处理", "color": "#E6A23C", "icon": "⏳", "sort_order": 1, "is_default": True},
|
||||
{"status": "review", "label": "待审查", "color": "#F56C6C", "icon": "🔍", "sort_order": 2},
|
||||
{"status": "draft", "label": "草稿", "color": "#909399", "icon": "📝", "sort_order": 3},
|
||||
{"status": "ready", "label": "待发布", "color": "#67C23A", "icon": "✅", "sort_order": 4},
|
||||
{"status": "published", "label": "已发布", "color": "#409EFF", "icon": "🚀", "sort_order": 5},
|
||||
]
|
||||
for s in statuses:
|
||||
db.add(TopicStatusConfig(**s))
|
||||
db.commit()
|
||||
print("✅ 插入状态配置")
|
||||
|
||||
field_map = {}
|
||||
for f in db.query(TopicField).all():
|
||||
field_map[f.name] = f.id
|
||||
|
||||
if db.query(Topic).count() == 0:
|
||||
if os.path.exists(TOPICS_FILE):
|
||||
topics = json.loads(open(TOPICS_FILE, encoding='utf-8').read())
|
||||
seen = {}
|
||||
for t in topics:
|
||||
seen[t['id']] = t
|
||||
unique_topics = list(seen.values())
|
||||
for t in unique_topics:
|
||||
field_id = field_map.get(t.get('field'))
|
||||
field_name = t.get('field')
|
||||
topic = Topic(
|
||||
id=t['id'],
|
||||
field_id=field_id,
|
||||
field_name=field_name,
|
||||
title=t['title'],
|
||||
format=t.get('format'),
|
||||
core_concept=t.get('core_concept'),
|
||||
audience_pain=t.get('audience_pain'),
|
||||
unique_angle=t.get('unique_angle'),
|
||||
priority=t.get('priority'),
|
||||
priority_score=t.get('priority_score', 0),
|
||||
total_score=t.get('total_score'),
|
||||
status=t.get('status', 'pending'),
|
||||
cases=t.get('cases', []),
|
||||
tags=t.get('tags', []),
|
||||
source_file=t.get('source_file'),
|
||||
ready_at=datetime.strptime(t['ready_at'], '%Y-%m-%d').date() if t.get('ready_at') else None,
|
||||
published_at=datetime.strptime(t['published_at'], '%Y-%m-%d').date() if t.get('published_at') else None,
|
||||
compliance_score=t.get('compliance_score'),
|
||||
platform_urls=t.get('platform_urls', {})
|
||||
)
|
||||
db.add(topic)
|
||||
db.commit()
|
||||
print(f"✅ 更新管理员密码")
|
||||
print(f"管理员已存在: {DEFAULT_ADMIN_USERNAME}")
|
||||
print(f"✅ 导入 {len(unique_topics)} 个选题")
|
||||
else:
|
||||
print(f"⚠️ 选题文件不存在: {TOPICS_FILE}")
|
||||
|
||||
if db.query(Case).count() == 0:
|
||||
if os.path.exists(CASES_FILE):
|
||||
cases_data = json.loads(open(CASES_FILE, encoding='utf-8').read())
|
||||
for c in cases_data:
|
||||
case = Case(
|
||||
id=c['id'],
|
||||
title=c['title'],
|
||||
field=c['field'],
|
||||
summary=c['summary'],
|
||||
key_metrics=c.get('key_metrics'),
|
||||
date=c.get('date'),
|
||||
source=c['source'],
|
||||
source_url=c.get('source_url'),
|
||||
credibility_rating=c.get('credibility_rating'),
|
||||
china_applicability=c.get('china_applicability')
|
||||
)
|
||||
db.add(case)
|
||||
db.commit()
|
||||
print(f"✅ 导入 {len(cases_data)} 条案例")
|
||||
|
||||
print("✅ 初始化完成")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print(f"初始化失败: {e}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
import_initial_data()
|
||||
import_initial_data()
|
||||
Reference in New Issue
Block a user