Files
Hulumath-Web/backend/math_life/management/commands/seed_initial_content.py
T

357 lines
14 KiB
Python

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
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": result.get("stats5", {}),
"portrait": result.get("portrait", ""),
},
)
story_document = load_json(story_path)
errors = validate_story_content(story_document)
if errors:
raise CommandError("; ".join(errors))
flagship, _ = Story.objects.update_or_create(
slug="believer-math-teen",
defaults={
"title": story_document["title"],
"summary": story_document.get("description", ""),
"kind": Story.Kind.FLAGSHIP,
"estimated_minutes": 120,
"is_visible": True,
},
)
StoryVersion.objects.update_or_create(
story=flagship,
version=1,
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)} 条视频"
)
)