Files
yu-zhi-ran/scripts/migrate_content_focus.py
yuzhiran 0258a9af9f feat(script): add DB migration script for content focus refactoring
Comprehensive DB migration: TopicField cleanup (5 old deactivated, 3 new updated), Topic reassignment (43/47 migrated), Article cleanup (15 deleted), CollectorCategory/Cleaning (12 old deactivated), TrendFieldMapping sync (11 entries)

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-23 13:26:22 +08:00

267 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
内容聚焦迁移脚本(Phase 2
目标:将数据库从"6领域分散"迁移到"3领域聚焦(AI与效率、科技人文、未来工作方式)"
执行:python3 scripts/migrate_content_focus.py
"""
import sys, json, logging, datetime
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "platform" / "backend"))
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
# 新领域配置(与 initial_data.py 对齐)
NEW_FIELDS = [
{"name": "AI与效率", "icon": "🤖", "color": "#764ba2", "description": "AI工具、AI工作流、效率方法、前沿资讯", "sort_order": 1},
{"name": "科技人文", "icon": "🔬", "color": "#f56c6c", "description": "AI伦理、科技反思、数字生活、人机关系", "sort_order": 2},
{"name": "未来工作方式", "icon": "💼", "color": "#667eea", "description": "远程工作、AI时代职业转型、一人企业", "sort_order": 3},
]
# 旧领域名称 → 新领域名称映射
FIELD_MIGRATION_MAP = {
"可持续生活系统": "AI与效率",
"可持续生活": "AI与效率",
"数字游民": "未来工作方式",
"个人成长": "科技人文",
"个人知识工厂": "AI与效率",
"科技人文交叉": "科技人文",
}
# 新采集分类
NEW_CATEGORIES = [
{"name": "AI前沿资讯", "search_query": "AI 人工智能 大模型 2026 前沿", "description": "AI行业动态、大模型发布、技术突破", "sort_order": 1, "is_active": True},
{"name": "AI工具实测", "search_query": "AI工具 效率提升 AI工作流 2026", "description": "AI工具评测、效率工作流、实操指南", "sort_order": 2, "is_active": True},
{"name": "AI与职场", "search_query": "AI 裁员 职业转型 AI技能 远程工作 2026", "description": "AI对就业影响、职业转型、技能升级", "sort_order": 3, "is_active": True},
{"name": "AI生活化", "search_query": "AI陪伴 AI心理咨询 AI生活助手 2026", "description": "AI心理咨询/生活搭子/AI人格化", "sort_order": 4, "is_active": True},
{"name": "科技人文", "search_query": "AI伦理 数字生活 科技反思 人机关系 2026", "description": "AI伦理/数字生活反思/科技温度", "sort_order": 5, "is_active": True},
]
# 旧分类名称(待停用)
OLD_CATEGORY_NAMES = ["循环消费", "低碳出行", "干净饮食", "零浪费生活", "绿色家电与节能", "碳普惠", "环保科技产品"]
# 新平台 tags
NEW_PLATFORM_TAGS = {
"zhihu": ["科技", "AI", "效率", "职场", "教育", "远程工作", "未来工作", "工具"],
"wechat": ["科技", "AI", "效率", "职场", "教育", "远程工作", "未来工作", "工具"],
"xiaohongshu": ["AI", "效率", "科技", "工具", "职场", "生活", "学习方法"],
}
def migrate():
try:
from app.database import SessionLocal
from app.models import TopicField, Topic, Article, CollectorCategory, CollectorSource, PlatformConfig
except ImportError as e:
logger.error(f"导入失败(确保在项目根目录执行): {e}")
sys.exit(1)
db = SessionLocal()
try:
# ========== 1. 处理 TopicField ==========
logger.info("--- 处理 TopicField ---")
existing_fields = {f.name: f for f in db.query(TopicField).all()}
field_id_map = {} # name → id
for nf in NEW_FIELDS:
if nf["name"] in existing_fields:
f = existing_fields[nf["name"]]
f.icon = nf["icon"]
f.color = nf["color"]
f.description = nf["description"]
f.sort_order = nf["sort_order"]
f.is_active = True
field_id_map[nf["name"]] = f.id
logger.info(f" 更新领域: {nf['name']} (id={f.id})")
else:
f = TopicField(**nf)
db.add(f)
db.flush()
field_id_map[nf["name"]] = f.id
logger.info(f" 创建领域: {nf['name']} (id={f.id})")
# 停用旧领域
for old_name in ["可持续生活系统", "可持续生活", "数字游民", "个人成长", "个人知识工厂"]:
if old_name in existing_fields and old_name not in [nf["name"] for nf in NEW_FIELDS]:
f = existing_fields[old_name]
f.is_active = False
logger.info(f" 停用旧领域: {old_name} (id={f.id})")
db.commit()
# ========== 2. 迁移 Topic ==========
logger.info("--- 迁移 Topic ---")
topics = db.query(Topic).all()
topic_count = len(topics)
migrated_count = 0
for t in topics:
old_field = t.field_name or ""
new_field = None
for old_k, new_v in FIELD_MIGRATION_MAP.items():
if old_k in old_field or old_field in old_k:
new_field = new_v
break
if not new_field and t.field_id:
# 按名称猜测
for old_k, new_v in FIELD_MIGRATION_MAP.items():
if old_k[:4] in old_field or old_field[:4] in old_k:
new_field = new_v
break
if new_field and new_field in field_id_map:
t.field_id = field_id_map[new_field]
t.field_name = new_field
migrated_count += 1
elif t.field_id and t.field_name not in [nf["name"] for nf in NEW_FIELDS]:
# 无法映射的旧领域,统一设为"AI与效率"
if "AI与效率" in field_id_map:
t.field_id = field_id_map["AI与效率"]
t.field_name = "AI与效率"
migrated_count += 1
db.commit()
logger.info(f"{topic_count} 个 Topic,已迁移 {migrated_count}")
# ========== 3. 清理旧 Article ==========
logger.info("--- 清理旧 Article ---")
all_articles = db.query(Article).all()
deleted_articles = []
for a in all_articles:
topic = db.query(Topic).filter(Topic.id == a.topic_id).first()
if topic:
# 只保留 topic 仍然活跃且在新领域内的
tf = db.query(TopicField).filter(TopicField.id == topic.field_id).first()
if tf and tf.is_active and tf.name in [nf["name"] for nf in NEW_FIELDS]:
continue
# 该 Article 关联的 topic 已失效,删除
deleted_articles.append(a.id)
db.delete(a)
db.commit()
logger.info(f" 已删除 {len(deleted_articles)} 篇旧 Article")
# ========== 4. 清理 CollectorCategory ==========
logger.info("--- 清理 CollectorCategory ---")
existing_cats = {c.name: c for c in db.query(CollectorCategory).all()}
# 创建新分类
for nc in NEW_CATEGORIES:
if nc["name"] in existing_cats:
c = existing_cats[nc["name"]]
c.search_query = nc["search_query"]
c.description = nc["description"]
c.sort_order = nc["sort_order"]
c.is_active = True
logger.info(f" 更新采集分类: {nc['name']}")
else:
db.add(CollectorCategory(**nc))
logger.info(f" 创建采集分类: {nc['name']}")
# 停用旧分类
for old_name in OLD_CATEGORY_NAMES:
if old_name in existing_cats:
c = existing_cats[old_name]
c.is_active = False
logger.info(f" 停用旧采集分类: {old_name}")
# 删除旧 '可持续生活' 和 '可持续发展' 变体
for cname, cobj in existing_cats.items():
if cname not in [nc["name"] for nc in NEW_CATEGORIES] and cname not in OLD_CATEGORY_NAMES:
if any(kw in cname for kw in ["可持续", "环保", "绿色", "低碳", "零浪费", "循环"]):
cobj.is_active = False
logger.info(f" 停用旧采集分类: {cname}")
db.commit()
# ========== 5. 清理 CollectorSource ==========
logger.info("--- 清理 CollectorSource ---")
all_sources = db.query(CollectorSource).all()
deactivated_sources = 0
for s in all_sources:
# 停用非 AI/科技 类采集源
if s.focus and any(kw in s.focus for kw in ["可持续", "环保", "绿色", "低碳", "零浪费"]):
s.is_active = False
deactivated_sources += 1
# 旧 URL 不再可访问的源
if s.url and any(domain in s.url for domain in ["greenbiz.com", "sustainablebrands.com", "treehugger.com"]):
s.is_active = False
deactivated_sources += 1
# 检查 category_id 是否对应已停用的分类
if s.category_id:
cat = db.query(CollectorCategory).filter(CollectorCategory.id == s.category_id).first()
if cat and not cat.is_active:
s.is_active = False
deactivated_sources += 1
# 新增源(由 initial_data.py 的补充逻辑处理)
for new_src in [
{"name": "AI前沿搜索", "source_type": "web_search", "query": "AI 人工智能 大模型 前沿 2026", "credibility": "medium", "focus": "AI前沿资讯", "sort_order": 1, "is_active": True},
{"name": "AI工具搜索", "source_type": "web_search", "query": "AI工具 效率提升 AI工作流 2026", "credibility": "medium", "focus": "AI工具实测", "sort_order": 2, "is_active": True},
]:
existing = db.query(CollectorSource).filter(CollectorSource.name == new_src["name"]).first()
if not existing:
db.add(CollectorSource(**new_src))
logger.info(f" 创建采集源: {new_src['name']}")
db.commit()
logger.info(f" 已停用 {deactivated_sources} 个旧采集源")
# ========== 6. 更新 PlatformConfig tags ==========
logger.info("--- 更新 PlatformConfig tags ---")
for p in db.query(PlatformConfig).all():
if p.platform in NEW_PLATFORM_TAGS:
rules = p.compliance_rules or {}
if isinstance(rules, dict) and "allowed_tags" in rules:
rules["allowed_tags"] = NEW_PLATFORM_TAGS[p.platform]
p.compliance_rules = rules
logger.info(f" 更新平台标签: {p.platform}")
db.commit()
# ========== 7. 更新 TrendFieldMapping ==========
logger.info("--- 更新 TrendFieldMapping ---")
from app.models import TrendFieldMapping
new_mappings = [
{"trend_keyword": "AI工具", "field_name": "AI与效率", "sort_order": 1},
{"trend_keyword": "AI创作", "field_name": "AI与效率", "sort_order": 2},
{"trend_keyword": "AI职场", "field_name": "AI与效率", "sort_order": 3},
{"trend_keyword": "效率工具", "field_name": "AI与效率", "sort_order": 4},
{"trend_keyword": "未来工作", "field_name": "未来工作方式", "sort_order": 5},
{"trend_keyword": "远程工作", "field_name": "未来工作方式", "sort_order": 6},
{"trend_keyword": "AI就业", "field_name": "未来工作方式", "sort_order": 7},
{"trend_keyword": "科技人文", "field_name": "科技人文", "sort_order": 8},
{"trend_keyword": "数字生活", "field_name": "科技人文", "sort_order": 9},
{"trend_keyword": "AI伦理", "field_name": "科技人文", "sort_order": 10},
{"trend_keyword": "AI情感", "field_name": "AI与效率", "sort_order": 11},
]
existing_mappings = {m.trend_keyword: m for m in db.query(TrendFieldMapping).all()}
# 标记旧的映射为停用
for keyword, mapping in existing_mappings.items():
if keyword not in [nm["trend_keyword"] for nm in new_mappings]:
mapping.is_active = False
# 创建/更新新映射
for nm in new_mappings:
if nm["trend_keyword"] in existing_mappings:
m = existing_mappings[nm["trend_keyword"]]
m.field_name = nm["field_name"]
m.sort_order = nm["sort_order"]
m.is_active = True
else:
db.add(TrendFieldMapping(**nm))
db.commit()
logger.info(f" 已同步 {len(new_mappings)} 条 TrendFieldMapping")
logger.info("=" * 50)
logger.info("✅ DB 迁移完成")
logger.info("=" * 50)
except Exception as e:
import traceback
traceback.print_exc()
logger.error(f"迁移失败: {e}")
db.rollback()
sys.exit(1)
finally:
db.close()
if __name__ == "__main__":
migrate()