467 lines
19 KiB
Python
467 lines
19 KiB
Python
# 数据库模型定义
|
||
|
||
from flask_sqlalchemy import SQLAlchemy
|
||
from datetime import datetime
|
||
import re
|
||
|
||
db = SQLAlchemy()
|
||
|
||
# 问题和目标的统一分类体系
|
||
ITEM_CATEGORIES = ['综合', '乐理相关', '演奏能力', '其他']
|
||
|
||
|
||
class User(db.Model):
|
||
"""管理员用户表"""
|
||
|
||
__tablename__ = "users"
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
username = db.Column(db.String(50), unique=True, nullable=False)
|
||
name = db.Column(db.String(50), nullable=True) # 姓名
|
||
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,
|
||
"name": self.name,
|
||
"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)
|
||
teacher_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) # 班主任/老师
|
||
level = db.Column(db.String(20), default="启蒙") # 班级级别:启蒙/入门/进阶/熟练/精通
|
||
active = db.Column(db.Boolean, default=True) # 进行中
|
||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||
|
||
# 关联
|
||
teacher = db.relationship("User", foreign_keys=[teacher_id])
|
||
|
||
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,
|
||
"teacher_id": self.teacher_id,
|
||
"level": self.level or "启蒙",
|
||
"teacher_name": self.teacher.name if self.teacher else None,
|
||
"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]
|
||
|
||
# 获取目标统计
|
||
goal_count = len(self.goal_records) if self.goal_records else 0
|
||
completed_goal_count = sum(1 for g in self.goal_records if g.achievement_date) if self.goal_records else 0
|
||
|
||
# 获取最新方案ID
|
||
latest_plan = self.plans.order_by(db.desc("created_at")).first()
|
||
latest_plan_id = latest_plan.id if latest_plan else None
|
||
|
||
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(),
|
||
"latest_plan_id": latest_plan_id,
|
||
"goal_count": goal_count,
|
||
"completed_goal_count": completed_goal_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(20), 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 or "综合",
|
||
}
|
||
|
||
|
||
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,
|
||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||
}
|
||
|
||
|
||
class Goal(db.Model):
|
||
"""目标表"""
|
||
__tablename__ = "goals"
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
name = db.Column(db.String(100), nullable=False)
|
||
content = db.Column(db.Text)
|
||
level = db.Column(db.String(20), default="入门") # 启蒙/入门/进阶/熟练/精通
|
||
category = db.Column(db.String(20), default="综合") # 分类:综合/乐理相关/演奏能力/其他
|
||
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,
|
||
"content": self.content,
|
||
"level": self.level,
|
||
"category": self.category or "综合",
|
||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||
}
|
||
|
||
|
||
class GoalRelation(db.Model):
|
||
"""目标关联表 - 自关联多对多"""
|
||
__tablename__ = "goal_relations"
|
||
|
||
parent_goal_id = db.Column(db.Integer, db.ForeignKey("goals.id"), primary_key=True)
|
||
child_goal_id = db.Column(db.Integer, db.ForeignKey("goals.id"), primary_key=True)
|
||
|
||
parent = db.relationship("Goal", foreign_keys=[parent_goal_id], backref="child_relations")
|
||
child = db.relationship("Goal", foreign_keys=[child_goal_id], backref="parent_relations")
|
||
|
||
|
||
class StudentGoal(db.Model):
|
||
"""学员目标记录表"""
|
||
__tablename__ = "student_goals"
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
student_id = db.Column(db.Integer, db.ForeignKey("students.id"), nullable=False)
|
||
goal_id = db.Column(db.Integer, db.ForeignKey("goals.id"), nullable=False)
|
||
start_date = db.Column(db.DateTime) # 开始日期
|
||
assessment_date = db.Column(db.DateTime) # 计划评估日期
|
||
status = db.Column(db.String(20), default="进行中") # 状态:未开始/进行中/已完成/已过期
|
||
mastery_level = db.Column(db.Integer, nullable=True) # 掌握程度(来自最新评估)
|
||
achievement_date = db.Column(db.DateTime, nullable=True) # 达成日期(来自最终评估)
|
||
comment = db.Column(db.Text, nullable=True) # 评语(来自最新评估)
|
||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||
|
||
student = db.relationship("Student", backref="goal_records")
|
||
goal = db.relationship("Goal")
|
||
|
||
def compute_status(self):
|
||
"""根据日期动态计算状态"""
|
||
from datetime import datetime
|
||
now = datetime.now()
|
||
if self.start_date and now < self.start_date:
|
||
return "未开始"
|
||
elif self.achievement_date:
|
||
return "已完成"
|
||
elif self.assessment_date and now > self.assessment_date:
|
||
return "已过期"
|
||
else:
|
||
return "进行中"
|
||
|
||
def sync_status(self):
|
||
"""同步状态到数据库"""
|
||
self.status = self.compute_status()
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"student_id": self.student_id,
|
||
"goal_id": self.goal_id,
|
||
"goal_name": self.goal.name if self.goal else None,
|
||
"goal_content": self.goal.content if self.goal else None,
|
||
"goal_level": self.goal.level if self.goal else None,
|
||
"goal_category": self.goal.category if self.goal else None,
|
||
"status": self.status,
|
||
"start_date": self.start_date.isoformat() if self.start_date else None,
|
||
"assessment_date": self.assessment_date.isoformat() if self.assessment_date else None,
|
||
"mastery_level": self.mastery_level,
|
||
"achievement_date": self.achievement_date.isoformat() if self.achievement_date else None,
|
||
"comment": self.comment,
|
||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||
}
|
||
|
||
|
||
class StudentGoalEvaluation(db.Model):
|
||
"""学员目标评估记录表"""
|
||
__tablename__ = "student_goal_evaluations"
|
||
|
||
id = db.Column(db.Integer, primary_key=True)
|
||
student_goal_id = db.Column(db.Integer, db.ForeignKey("student_goals.id"), nullable=False)
|
||
evaluator_id = db.Column(db.Integer, nullable=True) # 评估人(暂时用 nullable)
|
||
assessment_date = db.Column(db.DateTime, default=datetime.now) # 评估日期
|
||
mastery_level = db.Column(db.Integer, nullable=False) # 掌握程度 1-5
|
||
comment = db.Column(db.Text, nullable=True) # 评语
|
||
is_final = db.Column(db.Boolean, default=False) # 是否最终评估
|
||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||
|
||
student_goal = db.relationship("StudentGoal", backref="evaluations")
|
||
|
||
def to_dict(self):
|
||
return {
|
||
"id": self.id,
|
||
"student_goal_id": self.student_goal_id,
|
||
"evaluator_id": self.evaluator_id,
|
||
"assessment_date": self.assessment_date.isoformat() if self.assessment_date else None,
|
||
"mastery_level": self.mastery_level,
|
||
"comment": self.comment,
|
||
"is_final": self.is_final,
|
||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||
}
|
||
|
||
|
||
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_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) # 创建人
|
||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||
updated_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) # 更新人
|
||
updated_at = db.Column(db.DateTime, nullable=True) # 更新人,只在更新时设置
|
||
|
||
# 关联
|
||
template = db.relationship("Template", foreign_keys=[template_id])
|
||
creator = db.relationship("User", foreign_keys=[created_by])
|
||
updater = db.relationship("User", foreign_keys=[updated_by])
|
||
|
||
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 中提取问题列表(含级别和严重程度)
|
||
# 兼容旧数据:旧数据用 problem_name,新数据用 name
|
||
problems = content_obj.get("problems", [])
|
||
problem_details = [
|
||
{"name": p.get("name") or p.get("problem_name", ""), "level": p.get("level", ""), "severity": p.get("severity", "中等")}
|
||
for p in problems
|
||
] if problems else []
|
||
problem_names = [p["name"] for p in problem_details]
|
||
|
||
# 获取模板名称
|
||
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
|
||
|
||
# 采纳来源信息
|
||
adopted_from = content_obj.get("adopted_from")
|
||
|
||
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,
|
||
"problem_details": problem_details,
|
||
"created_by": self.creator.name if self.creator else None,
|
||
"created_at": self.created_at.strftime("%Y-%m-%d %H:%M")
|
||
if self.created_at
|
||
else None,
|
||
"updated_by": self.updater.name if self.updater else None,
|
||
"updated_at": self.updated_at.strftime("%Y-%m-%d %H:%M")
|
||
if self.updated_at
|
||
else None,
|
||
"content": self.content,
|
||
"adopted_from": adopted_from,
|
||
}
|
||
|
||
|
||
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"]
|