Files
piano-plan/docs/superpowers/plans/2026-04-23-goal-management-plan.md
hmo b54b6c7aec feat: 添加 Goal, GoalRelation, StudentGoal 三个数据模型
- Goal: 目标表,支持存储学习目标
- GoalRelation: 目标自关联多对多表,支持 DAG 结构
- StudentGoal: 学员目标记录表,关联学员和目标
2026-04-23 20:10:08 +08:00

16 KiB
Raw Permalink Blame History

目标管理模块实现计划

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 模型

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 模型
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 模型
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: 提交
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

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/<int:goal_id>", 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/<int:goal_id>", 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/<int:goal_id>", 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 注册蓝图
from app.routes import goals
app.register_blueprint(goals.goals_bp)
  • Step 3: 运行 lint 验证

Run: lsp_diagnostics('app/routes/goals.py')

  • Step 4: 提交
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: 添加循环检测函数

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: 添加关联接口
@goals_bp.route("/api/goals/<int:goal_id>/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/<int:goal_id>/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/<int:goal_id>/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/<int:goal_id>/children/<int:child_id>", 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: 提交
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

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/<int:student_id>/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/<int:student_id>/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/<int:student_id>/goals/<int:goal_id>", 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/<int:student_id>/goals/<int:goal_id>", 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 注册蓝图
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: 提交
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: 添加路由

@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: 提交
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: 提交
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() 之后添加:

# 检查 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: 提交
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: 提交
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 → ★-★★★★★)