更新:models/routes/services/templates/docs
This commit is contained in:
+26
-3
@@ -3,7 +3,7 @@
|
||||
from flask import request, jsonify, render_template, session
|
||||
from datetime import datetime, timedelta
|
||||
from app.routes import main_bp
|
||||
from app.models import db, Class, Student, StudentGoal, Goal
|
||||
from app.models import db, Class, Student, StudentGoal, Goal, User
|
||||
from app.routes.auth import login_required_json, admin_required
|
||||
import logging
|
||||
|
||||
@@ -17,12 +17,21 @@ def classes_page():
|
||||
return render_template("classes.html", active_nav="classes")
|
||||
|
||||
|
||||
@main_bp.route("/api/teachers", methods=["GET"])
|
||||
@login_required_json
|
||||
def api_teachers_list():
|
||||
"""用户列表(用于班主任选择,所有登录用户可访问)"""
|
||||
users = User.query.order_by(User.name, User.username).all()
|
||||
return jsonify([{"id": u.id, "name": u.name or u.username} for u in users])
|
||||
|
||||
|
||||
@main_bp.route("/api/classes", methods=["GET"])
|
||||
@login_required_json
|
||||
def api_classes_list():
|
||||
"""班级列表"""
|
||||
# 支持筛选参数: active=true, active=false, 不传则返回全部
|
||||
# 支持筛选参数: active=true/false, mine=true/false
|
||||
active_filter = request.args.get("active")
|
||||
mine_filter = request.args.get("mine")
|
||||
|
||||
query = Class.query
|
||||
if active_filter is not None:
|
||||
@@ -31,6 +40,12 @@ def api_classes_list():
|
||||
elif active_filter.lower() == "false":
|
||||
query = query.filter(Class.active == False)
|
||||
|
||||
# 我的班级筛选(当前用户作为老师的班级)
|
||||
if mine_filter and mine_filter.lower() == "true":
|
||||
user_id = session.get("user_id")
|
||||
if user_id:
|
||||
query = query.filter(Class.teacher_id == user_id)
|
||||
|
||||
classes = query.order_by(Class.created_at.desc()).all()
|
||||
return jsonify([c.to_dict() for c in classes])
|
||||
|
||||
@@ -42,6 +57,8 @@ def api_classes_create():
|
||||
data = request.get_json()
|
||||
name = data.get("name", "").strip()
|
||||
description = data.get("description", "")
|
||||
teacher_id = data.get("teacher_id") # 可以为空
|
||||
level = data.get("level", "启蒙")
|
||||
active = data.get("active", True) # 默认进行中
|
||||
|
||||
if not name:
|
||||
@@ -51,7 +68,7 @@ def api_classes_create():
|
||||
return jsonify({"error": "班级名称已存在", "code": "DUPLICATE_NAME"}), 400
|
||||
|
||||
try:
|
||||
cls = Class(name=name, description=description, active=active)
|
||||
cls = Class(name=name, description=description, teacher_id=teacher_id, level=level, active=active)
|
||||
db.session.add(cls)
|
||||
db.session.commit()
|
||||
return jsonify(cls.to_dict())
|
||||
@@ -78,6 +95,12 @@ def api_classes_update(class_id):
|
||||
if "description" in data:
|
||||
cls.description = data["description"]
|
||||
|
||||
if "teacher_id" in data:
|
||||
cls.teacher_id = data["teacher_id"] if data["teacher_id"] else None
|
||||
|
||||
if "level" in data:
|
||||
cls.level = data["level"]
|
||||
|
||||
if "active" in data:
|
||||
cls.active = data["active"]
|
||||
|
||||
|
||||
+30
-4
@@ -1,13 +1,13 @@
|
||||
from flask import Blueprint, request, jsonify, render_template
|
||||
from app.models import db, Goal
|
||||
from app.routes.auth import login_required_json, admin_required
|
||||
from flask import Blueprint, request, jsonify, render_template, session
|
||||
from app.models import db, Goal, StudentGoal, GoalRelation, StudentGoalEvaluation
|
||||
from app.routes.auth import login_required_json, admin_required, login_required
|
||||
from app.routes import main_bp
|
||||
|
||||
goals_bp = Blueprint("goals", __name__)
|
||||
|
||||
# 目标管理页面路由
|
||||
@main_bp.route("/goals")
|
||||
@admin_required
|
||||
@login_required
|
||||
def goals_page():
|
||||
"""目标管理页面"""
|
||||
return render_template("goals.html", active_nav="goals")
|
||||
@@ -57,7 +57,33 @@ def update_goal(goal_id):
|
||||
@goals_bp.route("/api/goals/<int:goal_id>", methods=["DELETE"])
|
||||
@login_required_json
|
||||
def delete_goal(goal_id):
|
||||
"""删除目标模板(仅管理员,需检查依赖)"""
|
||||
from app.models import User
|
||||
user = User.query.get(session.get("user_id"))
|
||||
if not user or user.role != "admin":
|
||||
return jsonify({"error": "权限不足,仅管理员可操作"}), 403
|
||||
|
||||
goal = Goal.query.get_or_404(goal_id)
|
||||
|
||||
# 检查依赖关系
|
||||
deps = []
|
||||
|
||||
# 1. 检查是否有学员分配了此目标
|
||||
student_goal_count = StudentGoal.query.filter_by(goal_id=goal_id).count()
|
||||
if student_goal_count > 0:
|
||||
deps.append(f"已被 {student_goal_count} 名学员分配")
|
||||
|
||||
# 2. 检查是否有目标关联(作为父目标或子目标)
|
||||
as_parent = GoalRelation.query.filter_by(parent_goal_id=goal_id).count()
|
||||
as_child = GoalRelation.query.filter_by(child_goal_id=goal_id).count()
|
||||
if as_parent > 0:
|
||||
deps.append(f"有 {as_parent} 个子目标关联")
|
||||
if as_child > 0:
|
||||
deps.append(f"作为子目标被 {as_child} 个目标引用")
|
||||
|
||||
if deps:
|
||||
return jsonify({"error": f"无法删除:{', '.join(deps)}"}), 400
|
||||
|
||||
db.session.delete(goal)
|
||||
db.session.commit()
|
||||
return jsonify({"message": "删除成功"})
|
||||
|
||||
+130
-42
@@ -13,7 +13,7 @@ from flask import (
|
||||
session,
|
||||
)
|
||||
from app.routes import main_bp
|
||||
from app.models import db, Student, PracticePlan, StudentProblem
|
||||
from app.models import db, Student, PracticePlan, StudentProblem, StudentGoal
|
||||
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, admin_required
|
||||
@@ -44,8 +44,10 @@ def get_all_plans():
|
||||
- template_id: 模板ID
|
||||
- is_typical: 是否典型 (true/false)
|
||||
- student_name: 学员姓名(模糊匹配)
|
||||
- mine: true/false(我的学员的方案)
|
||||
"""
|
||||
import json as json_module
|
||||
from app.models import Class
|
||||
|
||||
query = PracticePlan.query
|
||||
|
||||
@@ -84,6 +86,13 @@ def get_all_plans():
|
||||
)
|
||||
)
|
||||
|
||||
# 我的学员筛选(所在班级的老师是当前用户)
|
||||
mine = request.args.get('mine')
|
||||
if mine and mine.lower() == 'true':
|
||||
user_id = session.get('user_id')
|
||||
if user_id:
|
||||
query = query.join(Student).join(Class).filter(Class.teacher_id == user_id)
|
||||
|
||||
plans = query.order_by(PracticePlan.created_at.desc()).all()
|
||||
return jsonify([p.to_dict() for p in plans])
|
||||
|
||||
@@ -153,6 +162,10 @@ def generate_plan():
|
||||
student = Student.query.get_or_404(student_id)
|
||||
problems = student.problems.all()
|
||||
|
||||
# 获取学员的目标(只获取未完成的)
|
||||
goals = StudentGoal.query.filter_by(student_id=student_id).all()
|
||||
goal_data = [g.to_dict() for g in goals]
|
||||
|
||||
if not problems:
|
||||
return jsonify({"error": "请先记录学员的问题"}), 400
|
||||
|
||||
@@ -207,6 +220,7 @@ def generate_plan():
|
||||
student_name=student.name,
|
||||
problems=problem_data,
|
||||
practice_time=practice_time,
|
||||
goals=goal_data,
|
||||
)
|
||||
|
||||
yield sse_format(
|
||||
@@ -254,22 +268,27 @@ def generate_plan():
|
||||
)
|
||||
|
||||
# 先用dry_run模式获取提示词并显示给用户
|
||||
prompt, _, error = generate_ai_report(
|
||||
prompt, _, error, extra_info = generate_ai_report(
|
||||
student_name=student.name,
|
||||
wechat_nickname=student.wechat_nickname or "",
|
||||
problems=problem_data,
|
||||
practice_time=practice_time,
|
||||
time_config=time_config,
|
||||
template_id=template_id,
|
||||
dry_run=True
|
||||
dry_run=True,
|
||||
goals=goal_data
|
||||
)
|
||||
|
||||
# 发送提示词给前端显示
|
||||
# 发送提示词给前端显示(只发长度,不发完整内容避免SSE缓冲问题)
|
||||
prompt_length = len(prompt) if prompt else 0
|
||||
yield sse_format({
|
||||
"step": "ai_prompt",
|
||||
"message": "发送给AI的提示词:",
|
||||
"message": "AI提示词已生成",
|
||||
"progress": 60,
|
||||
"detail": prompt if prompt else "无"
|
||||
"prompt_length": prompt_length,
|
||||
"student_problems_length": extra_info.get("student_problems_length", 0) if extra_info else 0,
|
||||
"problems_length": extra_info.get("problems_length", 0) if extra_info else 0,
|
||||
"student_goals_length": extra_info.get("student_goals_length", 0) if extra_info else 0,
|
||||
})
|
||||
|
||||
if error:
|
||||
@@ -288,14 +307,15 @@ def generate_plan():
|
||||
)
|
||||
|
||||
# 真正调用API生成报告
|
||||
_, ai_report, error = generate_ai_report(
|
||||
_, ai_report, error, _ = generate_ai_report(
|
||||
student_name=student.name,
|
||||
wechat_nickname=student.wechat_nickname or "",
|
||||
problems=problem_data,
|
||||
practice_time=practice_time,
|
||||
time_config=time_config,
|
||||
template_id=template_id,
|
||||
dry_run=False
|
||||
dry_run=False,
|
||||
goals=goal_data
|
||||
)
|
||||
|
||||
if error:
|
||||
@@ -375,6 +395,8 @@ def generate_plan():
|
||||
"plan_id": plan.id if plan and hasattr(plan, 'id') else None,
|
||||
"content": plan_content,
|
||||
"ai_report": ai_report,
|
||||
"prompt_length": prompt_length,
|
||||
"ai_report_length": len(ai_report) if ai_report else 0,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -411,6 +433,7 @@ def get_plan(plan_id):
|
||||
"student_id": plan.student_id,
|
||||
"student_name": plan.student.name if plan.student else "",
|
||||
"created_at": plan.created_at.strftime("%Y-%m-%d %H:%M"),
|
||||
"is_typical": plan.is_typical,
|
||||
"content": content,
|
||||
}
|
||||
)
|
||||
@@ -423,14 +446,20 @@ def export_pdf(plan_id):
|
||||
plan = PracticePlan.query.get_or_404(plan_id)
|
||||
content = json.loads(plan.content)
|
||||
student_name = plan.student.name if plan.student else "未知学员"
|
||||
template_id = request.args.get('template_id', type=int)
|
||||
|
||||
# 尝试使用数据库中的报告模板
|
||||
# 获取报告模板
|
||||
report_template = None
|
||||
try:
|
||||
from app.models import Template
|
||||
tmpl = Template.query.filter_by(type="report").first()
|
||||
if tmpl:
|
||||
report_template = tmpl.content
|
||||
if template_id:
|
||||
tmpl = Template.query.get(template_id)
|
||||
if tmpl and tmpl.type == "report":
|
||||
report_template = tmpl.content
|
||||
else:
|
||||
tmpl = Template.query.filter_by(type="report").order_by(Template.sort_order.asc()).first()
|
||||
if tmpl:
|
||||
report_template = tmpl.content
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -442,6 +471,15 @@ def export_pdf(plan_id):
|
||||
rendered_report = rendered_report.replace("{practice_time}", content.get('practice_time', 'N/A'))
|
||||
rendered_report = rendered_report.replace("{total_minutes}", str(content.get('total_daily_minutes', 0)))
|
||||
rendered_report = rendered_report.replace("{generated_at}", content.get('generated_at', ''))
|
||||
# 获取当前用户姓名
|
||||
from app.models import User
|
||||
user_id = session.get('user_id')
|
||||
user_name = '未知'
|
||||
if user_id:
|
||||
user = User.query.get(user_id)
|
||||
if user and user.name:
|
||||
user_name = user.name
|
||||
rendered_report = rendered_report.replace("{generated_by}", user_name)
|
||||
|
||||
if content.get('ai_report'):
|
||||
rendered_report = rendered_report.replace("{ai_report}", content['ai_report'])
|
||||
@@ -453,11 +491,6 @@ def export_pdf(plan_id):
|
||||
problem_tags += f"- **{problem.get('name', '')}** ({problem.get('severity', '')})\n"
|
||||
rendered_report = rendered_report.replace("{problem_tags}", problem_tags or "(无)")
|
||||
|
||||
schedule_table = "| 阶段 | 时长 | 内容 | 目的 |\n|------|------|------|------|\n"
|
||||
for item in content.get('daily_schedule', []):
|
||||
schedule_table += f"| {item.get('phase', '')} | {item.get('duration', '')} | {item.get('content', '')} | {item.get('purpose', '')} |\n"
|
||||
rendered_report = rendered_report.replace("{schedule_table}", schedule_table)
|
||||
|
||||
pdf_path = generate_pdf(
|
||||
plan_id=plan_id,
|
||||
student_name=student_name,
|
||||
@@ -504,6 +537,16 @@ def export_md(plan_id):
|
||||
rendered = rendered.replace("{total_minutes}", str(content.get('total_daily_minutes', 0)))
|
||||
rendered = rendered.replace("{generated_at}", content.get('generated_at', ''))
|
||||
|
||||
# 获取当前用户姓名
|
||||
from app.models import User
|
||||
user_id = session.get('user_id')
|
||||
user_name = '未知'
|
||||
if user_id:
|
||||
user = User.query.get(user_id)
|
||||
if user and user.name:
|
||||
user_name = user.name
|
||||
rendered = rendered.replace("{generated_by}", user_name)
|
||||
|
||||
# AI报告
|
||||
if content.get('ai_report'):
|
||||
rendered = rendered.replace("{ai_report}", content['ai_report'])
|
||||
@@ -516,15 +559,9 @@ def export_md(plan_id):
|
||||
problem_tags += f"- **{problem.get('name', '')}** ({problem.get('severity', '')})\n"
|
||||
rendered = rendered.replace("{problem_tags}", problem_tags or "(无)")
|
||||
|
||||
# 每日计划表格
|
||||
schedule_table = "| 阶段 | 时长 | 内容 | 目的 |\n|------|------|------|------|\n"
|
||||
for item in content.get('daily_schedule', []):
|
||||
schedule_table += f"| {item.get('phase', '')} | {item.get('duration', '')} | {item.get('content', '')} | {item.get('purpose', '')} |\n"
|
||||
rendered = rendered.replace("{schedule_table}", schedule_table)
|
||||
|
||||
md_content = rendered
|
||||
else:
|
||||
# 兜底:生成完整内容
|
||||
# 兜底:生成完整内容(AI报告已包含所有内容)
|
||||
md_lines = [
|
||||
f"# {student_name} - 个性化练习方案\n",
|
||||
f"**练习时间**: {content.get('practice_time', 'N/A')} (共{content.get('total_daily_minutes', 0)}分钟)\n",
|
||||
@@ -533,25 +570,7 @@ def export_md(plan_id):
|
||||
]
|
||||
|
||||
if content.get('ai_report'):
|
||||
md_lines.append("## 📝 AI个性化报告\n")
|
||||
md_lines.append(content['ai_report'])
|
||||
md_lines.append("\n---\n")
|
||||
|
||||
md_lines.append("## 🔍 问题诊断\n")
|
||||
for problem in content.get('problems', []):
|
||||
md_lines.append(f"- **{problem.get('name', '')}** ({problem.get('severity', '')})\n")
|
||||
md_lines.append("\n")
|
||||
|
||||
if content.get('problem_details'):
|
||||
md_lines.append("## 📚 问题详解\n")
|
||||
for detail in content.get('problem_details', []):
|
||||
md_lines.append(f"### {detail.get('name', '')} ({detail.get('severity', '')})\n")
|
||||
md_lines.append(f"{detail.get('content', '')}\n\n")
|
||||
|
||||
md_lines.append("## 📅 每日练习计划\n")
|
||||
md_lines.append("| 阶段 | 时长 | 内容 | 目的 |\n|------|------|------|------|\n")
|
||||
for item in content.get('daily_schedule', []):
|
||||
md_lines.append(f"| {item.get('phase', '')} | {item.get('duration', '')} | {item.get('content', '')} | {item.get('purpose', '')} |\n")
|
||||
|
||||
md_content = ''.join(md_lines)
|
||||
|
||||
@@ -602,3 +621,72 @@ def update_plan_content(plan_id):
|
||||
plan.content = data["content"]
|
||||
db.session.commit()
|
||||
return jsonify({"message": "保存成功"})
|
||||
|
||||
|
||||
@main_bp.route("/api/generate-plan/preview", methods=["POST"])
|
||||
@login_required_json
|
||||
def generate_plan_preview():
|
||||
"""预览AI提示词 - 返回完整提示词内容"""
|
||||
data = request.get_json()
|
||||
student_id = data.get("student_id")
|
||||
template_id = data.get("template_id") # AI提示词模板ID
|
||||
|
||||
student = Student.query.get_or_404(student_id)
|
||||
problems = student.problems.all()
|
||||
|
||||
if not problems:
|
||||
return jsonify({"error": "请先记录学员的问题"}), 400
|
||||
|
||||
# 获取学员的目标
|
||||
goals = StudentGoal.query.filter_by(student_id=student_id).all()
|
||||
goal_data = [g.to_dict() for g in goals]
|
||||
|
||||
# 学员的统一练习时间
|
||||
practice_time = student.practice_time or "30-60分钟"
|
||||
|
||||
problem_data = []
|
||||
for p in problems:
|
||||
problem_obj = p.problem
|
||||
if problem_obj:
|
||||
problem_data.append(
|
||||
{
|
||||
"problem_id": problem_obj.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},
|
||||
"45分钟": {"total": 45, "basic": 15, "tech": 10, "piece": 20},
|
||||
"60分钟": {"total": 60, "basic": 20, "tech": 15, "piece": 25},
|
||||
"90分钟": {"total": 90, "basic": 25, "tech": 25, "piece": 40},
|
||||
"120分钟": {"total": 120, "basic": 30, "tech": 35, "piece": 55},
|
||||
"150分钟以上": {"total": 150, "basic": 35, "tech": 45, "piece": 70},
|
||||
}
|
||||
time_config = time_mapping.get(practice_time, time_mapping["30分钟"])
|
||||
|
||||
# 调用生成器获取提示词(dry_run模式)
|
||||
from app.services.plan_generator import generate_ai_report
|
||||
prompt, _, _, extra_info = generate_ai_report(
|
||||
student_name=student.name,
|
||||
wechat_nickname=student.wechat_nickname or "",
|
||||
problems=problem_data,
|
||||
practice_time=practice_time,
|
||||
time_config=time_config,
|
||||
template_id=template_id,
|
||||
dry_run=True,
|
||||
goals=goal_data
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"prompt": prompt,
|
||||
"prompt_length": len(prompt) if prompt else 0,
|
||||
"student_problems_length": extra_info.get("student_problems_length", 0) if extra_info else 0,
|
||||
"problems_length": extra_info.get("problems_length", 0) if extra_info else 0,
|
||||
"student_goals_length": extra_info.get("student_goals_length", 0) if extra_info else 0,
|
||||
})
|
||||
|
||||
+2
-10
@@ -18,23 +18,15 @@ def get_student_problems(student_id):
|
||||
@main_bp.route("/api/students/<int:student_id>/problems", methods=["POST"])
|
||||
@login_required_json
|
||||
def add_student_problem(student_id):
|
||||
"""为学员添加问题(同步模式:先删再加)"""
|
||||
"""为学员添加问题(增量模式:只添加或更新,不删除已有)"""
|
||||
student = Student.query.get_or_404(student_id)
|
||||
data = request.get_json()
|
||||
|
||||
problems = data.get("problems", [])
|
||||
submitted_ids = set()
|
||||
|
||||
# 删除旧的问题(如果勾选被取消)
|
||||
existing = StudentProblem.query.filter_by(student_id=student_id).all()
|
||||
for e in existing:
|
||||
if e.problem_id not in [p.get("problem_id") for p in problems]:
|
||||
db.session.delete(e)
|
||||
|
||||
# 添加或更新问题
|
||||
# 添加或更新问题(不删除已有的)
|
||||
for p in problems:
|
||||
problem_id = p.get("problem_id") # 这是 problems.id
|
||||
submitted_ids.add(problem_id)
|
||||
|
||||
# 检查是否已存在
|
||||
existing = StudentProblem.query.filter_by(
|
||||
|
||||
+104
-34
@@ -1,30 +1,22 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.models import db, StudentGoal, Goal
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
from app.models import db, StudentGoal, StudentGoalEvaluation, Goal
|
||||
from app.routes.auth import login_required_json
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
student_goals_bp = Blueprint("student_goals", __name__)
|
||||
|
||||
def compute_status(start_date, assessment_date):
|
||||
"""根据日期计算状态"""
|
||||
now = datetime.now()
|
||||
if start_date and now < start_date:
|
||||
return "未开始"
|
||||
elif assessment_date and now > assessment_date:
|
||||
return "已结束"
|
||||
else:
|
||||
return "进行中"
|
||||
|
||||
@student_goals_bp.route("/api/students/<int:student_id>/goals", methods=["GET"])
|
||||
@login_required_json
|
||||
def get_student_goals(student_id):
|
||||
records = StudentGoal.query.filter_by(student_id=student_id).all()
|
||||
|
||||
# 计算状态并排序
|
||||
# 同步状态并排序
|
||||
for r in records:
|
||||
r.sync_status()
|
||||
|
||||
def sort_key(r):
|
||||
status_order = {"进行中": 1, "未开始": 2, "已结束": 3}
|
||||
status = compute_status(r.start_date, r.assessment_date)
|
||||
order = status_order.get(status, 4)
|
||||
status_order = {"进行中": 1, "未开始": 2, "已过期": 3, "已完成": 4}
|
||||
order = status_order.get(r.status, 5)
|
||||
assessment = r.assessment_date if r.assessment_date else datetime.min
|
||||
return (order, -assessment.timestamp() if assessment else 0)
|
||||
|
||||
@@ -79,25 +71,58 @@ def update_student_goal(student_id, goal_id):
|
||||
record = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first_or_404()
|
||||
data = request.get_json()
|
||||
|
||||
if "start_date" in data:
|
||||
if data["start_date"]:
|
||||
record.start_date = datetime.fromisoformat(data["start_date"])
|
||||
# 评估操作
|
||||
if "mastery_level" in data or "comment" in data:
|
||||
is_final = data.get("is_final", False)
|
||||
assessment_date = datetime.now()
|
||||
if data.get("assessment_date"):
|
||||
assessment_date = datetime.fromisoformat(data["assessment_date"])
|
||||
|
||||
# 如果提供了 evaluation_id,则是更新现有评估记录
|
||||
if data.get("evaluation_id"):
|
||||
evaluation = StudentGoalEvaluation.query.get(data["evaluation_id"])
|
||||
if evaluation and evaluation.student_goal_id == record.id:
|
||||
evaluation.mastery_level = data["mastery_level"]
|
||||
evaluation.comment = data.get("comment")
|
||||
evaluation.is_final = is_final
|
||||
evaluation.updated_at = datetime.now()
|
||||
# 如果是最终评估,同步到 StudentGoal
|
||||
if is_final:
|
||||
record.achievement_date = assessment_date
|
||||
record.mastery_level = data["mastery_level"]
|
||||
record.comment = data.get("comment")
|
||||
record.sync_status()
|
||||
else:
|
||||
record.start_date = None
|
||||
if "assessment_date" in data:
|
||||
if data["assessment_date"]:
|
||||
record.assessment_date = datetime.fromisoformat(data["assessment_date"])
|
||||
else:
|
||||
record.assessment_date = None
|
||||
if "mastery_level" in data:
|
||||
record.mastery_level = data["mastery_level"]
|
||||
if "achievement_date" in data:
|
||||
if data["achievement_date"]:
|
||||
record.achievement_date = datetime.fromisoformat(data["achievement_date"])
|
||||
else:
|
||||
record.achievement_date = None
|
||||
if "comment" in data:
|
||||
record.comment = data["comment"]
|
||||
# 创建新评估记录
|
||||
evaluation = StudentGoalEvaluation(
|
||||
student_goal_id=record.id,
|
||||
evaluator_id=None,
|
||||
assessment_date=assessment_date,
|
||||
mastery_level=data["mastery_level"],
|
||||
comment=data.get("comment"),
|
||||
is_final=is_final
|
||||
)
|
||||
db.session.add(evaluation)
|
||||
|
||||
# 如果是最终评估,同步到 StudentGoal
|
||||
if is_final:
|
||||
record.achievement_date = assessment_date
|
||||
record.mastery_level = data["mastery_level"]
|
||||
record.comment = data.get("comment")
|
||||
record.sync_status()
|
||||
else:
|
||||
# 调整操作:只更新日期
|
||||
if "start_date" in data:
|
||||
if data["start_date"]:
|
||||
record.start_date = datetime.fromisoformat(data["start_date"])
|
||||
else:
|
||||
record.start_date = None
|
||||
if "assessment_date" in data:
|
||||
if data["assessment_date"]:
|
||||
record.assessment_date = datetime.fromisoformat(data["assessment_date"])
|
||||
else:
|
||||
record.assessment_date = None
|
||||
record.sync_status()
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(record.to_dict())
|
||||
@@ -105,7 +130,52 @@ def update_student_goal(student_id, goal_id):
|
||||
@student_goals_bp.route("/api/students/<int:student_id>/goals/<int:goal_id>", methods=["DELETE"])
|
||||
@login_required_json
|
||||
def remove_student_goal(student_id, goal_id):
|
||||
"""移除学员的目标分配(仅管理员)"""
|
||||
from app.models import User
|
||||
user = User.query.get(session.get("user_id"))
|
||||
if not user or user.role != "admin":
|
||||
return jsonify({"error": "权限不足,仅管理员可操作"}), 403
|
||||
|
||||
record = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first_or_404()
|
||||
|
||||
# 先删除关联的评估记录
|
||||
StudentGoalEvaluation.query.filter_by(student_goal_id=record.id).delete()
|
||||
|
||||
db.session.delete(record)
|
||||
db.session.commit()
|
||||
return jsonify({"message": "移除成功"})
|
||||
|
||||
@student_goals_bp.route("/api/evaluations/<int:evaluation_id>", methods=["DELETE"])
|
||||
@login_required_json
|
||||
def delete_evaluation(evaluation_id):
|
||||
"""删除评估记录"""
|
||||
evaluation = StudentGoalEvaluation.query.get_or_404(evaluation_id)
|
||||
db.session.delete(evaluation)
|
||||
db.session.commit()
|
||||
return jsonify({"message": "删除成功"})
|
||||
|
||||
|
||||
@student_goals_bp.route("/api/students/<int:student_id>/evaluations", methods=["GET"])
|
||||
@login_required_json
|
||||
def get_student_evaluations(student_id):
|
||||
"""获取学员所有目标的所有评估记录"""
|
||||
evaluations = StudentGoalEvaluation.query.join(StudentGoal).filter(
|
||||
StudentGoal.student_id == student_id
|
||||
).order_by(StudentGoalEvaluation.assessment_date.desc()).all()
|
||||
|
||||
# 补充目标信息
|
||||
result = []
|
||||
for e in evaluations:
|
||||
d = e.to_dict()
|
||||
# 获取关联的目标信息
|
||||
sg = StudentGoal.query.get(e.student_goal_id)
|
||||
if sg:
|
||||
d["goal_name"] = sg.goal.name if sg.goal else None
|
||||
d["goal_level"] = sg.goal.level if sg.goal else None
|
||||
d["student_goal_id"] = sg.id
|
||||
d["student_goal_goal_id"] = sg.goal_id
|
||||
d["goal_start_date"] = sg.start_date.isoformat() if sg.start_date else None
|
||||
d["goal_assessment_date"] = sg.assessment_date.isoformat() if sg.assessment_date else None
|
||||
result.append(d)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
@@ -71,9 +71,10 @@ def students_page():
|
||||
@login_required_json
|
||||
def get_students():
|
||||
"""获取学员列表"""
|
||||
# 支持筛选参数: class_id, name(模糊搜索)
|
||||
# 支持筛选参数: class_id, name(模糊搜索), mine=true/false
|
||||
class_id = request.args.get("class_id")
|
||||
name = request.args.get("name")
|
||||
mine_filter = request.args.get("mine")
|
||||
|
||||
query = Student.query
|
||||
if class_id:
|
||||
@@ -81,6 +82,12 @@ def get_students():
|
||||
if name:
|
||||
query = query.filter(Student.name.contains(name))
|
||||
|
||||
# 我的学员筛选(所在班级的老师是当前用户)
|
||||
if mine_filter and mine_filter.lower() == "true":
|
||||
user_id = session.get("user_id")
|
||||
if user_id:
|
||||
query = query.join(Class, Student.class_id == Class.id).filter(Class.teacher_id == user_id)
|
||||
|
||||
students = query.order_by(Student.created_at.desc()).all()
|
||||
return jsonify([s.to_dict() for s in students])
|
||||
|
||||
|
||||
+13
-74
@@ -2,83 +2,15 @@
|
||||
|
||||
from flask import Blueprint, request, jsonify
|
||||
from app.models import db, Template
|
||||
from app.routes.auth import admin_required
|
||||
from app.routes.auth import admin_required, login_required_json
|
||||
|
||||
templates_bp = Blueprint('templates', __name__, url_prefix='/templates')
|
||||
|
||||
|
||||
# 默认模板
|
||||
DEFAULT_TEMPLATES = {
|
||||
"ai_prompt": {
|
||||
"name": "AI提示词模板",
|
||||
"type": "ai_prompt",
|
||||
"description": "生成练习方案时发送给AI的提示词",
|
||||
"sort_order": 0,
|
||||
"content": """你是一位资深的钢琴教师。请根据学员的具体问题详情,生成一份个性化练习方案报告。
|
||||
|
||||
## 学员基本信息
|
||||
- **姓名**: {student_name}
|
||||
- **微信昵称**: {wechat_nickname}
|
||||
- **每日可练习时间**: {practice_time}
|
||||
|
||||
## 学员被诊断的问题
|
||||
{student_problems}
|
||||
|
||||
## 每个问题的详细信息和练习方法(请务必基于这些内容生成方案)
|
||||
|
||||
{problems}
|
||||
|
||||
## 任务要求
|
||||
请根据上述学员的问题诊断和详细信息,生成一份针对性的练习方案报告:
|
||||
1. 先简述该学员当前存在的主要问题
|
||||
2. 给出一个每日练习安排建议(你可以根据问题特点灵活安排热身、技术练习、曲目练习等环节)
|
||||
3. 针对每个问题给出具体的日常练习方法
|
||||
4. 给出3-5条重点注意事项
|
||||
|
||||
请使用Markdown格式,语言专业、简洁、有鼓励性。""",
|
||||
},
|
||||
"report": {
|
||||
"name": "报告导出模板",
|
||||
"type": "report",
|
||||
"description": "导出方案时使用的Markdown模板",
|
||||
"sort_order": 0,
|
||||
"content": """# 钢琴练习方案 - {student_name}
|
||||
|
||||
**练习时间**: {practice_time} (共{total_minutes}分钟)
|
||||
**生成时间**: {generated_at}
|
||||
|
||||
---
|
||||
|
||||
## AI个性化报告
|
||||
{ai_report}
|
||||
|
||||
## 问题诊断
|
||||
{problem_tags}
|
||||
|
||||
## 每日练习计划
|
||||
{schedule_table}
|
||||
|
||||
---
|
||||
|
||||
*坚持练习 · 必有进步*""",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def init_default_templates():
|
||||
"""初始化默认模板(如果不存在)"""
|
||||
for key, tmpl in DEFAULT_TEMPLATES.items():
|
||||
existing = Template.query.filter_by(name=tmpl["name"]).first()
|
||||
if not existing:
|
||||
t = Template(
|
||||
name=tmpl["name"],
|
||||
type=tmpl["type"],
|
||||
content=tmpl["content"],
|
||||
description=tmpl["description"],
|
||||
sort_order=tmpl.get("sort_order", 0)
|
||||
)
|
||||
db.session.add(t)
|
||||
db.session.commit()
|
||||
# 默认模板从数据库获取,按 sort_order 排序
|
||||
pass # 不再使用代码中的硬编码模板
|
||||
|
||||
|
||||
@templates_bp.route("/")
|
||||
@@ -90,7 +22,7 @@ def templates_page():
|
||||
|
||||
|
||||
@templates_bp.route("/templates", methods=["GET"])
|
||||
@admin_required
|
||||
@login_required_json
|
||||
def get_templates():
|
||||
"""获取所有模板(按sort_order排序,可按type筛选)"""
|
||||
query = Template.query.order_by(Template.sort_order.asc())
|
||||
@@ -151,6 +83,13 @@ def update_template(template_id):
|
||||
def delete_template(template_id):
|
||||
"""删除模板"""
|
||||
tmpl = Template.query.get_or_404(template_id)
|
||||
template_type = tmpl.type
|
||||
|
||||
# 检查该类型模板总数,删除后必须至少剩1个
|
||||
count = Template.query.filter_by(type=template_type).count()
|
||||
if count <= 1:
|
||||
return jsonify({"error": f"无法删除:{template_type}类型至少需要保留1个模板"}), 400
|
||||
|
||||
db.session.delete(tmpl)
|
||||
db.session.commit()
|
||||
return jsonify({"message": "删除成功"})
|
||||
@@ -159,9 +98,9 @@ def delete_template(template_id):
|
||||
@templates_bp.route("/templates/<string:template_type>/render", methods=["POST"])
|
||||
@admin_required
|
||||
def render_template_preview(template_type):
|
||||
"""渲染模板(用于预览)"""
|
||||
"""渲染模板(用于预览)- 使用该类型中 sort_order 最小的模板"""
|
||||
data = request.get_json()
|
||||
tmpl = Template.query.filter_by(type=template_type).first()
|
||||
tmpl = Template.query.filter_by(type=template_type).order_by(Template.sort_order.asc()).first()
|
||||
|
||||
if not tmpl:
|
||||
return jsonify({"error": "模板不存在"}), 404
|
||||
|
||||
+9
-4
@@ -27,11 +27,14 @@ def api_users_create():
|
||||
"""新增用户"""
|
||||
data = request.get_json()
|
||||
username = data.get("username", "").strip()
|
||||
name = data.get("name", "").strip() or None
|
||||
password = data.get("password", "")
|
||||
role = data.get("role", "user")
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"error": "请输入用户名和密码"}), 400
|
||||
if not username:
|
||||
return jsonify({"error": "请输入用户名"}), 400
|
||||
if not password:
|
||||
return jsonify({"error": "请输入密码"}), 400
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
return jsonify({"error": "用户名已存在"}), 400
|
||||
@@ -40,7 +43,7 @@ def api_users_create():
|
||||
return jsonify({"error": "无效的角色"}), 400
|
||||
|
||||
try:
|
||||
user = User(username=username, role=role)
|
||||
user = User(username=username, name=name, role=role)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
@@ -55,13 +58,15 @@ def api_users_create():
|
||||
@main_bp.route("/api/users/<int:user_id>", methods=["PUT"])
|
||||
@admin_required
|
||||
def api_users_update(user_id):
|
||||
"""编辑用户(仅管理员可改角色)"""
|
||||
"""编辑用户"""
|
||||
user = User.query.get_or_404(user_id)
|
||||
data = request.get_json()
|
||||
|
||||
if "role" in data:
|
||||
if data["role"] in ["admin", "user"]:
|
||||
user.role = data["role"]
|
||||
if "name" in data:
|
||||
user.name = data["name"].strip() or None
|
||||
|
||||
try:
|
||||
db.session.commit()
|
||||
|
||||
Reference in New Issue
Block a user