# 用户管理与班级管理实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 扩展钢琴练习方案系统,增加用户管理、角色权限管理、班级管理功能 **Architecture:** 在现有 User 模型上增加 role 字段,新建 Class 表,Student 表增加 class_id 外键。前端动态渲染导航栏,后端通过装饰器实现权限控制。 **Tech Stack:** Flask + SQLAlchemy + SQLite + Bootstrap5 --- ## 文件修改概览 | 操作 | 文件 | 说明 | |------|------|------| | 修改 | `app/models.py` | User 增加 role 字段,新建 Class 模型,Student 增加 class_id | | 修改 | `app/routes/auth.py` | 增加权限装饰器,修改密码API | | 新建 | `app/routes/classes.py` | 班级管理 API | | 新建 | `app/routes/users.py` | 用户管理 API | | 修改 | `app/templates/index.html` | 导航栏动态渲染,学员列表增加班级列 | | 新建 | `app/templates/users.html` | 用户管理页面 | | 新建 | `app/templates/classes.html` | 班级管理页面 | | 修改 | `app/templates/login.html` | 登录后返回 role 信息 | --- ## Task 1: 数据库模型扩展 **Files:** - Modify: `app/models.py:1-180` - [ ] **Step 1: 修改 User 模型,添加 role 字段** 在 `app/models.py` 中找到 `class User` 定义,在 `created_at` 字段后添加 `role` 字段: ```python 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) ``` - [ ] **Step 2: 在 User.to_dict() 中添加 role 字段** ```python 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, } ``` - [ ] **Step 3: 新建 Class 模型** 在 `User` 类之后、`Student` 类之前添加: ```python 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) created_at = db.Column(db.DateTime, default=datetime.now) def to_dict(self): return { "id": self.id, "name": self.name, "description": self.description, "student_count": self.students.count(), "created_at": self.created_at.strftime("%Y-%m-%d %H:%M") if self.created_at else None, } ``` - [ ] **Step 4: 修改 Student 模型,添加 class_id 字段** 在 `Student` 类中找到 `notes` 字段后添加: ```python class_id = db.Column(db.Integer, db.ForeignKey("classes.id")) # 班级 ``` - [ ] **Step 5: 更新 Student.to_dict() 添加班级信息** ```python def to_dict(self): 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(), "plan_count": self.plans.count(), } ``` - [ ] **Step 6: 添加 ROLE_OPTIONS 常量** 在文件末尾添加: ```python ROLE_OPTIONS = ["admin", "user"] ``` --- ## Task 2: 权限装饰器 **Files:** - Modify: `app/routes/auth.py:94-105` - [ ] **Step 1: 修改登录状态检查,返回 role 信息** 修改 `check_login` 函数: ```python @main_bp.route("/api/check-login", methods=["GET"]) def check_login(): """检查登录状态""" if session.get("user_id"): user = User.query.get(session.get("user_id")) return jsonify({ "logged_in": True, "username": session.get("username"), "role": user.role if user else "user" }) return jsonify({"logged_in": False}) ``` - [ ] **Step 2: 添加 admin_required 装饰器** 在文件末尾添加: ```python def admin_required(f): """管理员权限装饰器""" from functools import wraps @wraps(f) def decorated(*args, **kwargs): if not session.get("user_id"): return jsonify({"error": "请先登录"}), 401 user = User.query.get(session.get("user_id")) if not user or user.role != "admin": return jsonify({"error": "权限不足"}), 403 return f(*args, **kwargs) return decorated def login_required_json(f): """登录验证装饰器(返回JSON)""" from functools import wraps @wraps(f) def decorated(*args, **kwargs): if not session.get("user_id"): return jsonify({"error": "请先登录"}), 401 return f(*args, **kwargs) return decorated ``` - [ ] **Step 3: 添加获取当前用户角色的函数** 在文件末尾添加: ```python def get_current_user(): """获取当前登录用户""" user_id = session.get("user_id") if user_id: return User.query.get(user_id) return None def is_admin(): """判断当前用户是否是管理员""" user = get_current_user() return user and user.role == "admin" ``` --- ## Task 3: 用户管理 API **Files:** - Create: `app/routes/users.py` - Modify: `app/routes/__init__.py` - [ ] **Step 1: 创建 users.py** 新建文件 `app/routes/users.py`: ```python # 用户管理路由 from flask import request, jsonify, render_template, session from app.routes import main_bp from app.models import db, User, login_required_json, admin_required @main_bp.route("/users") @admin_required def users_page(): """用户管理页面""" return render_template("users.html") @main_bp.route("/api/users", methods=["GET"]) @admin_required def api_users_list(): """用户列表""" users = User.query.order_by(User.created_at.desc()).all() return jsonify([u.to_dict() for u in users]) @main_bp.route("/api/users", methods=["POST"]) @admin_required def api_users_create(): """新增用户""" data = request.get_json() username = data.get("username", "").strip() password = data.get("password", "") role = data.get("role", "user") if not username or not password: return jsonify({"error": "请输入用户名和密码"}), 400 if User.query.filter_by(username=username).first(): return jsonify({"error": "用户名已存在"}), 400 if role not in ["admin", "user"]: return jsonify({"error": "无效的角色"}), 400 try: user = User(username=username, role=role) user.set_password(password) db.session.add(user) db.session.commit() return jsonify(user.to_dict()) except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: db.session.rollback() return jsonify({"error": "创建失败: " + str(e)}), 500 @main_bp.route("/api/users/", 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"] try: db.session.commit() return jsonify(user.to_dict()) except Exception as e: db.session.rollback() return jsonify({"error": "更新失败: " + str(e)}), 500 @main_bp.route("/api/users/", methods=["DELETE"]) @admin_required def api_users_delete(user_id): """删除用户""" if user_id == session.get("user_id"): return jsonify({"error": "不能删除自己"}), 400 user = User.query.get_or_404(user_id) try: db.session.delete(user) db.session.commit() return jsonify({"message": "删除成功"}) except Exception as e: db.session.rollback() return jsonify({"error": "删除失败: " + str(e)}), 500 @main_bp.route("/api/users//reset-password", methods=["POST"]) @admin_required def api_users_reset_password(user_id): """重置用户密码(管理员无需知道原密码)""" user = User.query.get_or_404(user_id) data = request.get_json() new_password = data.get("new_password", "") if not new_password: return jsonify({"error": "请输入新密码"}), 400 try: user.set_password(new_password) db.session.commit() return jsonify({"message": "密码重置成功"}) except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: db.session.rollback() return jsonify({"error": "重置失败: " + str(e)}), 500 @main_bp.route("/api/users/change-password", methods=["POST"]) @login_required_json def api_users_change_password(): """修改当前用户密码""" user = User.query.get(session.get("user_id")) data = request.get_json() old_password = data.get("old_password", "") new_password = data.get("new_password", "") confirm_password = data.get("confirm_password", "") if not old_password or not new_password: return jsonify({"error": "请填写完整"}), 400 if not user.check_password(old_password): return jsonify({"error": "原密码错误"}), 400 if new_password != confirm_password: return jsonify({"error": "两次密码输入不一致"}), 400 try: user.set_password(new_password) db.session.commit() return jsonify({"message": "密码修改成功"}) except ValueError as e: return jsonify({"error": str(e)}), 400 except Exception as e: db.session.rollback() return jsonify({"error": "修改失败: " + str(e)}), 500 ``` - [ ] **Step 2: 注册蓝图** 修改 `app/routes/__init__.py`,确保 users 蓝图已注册: ```python from flask import Blueprint main_bp = Blueprint("main", __name__) # 导入所有路由 from app.routes import students, problems, plans, settings, auth, classes, users ``` --- ## Task 4: 班级管理 API **Files:** - Create: `app/routes/classes.py` - [ ] **Step 1: 创建 classes.py** 新建文件 `app/routes/classes.py`: ```python # 班级管理路由 from flask import request, jsonify, render_template, session from app.routes import main_bp from app.models import db, Class, Student, User, login_required_json, admin_required @main_bp.route("/classes") @login_required_json def classes_page(): """班级管理页面""" return render_template("classes.html") @main_bp.route("/api/classes", methods=["GET"]) @login_required_json def api_classes_list(): """班级列表""" classes = Class.query.order_by(Class.created_at.desc()).all() return jsonify([c.to_dict() for c in classes]) @main_bp.route("/api/classes", methods=["POST"]) @admin_required def api_classes_create(): """新增班级""" data = request.get_json() name = data.get("name", "").strip() description = data.get("description", "") if not name: return jsonify({"error": "请输入班级名称"}), 400 if Class.query.filter_by(name=name).first(): return jsonify({"error": "班级名称已存在"}), 400 try: cls = Class(name=name, description=description) db.session.add(cls) db.session.commit() return jsonify(cls.to_dict()) except Exception as e: db.session.rollback() return jsonify({"error": "创建失败: " + str(e)}), 500 @main_bp.route("/api/classes/", methods=["PUT"]) @admin_required def api_classes_update(class_id): """编辑班级""" cls = Class.query.get_or_404(class_id) data = request.get_json() if "name" in data and data["name"]: # 检查名称是否与其他班级冲突 existing = Class.query.filter_by(name=data["name"]).first() if existing and existing.id != class_id: return jsonify({"error": "班级名称已存在"}), 400 cls.name = data["name"] if "description" in data: cls.description = data["description"] try: db.session.commit() return jsonify(cls.to_dict()) except Exception as e: db.session.rollback() return jsonify({"error": "更新失败: " + str(e)}), 500 @main_bp.route("/api/classes/", methods=["DELETE"]) @admin_required def api_classes_delete(class_id): """删除班级(不删除学员,仅解除关联)""" cls = Class.query.get_or_404(class_id) try: # 解除学员与班级的关联 Student.query.filter_by(class_id=class_id).update({"class_id": None}) db.session.delete(cls) db.session.commit() return jsonify({"message": "删除成功"}) except Exception as e: db.session.rollback() return jsonify({"error": "删除失败: " + str(e)}), 500 @main_bp.route("/api/classes//students", methods=["GET"]) @login_required_json def api_classes_students(class_id): """班级学员列表""" cls = Class.query.get_or_404(class_id) students = cls.students.all() return jsonify([{ "id": s.id, "name": s.name, "phone": s.phone, "practice_time": s.practice_time } for s in students]) @main_bp.route("/api/classes//assign", methods=["POST"]) @login_required_json def api_classes_assign(class_id): """分配学员到班级""" cls = Class.query.get_or_404(class_id) data = request.get_json() student_ids = data.get("student_ids", []) try: # 先清除这些学员的班级关联 Student.query.filter(Student.id.in_(student_ids)).update({"class_id": None}) # 再设置新的班级关联 Student.query.filter(Student.id.in_(student_ids)).update({"class_id": class_id}) db.session.commit() return jsonify({"message": "分配成功"}) except Exception as e: db.session.rollback() return jsonify({"error": "分配失败: " + str(e)}), 500 ``` - [ ] **Step 2: 导出 Class 模型** 在 `app/models.py` 文件末尾确保导出 Class: ```python __all__ = ["db", "User", "Student", "StudentProblem", "PracticePlan", "Class"] ``` --- ## Task 5: 修改现有路由添加权限控制 **Files:** - Modify: `app/routes/students.py` - Modify: `app/routes/problems.py` - Modify: `app/routes/plans.py` - Modify: `app/routes/settings.py` - [ ] **Step 1: 修改 students.py 添加登录验证** 在 `app/routes/students.py` 顶部导入装饰器,并在每个 API 添加装饰器: ```python from app.models import db, Student, StudentProblem, login_required_json ``` 为以下路由添加 `@login_required_json`: - `api_students_list` - `api_students_create` - `api_students_detail` - `api_students_update` - `api_students_delete` - [ ] **Step 2: 修改 problems.py 添加登录验证** ```python from app.models import db, StudentProblem, login_required_json ``` 为以下路由添加 `@login_required_json`: - `api_problems_list` - `api_problems_add` - `api_problems_delete` - [ ] **Step 3: 修改 plans.py 添加登录验证** ```python from app.models import db, PracticePlan, login_required_json ``` 为以下路由添加 `@login_required_json`: - `api_generate_plan` - `api_plans_detail` - `api_plans_delete` - [ ] **Step 4: 修改 settings.py 添加管理员权限** ```python from app.models import db, admin_required ``` 将以下路由的装饰器从 `@login_required` 改为 `@admin_required`: - `settings_page` - `api_problems_list`(创建/编辑/删除需要管理员) - `api_problems_create` - `api_problems_update` - `api_problems_delete` - `api_config_get` - `api_config_set` - `api_config_test` --- ## Task 6: 前端 - 用户管理页面 **Files:** - Create: `app/templates/users.html` - [ ] **Step 1: 创建用户管理页面** 新建 `app/templates/users.html`: ```html 用户管理 - 钢琴练习方案系统

用户管理

ID 用户名 角色 创建时间 操作
``` --- ## Task 7: 前端 - 班级管理页面 **Files:** - Create: `app/templates/classes.html` - [ ] **Step 1: 创建班级管理页面** 新建 `app/templates/classes.html`: ```html 班级管理 - 钢琴练习方案系统

班级管理

ID 班级名称 描述 学员数 创建时间 操作
``` --- ## Task 8: 前端 - 导航栏动态渲染 **Files:** - Modify: `app/templates/index.html` - [ ] **Step 1: 修改导航栏,动态显示菜单** 找到 `` 后添加: ```html ``` - [ ] **Step 3: 修改 JavaScript,动态控制菜单显示** 修改 `