feat: 初始提交 v1.2.0 - 钢琴练习方案生成系统
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
# 学员管理路由
|
||||
|
||||
import csv
|
||||
import io
|
||||
from flask import (
|
||||
request,
|
||||
jsonify,
|
||||
render_template,
|
||||
session,
|
||||
redirect,
|
||||
url_for,
|
||||
Response,
|
||||
)
|
||||
from app.routes import main_bp
|
||||
from app.models import (
|
||||
db,
|
||||
Student,
|
||||
PROBLEM_LIST,
|
||||
SEVERITY_LEVELS,
|
||||
PRACTICE_TIME_OPTIONS,
|
||||
User,
|
||||
Class,
|
||||
)
|
||||
from app.routes.auth import login_required_json, check_login
|
||||
|
||||
|
||||
@main_bp.route("/")
|
||||
def index():
|
||||
"""首页 - 学员列表"""
|
||||
if not check_login():
|
||||
return redirect("/login")
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
problem_list=PROBLEM_LIST,
|
||||
severity_levels=SEVERITY_LEVELS,
|
||||
practice_time_options=PRACTICE_TIME_OPTIONS,
|
||||
)
|
||||
|
||||
|
||||
@main_bp.route("/api/students", methods=["GET"])
|
||||
@login_required_json
|
||||
def get_students():
|
||||
"""获取学员列表"""
|
||||
# 支持筛选参数: class_id, name(模糊搜索)
|
||||
class_id = request.args.get("class_id")
|
||||
name = request.args.get("name")
|
||||
|
||||
query = Student.query
|
||||
if class_id:
|
||||
query = query.filter(Student.class_id == int(class_id))
|
||||
if name:
|
||||
query = query.filter(Student.name.contains(name))
|
||||
|
||||
students = query.order_by(Student.created_at.desc()).all()
|
||||
return jsonify([s.to_dict() for s in students])
|
||||
|
||||
|
||||
@main_bp.route("/api/students", methods=["POST"])
|
||||
@login_required_json
|
||||
def create_student():
|
||||
"""创建新学员"""
|
||||
data = request.get_json()
|
||||
student = Student(
|
||||
name=data.get("name"),
|
||||
phone=data.get("phone", ""),
|
||||
wechat_nickname=data.get("wechat_nickname", ""),
|
||||
practice_time=data.get("practice_time", "30分钟"),
|
||||
notes=data.get("notes", ""),
|
||||
class_id=data.get("class_id"),
|
||||
)
|
||||
db.session.add(student)
|
||||
db.session.commit()
|
||||
return jsonify(student.to_dict())
|
||||
|
||||
|
||||
@main_bp.route("/api/students/<int:student_id>", methods=["GET"])
|
||||
@login_required_json
|
||||
def get_student(student_id):
|
||||
"""获取学员详情"""
|
||||
student = Student.query.get_or_404(student_id)
|
||||
problems = student.problems.all()
|
||||
plans = student.plans.order_by(db.desc("created_at")).all()
|
||||
|
||||
# 获取班级名称
|
||||
class_name = None
|
||||
if student.class_id:
|
||||
cls = Class.query.get(student.class_id)
|
||||
if cls:
|
||||
class_name = cls.name
|
||||
|
||||
student_dict = student.to_dict()
|
||||
student_dict["class_name"] = class_name
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"student": student_dict,
|
||||
"problems": [p.to_dict() for p in problems],
|
||||
"plans": [p.to_dict() for p in plans],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@main_bp.route("/api/students/<int:student_id>", methods=["PUT"])
|
||||
@login_required_json
|
||||
def update_student(student_id):
|
||||
"""更新学员信息"""
|
||||
student = Student.query.get_or_404(student_id)
|
||||
data = request.get_json()
|
||||
|
||||
if "name" in data:
|
||||
student.name = data["name"]
|
||||
if "phone" in data:
|
||||
student.phone = data["phone"]
|
||||
if "wechat_nickname" in data:
|
||||
student.wechat_nickname = data["wechat_nickname"]
|
||||
if "practice_time" in data:
|
||||
student.practice_time = data["practice_time"]
|
||||
if "notes" in data:
|
||||
student.notes = data["notes"]
|
||||
if "class_id" in data:
|
||||
student.class_id = data["class_id"]
|
||||
|
||||
db.session.commit()
|
||||
return jsonify(student.to_dict())
|
||||
|
||||
|
||||
@main_bp.route("/api/students/<int:student_id>", methods=["DELETE"])
|
||||
@login_required_json
|
||||
def delete_student(student_id):
|
||||
"""删除学员"""
|
||||
student = Student.query.get_or_404(student_id)
|
||||
db.session.delete(student)
|
||||
db.session.commit()
|
||||
return jsonify({"message": "删除成功"})
|
||||
|
||||
|
||||
@main_bp.route("/api/students/template")
|
||||
@login_required_json
|
||||
def download_student_template():
|
||||
"""下载CSV导入模板"""
|
||||
headers = ["ID", "姓名", "电话", "微信昵称", "每日练习时间", "班级", "备注"]
|
||||
# 练习时间选项
|
||||
practice_times = PRACTICE_TIME_OPTIONS
|
||||
# 示例数据(ID留空,表示新增)
|
||||
example = ["", "张三", "13800138000", "昵称", "30分钟", "班级A", "备注信息"]
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(headers)
|
||||
writer.writerow(example)
|
||||
|
||||
# 添加22行空行,让可选值出现在第25-26行
|
||||
for _ in range(22):
|
||||
writer.writerow([])
|
||||
|
||||
# 获取进行中的班级列表
|
||||
active_classes = Class.query.filter_by(active=True).all()
|
||||
class_names = [c.name for c in active_classes] if active_classes else []
|
||||
|
||||
# 添加可选练习时间和可选班级说明(第25-26行)
|
||||
writer.writerow(["可选练习时间:"] + list(practice_times))
|
||||
if class_names:
|
||||
writer.writerow(["可选班级:"] + class_names)
|
||||
else:
|
||||
writer.writerow(["可选班级:无进行中班级"])
|
||||
|
||||
# 使用BytesIO处理编码
|
||||
csv_content = output.getvalue().encode("utf-8-sig")
|
||||
return Response(
|
||||
csv_content,
|
||||
mimetype="text/csv; charset=utf-8-sig",
|
||||
headers={"Content-Disposition": "attachment; filename=student_template.csv"},
|
||||
)
|
||||
|
||||
|
||||
@main_bp.route("/api/students/export")
|
||||
@login_required_json
|
||||
def export_students():
|
||||
"""导出学员CSV"""
|
||||
# 获取所有学员
|
||||
students = Student.query.order_by(Student.created_at.desc()).all()
|
||||
|
||||
# 班级ID到名称映射
|
||||
class_map = {c.id: c.name for c in Class.query.all()}
|
||||
|
||||
headers = ["ID", "姓名", "电话", "微信昵称", "每日练习时间", "班级", "备注"]
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(headers)
|
||||
|
||||
for s in students:
|
||||
class_name = class_map.get(s.class_id, "") if s.class_id else ""
|
||||
writer.writerow(
|
||||
[
|
||||
s.id,
|
||||
s.name,
|
||||
s.phone or "",
|
||||
s.wechat_nickname or "",
|
||||
s.practice_time,
|
||||
class_name,
|
||||
s.notes or "",
|
||||
]
|
||||
)
|
||||
|
||||
csv_content = output.getvalue().encode("utf-8-sig")
|
||||
return Response(
|
||||
csv_content,
|
||||
mimetype="text/csv; charset=utf-8-sig",
|
||||
headers={"Content-Disposition": "attachment; filename=students_export.csv"},
|
||||
)
|
||||
|
||||
|
||||
@main_bp.route("/api/students/import", methods=["POST"])
|
||||
@login_required_json
|
||||
def import_students():
|
||||
"""导入学员CSV"""
|
||||
if "file" not in request.files:
|
||||
return jsonify({"error": "请选择文件"}), 400
|
||||
|
||||
file = request.files["file"]
|
||||
if file.filename == "":
|
||||
return jsonify({"error": "请选择文件"}), 400
|
||||
|
||||
if not file.filename.endswith(".csv"):
|
||||
return jsonify({"error": "请上传CSV文件"}), 400
|
||||
|
||||
try:
|
||||
raw_content = file.read()
|
||||
# 尝试多种编码
|
||||
try:
|
||||
content = raw_content.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
try:
|
||||
content = raw_content.decode("gbk")
|
||||
except UnicodeDecodeError:
|
||||
content = raw_content.decode("utf-8", errors="ignore")
|
||||
|
||||
reader = csv.DictReader(io.StringIO(content))
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
errors = []
|
||||
|
||||
# 获取班级名称到ID的映射
|
||||
class_map = {c.name: c.id for c in Class.query.all()}
|
||||
# 练习时间列表
|
||||
practice_time_list = PRACTICE_TIME_OPTIONS
|
||||
|
||||
for row_num, row in enumerate(reader, start=2):
|
||||
# 检查是否为空白行(所有字段都为空)
|
||||
is_empty = all(not (v and v.strip()) for v in row.values())
|
||||
if is_empty:
|
||||
# 遇到空行后停止导入(空行后是可选值说明)
|
||||
break
|
||||
|
||||
# 跳过说明行
|
||||
if row.get("姓名", "").strip() in ["可选练习时间:", "可选班级:"]:
|
||||
continue
|
||||
|
||||
try:
|
||||
name = row.get("姓名", "").strip()
|
||||
phone = row.get("电话", "").strip()
|
||||
wechat_nickname = row.get("微信昵称", "").strip()
|
||||
practice_time = row.get("每日练习时间", "30分钟").strip()
|
||||
class_name = row.get("班级", "").strip()
|
||||
notes = row.get("备注", "").strip()
|
||||
|
||||
if not name:
|
||||
continue
|
||||
|
||||
# 验证并转换练习时间
|
||||
if practice_time not in practice_time_list:
|
||||
practice_time = "30分钟"
|
||||
|
||||
# 查找班级ID
|
||||
class_id = None
|
||||
if class_name:
|
||||
class_id = class_map.get(class_name)
|
||||
|
||||
# 优先用ID查找,否则用姓名+电话
|
||||
existing = None
|
||||
if row.get("ID", "").strip():
|
||||
try:
|
||||
student_id = int(row.get("ID", "").strip())
|
||||
existing = Student.query.get(student_id)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 如果没找到,尝试用姓名+电话
|
||||
if not existing:
|
||||
existing = Student.query.filter_by(
|
||||
name=name, phone=phone if phone else None
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# 更新已存在的学员(所有字段都允许更新)
|
||||
existing.name = name
|
||||
existing.phone = phone
|
||||
existing.wechat_nickname = wechat_nickname
|
||||
existing.practice_time = practice_time
|
||||
existing.class_id = class_id
|
||||
existing.notes = notes
|
||||
else:
|
||||
# 创建新学员
|
||||
student = Student(
|
||||
name=name,
|
||||
phone=phone,
|
||||
wechat_nickname=wechat_nickname,
|
||||
practice_time=practice_time,
|
||||
notes=notes,
|
||||
class_id=class_id,
|
||||
)
|
||||
db.session.add(student)
|
||||
|
||||
success_count += 1
|
||||
|
||||
except Exception as e:
|
||||
error_count += 1
|
||||
errors.append(f"第{row_num}行: {str(e)}")
|
||||
|
||||
db.session.commit()
|
||||
|
||||
result = {"message": f"导入成功: {success_count}条"}
|
||||
if error_count > 0:
|
||||
result["error_details"] = errors[:10] # 只返回前10条错误
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"导入失败: {str(e)}"}), 500
|
||||
Reference in New Issue
Block a user