feat: 问题数据迁移到数据库;学员详情页URL导航改造;侧边栏统一

- 问题从文件系统迁移到数据库 problems 表
- 移除 PROBLEMS_DIR 配置和文件读取逻辑
- student.html 完整重写:编辑/添加/删除问题,生成方案进度显示
- 学员详情页支持独立URL访问 (/student/<id>)
- 统一侧边栏到 base.html
- 更新文档:DEPLOYMENT_SOP, MODELS, STRUCTURE, FRONTEND_ARCH
- 部署到生产环境 v1.2.0
This commit is contained in:
hmo
2026-04-23 06:35:32 +08:00
parent fd593bddf4
commit 18351212e8
18 changed files with 857 additions and 488 deletions
+57 -198
View File
@@ -5,6 +5,7 @@ import shutil
from datetime import datetime
from flask import request, jsonify, render_template, current_app, session, redirect
from app.routes import main_bp
from app.models import db, Problem, StudentProblem
from app.config import load_api_config, save_api_config
from app.routes.auth import login_required_json, admin_required
@@ -13,14 +14,14 @@ from app.routes.auth import login_required_json, admin_required
@login_required_json
def settings():
"""问题配置页面 - 所有登录用户可访问"""
return render_template("settings.html")
return render_template("settings.html", active_nav="settings")
@main_bp.route("/api-settings")
@admin_required
def api_settings_page():
"""API设置页面 - 仅管理员"""
return render_template("api_settings.html")
return render_template("api_settings.html", active_nav="api-settings")
# ==================== API配置接口 ====================
@@ -81,241 +82,99 @@ def update_api_config():
@main_bp.route("/api/problems", methods=["GET"])
@login_required_json
def get_problems():
"""获取问题列表(从文件夹动态读取)"""
problems_dir = current_app.config.get("PROBLEMS_DIR")
"""获取问题列表(从数据库读取)"""
from app.models import Problem
problems = []
if problems_dir and os.path.exists(problems_dir):
for filename in sorted(os.listdir(problems_dir)):
if (
filename.endswith(".md")
and not filename.startswith("模板")
and not filename.startswith("针对性练习建议")
):
name = filename.replace(".md", "")
if "汇总" in name:
continue
parts = name.split("_", 1)
if len(parts) == 2:
problem_id = parts[0]
problem_name = parts[1]
category = "技术类"
filepath = os.path.join(problems_dir, filename)
try:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
if "认知类" in content:
category = "认知类"
elif "节奏类" in content:
category = "节奏类"
elif "表现类" in content:
category = "表现类"
elif "习惯类" in content:
category = "习惯类"
elif "综合类" in content:
category = "综合类"
except:
pass
try:
mtime = os.path.getmtime(filepath)
except:
mtime = 0
problems.append(
{
"id": problem_id,
"name": problem_name,
"category": category,
"file": filename,
"mtime": mtime,
}
)
return jsonify(sorted(problems, key=lambda x: x["id"]))
problems = Problem.query.order_by(Problem.no).all()
return jsonify([p.to_dict() for p in problems])
@main_bp.route("/api/problems/<problem_id>", methods=["GET"])
@main_bp.route("/api/problems/<int:problem_id>", methods=["GET"])
@login_required_json
def get_problem_detail(problem_id):
"""获取单个问题的详细信息"""
problems_dir = current_app.config.get("PROBLEMS_DIR")
from app.models import Problem
if problems_dir and os.path.exists(problems_dir):
for filename in os.listdir(problems_dir):
if filename.startswith(f"{problem_id}_") and filename.endswith(".md"):
filepath = os.path.join(problems_dir, filename)
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
return jsonify(
{"id": problem_id, "filename": filename, "content": content}
)
problem = Problem.query.get(problem_id)
if not problem:
return jsonify({"error": "问题不存在"}), 404
return jsonify({"error": "问题不存在"}), 404
return jsonify({
"id": problem.id,
"no": problem.no,
"name": problem.name,
"category": problem.category,
"content": problem.content,
})
@main_bp.route("/api/problems", methods=["POST"])
@admin_required
def create_problem():
"""创建新问题 - 仅管理员"""
"""创建新问题"""
from app.models import Problem
data = request.get_json()
problem_id = data.get("id", "").strip()
problem_name = data.get("name", "").strip()
no = data.get("no", "").strip()
name = data.get("name", "").strip()
category = data.get("category", "技术类")
content = data.get("content", "")
if not problem_id or not problem_name:
return jsonify({"error": "ID和名称不能为空"}), 400
if not no or not name:
return jsonify({"error": "编号和名称不能为空"}), 400
# 格式化ID
problem_id = problem_id.zfill(2)
# 检查编号是否已存在
existing = Problem.query.filter_by(no=no).first()
if existing:
return jsonify({"error": "该编号已存在"}), 400
problems_dir = current_app.config.get("PROBLEMS_DIR")
filename = f"{problem_id}_{problem_name}.md"
filepath = os.path.join(problems_dir, filename)
# 创建问题
problem = Problem(no=no, name=name, category=category, content=content)
db.session.add(problem)
db.session.commit()
if os.path.exists(filepath):
return jsonify({"error": "问题已存在"}), 400
# 生成默认内容
content = f"""# {problem_name}
> 所属系列:钢琴学习常见问题针对性练习建议
> 配合目标体系使用,针对性补齐短板
---
## 问题表现
- 请描述具体表现症状
## 原因分析
- 可能的原因1
- 可能的原因2
## 针对性练习方案
### 日常基础练习
| 练习名称 | 时长 | 频率 | 目的 |
|---------|------|------|------|
| 练习1 | 10分钟 | 每天 | 目的1 |
| 练习2 | 5分钟 | 每天 | 目的2 |
### 具体操作
```
练习1:练习名称
- 步骤1
- 步骤2
- 步骤3
```
## 练习提醒
### ⚠️ 禁忌
- 禁忌1
- 禁忌2
### ✓ 正确做法
- 正确做法1
- 正确做法2
## 评估标准
| 等级 | 标准 |
|------|------|
| 入门 | 达到的标准 |
| 进阶 | 达到的标准 |
| 熟练 | 达到的标准 |
| 精通 | 达到的标准 |
---
> **版本**V1.0
> **创建时间**{datetime.now().strftime("%Y-%m-%d")}
> **适用场景**:成人钢琴集体课学员个性化辅导"""
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
return jsonify({"message": "创建成功", "id": problem_id, "name": problem_name})
return jsonify({"message": "创建成功", "id": problem.id, "no": problem.no, "name": problem.name})
@main_bp.route("/api/problems/<problem_id>", methods=["PUT"])
@main_bp.route("/api/problems/<int:problem_id>", methods=["PUT"])
@login_required_json
def update_problem(problem_id):
"""更新问题"""
data = request.get_json()
new_name = data.get("name", "").strip()
new_content = data.get("content", "").strip()
from app.models import Problem
if not new_name:
return jsonify({"error": "名称不能为空"}), 400
problems_dir = current_app.config.get("PROBLEMS_DIR")
# 找到旧文件
old_filename = None
for f in os.listdir(problems_dir):
if f.startswith(f"{problem_id}_") and f.endswith(".md"):
old_filename = f
break
if not old_filename:
problem = Problem.query.get(problem_id)
if not problem:
return jsonify({"error": "问题不存在"}), 404
old_filepath = os.path.join(problems_dir, old_filename)
new_filename = f"{problem_id}_{new_name}.md"
new_filepath = os.path.join(problems_dir, new_filename)
# 如果名称改变,需要重命名文件
if old_filename != new_filename:
if os.path.exists(new_filepath):
return jsonify({"error": "同名问题已存在"}), 400
os.rename(old_filepath, new_filepath)
filepath = new_filepath
else:
filepath = old_filepath
# 更新内容
with open(filepath, "w", encoding="utf-8") as f:
f.write(new_content)
data = request.get_json()
if "name" in data:
problem.name = data["name"].strip()
if "category" in data:
problem.category = data["category"]
if "content" in data:
problem.content = data["content"]
db.session.commit()
return jsonify({"message": "更新成功"})
@main_bp.route("/api/problems/<problem_id>", methods=["DELETE"])
@main_bp.route("/api/problems/<int:problem_id>", methods=["DELETE"])
@admin_required
def delete_problem(problem_id):
"""删除问题"""
problems_dir = current_app.config.get("PROBLEMS_DIR")
from app.models import Problem
# 找到文件
filename = None
for f in os.listdir(problems_dir):
if f.startswith(f"{problem_id}_") and f.endswith(".md"):
filename = f
break
if not filename:
problem = Problem.query.get(problem_id)
if not problem:
return jsonify({"error": "问题不存在"}), 404
filepath = os.path.join(problems_dir, filename)
# 检查是否有关联数据
from app.models import StudentProblem
if StudentProblem.query.filter_by(problem_db_id=problem_id).first():
return jsonify({"error": "该问题已被学员使用,无法删除"}), 400
# 移动到备份目录
trash_dir = os.path.join(problems_dir, "bk")
os.makedirs(trash_dir, exist_ok=True)
import time
backup_name = f"{filename.replace('.md', '')}_{int(time.time())}.md"
shutil.move(filepath, os.path.join(trash_dir, backup_name))
db.session.delete(problem)
db.session.commit()
return jsonify({"message": "删除成功"})