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
+124 -22
View File
@@ -13,10 +13,10 @@ from flask import (
session,
)
from app.routes import main_bp
from app.models import db, Student, PracticePlan
from app.models import db, Student, PracticePlan, StudentProblem
from app.services.plan_generator import generate_practice_plan, generate_ai_report
from app.services.pdf_generator import generate_pdf
from app.routes.auth import login_required_json
from app.routes.auth import login_required_json, admin_required
def sse_format(data):
@@ -33,6 +33,114 @@ def get_student_plans(student_id):
return jsonify([p.to_dict() for p in plans])
@main_bp.route("/api/plans", methods=["GET"])
@login_required_json
def get_all_plans():
"""获取所有方案(支持多条件筛选)
查询参数:
- class_id: 班级ID
- problem_ids: Problem.id 列表,逗号分隔
- template_id: 模板ID
- is_typical: 是否典型 (true/false)
- student_name: 学员姓名(模糊匹配)
"""
import json as json_module
query = PracticePlan.query
# 按班级筛选
class_id = request.args.get('class_id', type=int)
if class_id:
query = query.join(Student).filter(Student.class_id == class_id)
# 按模板筛选
template_id = request.args.get('template_id', type=int)
if template_id:
query = query.filter(PracticePlan.template_id == template_id)
# 按典型状态筛选
is_typical = request.args.get('is_typical')
if is_typical and is_typical.lower() == 'true':
query = query.filter(PracticePlan.is_typical == True)
# 按学员姓名模糊筛选
student_name = request.args.get('student_name')
if student_name:
query = query.join(Student).filter(Student.name.like(f'%%{student_name}%%'))
# 按问题筛选(通过 problem_id 关联到学员的问题)
problem_ids = request.args.get('problem_ids')
if problem_ids:
problem_id_list = [int(pid.strip()) for pid in problem_ids.split(',') if pid.strip()]
if problem_id_list:
# 筛选:方案对应的学员有指定问题之一的
# 使用子查询避免笛卡尔积导致的重复
from sqlalchemy import exists
query = query.join(Student).filter(
exists().where(
(StudentProblem.student_id == Student.id) &
(StudentProblem.problem_id.in_(problem_id_list))
)
)
plans = query.order_by(PracticePlan.created_at.desc()).all()
return jsonify([p.to_dict() for p in plans])
@main_bp.route("/api/plans/<int:plan_id>/typical", methods=["POST"])
@login_required_json
def toggle_plan_typical(plan_id):
"""切换方案的典型状态"""
plan = PracticePlan.query.get_or_404(plan_id)
plan.is_typical = not plan.is_typical
db.session.commit()
return jsonify({"success": True, "is_typical": plan.is_typical})
@main_bp.route("/plans")
@login_required_json
def plans_page():
"""方案管理页面"""
return render_template("plans.html", active_nav="plans")
@main_bp.route("/api/admin/fix-plan-templates", methods=["GET"])
@admin_required
def fix_plan_templates():
"""临时接口:修复所有方案的模板关联"""
from app.models import Template, PracticePlan
simple_template = Template.query.filter_by(name='简单文字版').first()
formal_template = Template.query.filter_by(name='正式报告版').first()
if not simple_template or not formal_template:
return jsonify({"error": "模板没找到"}), 400
plans = PracticePlan.query.order_by(PracticePlan.created_at.desc()).all()
if plans:
plans[0].template_id = simple_template.id
for plan in plans[1:]:
plan.template_id = formal_template.id
db.session.commit()
return jsonify({"ok": True, "updated": len(plans)})
@main_bp.route("/plan/<int:plan_id>")
@login_required_json
def plan_detail_page(plan_id):
"""方案详情页面"""
return render_template("plan_detail.html", active_nav="plans")
@main_bp.route("/plan/<int:plan_id>/edit")
@login_required_json
def plan_edit_page(plan_id):
"""方案编辑页面"""
return render_template("plan_edit.html", active_nav="plans", plan_id=plan_id)
@main_bp.route("/api/generate-plan", methods=["POST"])
@login_required_json
def generate_plan():
@@ -49,30 +157,24 @@ def generate_plan():
return jsonify({"error": "请先记录学员的问题"}), 400
# 预先收集所有数据,避免在generator中访问数据库
problems_dir = current_app.config["PROBLEMS_DIR"]
# 学员的统一练习时间
practice_time = student.practice_time or "30-60分钟"
problem_data = []
for p in problems:
# problem_id 已经是完整标识(如 "01_手小"),直接用作文件名
problem_file = os.path.join(problems_dir, f"{p.problem_id}.md")
content = ""
if os.path.exists(problem_file):
with open(problem_file, "r", encoding="utf-8") as f:
content = f.read()
problem_data.append(
{
"problem_id": p.problem_id,
"problem_name": p.problem_name,
"severity": p.severity,
"level": p.level,
"content": content,
}
)
# 使用 Problem 关联获取问题信息
problem_obj = p.problem
if problem_obj:
problem_data.append(
{
"problem_id": problem_obj.id, # 使用 Problem.id
"problem_name": problem_obj.name,
"problem_no": problem_obj.no,
"severity": p.severity,
"level": p.level,
"content": problem_obj.content or "",
}
)
time_mapping = {
"15分钟": {"total": 15, "basic": 10, "tech": 2, "piece": 3},
"30分钟": {"total": 30, "basic": 15, "tech": 5, "piece": 10},
@@ -104,7 +206,6 @@ def generate_plan():
plan_content = generate_practice_plan(
student_name=student.name,
problems=problem_data,
problems_dir=problems_dir,
practice_time=practice_time,
)
@@ -241,6 +342,7 @@ def generate_plan():
try:
plan = PracticePlan(
student_id=student_id,
template_id=template_id,
content=json.dumps(plan_content, ensure_ascii=False),
)
db.session.add(plan)