# 目标管理模块实现计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan. **Goal:** 实现目标管理模块,支持目标的CRUD、目标间的DAG关联、与学员的关联记录 **Architecture:** 使用Flask-SQLAlchemy定义数据模型,通过RESTful API暴露接口,前端复用现有EasyMDE和星级组件 **Tech Stack:** Flask, SQLite, SQLAlchemy, EasyMDE --- ## 文件结构 ``` app/ ├── models.py # Goal, GoalRelation, StudentGoal 模型 ├── routes/ │ ├── goals.py # 目标 CRUD + 关联管理 API │ └── student_goals.py # 学员目标 API └── templates/ ├── goals.html # 目标管理页面 └── student.html # 扩展学员详情页的目标区块 docs/ └── superpowers/plans/ └── 2026-04-23-goal-management.md # 设计文档 ``` --- ## Task 1: 数据模型 **Files:** - Modify: `app/models.py:193-240` (在 PracticePlan 之前添加新模型) - [ ] **Step 1: 在 models.py 添加 Goal 模型** ```python 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) 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, "created_at": self.created_at.isoformat() if self.created_at else None, "updated_at": self.updated_at.isoformat() if self.updated_at else None, } ``` - [ ] **Step 2: 在 models.py 添加 GoalRelation 模型** ```python 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") ``` - [ ] **Step 3: 在 models.py 添加 StudentGoal 模型** ```python 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) status = db.Column(db.String(20), default="未开始") # 未开始/进行中/已完成 mastery_level = db.Column(db.Integer, default=1) # 1-5 deadline = db.Column(db.DateTime) completed_at = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=datetime.now) student = db.relationship("Student", backref="goal_records") goal = db.relationship("Goal") 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, "status": self.status, "mastery_level": self.mastery_level, "deadline": self.deadline.isoformat() if self.deadline else None, "completed_at": self.completed_at.isoformat() if self.completed_at else None, } ``` - [ ] **Step 4: 运行 lint 验证** Run: `lsp_diagnostics('app/models.py')` - [ ] **Step 5: 提交** ```bash git add app/models.py git commit -m "feat: 添加 Goal, GoalRelation, StudentGoal 模型" ``` --- ## Task 2: 目标 CRUD API **Files:** - Create: `app/routes/goals.py` - [ ] **Step 1: 创建 routes/goals.py** ```python from flask import Blueprint, request, jsonify from app.models import db, Goal from app.routes.auth import login_required_json goals_bp = Blueprint("goals", __name__) @goals_bp.route("/api/goals", methods=["GET"]) @login_required_json def get_goals(): goals = Goal.query.order_by(Goal.created_at.desc()).all() return jsonify([g.to_dict() for g in goals]) @goals_bp.route("/api/goals", methods=["POST"]) @login_required_json def create_goal(): data = request.get_json() goal = Goal(name=data["name"], content=data.get("content", "")) db.session.add(goal) db.session.commit() return jsonify(goal.to_dict()), 201 @goals_bp.route("/api/goals/", methods=["GET"]) @login_required_json def get_goal(goal_id): goal = Goal.query.get_or_404(goal_id) return jsonify(goal.to_dict()) @goals_bp.route("/api/goals/", methods=["PUT"]) @login_required_json def update_goal(goal_id): goal = Goal.query.get_or_404(goal_id) data = request.get_json() if "name" in data: goal.name = data["name"] if "content" in data: goal.content = data["content"] db.session.commit() return jsonify(goal.to_dict()) @goals_bp.route("/api/goals/", methods=["DELETE"]) @login_required_json def delete_goal(goal_id): goal = Goal.query.get_or_404(goal_id) db.session.delete(goal) db.session.commit() return jsonify({"message": "删除成功"}) ``` - [ ] **Step 2: 在 app/routes/__init__.py 注册蓝图** ```python from app.routes import goals app.register_blueprint(goals.goals_bp) ``` - [ ] **Step 3: 运行 lint 验证** Run: `lsp_diagnostics('app/routes/goals.py')` - [ ] **Step 4: 提交** ```bash git add app/routes/goals.py app/routes/__init__.py git commit -m "feat: 目标 CRUD API" ``` --- ## Task 3: 目标关联 API + 循环检测 **Files:** - Modify: `app/routes/goals.py` (添加关联接口) - [ ] **Step 1: 添加循环检测函数** ```python def _has_cycle(parent_id, child_id): """检测添加 child_id 作为 parent_id 的子目标是否会形成循环""" visited = set() stack = [child_id] while stack: current = stack.pop() if current == parent_id: return True if current in visited: continue visited.add(current) for rel in GoalRelation.query.filter_by(parent_goal_id=current).all(): stack.append(rel.child_goal_id) return False ``` - [ ] **Step 2: 添加关联接口** ```python @goals_bp.route("/api/goals//children", methods=["GET"]) @login_required_json def get_goal_children(goal_id): relations = GoalRelation.query.filter_by(parent_goal_id=goal_id).all() child_ids = [r.child_goal_id for r in relations] children = Goal.query.filter(Goal.id.in_(child_ids)).all() if child_ids else [] return jsonify([c.to_dict() for c in children]) @goals_bp.route("/api/goals//parents", methods=["GET"]) @login_required_json def get_goal_parents(goal_id): relations = GoalRelation.query.filter_by(child_goal_id=goal_id).all() parent_ids = [r.parent_goal_id for r in relations] parents = Goal.query.filter(Goal.id.in_(parent_ids)).all() if parent_ids else [] return jsonify([p.to_dict() for p in parents]) @goals_bp.route("/api/goals//children", methods=["POST"]) @login_required_json def add_goal_child(goal_id): data = request.get_json() child_id = data["child_goal_id"] if goal_id == child_id: return jsonify({"error": "不能将目标关联到自身"}), 400 if _has_cycle(goal_id, child_id): return jsonify({"error": "添加此关联会形成循环引用"}), 400 # 检查是否已存在 existing = GoalRelation.query.filter_by(parent_goal_id=goal_id, child_goal_id=child_id).first() if existing: return jsonify({"error": "关联已存在"}), 400 relation = GoalRelation(parent_goal_id=goal_id, child_goal_id=child_id) db.session.add(relation) db.session.commit() return jsonify({"message": "添加成功"}) @goals_bp.route("/api/goals//children/", methods=["DELETE"]) @login_required_json def remove_goal_child(goal_id, child_id): relation = GoalRelation.query.filter_by(parent_goal_id=goal_id, child_goal_id=child_id).first() if relation: db.session.delete(relation) db.session.commit() return jsonify({"message": "移除成功"}) ``` - [ ] **Step 3: 运行 lint 验证** Run: `lsp_diagnostics('app/routes/goals.py')` - [ ] **Step 4: 提交** ```bash git add app/routes/goals.py git commit -m "feat: 目标关联 API + 循环检测" ``` --- ## Task 4: 学员目标 API **Files:** - Create: `app/routes/student_goals.py` - [ ] **Step 1: 创建 student_goals.py** ```python from flask import Blueprint, request, jsonify from app.models import db, Student, Goal, StudentGoal from app.routes.auth import login_required_json from datetime import datetime student_goals_bp = Blueprint("student_goals", __name__) @student_goals_bp.route("/api/students//goals", methods=["GET"]) @login_required_json def get_student_goals(student_id): Student.query.get_or_404(student_id) records = StudentGoal.query.filter_by(student_id=student_id).all() return jsonify([r.to_dict() for r in records]) @student_goals_bp.route("/api/students//goals", methods=["POST"]) @login_required_json def add_student_goal(student_id): Student.query.get_or_404(student_id) data = request.get_json() goal_id = data["goal_id"] # 检查目标是否存在 Goal.query.get_or_404(goal_id) # 检查是否已添加 existing = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first() if existing: return jsonify({"error": "该目标已添加"}), 400 record = StudentGoal( student_id=student_id, goal_id=goal_id, status="未开始", mastery_level=1 ) db.session.add(record) db.session.commit() return jsonify(record.to_dict()), 201 @student_goals_bp.route("/api/students//goals/", methods=["PUT"]) @login_required_json 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 "status" in data: record.status = data["status"] if data["status"] == "已完成": record.completed_at = datetime.now() if "mastery_level" in data: record.mastery_level = data["mastery_level"] if "deadline" in data: record.deadline = datetime.fromisoformat(data["deadline"]) if data["deadline"] else None db.session.commit() return jsonify(record.to_dict()) @student_goals_bp.route("/api/students//goals/", methods=["DELETE"]) @login_required_json def remove_student_goal(student_id, goal_id): record = StudentGoal.query.filter_by(student_id=student_id, goal_id=goal_id).first_or_404() db.session.delete(record) db.session.commit() return jsonify({"message": "移除成功"}) ``` - [ ] **Step 2: 在 app/routes/__init__.py 注册蓝图** ```python from app.routes import student_goals app.register_blueprint(student_goals.student_goals_bp) ``` - [ ] **Step 3: 运行 lint 验证** Run: `lsp_diagnostics('app/routes/student_goals.py')` - [ ] **Step 4: 提交** ```bash git add app/routes/student_goals.py app/routes/__init__.py git commit -m "feat: 学员目标 API" ``` --- ## Task 5: 目标管理页面 **Files:** - Create: `app/templates/goals.html` - [ ] **Step 1: 创建 goals.html** 参考现有模板结构,包含: - 目标列表(树状或平铺) - 创建/编辑目标 Modal(使用 EasyMDE) - 关联管理(添加/移除子目标) - [ ] **Step 2: 添加路由** ```python @goals_bp.route("/goals") @login_required_json def goals_page(): return render_template("goals.html") ``` - [ ] **Step 3: 运行 lint 验证** Run: `lsp_diagnostics('app/templates/goals.html')` - [ ] **Step 4: 提交** ```bash git add app/templates/goals.html app/routes/goals.py git commit -m "feat: 目标管理页面" ``` --- ## Task 6: 学员详情页目标区块 **Files:** - Modify: `app/templates/student.html` - [ ] **Step 1: 在 student.html 添加目标区块** 在现有问题记录和练习方案之间添加目标区块: - 显示学员目标列表(名称、状态、★完成度、截止日期) - 添加/移除目标 - 编辑目标状态 - [ ] **Step 2: 添加 JS 函数** - `loadGoals()` - 加载目标列表 - `renderGoalList()` - 渲染目标 - `showAddGoalModal()` - 显示添加目标弹窗 - `saveAddGoal()` - 保存添加 - `updateGoalStatus()` - 更新状态 - [ ] **Step 3: 运行 lint 验证** Run: `lsp_diagnostics('app/templates/student.html')` - [ ] **Step 4: 提交** ```bash git add app/templates/student.html git commit -m "feat: 学员详情页目标区块" ``` --- ## Task 7: 数据库迁移 **Files:** - Modify: `app/__init__.py` (在 db.create_all() 之前添加迁移逻辑) - [ ] **Step 1: 添加表存在性检测和创建** 在 `create_app()` 中 db.create_all() 之后添加: ```python # 检查 goals 表是否存在,不存在则创建 result = db.session.execute( text("SELECT name FROM sqlite_master WHERE type='table' AND name='goals'") ) if not result.fetchone(): db.session.execute(text(""" CREATE TABLE goals ( id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(100) NOT NULL, content TEXT, created_at DATETIME, updated_at DATETIME ) """)) db.session.commit() # 检查 goal_relations 表 result = db.session.execute( text("SELECT name FROM sqlite_master WHERE type='table' AND name='goal_relations'") ) if not result.fetchone(): db.session.execute(text(""" CREATE TABLE goal_relations ( parent_goal_id INTEGER NOT NULL, child_goal_id INTEGER NOT NULL, PRIMARY KEY (parent_goal_id, child_goal_id), FOREIGN KEY (parent_goal_id) REFERENCES goals(id), FOREIGN KEY (child_goal_id) REFERENCES goals(id) ) """)) db.session.commit() # 检查 student_goals 表 result = db.session.execute( text("SELECT name FROM sqlite_master WHERE type='table' AND name='student_goals'") ) if not result.fetchone(): db.session.execute(text(""" CREATE TABLE student_goals ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, goal_id INTEGER NOT NULL, status VARCHAR(20) DEFAULT '未开始', mastery_level INTEGER DEFAULT 1, deadline DATETIME, completed_at DATETIME, created_at DATETIME, FOREIGN KEY (student_id) REFERENCES students(id), FOREIGN KEY (goal_id) REFERENCES goals(id) ) """)) db.session.commit() ``` - [ ] **Step 2: 提交** ```bash git add app/__init__.py git commit -m "feat: 数据库迁移脚本 - goals, goal_relations, student_goals 表" ``` --- ## Task 8: 更新文档 **Files:** - Modify: `docs/MODELS.md` (添加新表说明) - Modify: `docs/API.md` (添加新 API 说明) - Modify: `docs/STRUCTURE.md` (添加新文件) - [ ] **Step 1: 更新 MODELS.md** 添加 Goal, GoalRelation, StudentGoal 表说明 - [ ] **Step 2: 更新 API.md** 添加目标 API 和学员目标 API 说明 - [ ] **Step 3: 提交** ```bash git add docs/MODELS.md docs/API.md docs/STRUCTURE.md git commit -m "docs: 更新文档 - 目标管理模块" ``` --- ## 验证清单 - [ ]goals 表创建成功 - [ ]goal_relations 表创建成功 - [ ]student_goals 表创建成功 - [ ] 目标 CRUD API 测试通过 - [ ] 目标关联 API 测试通过(包含循环检测) - [ ] 学员目标 API 测试通过 - [ ] 目标管理页面可访问 - [ ] 学员详情页目标区块正常显示 - [ ] 星级显示正确(1-5 → ★-★★★★★)