@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
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)} 条视频"
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user