import json from pathlib import Path from django.conf import settings from django.core.management.base import BaseCommand, CommandError from django.utils import timezone from accounts.models import InviteCode from content.models import ContentItem from latex_lab.models import LatexCourse, LatexExercise, LatexLesson from math_life.models import ( MathBTIAssessment, MathIdentity, SkillPackage, Story, StoryVersion, ) from math_life.services import validate_story_content from math_life.story_v12 import V12_IDENTITY_SCORES, build_v12_story DISCIPLINE_ICONS = { "人工智能": "🤖", "计算机": "💻", "电子信息": "📡", "微电子": "🔌", "机械工程": "⚙", "自动化": "◉", "能源动力": "🔥", "化工材料": "⚗", "生物医学": "✦", "经济管理": "▥", "环境工程": "♧", "数学建模": "📐", } def load_json(path): with path.open(encoding="utf-8") as source: return json.load(source) def video_ability(video): title = video["title"] module = video["module"] discipline = video["discipline"] if module == "数思" or title == "你被平均数骗过吗?": return ContentItem.Ability.VISION if module == "数说" or title == "消费者行为预测的回归分析": return ContentItem.Ability.HUMANITIES if discipline in {"生物医学", "自动化"}: return ContentItem.Ability.DETECTION if discipline in {"机械工程", "化工材料", "环境工程", "能源动力", "数学建模"}: return ContentItem.Ability.MODELING if title in {"金融里的数学:为什么风险可以算出来", "博弈论与市场竞争"}: return ContentItem.Ability.MODELING return ContentItem.Ability.CONNECTION def skill_content(title, person, dilemma): return { "title": title, "start_node": "opening", "nodes": { "opening": { "character": "旁白", "scene": f"你成为青年时期的{person}。{dilemma}", "choices": [ { "text": "接受风险,争取更大的可能", "next": "risk", "effects": {"time": -2, "reputation": 1}, }, { "text": "先保住当下,再等待机会", "next": "steady", "effects": {"money": 2, "reputation": -1}, }, ], }, "risk": { "character": person, "scene": "选择带来了压力,也让你接触到原本看不见的问题。", "choices": [ { "text": "把有限时间投入研究", "next": "crossroads", "effects": {"energy": -2, "research": 3}, } ], }, "steady": { "character": person, "scene": "稳定让你积累了资源,但窗口正在逐渐关闭。", "choices": [ { "text": "用积累换一次尝试", "next": "crossroads", "effects": {"money": -1, "research": 2}, } ], }, "crossroads": { "character": "同伴", "scene": "同伴提供了不完整的消息。你必须决定是独自推进,还是公开方法。", "choices": [ { "text": "先完成证明,再公开", "next": "ending_scholar", "effects": {"research": 2, "cooperation": -1}, }, { "text": "邀请同伴共同验证", "next": "ending_bridge", "effects": {"cooperation": 3, "reputation": 1}, }, ], }, "ending_scholar": { "character": "旁白", "scene": "你守住了方法的完整性,也承担了独行的代价。", "choices": [], }, "ending_bridge": { "character": "旁白", "scene": "成果不再只属于一个人,你让更多人能够继续向前。", "choices": [], }, }, } class Command(BaseCommand): help = "导入 MathBTI、信仰者主线和两个人物 Skill 样板" def handle(self, *args, **options): docs = Path(settings.PROJECT_ROOT) / "docs" mathbti_path = docs / "old_scripts" / "seed_mathbti.json" story_path = docs / "信仰者线_story.json" videos_path = docs / "old_scripts" / "seed_videos.json" if not mathbti_path.exists() or not story_path.exists() or not videos_path.exists(): raise CommandError("缺少 docs 中的初始内容文件") definition = load_json(mathbti_path) assessment, _ = MathBTIAssessment.objects.update_or_create( version=definition["version"], defaults={ "title": definition["title"], "definition": definition, "is_published": True, "published_at": timezone.now(), }, ) for code, result in definition["results"].items(): MathIdentity.objects.update_or_create( code=code, defaults={ "name": result["name"], "clan": result["clan_name"], "mathematician": result["mathematician"], "description": result["description"], "initial_abilities": V12_IDENTITY_SCORES.get( code, result.get("stats5", {}), ), "portrait": result.get("portrait", ""), }, ) story_document = build_v12_story(load_json(story_path)) errors = validate_story_content(story_document) if errors: raise CommandError("; ".join(errors)) flagship, _ = Story.objects.update_or_create( slug="believer", defaults={ "title": story_document["title"], "summary": story_document.get("description", ""), "kind": Story.Kind.FLAGSHIP, "estimated_minutes": 120, "is_visible": True, }, ) StoryVersion.objects.filter(story=flagship).exclude(version=2).update( is_published=False ) StoryVersion.objects.update_or_create( story=flagship, version=2, defaults={ "content": story_document, "is_published": True, "published_at": timezone.now(), }, ) samples = [ ( "hua-luogeng-skill", "华罗庚:从自学到远行", "华罗庚", "你没有完整的学院路径,却收到一次改变研究方向的机会。", ["《华罗庚传》", "中国科学院公开人物资料"], ), ( "su-buqing-skill", "苏步青:选择回国", "苏步青", "海外研究条件优越,故乡却需要从零建设数学教育。", ["复旦大学校史资料", "中国科学院公开人物资料"], ), ] for slug, title, person, dilemma, sources in samples: story, _ = Story.objects.update_or_create( slug=slug, defaults={ "title": title, "summary": dilemma, "kind": Story.Kind.SKILL, "estimated_minutes": 20, "is_visible": True, }, ) document = skill_content(title, person, dilemma) StoryVersion.objects.update_or_create( story=story, version=1, defaults={ "content": document, "is_published": True, "published_at": timezone.now(), }, ) SkillPackage.objects.update_or_create( story=story, defaults={ "real_person": person, "historical_context": dilemma, "fact_sources": sources, "fictional_scope": "人生节点基于公开资料,具体对话与选择为艺术加工。", "creator": "葫芦数学内容组", }, ) InviteCode.objects.get_or_create( code="HULU2026", defaults={"group": "本地首发体验", "max_uses": 100, "is_active": True}, ) course, _ = LatexCourse.objects.update_or_create( slug="latex-from-zero", defaults={ "title": "LaTeX 零基础表达", "description": "从上下标到完整数学证明的六步课程。", "order": 1, "is_published": True, }, ) lesson_specs = [ ("basics", "上标、下标和基础运算", r"x^2 + y_1", r"x^2+y_1"), ("fractions", "分式、根式和括号", r"\frac{a}{b}+\sqrt{x}", r"\frac{a}{b}+\sqrt{x}"), ("calculus", "求和、积分和极限", r"\sum_{i=1}^{n}i", r"\sum_{i=1}^{n}i"), ("matrix", "矩阵和方程组", r"\begin{matrix}a&b\\c&d\end{matrix}", r"\begin{matrix}a&b\\c&d\end{matrix}"), ("alignment", "多行公式与对齐", r"\begin{aligned}a&=b\\&=c\end{aligned}", r"\begin{aligned}a&=b\\&=c\end{aligned}"), ("proof", "完整解答与证明排版", r"\because a=b,\ \therefore a+c=b+c", r"\because a=b,\therefore a+c=b+c"), ] for order, (slug, title, example, expected) in enumerate(lesson_specs, start=1): lesson, _ = LatexLesson.objects.update_or_create( course=course, slug=slug, defaults={ "title": title, "content": f"本节通过可运行示例学习{title}。", "example_source": example, "order": order, }, ) LatexExercise.objects.update_or_create( lesson=lesson, order=1, defaults={ "prompt": f"输入与示例等价的 {title} 公式。", "expected_source": expected, "explanation": "注意命令、花括号和环境闭合。", }, ) content_specs = [ ( "why-proof-matters", "为什么数学家坚持证明", ContentItem.Kind.KNOWLEDGE, "答案正确并不等于我们知道它为什么正确。", ["证明", "数学精神"], ), ( "gauss-17-gon", "高斯与正十七边形", ContentItem.Kind.PERSON, "一个十九岁少年的发现,如何连接古典几何与代数。", ["高斯", "几何"], ), ( "model-is-not-world", "模型不是现实本身", ContentItem.Kind.KNOWLEDGE, "建模从选择变量开始,也从那一刻开始承担遗漏的代价。", ["建模", "应用"], ), ] for slug, title, kind, summary, topics in content_specs: ContentItem.objects.update_or_create( slug=slug, defaults={ "title": title, "kind": kind, "summary": summary, "body": summary, "topics": topics, "source": "葫芦数学首发内容", "is_published": True, "published_at": timezone.now(), }, ) videos = load_json(videos_path) for index, video in enumerate(videos, start=1): ContentItem.objects.update_or_create( slug=f"legacy-video-{index:03d}", defaults={ "title": video["title"], "kind": ContentItem.Kind.VIDEO, "summary": video.get("description", ""), "body": video.get("description", ""), "cover_url": video.get("cover_url", ""), "media_url": video.get("video_url", ""), "topics": [ video.get("module", ""), video.get("sub_category", ""), video.get("discipline", ""), ], "source": "老版志愿者视频流", "ability_dimension": video_ability(video), "module": video.get("module", ""), "sub_category": video.get("sub_category", ""), "discipline": video.get("discipline", ""), "discipline_icon": DISCIPLINE_ICONS.get( video.get("discipline"), video.get("discipline_icon") or "∑", ), "author": video.get("author", ""), "duration_seconds": int(video.get("duration_min", 0)) * 60, "view_count": 800 + index * 137, "comment_count": 20 + (index * 29) % 760, "is_published": True, "published_at": timezone.now(), }, ) self.stdout.write( self.style.SUCCESS( f"已导入 MathBTI {assessment.version}、人生内容、LaTeX 课程和 {len(videos)} 条视频" ) )