Commit 05ab73d7 authored by uuo00_n's avatar uuo00_n

feat(学生绑定): 实现用户与学生绑定功能及接口

- 新增学生绑定服务与API路由
- 修改dashboard服务去除随机学生fallback逻辑
- 在students集合添加user_id稀疏唯一索引
- 更新init_db.py添加索引初始化
- 完善错误处理与返回数据序列化
parent 65250db5
## 现状与差距
- 已有接口:`GET /api/v1/dashboard/homeroom/current`(角色≥2,受版别限制)。功能包含:
- 当前时段课程与地点(按登录时间映射节次)
- 所辖班级今日整体出勤率(present/total)
- 今日请假列表(time-window 过滤)
- 部门级近期指示(倒序取前 N 条)
- 待完善:
- 按“当前节次”统计出勤(而不是全天)
- 请假与指示的过滤更精确(按系部/班级目标)
- 响应统一可序列化(去除 `ObjectId` 等)
## 接口与权限
- 路由:`GET /api/v1/dashboard/homeroom/current`
- 依赖:`Depends(require_edition_for_mode())` + `Depends(require_role(2))`
- 当前用户定位班级:`classes.head_teacher_id == current_user._id`
## 数据口径(最终返回)
- `current_lessons`
- 每个所辖班在当前节次的课程名、地点(从共享节次 `schedules.classes` 中匹配 `class_id`
- `attendance_rates`
- 每班“当前节次”的出勤数据:`present/total/rate`(从 `attendance``lesson_id=W{weekday}-P{period}` 精确统计)
- `leaves`
- 今日处于有效区间的请假记录(`from_date<=today<=to_date`),返回 `student_id/class_id/reason/status`
- `directives`
- 部门级指示(可选:按目标包含所辖班级或专业筛选),倒序最近 N 条
## 技术实现
- 服务层(`app/services/dashboard.py`
- 统一序列化字典,不返回不可序列化类型
- 当前节次计算:使用登录时间(小时→节次)
- 出勤统计粒度:`attendance.count_documents({class_id, date=today, lesson_id=current_lesson_id, status='出勤'})`
- 索引与性能
- 已有索引:`students.user_id``classes.head_teacher_id``schedules(classes.class_id, weekday, period)``attendance(lesson_id, student_id)``leaves(class_id, from_date)`
- 查询按索引字段过滤,保持 O(logN) 级命中
## 验证方案
- MCP 校验:
-`head_teacher_id` 列出所辖班;按 `weekday+period` 查当前节次课表;按 `lesson_id+class_id` 统计出勤
- 接口联调:
- 使用 `manager_edu` 登录,访问 `/dashboard/homeroom/current` 检查四块数据完整性
## 交付步骤
1. 优化 `homeroom_current_summary` 的出勤统计粒度到“当前节次”
2. 指示列表按部门或目标过滤,统一序列化输出
3. 加入最小限度的错误兜底(无所辖班时返回空集合)
4. 通过 MCP 与本地接口联调验证
## 安全与回滚
- 仅读取查询,无数据破坏;若需回滚,保留原全天出勤实现分支
确认后我将立即完善服务实现并复核接口返回,随后重启后端(端口可改回 8000 或继续 8001)进行联调验证。
\ No newline at end of file
## 问题与目标
- 现状:仅示例用户 `user` 绑定了一个学生,其余学生未与用户账户关联,导致学生端接口需要 fallback,功能割裂。
- 目标:建立稳定的“用户↔学生”绑定关系与接口,使学生端数据根据登录用户精确返回;同时保证班主任、中层、校级端不受影响。
## 数据库与索引
1. `students` 集合新增索引:
- `user_id` 唯一(稀疏),确保一个用户仅绑定一个学生;便于按用户查学生
- 维持现有:`student_id` 唯一、`class_id` 索引
2. 可选(若需要反查更快):
- `users` 集合新增字段:`student_id`(非必需);无需索引,主要用于展示与快捷反查
## 服务与接口
1. 新增服务 `app/services/student_binding.py`
- `bind_user_to_student(user_id, student_id)`:校验重复绑定,写入 `students.user_id`,可同步回写 `users.student_id`
- `unbind_user_from_student(student_id)`:移除绑定;可同步清理 `users.student_id`
- `get_student_by_user(user_id)`:按 `students.user_id` 查询返回学生
2. 新增路由 `app/api/v1/students.py`
- `POST /students/{student_id}/bind`(管理员或班主任,≥2级):请求体携带 `user_id`
- `DELETE /students/{student_id}/bind`(管理员或班主任,≥2级)
- `GET /students/me`(学生本人,≥1级):返回当前登录用户绑定的学生信息
- 路由统一挂载 `Depends(require_edition_for_mode())`,角色控制用 `require_role(min_level)`
## 学生端接口对齐
- 更新 `app/services/dashboard.py`
- 去掉“未绑定时随机学生”的 fallback,若当前用户未绑定学生则返回 404(或明确错误)
- 查询逻辑改为严格基于 `students.user_id``student_id/class_id`,再取当天课表/出勤/操行
## 初始化数据与迁移
-`init_db.py` 中:
- 保留已绑定示例 `user ↔ SW22-1-001`
- 新增批量绑定工具函数(可选):用于演示环境将若干学生与新创建的用户批量绑定(仅本地开发)
- 正式环境:由管理员通过新接口进行绑定
## 验证
- 使用 MCP:
- 检查 `students` 索引与绑定结果(`count``query`
- 按用户查询学生 `get_student_by_user` 的返回
- 通过接口:
- 学生登录访问 `/dashboard/student/today` 能准确返回本人数据;未绑定返回明确错误
- 班主任、中层、校级端接口不受影响
## 变更范围
- 新增文件:`app/services/student_binding.py``app/api/v1/students.py`
- 修改文件:`app/services/dashboard.py`(去除 fallback)、`app/api/v1/router.py`(注册 students 路由)、`init_db.py`(索引与演示绑定可选)
## 回滚与安全
- 仅在 `students.user_id` 写入引用,不影响学生主键;解绑接口可恢复
- 索引为稀疏唯一,避免未绑定数据被约束;写入时做并发校验,防止竞态
请确认以上方案,我将按此落地实现并完成 MCP 与接口验证。
\ No newline at end of file
from fastapi import APIRouter
from app.api.v1 import auth, conversation, admin, dashboard
from app.api.v1 import auth, conversation, admin, dashboard, students
api_router = APIRouter()
......@@ -7,4 +7,5 @@ api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["认证"])
api_router.include_router(conversation.router, prefix="/conversations", tags=["对话"])
api_router.include_router(admin.router, prefix="/admin", tags=["管理员"])
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["仪表盘"])
\ No newline at end of file
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["仪表盘"])
api_router.include_router(students.router, prefix="/students", tags=["学生"])
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from app.api.deps import require_edition_for_mode, require_role
from app.services.student_binding import bind_user_to_student, unbind_user_from_student, get_student_by_user
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class BindPayload(BaseModel):
user_id: str
@router.post("/{student_id}/bind")
async def bind(student_id: str, payload: BindPayload, current_user: dict = Depends(require_role(2))):
ok = await bind_user_to_student(payload.user_id, student_id)
if not ok:
raise HTTPException(status_code=400, detail="用户已绑定其他学生或学生不存在")
return {"success": True}
@router.delete("/{student_id}/bind")
async def unbind(student_id: str, current_user: dict = Depends(require_role(2))):
ok = await unbind_user_from_student(student_id)
if not ok:
raise HTTPException(status_code=404, detail="学生不存在或未绑定")
return {"success": True}
@router.get("/me")
async def me(current_user: dict = Depends(require_role(1))):
s = await get_student_by_user(str(current_user["_id"]))
if not s:
raise HTTPException(status_code=404, detail="当前用户未绑定学生")
s["_id"] = str(s["_id"]) # 简化返回
return s
\ No newline at end of file
from datetime import datetime
from typing import Dict, List, Any
from bson import ObjectId
from fastapi import HTTPException
from app.db.mongodb import db
async def _today_iso() -> str:
......@@ -23,12 +24,12 @@ async def _get_student_by_user(user_id: ObjectId) -> Dict[str, Any]:
s = await db.db.students.find_one({"user_id": user_id})
if s:
return s
return await db.db.students.find_one({})
raise HTTPException(status_code=404, detail="当前用户未绑定学生")
async def student_today_summary(current_user: Dict[str, Any]) -> Dict[str, Any]:
today = await _today_iso()
weekday = await _weekday()
student = await _get_student_by_user(ObjectId(current_user["_id"]))
student = await _get_student_by_user(current_user["_id"])
class_id = student.get("class_id") if student else None
schedules: List[Dict[str, Any]] = []
......@@ -56,7 +57,16 @@ async def student_today_summary(current_user: Dict[str, Any]) -> Dict[str, Any]:
"status": a.get("status"),
})
conduct = await db.db.conduct.find_one({"student_id": student.get("student_id") if student else None, "date": today})
conduct_doc = await db.db.conduct.find_one({"student_id": student.get("student_id") if student else None, "date": today})
conduct = {}
if conduct_doc:
conduct = {
"date": conduct_doc.get("date"),
"metrics": conduct_doc.get("metrics"),
"teacher_comment": conduct_doc.get("teacher_comment"),
"head_teacher_comment": conduct_doc.get("head_teacher_comment"),
"score": conduct_doc.get("score"),
}
return {
"student": {
......@@ -66,7 +76,7 @@ async def student_today_summary(current_user: Dict[str, Any]) -> Dict[str, Any]:
},
"today_schedule": schedules,
"today_attendance": attendance,
"today_conduct": conduct or {},
"today_conduct": conduct,
}
async def homeroom_current_summary(current_user: Dict[str, Any]) -> Dict[str, Any]:
......
from bson import ObjectId
from app.db.mongodb import db
async def bind_user_to_student(user_id: str, student_id: str) -> bool:
uid = ObjectId(user_id)
exists = await db.db.students.find_one({"user_id": uid})
if exists:
return False
res = await db.db.students.update_one({"student_id": student_id}, {"$set": {"user_id": uid}})
return res.modified_count == 1
async def unbind_user_from_student(student_id: str) -> bool:
res = await db.db.students.update_one({"student_id": student_id}, {"$unset": {"user_id": ""}})
return res.modified_count == 1
async def get_student_by_user(user_id: str):
return await db.db.students.find_one({"user_id": ObjectId(user_id)})
\ No newline at end of file
......@@ -346,6 +346,7 @@ async def seed_school_data(db, mode: str):
# 索引
await db.students.create_index("student_id", unique=True)
await db.students.create_index("class_id")
await db.students.create_index("user_id", unique=True, sparse=True)
await db.classes.create_index("class_id", unique=True)
await db.classes.create_index("head_teacher_id")
await db.schedules.create_index([("classes.class_id", 1), ("weekday", 1), ("period", 1)])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment