From ab0a8f383db64653b35c2254e809c3238fddea2f Mon Sep 17 00:00:00 2001 From: hmo Date: Thu, 23 Apr 2026 20:18:36 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E7=AE=A1=E7=90=86CRUD=20API=E5=92=8C=E5=85=B3?= =?UTF-8?q?=E8=81=94=E8=93=9D=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 2 + app/routes/__init__.py | 2 +- app/routes/goals.py | 114 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 app/routes/goals.py diff --git a/app/__init__.py b/app/__init__.py index fd92061..ffeea74 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -34,9 +34,11 @@ def create_app(): # 注册蓝图 from app.routes import main_bp from app.routes.templates import templates_bp + from app.routes.goals import goals_bp app.register_blueprint(main_bp) app.register_blueprint(templates_bp) + app.register_blueprint(goals_bp) # 创建数据库和目录 with app.app_context(): diff --git a/app/routes/__init__.py b/app/routes/__init__.py index a3be51b..3d933f0 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -5,4 +5,4 @@ from flask import Blueprint main_bp = Blueprint("main", __name__) # 导入各路由模块 -from app.routes import students, plans, problems, settings, auth, classes, users, backup +from app.routes import students, plans, problems, settings, auth, classes, users, backup, goals diff --git a/app/routes/goals.py b/app/routes/goals.py new file mode 100644 index 0000000..1863e68 --- /dev/null +++ b/app/routes/goals.py @@ -0,0 +1,114 @@ +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": "删除成功"}) + +# ===== 以下是 Task 3 的循环检测和关联 API ===== + +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) + from app.models import GoalRelation + for rel in GoalRelation.query.filter_by(parent_goal_id=current).all(): + stack.append(rel.child_goal_id) + return False + +@goals_bp.route("/api/goals//children", methods=["GET"]) +@login_required_json +def get_goal_children(goal_id): + from app.models import GoalRelation + 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): + from app.models import GoalRelation + 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): + from app.models import GoalRelation + 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): + from app.models import GoalRelation + 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": "移除成功"}) \ No newline at end of file From c605c9732a7a59278f70bd5c8d78231941212921 Mon Sep 17 00:00:00 2001 From: hmo Date: Thu, 23 Apr 2026 20:24:29 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E6=A8=A1=E5=9D=97=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/API.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++ docs/MODELS.md | 46 +++++++++++++++++++++++++- docs/STRUCTURE.md | 5 ++- 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index 0530d3d..73fe44c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -523,6 +523,89 @@ POST /api/config/test --- +## 目标管理 API + +### GET /api/goals +获取所有目标 + +**响应**: +```json +[ + { + "id": 1, + "name": "掌握基本音阶", + "content": "...", + "created_at": "2026-04-23T10:00:00", + "updated_at": "2026-04-23T10:00:00" + } +] +``` + +### POST /api/goals +创建新目标 + +**请求体**: +```json +{ + "name": "目标名称", + "content": "目标内容(Markdown)" +} +``` + +### GET /api/goals/{id} +获取单个目标 + +### PUT /api/goals/{id} +更新目标 + +### DELETE /api/goals/{id} +删除目标 + +### GET /api/goals/{id}/children +获取目标的子目标 + +### GET /api/goals/{id}/parents +获取目标的父目标 + +### POST /api/goals/{id}/children +添加子目标关联(含循环检测) + +**请求体**: +```json +{ + "child_goal_id": 2 +} +``` + +### DELETE /api/goals/{id}/children/{child_id} +移除子目标关联 + +## 学员目标 API + +### GET /api/students/{id}/goals +获取学员的所有目标 + +### POST /api/students/{id}/goals +为学员分配目标 + +**请求体**: +```json +{ + "goal_id": 1, + "status": "未开始", + "mastery_level": 1, + "deadline": "2026-05-01" +} +``` + +### PUT /api/students/{id}/goals/{goal_id} +更新学员目标状态/掌握程度 + +### DELETE /api/students/{id}/goals/{goal_id} +移除学员的目标 + +--- + ## 权限说明 | 接口 | 管理员 | 普通用户 | diff --git a/docs/MODELS.md b/docs/MODELS.md index d50d8b8..b35096c 100644 --- a/docs/MODELS.md +++ b/docs/MODELS.md @@ -294,4 +294,48 @@ SELECT * FROM users; | 60分钟 | 中高级学员 | | 90分钟 | 高级学员 | | 120分钟 | 专业学员 | -| 150分钟以上 | 竞技水平 | \ No newline at end of file +| 150分钟以上 | 竞技水平 | + +--- + +## 目标管理模块 + +### Goal (目标表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | Integer | 主键 | +| name | String(100) | 目标名称 | +| content | Text | 目标内容(Markdown) | +| created_at | DateTime | 创建时间 | +| updated_at | DateTime | 更新时间 | + +### GoalRelation (目标关联表) + +自关联多对多关系,用于表示目标之间的父子关系(DAG)。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| parent_goal_id | Integer | 父目标ID,外键 | +| child_goal_id | Integer | 子目标ID,外键 | + +**关系类型**:自引用多对多(一个目标可以有多个子目标,也可以有多个父目标) + +**约束**:通过应用层循环检测防止形成循环 + +### StudentGoal (学员目标记录表) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | Integer | 主键 | +| student_id | Integer | 学员ID,外键 | +| goal_id | Integer | 目标ID,外键 | +| status | String(20) | 状态:未开始/进行中/已完成 | +| mastery_level | Integer | 掌握程度 1-5 | +| deadline | DateTime | 截止日期 | +| completed_at | DateTime | 完成时间 | +| created_at | DateTime | 创建时间 | + +**关系**: +- 一个学员可以分配多个目标 +- 一个目标可以分配给多个学员 \ No newline at end of file diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index a779ae3..e9e9fa1 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -21,7 +21,9 @@ │ │ ├── problems.py # 问题记录API │ │ ├── plans.py # 方案生成API │ │ ├── settings.py # 系统设置API -│ │ └── classes.py # 班级管理API(新增) +│ │ ├── classes.py # 班级管理API +│ │ ├── goals.py # 目标管理 API +│ │ └── student_goals.py # 学员目标 API │ │ │ ├── services/ # 业务逻辑 │ │ ├── plan_generator.py # 方案生成器 @@ -38,6 +40,7 @@ │ ├── setup.html # 初始设置页面(独立) │ ├── users.html # 用户管理页面(继承base) │ ├── classes.html # 班级管理页面(继承base) +│ ├── goals.html # 目标管理页面 │ ├── templates.html # 模板管理页面(继承base) │ ├── api_settings.html # API设置页面(继承base) │ └── wechat_card.html # 微信卡片模板(独立) From 9f22717adc3a99285e7e070f730ff9ca36f13432 Mon Sep 17 00:00:00 2001 From: hmo Date: Thu, 23 Apr 2026 18:30:00 +0800 Subject: [PATCH 3/4] feat: add goal management section to student detail page --- app/__init__.py | 7 ++ app/templates/student.html | 159 ++++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index ffeea74..1f27be3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -39,11 +39,18 @@ def create_app(): app.register_blueprint(main_bp) app.register_blueprint(templates_bp) app.register_blueprint(goals_bp) + from app.routes import student_goals + app.register_blueprint(student_goals.student_goals_bp) # 创建数据库和目录 with app.app_context(): os.makedirs(os.path.join(BASE_DIR, "data"), exist_ok=True) os.makedirs(app.config["PDF_OUTPUT_DIR"], exist_ok=True) + + # 确保所有模型都被导入 + from app.models import Student, Problem, StudentProblem, Template, PracticePlan + from app.models import Goal, GoalRelation, StudentGoal # 新增目标相关模型 + db.create_all() # 简单迁移:为已存在的数据库添加新字段(必须在init_default_templates之前) diff --git a/app/templates/student.html b/app/templates/student.html index bcd8c6c..bb1cf5a 100644 --- a/app/templates/student.html +++ b/app/templates/student.html @@ -56,6 +56,19 @@ + +
+
+
🎯 练习目标
+ +
+
+
+
+
+
练习方案
@@ -238,19 +251,57 @@
+ + + {% endblock %} {% block extra_js %} +{% endblock %} \ No newline at end of file