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:
+124
-22
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user