feat: 添加 Goal, GoalRelation, StudentGoal 三个数据模型
- Goal: 目标表,支持存储学习目标 - GoalRelation: 目标自关联多对多表,支持 DAG 结构 - StudentGoal: 学员目标记录表,关联学员和目标
This commit is contained in:
@@ -0,0 +1,553 @@
|
||||
# 目标管理模块实现计划
|
||||
|
||||
> **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/<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 注册蓝图**
|
||||
|
||||
```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/<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: 提交**
|
||||
|
||||
```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/<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 注册蓝图**
|
||||
|
||||
```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 → ★-★★★★★)
|
||||
@@ -0,0 +1,143 @@
|
||||
# 目标管理模块设计
|
||||
|
||||
> 日期:2026-04-23
|
||||
> 状态:待评审
|
||||
|
||||
## 概述
|
||||
|
||||
目标管理模块用于管理系统化的钢琴学习目标,支持目标间的多对多关联(DAG结构),以及目标与学员的关联记录。
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 1. Goal (目标)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Integer | 主键,自增 |
|
||||
| name | String(100) | 目标名称,必填 |
|
||||
| content | Text | Markdown 格式详细内容 |
|
||||
| created_at | DateTime | 创建时间 |
|
||||
| updated_at | DateTime | 更新时间 |
|
||||
|
||||
### 2. GoalRelation (目标关联 - 自关联多对多)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| parent_goal_id | Integer | 外键 → goals.id |
|
||||
| child_goal_id | Integer | 外键 → goals.id |
|
||||
| PRIMARY KEY | (parent_goal_id, child_goal_id) | 联合主键 |
|
||||
|
||||
**约束**:
|
||||
- 禁止循环引用(A→B→C→A)
|
||||
- 自关联:goal 可以是自身的父/子节点
|
||||
|
||||
### 3. StudentGoal (学员目标记录)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | Integer | 主键,自增 |
|
||||
| student_id | Integer | 外键 → students.id |
|
||||
| goal_id | Integer | 外键 → goals.id |
|
||||
| status | String(20) | 状态:未开始/进行中/已完成 |
|
||||
| mastery_level | Integer | 完成度:1-5(1最少,5最精通) |
|
||||
| deadline | DateTime | 截止日期 |
|
||||
| completed_at | DateTime | 完成日期 |
|
||||
| created_at | DateTime | 创建时间 |
|
||||
|
||||
**显示规则**:
|
||||
- `mastery_level` 直接渲染为对应数量的五角星(★)
|
||||
|
||||
---
|
||||
|
||||
## API 接口
|
||||
|
||||
### 目标管理
|
||||
|
||||
```
|
||||
GET /api/goals # 获取所有目标
|
||||
POST /api/goals # 创建目标
|
||||
GET /api/goals/<id> # 获取目标详情
|
||||
PUT /api/goals/<id> # 更新目标
|
||||
DELETE /api/goals/<id> # 删除目标
|
||||
```
|
||||
|
||||
### 目标关联
|
||||
|
||||
```
|
||||
GET /api/goals/<id>/parents # 获取父目标列表
|
||||
GET /api/goals/<id>/children # 获取子目标列表
|
||||
POST /api/goals/<id>/children # 添加子目标关联
|
||||
DELETE /api/goals/<id>/children/<child_id> # 移除关联
|
||||
```
|
||||
|
||||
### 学员目标
|
||||
|
||||
```
|
||||
GET /api/students/<student_id>/goals # 获取学员的目标列表
|
||||
POST /api/students/<student_id>/goals # 为学员添加目标
|
||||
PUT /api/students/<student_id>/goals/<goal_id> # 更新学员目标状态
|
||||
DELETE /api/students/<student_id>/goals/<goal_id> # 移除学员的目标
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 循环引用检测
|
||||
|
||||
在添加关联时必须检测:
|
||||
|
||||
```python
|
||||
def has_cycle(goal_id, new_child_id):
|
||||
"""检测添加 new_child_id 作为 goal_id 的子目标是否会形成循环"""
|
||||
visited = set()
|
||||
stack = [new_child_id]
|
||||
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
if current == goal_id:
|
||||
return True # 发现循环
|
||||
if current in visited:
|
||||
continue
|
||||
visited.add(current)
|
||||
# 获取 current 的所有子目标,继续检测
|
||||
for child in get_children(current):
|
||||
stack.append(child)
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端页面
|
||||
|
||||
### 1. 目标管理页面 (`/goals`)
|
||||
|
||||
- 目标列表(可树状展示)
|
||||
- 创建/编辑/删除目标
|
||||
- 目标内容 Markdown 编辑器
|
||||
- 关联管理(拖拽或选择器添加关联)
|
||||
|
||||
### 2. 学员详情页目标区块
|
||||
|
||||
在现有 `student.html` 中扩展:
|
||||
- 显示学员的目标列表
|
||||
- 每项目标显示:名称、状态、★完成度、截止日期
|
||||
- 可添加/移除/编辑目标
|
||||
- 状态变更触发刷新
|
||||
|
||||
---
|
||||
|
||||
## 实现顺序
|
||||
|
||||
1. **数据模型** - goals, goal_relations, student_goals 表
|
||||
2. **目标 CRUD API** - 基础的增删改查
|
||||
3. **目标关联 API** - 关联管理 + 循环检测
|
||||
4. **学员目标 API** - 学员与目标的关联管理
|
||||
5. **目标管理页面** - 目标列表 + 关联管理
|
||||
6. **学员详情页扩展** - 目标区块
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
- 无外部依赖
|
||||
- 复用现有的 Markdown 编辑器(EasyMDE)和星级组件
|
||||
Reference in New Issue
Block a user