更新:models/routes/services/templates/docs

This commit is contained in:
hmo
2026-04-26 18:02:36 +08:00
parent f7a82ac48a
commit 6abdd49c04
31 changed files with 1480 additions and 676 deletions
+26 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+8 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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()