release: prepare Hulumath v1.2.0
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
# 包含 #、空格等特殊字符的值请用单引号包裹。
|
# 包含 #、空格等特殊字符的值请用单引号包裹。
|
||||||
|
|
||||||
DJANGO_SETTINGS_MODULE=config.settings
|
DJANGO_SETTINGS_MODULE=config.settings
|
||||||
|
APP_VERSION=1.2.0
|
||||||
DJANGO_SECRET_KEY='replace-with-at-least-50-random-characters'
|
DJANGO_SECRET_KEY='replace-with-at-least-50-random-characters'
|
||||||
DJANGO_DEBUG=false
|
DJANGO_DEBUG=false
|
||||||
DJANGO_USE_HTTPS=true
|
DJANGO_USE_HTTPS=true
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
面向全年龄数学兴趣用户的“数学人生宇宙”。当前仓库包含可运行的 Django 模块化单体、响应式 Web 客户端、运营后台、内容种子、实时比赛基础设施和生产部署配置。
|
面向全年龄数学兴趣用户的“数学人生宇宙”。当前仓库包含可运行的 Django 模块化单体、响应式 Web 客户端、运营后台、内容种子、实时比赛基础设施和生产部署配置。
|
||||||
|
|
||||||
|
当前发布版本:**v1.2.0**。变更与迁移说明见
|
||||||
|
[v1.2.0 Release Notes](docs/RELEASE_NOTES_V1.2.0.md)。
|
||||||
|
|
||||||
## 参与贡献
|
## 参与贡献
|
||||||
|
|
||||||
提交代码、内容或文档前,请先完整阅读 [代码贡献指南](CONTRIBUTING.md)。
|
提交代码、内容或文档前,请先完整阅读 [代码贡献指南](CONTRIBUTING.md)。
|
||||||
@@ -14,13 +17,14 @@
|
|||||||
- 12 题 MathBTI、16 种数学人格、人物卡与数学精灵初始化
|
- 12 题 MathBTI、16 种数学人格、人物卡与数学精灵初始化
|
||||||
- 统一版本化剧情引擎、85 节点信仰者主线、2 个人物 Skill 样板
|
- 统一版本化剧情引擎、85 节点信仰者主线、2 个人物 Skill 样板
|
||||||
- 剧情服务端存档、嵌套资源效果、结局与幂等选择
|
- 剧情服务端存档、嵌套资源效果、结局与幂等选择
|
||||||
- 入门、标准、进阶三赛道的实时 1v1、今日挑战、单人闯关、24 点和数独
|
- 统一玩家池的口算、数独 Timerun、24 点实时 1v1,以及随机今日挑战和单人闯关
|
||||||
- 题目版本、服务端计时判分、Elo Rating、排行榜和基础反作弊
|
- 题目版本、服务端计时判分、Elo Rating、排行榜和基础反作弊
|
||||||
- Channels WebSocket 比赛进度通道,Redis Channel Layer
|
- Channels WebSocket 比赛进度通道,Redis Channel Layer
|
||||||
- LaTeX 文档与版本、六级零基础课程、练习判定
|
- LaTeX 文档与版本、六级零基础课程、练习判定
|
||||||
- 54 条旧版志愿者视频、五维能力地图、专业筛选与融合视频流
|
- 54 条旧版志愿者视频、五维能力地图、专业筛选与融合视频流
|
||||||
- 视频观看进度、幂等奖励、收藏、五维能力、数学精灵和人物卡册
|
- 视频观看进度、幂等奖励、收藏、五维能力、数学精灵和人物卡册
|
||||||
- 多工具工具箱:强计算器、增强函数绘图、数学白板、几何画板、符号查询和 LaTeX Lab
|
- 多工具工具箱:强计算器、增强函数绘图、统一联机数学画板、符号查询和 LaTeX Lab
|
||||||
|
- 信仰者人生 20 印记、4 个组合彩蛋、4 个直博方向与独立全屏交互
|
||||||
- Django Admin、健康检查、请求 ID、限流与统一 API 错误结构
|
- Django Admin、健康检查、请求 ID、限流与统一 API 错误结构
|
||||||
- MySQL 8.0/Redis Docker Compose、Gitea CI 和自动化测试
|
- MySQL 8.0/Redis Docker Compose、Gitea CI 和自动化测试
|
||||||
|
|
||||||
|
|||||||
@@ -180,3 +180,13 @@ def test_math_life_使用独立界面印记面板且清理交叉点():
|
|||||||
assert "function renderStoryMarks(run)" in app
|
assert "function renderStoryMarks(run)" in app
|
||||||
assert "choice.special" in app
|
assert "choice.special" in app
|
||||||
assert ".story-experience { position: fixed; inset: 0" in styles
|
assert ".story-experience { position: fixed; inset: 0" in styles
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_公众号入口可复制并跳转微信():
|
||||||
|
template = (settings.BASE_DIR / "templates" / "index.html").read_text(encoding="utf-8")
|
||||||
|
app = (STATIC_ROOT / "js" / "app.js").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert 'id="wechat-copy"' in template
|
||||||
|
assert 'href="weixin://"' in template
|
||||||
|
assert "function copyWechatName()" in app
|
||||||
|
assert 'navigator.clipboard.writeText(name)' in app
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import pytest
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_health_包含当前发布版本(client):
|
||||||
|
response = client.get("/health/")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {
|
||||||
|
"status": "ok",
|
||||||
|
"database": "ok",
|
||||||
|
"version": settings.APP_VERSION,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_展示公众号入口与版本(client):
|
||||||
|
response = client.get("/")
|
||||||
|
content = response.content.decode()
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "公众号" in content
|
||||||
|
assert "葫芦数学" in content
|
||||||
|
assert f"Hulumath v{settings.APP_VERSION}" in content
|
||||||
|
assert 'href="weixin://"' in content
|
||||||
@@ -1,14 +1,21 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
return render(request, "index.html")
|
return render(request, "index.html", {"app_version": settings.APP_VERSION})
|
||||||
|
|
||||||
|
|
||||||
def health(request):
|
def health(request):
|
||||||
with connection.cursor() as cursor:
|
with connection.cursor() as cursor:
|
||||||
cursor.execute("SELECT 1")
|
cursor.execute("SELECT 1")
|
||||||
cursor.fetchone()
|
cursor.fetchone()
|
||||||
return JsonResponse({"status": "ok", "database": "ok"})
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"database": "ok",
|
||||||
|
"version": settings.APP_VERSION,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
PROJECT_ROOT = BASE_DIR.parent
|
PROJECT_ROOT = BASE_DIR.parent
|
||||||
|
APP_VERSION = os.getenv("APP_VERSION", "1.2.0")
|
||||||
|
|
||||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-change-before-production")
|
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-change-before-production")
|
||||||
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
||||||
|
|||||||
@@ -117,6 +117,21 @@ def _generate_advanced():
|
|||||||
result = n * (n + 1) // 2
|
result = n * (n + 1) // 2
|
||||||
questions.append((f"a-auto-sum-{idx:04d}", f"1 到 {n} 的整数和", str(result)))
|
questions.append((f"a-auto-sum-{idx:04d}", f"1 到 {n} 的整数和", str(result)))
|
||||||
idx += 1
|
idx += 1
|
||||||
|
# 一元一次方程,补足稳定的 200 题池
|
||||||
|
for solution in range(2, 60):
|
||||||
|
if idx >= 200:
|
||||||
|
break
|
||||||
|
coefficient = solution % 7 + 2
|
||||||
|
offset = solution % 11 + 1
|
||||||
|
total = coefficient * solution + offset
|
||||||
|
questions.append(
|
||||||
|
(
|
||||||
|
f"a-auto-linear-{idx:04d}",
|
||||||
|
f"{coefficient}x + {offset} = {total},求 x",
|
||||||
|
str(solution),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
idx += 1
|
||||||
return questions[:200]
|
return questions[:200]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def fill_advanced_pool(apps, schema_editor):
|
||||||
|
Contest = apps.get_model("contest", "Contest")
|
||||||
|
ContestQuestion = apps.get_model("contest", "ContestQuestion")
|
||||||
|
Question = apps.get_model("contest", "Question")
|
||||||
|
QuestionVersion = apps.get_model("contest", "QuestionVersion")
|
||||||
|
|
||||||
|
versions = []
|
||||||
|
for offset, solution in enumerate(range(2, 22), start=180):
|
||||||
|
coefficient = solution % 7 + 2
|
||||||
|
constant = solution % 11 + 1
|
||||||
|
total = coefficient * solution + constant
|
||||||
|
question, _ = Question.objects.update_or_create(
|
||||||
|
slug=f"a-auto-linear-{offset:04d}",
|
||||||
|
defaults={
|
||||||
|
"track": "advanced",
|
||||||
|
"tags": ["口算"],
|
||||||
|
"is_active": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
version, _ = QuestionVersion.objects.update_or_create(
|
||||||
|
question=question,
|
||||||
|
version=1,
|
||||||
|
defaults={
|
||||||
|
"prompt": f"{coefficient}x + {constant} = {total},求 x",
|
||||||
|
"answer": str(solution),
|
||||||
|
"explanation": f"答案为 {solution}",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
versions.append(version)
|
||||||
|
|
||||||
|
for contest in Contest.objects.filter(track="advanced").iterator():
|
||||||
|
existing_ids = set(
|
||||||
|
ContestQuestion.objects.filter(contest=contest).values_list(
|
||||||
|
"question_version_id",
|
||||||
|
flat=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
next_order = (
|
||||||
|
ContestQuestion.objects.filter(contest=contest)
|
||||||
|
.order_by("-order")
|
||||||
|
.values_list("order", flat=True)
|
||||||
|
.first()
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
additions = []
|
||||||
|
for version in versions:
|
||||||
|
if version.id in existing_ids:
|
||||||
|
continue
|
||||||
|
next_order += 1
|
||||||
|
additions.append(
|
||||||
|
ContestQuestion(
|
||||||
|
contest_id=contest.id,
|
||||||
|
question_version_id=version.id,
|
||||||
|
order=next_order,
|
||||||
|
points=100,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ContestQuestion.objects.bulk_create(additions)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("contest", "0007_remove_realtimematch_matchmaking_lookup_idx_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
fill_advanced_pool,
|
||||||
|
migrations.RunPython.noop,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from contest.management.commands.seed_contests import QUESTIONS
|
||||||
|
from contest.models import Question
|
||||||
|
|
||||||
|
|
||||||
|
def test_seed_question_pool_三个历史赛道题量充足():
|
||||||
|
assert len(QUESTIONS[Question.Track.BEGINNER]) == 400
|
||||||
|
assert len(QUESTIONS[Question.Track.STANDARD]) == 400
|
||||||
|
assert len(QUESTIONS[Question.Track.ADVANCED]) == 200
|
||||||
@@ -50,6 +50,7 @@ button { color: inherit; }
|
|||||||
.sidebar-foot { margin-top: auto; display: grid; gap: 15px; }
|
.sidebar-foot { margin-top: auto; display: grid; gap: 15px; }
|
||||||
.system-status { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .1em; }
|
.system-status { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .1em; }
|
||||||
.system-status i { width: 7px; height: 7px; margin-right: 7px; display: inline-block; border-radius: 50%; background: #5bb67c; box-shadow: 0 0 0 4px rgba(91,182,124,.13); }
|
.system-status i { width: 7px; height: 7px; margin-right: 7px; display: inline-block; border-radius: 50%; background: #5bb67c; box-shadow: 0 0 0 4px rgba(91,182,124,.13); }
|
||||||
|
.app-version { color: var(--muted); font-size: 9px; letter-spacing: .1em; }
|
||||||
.ghost-button, .text-button {
|
.ghost-button, .text-button {
|
||||||
background: transparent; border: 1px solid var(--line); border-radius: 10px; padding: 11px 14px; cursor: pointer;
|
background: transparent; border: 1px solid var(--line); border-radius: 10px; padding: 11px 14px; cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -79,6 +80,7 @@ button { color: inherit; }
|
|||||||
content: ""; position: absolute; width: 330px; height: 330px; right: -100px; bottom: -150px;
|
content: ""; position: absolute; width: 330px; height: 330px; right: -100px; bottom: -150px;
|
||||||
border: 55px solid rgba(25,101,72,.09); border-radius: 50%;
|
border: 55px solid rgba(25,101,72,.09); border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
.wechat-entry { display: flex; align-items: center; justify-content: space-between; gap: 28px; margin-top: 18px; padding: 28px 32px; border: 1px solid rgba(25,101,72,.16); border-radius: 20px; background: linear-gradient(120deg, rgba(25,101,72,.08), rgba(204,232,91,.18)); }.wechat-entry h2 { margin: 8px 0; font: 28px Georgia, serif; }.wechat-entry p { margin: 0; color: var(--muted); line-height: 1.7; }.wechat-entry-actions { display: flex; gap: 9px; flex-shrink: 0; }.wechat-entry-actions a { display: inline-flex; align-items: center; text-decoration: none; }
|
||||||
.kicker { color: var(--green); font-size: 10px; font-weight: 700; letter-spacing: .24em; }
|
.kicker { color: var(--green); font-size: 10px; font-weight: 700; letter-spacing: .24em; }
|
||||||
.hero h1, .page-title h1 { margin: 20px 0 18px; font: 500 clamp(42px, 5vw, 76px)/1.03 Georgia, "Songti SC", serif; letter-spacing: -.04em; }
|
.hero h1, .page-title h1 { margin: 20px 0 18px; font: 500 clamp(42px, 5vw, 76px)/1.03 Georgia, "Songti SC", serif; letter-spacing: -.04em; }
|
||||||
.hero h1 em { color: var(--green); font-weight: inherit; }
|
.hero h1 em { color: var(--green); font-weight: inherit; }
|
||||||
@@ -307,6 +309,7 @@ dialog::backdrop { background: rgba(17,25,20,.55); backdrop-filter: blur(5px); }
|
|||||||
.match-mode-switch { grid-template-columns: 1fr; }
|
.match-mode-switch { grid-template-columns: 1fr; }
|
||||||
.board-online-panel { grid-template-columns: 1fr; }.board-online-panel > div, .board-online-panel form { display: grid; grid-template-columns: 1fr; }.board-online-panel p, .board-online-panel > strong { grid-column: 1; }
|
.board-online-panel { grid-template-columns: 1fr; }.board-online-panel > div, .board-online-panel form { display: grid; grid-template-columns: 1fr; }.board-online-panel p, .board-online-panel > strong { grid-column: 1; }
|
||||||
.story-experience { padding: 14px 14px 40px; }.story-experience-header { align-items: flex-start; }.story-experience-header .ghost-button { width: auto; padding: 9px 11px; font-size: 10px; }.story-chapter-bar, .story-experience-layout { grid-template-columns: 1fr; }.story-scene-panel { min-height: 0; padding: 25px 20px; }.story-scene-panel .scene { min-height: 120px; }.story-mark-panel { position: static; }.story-domain-counts { grid-template-columns: repeat(2, 1fr); }
|
.story-experience { padding: 14px 14px 40px; }.story-experience-header { align-items: flex-start; }.story-experience-header .ghost-button { width: auto; padding: 9px 11px; font-size: 10px; }.story-chapter-bar, .story-experience-layout { grid-template-columns: 1fr; }.story-scene-panel { min-height: 0; padding: 25px 20px; }.story-scene-panel .scene { min-height: 120px; }.story-mark-panel { position: static; }.story-domain-counts { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.wechat-entry { align-items: flex-start; flex-direction: column; padding: 24px; }.wechat-entry-actions { width: 100%; display: grid; grid-template-columns: 1fr 1fr; }.wechat-entry-actions > * { justify-content: center; text-align: center; }
|
||||||
.tool-card { min-height: 125px; }.workspace-heading, .map-heading { display: block; }.workspace-heading p { margin-top: 12px; }.tool-workspace { padding: 16px; }.calculator-controls { grid-template-columns: 1fr 1fr; }.calculator-display { min-height: 120px; padding: 18px; }.calculator-display output { font-size: 34px; }.calc-examples .primary-button { width: 100%; margin-left: 0; }.drawing-toolbar { align-items: stretch; }.drawing-toolbar .primary-button { width: 100%; margin-left: 0; }.canvas-stage canvas { width: 100%; min-width: 0; }
|
.tool-card { min-height: 125px; }.workspace-heading, .map-heading { display: block; }.workspace-heading p { margin-top: 12px; }.tool-workspace { padding: 16px; }.calculator-controls { grid-template-columns: 1fr 1fr; }.calculator-display { min-height: 120px; padding: 18px; }.calculator-display output { font-size: 34px; }.calc-examples .primary-button { width: 100%; margin-left: 0; }.drawing-toolbar { align-items: stretch; }.drawing-toolbar .primary-button { width: 100%; margin-left: 0; }.canvas-stage canvas { width: 100%; min-width: 0; }
|
||||||
.math-games-section { margin-top: 45px; }.game-card { min-height: 260px; padding: 21px; }.game-card-controls { align-items: stretch; }.game-card-controls .primary-button { flex: 1; }.twenty-four-numbers { gap: 7px; }.twenty-four-numbers button { border-radius: 13px; font-size: 28px; }.sudoku-board input { font-size: clamp(13px, 4.5vw, 20px); }.sudoku-actions { display: grid; grid-template-columns: 1fr 1fr; }.game-keypad { gap: 5px; }
|
.math-games-section { margin-top: 45px; }.game-card { min-height: 260px; padding: 21px; }.game-card-controls { align-items: stretch; }.game-card-controls .primary-button { flex: 1; }.twenty-four-numbers { gap: 7px; }.twenty-four-numbers button { border-radius: 13px; font-size: 28px; }.sudoku-board input { font-size: clamp(13px, 4.5vw, 20px); }.sudoku-actions { display: grid; grid-template-columns: 1fr 1fr; }.game-keypad { gap: 5px; }
|
||||||
.challenge-panel { grid-template-columns: 1fr; padding: 20px; }.challenge-actions form { grid-template-columns: 1fr; }.challenge-actions .dark-button { width: 100%; }.challenge-code { width: 100%; padding: 14px 10px; font-size: 27px; }.realtime-status-line { display: grid; }.realtime-progress-panel { grid-template-columns: 1fr 1fr; }
|
.challenge-panel { grid-template-columns: 1fr; padding: 20px; }.challenge-actions form { grid-template-columns: 1fr; }.challenge-actions .dark-button { width: 100%; }.challenge-code { width: 100%; padding: 14px 10px; font-size: 27px; }.realtime-status-line { display: grid; }.realtime-progress-panel { grid-template-columns: 1fr 1fr; }
|
||||||
|
|||||||
@@ -148,6 +148,16 @@ function showToast(message) {
|
|||||||
window.setTimeout(() => toast.classList.remove("show"), 2600);
|
window.setTimeout(() => toast.classList.remove("show"), 2600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyWechatName() {
|
||||||
|
const name = $("#wechat-copy").dataset.wechatName;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(name);
|
||||||
|
showToast(`已复制公众号名称:${name}`);
|
||||||
|
} catch {
|
||||||
|
showToast(`请在微信内搜索:${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function navigate(view) {
|
function navigate(view) {
|
||||||
$$(".nav-item").forEach((button) => button.classList.toggle("active", button.dataset.view === view));
|
$$(".nav-item").forEach((button) => button.classList.toggle("active", button.dataset.view === view));
|
||||||
$$(".view").forEach((section) => section.classList.toggle("active", section.id === `view-${view}`));
|
$$(".view").forEach((section) => section.classList.toggle("active", section.id === `view-${view}`));
|
||||||
@@ -1273,6 +1283,7 @@ function bindUI() {
|
|||||||
$$("[data-jump]").forEach((button) => button.addEventListener("click", () => navigate(button.dataset.jump)));
|
$$("[data-jump]").forEach((button) => button.addEventListener("click", () => navigate(button.dataset.jump)));
|
||||||
$$("[data-open-auth]").forEach((button) => button.addEventListener("click", openAuth));
|
$$("[data-open-auth]").forEach((button) => button.addEventListener("click", openAuth));
|
||||||
$("#start-mathbti").addEventListener("click", startMathBTI);
|
$("#start-mathbti").addEventListener("click", startMathBTI);
|
||||||
|
$("#wechat-copy").addEventListener("click", copyWechatName);
|
||||||
$("#story-experience-close").addEventListener("click", closeStoryExperience);
|
$("#story-experience-close").addEventListener("click", closeStoryExperience);
|
||||||
$("#save-formula").addEventListener("click", saveFormula);
|
$("#save-formula").addEventListener("click", saveFormula);
|
||||||
$("#latex-source").addEventListener("input", (event) => { renderLatexPreview(event.target.value); });
|
$("#latex-source").addEventListener("input", (event) => { renderLatexPreview(event.target.value); });
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<div class="sidebar-foot">
|
<div class="sidebar-foot">
|
||||||
<div class="system-status"><i></i>系统在线</div>
|
<div class="system-status"><i></i>系统在线</div>
|
||||||
|
<small class="app-version">Hulumath v{{ app_version }}</small>
|
||||||
<button id="auth-button" class="ghost-button">登录 / 注册</button>
|
<button id="auth-button" class="ghost-button">登录 / 注册</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
@@ -66,6 +67,18 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="wechat-entry">
|
||||||
|
<div>
|
||||||
|
<span class="kicker">WECHAT OFFICIAL ACCOUNT</span>
|
||||||
|
<h2>在公众号继续收到数学人生更新</h2>
|
||||||
|
<p>微信内搜索公众号「葫芦数学」,获取新剧情、比赛活动与版本公告。</p>
|
||||||
|
</div>
|
||||||
|
<div class="wechat-entry-actions">
|
||||||
|
<button id="wechat-copy" class="ghost-button" data-wechat-name="葫芦数学">复制公众号名称</button>
|
||||||
|
<a class="primary-button" href="weixin://">打开微信</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<div><span class="kicker">FOUR SPIRITS</span><h2>四种数学精神</h2></div>
|
<div><span class="kicker">FOUR SPIRITS</span><h2>四种数学精神</h2></div>
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Hulumath v1.2.0 Release Notes
|
||||||
|
|
||||||
|
发布日期:待 `release/v1.2.0` 合并并通过生产冒烟后填写。
|
||||||
|
|
||||||
|
## 版本主题
|
||||||
|
|
||||||
|
v1.2 将比赛、数学画板和数学人生从首发功能升级为可持续使用的完整闭环,同时清理
|
||||||
|
账号、档案、LaTeX 和本地调试中的 P0 问题。
|
||||||
|
|
||||||
|
## 主要变化
|
||||||
|
|
||||||
|
### 比赛与记录
|
||||||
|
|
||||||
|
- 取消比赛难度入口,实时匹配使用统一玩家池。
|
||||||
|
- 每局从完整题库生成不可变随机快照;历史 Attempt 不受影响。
|
||||||
|
- 题库包含入门 400、标准 400、进阶 200 道基础题。
|
||||||
|
- 实时匹配新增数独 Timerun 与 24 点竞速。
|
||||||
|
- 三种玩法共享联机码、WebSocket 状态、服务端计时、Rating 和历史记录。
|
||||||
|
- 结算响应立即刷新右上角 Rating。
|
||||||
|
- 首页和“我的”展示实时比赛历史、对手、结果、时间和 Rating 变化。
|
||||||
|
|
||||||
|
### 数学工具箱
|
||||||
|
|
||||||
|
- 数学白板与几何画板合并为“数学画板”单入口。
|
||||||
|
- 支持 6 位联机码、白板快照和几何状态实时同步。
|
||||||
|
- 新增数学你画我猜:服务端出题、猜题判定和计分。
|
||||||
|
- 函数绘图增加语法规则、示例和错误反馈。
|
||||||
|
- LaTeX 长公式横向滚动,保存按钮保持在独立操作行。
|
||||||
|
|
||||||
|
### 数学人生与 MathBTI
|
||||||
|
|
||||||
|
- 信仰者人生从弹窗升级为独立全屏体验,提供章节、进度和印记档案。
|
||||||
|
- `xinzhi`、`shuli`、`xiayi` 旧数值完全退出 v2 剧本。
|
||||||
|
- 新增 4 领域 × 5 个印记,共 20 个永久收藏印记。
|
||||||
|
- 新增高斯、祖冲之、欧拉、诺特 4 个组合彩蛋。
|
||||||
|
- 统一直博结局按单次人生中印记最多的领域分发到数论、代数几何、分析或应用数学。
|
||||||
|
- 16 位数学家五维评分重新评定,并提供史料和评分口径文档。
|
||||||
|
- 清理数学人生大厅的“数学人生交叉点”和背景圆球。
|
||||||
|
|
||||||
|
### 账号、入口与运维
|
||||||
|
|
||||||
|
- 登录或注册成功后,“我的”页面立即刷新。
|
||||||
|
- 新增仅限 `DEBUG=true` 的本地管理员初始化命令:`make local-admin`。
|
||||||
|
- 首页新增公众号入口,可复制“葫芦数学”并尝试唤起微信。
|
||||||
|
- `/health/` 返回 `version: "1.2.0"`。
|
||||||
|
- 生产静态资源检查、数据库备份、迁移、HTTP/WebSocket 冒烟和失败回退保持启用。
|
||||||
|
|
||||||
|
## Issue 对照
|
||||||
|
|
||||||
|
| Issue | 状态 | v1.2 验收位置 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| #10 | 完成 | 统一玩家池、前端移除难度 |
|
||||||
|
| #15 | 完成 | 数学你画我猜 |
|
||||||
|
| #17 | 完成 | LaTeX 三段式布局与长公式滚动 |
|
||||||
|
| #18 | 完成 | 大厅残留元素清理 |
|
||||||
|
| #21 | 完成 | Rating 即时刷新 |
|
||||||
|
| #22 | 完成 | Attempt 随机题目快照 |
|
||||||
|
| #24 | 并入 #36 | 独立剧情界面;需仓库所有者手动关闭 #24 |
|
||||||
|
| #25 | 完成 | 首页与档案比赛记录 |
|
||||||
|
| #26 | 完成 | 工具按计算、查询、绘图、画板、表达排序 |
|
||||||
|
| #27 | 完成 | 统一画板与联机码 |
|
||||||
|
| #28 | 完成 | 1000 道分层题库 |
|
||||||
|
| #29 | 完成 | 数独与 24 点实时竞速 |
|
||||||
|
| #30 | 完成 | 主页公众号入口 |
|
||||||
|
| #31 | 完成 | 本地管理员可登录 |
|
||||||
|
| #32 | 完成 | 登录后档案刷新 |
|
||||||
|
| #33 | 完成 | 函数语法说明 |
|
||||||
|
| #34 | 完成 | 安全的本地管理员初始化方案 |
|
||||||
|
| #35 | 完成 | 16 位数学家五维重评与依据 |
|
||||||
|
| #36 | 完成 | 20 印记、4 彩蛋、4 结局、独立界面 |
|
||||||
|
|
||||||
|
## 数据库迁移
|
||||||
|
|
||||||
|
- Contest Attempt 新增题目快照。
|
||||||
|
- Realtime Match 新增玩法类型;Math Game Attempt 可关联实时比赛。
|
||||||
|
- ContestQuestion 题池扩充为当前赛道的全部最新有效题目。
|
||||||
|
- 新增 Toolbox BoardSession。
|
||||||
|
- 新增 StoryMark,并发布信仰者人生 StoryVersion v2。
|
||||||
|
- 旧信仰者 v1 取消发布;旧进行中存档标记为已放弃,历史选择保留。
|
||||||
|
|
||||||
|
## 发布检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make check
|
||||||
|
make test
|
||||||
|
cd backend && ../.venv/bin/python manage.py migrate --plan
|
||||||
|
cd backend && ../.venv/bin/python manage.py collectstatic --noinput
|
||||||
|
```
|
||||||
|
|
||||||
|
生产部署后检查:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/health/ version == 1.2.0
|
||||||
|
首页、后台、视频目录正常
|
||||||
|
联机比赛 WebSocket 正常
|
||||||
|
统一画板 WebSocket 正常
|
||||||
|
```
|
||||||
@@ -31,7 +31,11 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
health = json.loads(fetch(args.base_url, "/health/"))
|
health = json.loads(fetch(args.base_url, "/health/"))
|
||||||
if health != {"status": "ok", "database": "ok"}:
|
if (
|
||||||
|
health.get("status") != "ok"
|
||||||
|
or health.get("database") != "ok"
|
||||||
|
or not health.get("version")
|
||||||
|
):
|
||||||
raise RuntimeError(f"unexpected health payload: {health}")
|
raise RuntimeError(f"unexpected health payload: {health}")
|
||||||
|
|
||||||
homepage = fetch(args.base_url, "/").decode("utf-8")
|
homepage = fetch(args.base_url, "/").decode("utf-8")
|
||||||
@@ -49,7 +53,8 @@ def main():
|
|||||||
asyncio.run(check_websocket(args.ws_url))
|
asyncio.run(check_websocket(args.ws_url))
|
||||||
print(
|
print(
|
||||||
"Production smoke checks passed: "
|
"Production smoke checks passed: "
|
||||||
f"health, homepage, admin, {catalog['total']} videos, websocket"
|
f"health v{health['version']}, homepage, admin, "
|
||||||
|
f"{catalog['total']} videos, websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user