Files
piano-plan/app/models.py
T
hmo 18351212e8 feat: 问题数据迁移到数据库;学员详情页URL导航改造;侧边栏统一
- 问题从文件系统迁移到数据库 problems 表
- 移除 PROBLEMS_DIR 配置和文件读取逻辑
- student.html 完整重写:编辑/添加/删除问题,生成方案进度显示
- 学员详情页支持独立URL访问 (/student/<id>)
- 统一侧边栏到 base.html
- 更新文档:DEPLOYMENT_SOP, MODELS, STRUCTURE, FRONTEND_ARCH
- 部署到生产环境 v1.2.0
2026-04-23 06:35:32 +08:00

302 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据库模型定义
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import re
db = SQLAlchemy()
class User(db.Model):
"""管理员用户表"""
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password_hash = db.Column(db.String(200), nullable=False)
role = db.Column(db.String(20), default="user") # admin / user
created_at = db.Column(db.DateTime, default=datetime.now)
def set_password(self, password):
"""设置密码(验证复杂度后hash"""
if not self.validate_password(password):
raise ValueError("密码不符合要求")
import hashlib
self.password_hash = hashlib.sha256(password.encode()).hexdigest()
def check_password(self, password):
"""验证密码"""
import hashlib
return self.password_hash == hashlib.sha256(password.encode()).hexdigest()
@staticmethod
def validate_password(password):
"""验证密码复杂度:大写+小写+数字+特殊字符,8位以上"""
if len(password) < 8:
return False
if not re.search(r"[A-Z]", password):
return False
if not re.search(r"[a-z]", password):
return False
if not re.search(r"[0-9]", password):
return False
if not re.search(r"['!@#$%^&*(),.?\":{}|<>\-_]", password):
return False
return True
def to_dict(self):
return {
"id": self.id,
"username": self.username,
"role": self.role,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M")
if self.created_at
else None,
}
class Class(db.Model):
"""班级表"""
__tablename__ = "classes"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
active = db.Column(db.Boolean, default=True) # 进行中
created_at = db.Column(db.DateTime, default=datetime.now)
# 关联 - 在Student模型中定义backref
def to_dict(self):
# 直接查询学员数量,避免relationship问题
from app.models import Student
student_count = Student.query.filter_by(class_id=self.id).count()
return {
"id": self.id,
"name": self.name,
"description": self.description,
"active": self.active if self.active is not None else True,
"student_count": student_count,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M")
if self.created_at
else None,
}
class Student(db.Model):
"""学员表"""
__tablename__ = "students"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
phone = db.Column(db.String(20))
wechat_nickname = db.Column(db.String(100)) # 微信昵称
practice_time = db.Column(db.String(20), default="30分钟") # 每日练习时间
notes = db.Column(db.Text) # 备注
class_id = db.Column(db.Integer, db.ForeignKey("classes.id")) # 班级
created_at = db.Column(db.DateTime, default=datetime.now)
# 关联
problems = db.relationship(
"StudentProblem",
backref="student",
lazy="dynamic",
cascade="all, delete-orphan",
)
plans = db.relationship(
"PracticePlan", backref="student", lazy="dynamic", cascade="all, delete-orphan"
)
class_obj = db.relationship("Class", backref="students")
def to_dict(self):
# 获取问题列表,按严重程度排序(严重 > 中等 > 轻微)
severity_order = {"严重": 0, "中等": 1, "轻微": 2}
problems_list = sorted(
self.problems.all(),
key=lambda p: (severity_order.get(p.severity, 1), p.created_at)
)
# 通过关联获取问题名称
problem_names = [p.problem.name if p.problem else p.problem_name for p in problems_list]
return {
"id": self.id,
"name": self.name,
"phone": self.phone,
"wechat_nickname": self.wechat_nickname,
"practice_time": self.practice_time,
"notes": self.notes,
"class_id": self.class_id,
"class_name": self.class_obj.name if self.class_obj else None,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M")
if self.created_at
else None,
"problem_count": self.problems.count(),
"problem_names": problem_names, # 问题名称列表(按严重程度排序)
"plan_count": self.plans.count(),
}
class Problem(db.Model):
"""问题表"""
__tablename__ = "problems"
id = db.Column(db.Integer, primary_key=True)
no = db.Column(db.String(10), unique=True, nullable=False) # 编号:01, 02...
name = db.Column(db.String(100), nullable=False) # 问题名称
category = db.Column(db.String(50), default="技术类") # 分类
content = db.Column(db.Text) # 问题详细内容
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
def to_dict(self):
return {
"id": self.id,
"no": self.no,
"name": self.name,
"category": self.category,
}
class StudentProblem(db.Model):
"""学员问题记录表"""
__tablename__ = "student_problems"
id = db.Column(db.Integer, primary_key=True)
student_id = db.Column(db.Integer, db.ForeignKey("students.id"), nullable=False)
problem_id = db.Column(db.Integer, db.ForeignKey("problems.id"), nullable=False) # 外键
severity = db.Column(db.String(10), nullable=False) # 轻微/中等/严重
level = db.Column(db.String(20)) # 启蒙/入门/进阶/熟练/精通
created_at = db.Column(db.DateTime, default=datetime.now)
# 关联到 Problem
problem = db.relationship("Problem", foreign_keys=[problem_id])
def to_dict(self):
return {
"id": self.id,
"student_id": self.student_id,
"problem_id": self.problem_id, # 外键关联到 problems.id
"problem_name": self.problem.name if self.problem else None,
"problem_no": self.problem.no if self.problem else None,
"severity": self.severity,
"level": self.level,
}
class PracticePlan(db.Model):
"""练习方案表"""
__tablename__ = "practice_plans"
id = db.Column(db.Integer, primary_key=True)
student_id = db.Column(db.Integer, db.ForeignKey("students.id"), nullable=False)
template_id = db.Column(db.Integer, db.ForeignKey("templates.id"), nullable=True) # 使用的AI提示词模板
is_typical = db.Column(db.Boolean, default=False, nullable=False) # 是否为典型方案
content = db.Column(db.Text, nullable=False) # JSON格式存储方案内容
created_at = db.Column(db.DateTime, default=datetime.now)
# 关联
template = db.relationship("Template", foreign_keys=[template_id])
def to_dict(self):
import json as json_module
content_obj = {}
try:
content_obj = json_module.loads(self.content) if self.content else {}
except:
pass
# 从 content 中提取问题列表
problems = content_obj.get("problems", [])
problem_names = [p.get("name", "") for p in problems] if problems else []
# 获取模板名称
template_name = self.template.name if self.template else None
# 获取学员班级
class_name = None
if self.student and self.student.class_obj:
class_name = self.student.class_obj.name
return {
"id": self.id,
"student_id": self.student_id,
"student_name": self.student.name if self.student else "",
"class_name": class_name,
"template_id": self.template_id,
"template_name": template_name,
"is_typical": self.is_typical,
"problem_names": problem_names,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M")
if self.created_at
else None,
"content": self.content,
}
class Template(db.Model):
"""模板配置表"""
__tablename__ = "templates"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False) # 模板名称
type = db.Column(db.String(20), nullable=False) # ai_prompt / report
content = db.Column(db.Text, nullable=False) # 模板内容
description = db.Column(db.String(200)) # 模板描述
sort_order = db.Column(db.Integer, default=0) # 排序,数字越小越靠前
created_at = db.Column(db.DateTime, default=datetime.now)
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"type": self.type,
"content": self.content,
"description": self.description,
"sort_order": self.sort_order,
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M") if self.created_at else None,
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M") if self.updated_at else None,
}
# 问题配置(从文件读取)
PROBLEM_LIST = [
{"id": "01_手小", "name": "手小", "category": "技术类(生理限制)"},
{"id": "02_识谱慢", "name": "识谱慢", "category": "识谱类"},
{"id": "03_节奏感差", "name": "节奏感差", "category": "综合类"},
{"id": "04_压手腕", "name": "压手腕", "category": "技术类(手型)"},
{"id": "05_掌关节支撑差", "name": "掌关节支撑差", "category": "技术类(手型)"},
{"id": "06_第一关节支撑差", "name": "第一关节支撑差", "category": "技术类(手型)"},
{"id": "07_对键盘不熟悉", "name": "对键盘不熟悉", "category": "识谱类"},
{"id": "08_手指僵硬_紧张", "name": "手指僵硬、紧张", "category": "技术类(手型)"},
{"id": "09_手指不会跑动", "name": "手指不会跑动", "category": "技术类"},
{"id": "10_力度不会把握", "name": "力度不会把握", "category": "技术类"},
{"id": "11_左右手不协调", "name": "左右手不协调", "category": "综合类"},
{"id": "12_不会用节拍器", "name": "不会用节拍器", "category": "识谱类"},
{"id": "13_不会编配指法", "name": "不会编配指法", "category": "综合类"},
{"id": "14_基本功练习", "name": "基本功练习", "category": "综合类"},
{"id": "15_练习缺乏监督", "name": "练习缺乏监督", "category": "综合类"},
]
SEVERITY_LEVELS = ["轻微", "中等", "严重"]
LEVEL_OPTIONS = ["启蒙", "入门", "进阶", "熟练", "精通"]
PRACTICE_TIME_OPTIONS = [
"15分钟",
"30分钟",
"45分钟",
"60分钟",
"90分钟",
"120分钟",
"150分钟以上",
]
ROLE_OPTIONS = ["admin", "user"]