Commit d149e42b authored by uuo00_n's avatar uuo00_n

feat(api): 为多个API端点添加响应模型定义

为teachers、persons、classes、conversation、schedules、students、bindings和dashboard模块的API端点添加响应模型定义,确保返回数据结构的明确性和类型安全
parent 35d99c7d
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional
from app.api.deps import require_edition_for_mode, get_current_active_user, require_role
from app.services.bindings import create_binding, delete_binding, get_binding_by_account
......@@ -10,12 +11,23 @@ class BindingPayload(BaseModel):
type: str
primary: bool = True
class ActionResult(BaseModel):
success: bool
class BindingOut(BaseModel):
_id: str
account_id: str
person_id: str
type: str
primary: bool
@router.post(
"",
summary="创建主绑定",
description="为当前账号创建人物主绑定(student/teacher),同类型主绑定唯一。",
response_model=ActionResult,
)
async def bind(payload: BindingPayload, current_user: dict = Depends(require_role(2))):
async def bind(payload: BindingPayload, current_user: dict = Depends(require_role(2))) -> ActionResult:
ok = await create_binding(str(current_user["_id"]), payload.person_id, payload.type, payload.primary)
if not ok:
raise HTTPException(status_code=400, detail="主绑定已存在或参数错误")
......@@ -25,8 +37,9 @@ async def bind(payload: BindingPayload, current_user: dict = Depends(require_rol
"/{person_id}",
summary="删除绑定",
description="删除当前账号与指定人物的绑定。",
response_model=ActionResult,
)
async def unbind(person_id: str, current_user: dict = Depends(require_role(2))):
async def unbind(person_id: str, current_user: dict = Depends(require_role(2))) -> ActionResult:
ok = await delete_binding(str(current_user["_id"]), person_id)
if not ok:
raise HTTPException(status_code=404, detail="未找到绑定")
......@@ -36,8 +49,9 @@ async def unbind(person_id: str, current_user: dict = Depends(require_role(2))):
"/me",
summary="查询当前账号的主绑定",
description="返回当前账号的主绑定信息(account_id/person_id/type/primary)。",
response_model=BindingOut,
)
async def me(current_user: dict = Depends(get_current_active_user)):
async def me(current_user: dict = Depends(get_current_active_user)) -> BindingOut:
b = await get_binding_by_account(str(current_user["_id"]))
if not b:
raise HTTPException(status_code=404, detail="未绑定人物")
......
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from bson import ObjectId
from app.api.deps import require_edition_for_mode, require_role
from app.db.mongodb import db
......@@ -9,12 +10,22 @@ router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class HeadTeacherPayload(BaseModel):
head_teacher_person_id: str
class ActionResult(BaseModel):
success: bool
class ClassOut(BaseModel):
_id: Optional[str] = None
class_id: str
head_teacher_person_id: Optional[str] = None
students_count: Optional[int] = 0
@router.put(
"/{class_id}/head-teacher",
summary="设置班主任人物",
description="为班级设置 head_teacher_person_id(指向教师人物)。",
response_model=ActionResult,
)
async def set_head_teacher(class_id: str, payload: HeadTeacherPayload, current_user: dict = Depends(require_role(3))):
async def set_head_teacher(class_id: str, payload: HeadTeacherPayload, current_user: dict = Depends(require_role(3))) -> ActionResult:
res = await db.db.classes.update_one({"class_id": class_id}, {"$set": {"head_teacher_person_id": ObjectId(payload.head_teacher_person_id)}})
if res.matched_count == 0:
raise HTTPException(status_code=404, detail="班级不存在")
......@@ -24,8 +35,9 @@ async def set_head_teacher(class_id: str, payload: HeadTeacherPayload, current_u
"",
summary="列出班级",
description="按 class_id 排序列出班级信息(含 head_teacher_person_id)。",
response_model=List[ClassOut],
)
async def list_classes(current_user: dict = Depends(require_role(2))):
async def list_classes(current_user: dict = Depends(require_role(2))) -> List[ClassOut]:
res = []
cursor = db.db.classes.find({}).sort("class_id", 1)
async for d in cursor:
......
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List
from app.api.deps import get_current_active_user, require_edition_for_mode
from app.schemas.conversation import MessageCreate, ConversationResponse
from app.schemas.conversation import MessageCreate, ConversationDocOut, CreatedId, MessageSendResult
from app.services.conversation import create_conversation, get_conversation, add_message, get_user_conversations
# 在路由层挂载版别运行模式依赖,限制仅允许当前模式的用户访问
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
@router.post("/", status_code=status.HTTP_201_CREATED)
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=CreatedId)
async def create_new_conversation(current_user: dict = Depends(get_current_active_user)):
"""创建新对话"""
conversation_id = await create_conversation(str(current_user["_id"]))
return {"id": conversation_id}
@router.get("/", response_model=list)
@router.get("/", response_model=List[ConversationDocOut])
async def list_conversations(current_user: dict = Depends(get_current_active_user)):
"""获取用户的所有对话"""
conversations = await get_user_conversations(str(current_user["_id"]))
return conversations
@router.get("/{conversation_id}", response_model=dict)
@router.get("/{conversation_id}", response_model=ConversationDocOut)
async def get_single_conversation(
conversation_id: str,
current_user: dict = Depends(get_current_active_user)
......@@ -32,7 +33,7 @@ async def get_single_conversation(
)
return conversation
@router.post("/{conversation_id}/messages")
@router.post("/{conversation_id}/messages", response_model=MessageSendResult)
async def send_message(
conversation_id: str,
message: MessageCreate,
......
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from app.api.deps import require_edition_for_mode, require_role, require_binding
from app.services.dashboard import (
student_today_summary,
......@@ -9,34 +11,112 @@ from app.services.dashboard import (
router = APIRouter(dependencies=[Depends(require_edition_for_mode())])
class StudentTodaySchedule(BaseModel):
lesson_id: Optional[str]
period: Optional[int]
course_name: Optional[str]
location: Optional[str]
class StudentTodayAttendance(BaseModel):
lesson_id: Optional[str]
status: Optional[str]
class StudentTodayConduct(BaseModel):
date: Optional[str]
metrics: Optional[Dict[str, Any]]
teacher_comment: Optional[str]
head_teacher_comment: Optional[str]
score: Optional[float]
class StudentTodaySummary(BaseModel):
student: Dict[str, Optional[str]]
today_schedule: List[StudentTodaySchedule]
today_attendance: List[StudentTodayAttendance]
today_conduct: Dict[str, Any]
class HomeroomLesson(BaseModel):
class_id: Optional[str]
course_name: Optional[str]
location: Optional[str]
class HomeroomRate(BaseModel):
class_id: Optional[str]
present: int
total: int
rate: float
class HomeroomLeave(BaseModel):
student_id: Optional[str]
class_id: Optional[str]
reason: Optional[str]
status: Optional[str]
class DirectiveItem(BaseModel):
content: Optional[str]
created_at: Optional[str]
class HomeroomCurrentSummary(BaseModel):
current_lessons: List[HomeroomLesson]
attendance_rates: List[HomeroomRate]
leaves: List[HomeroomLeave]
directives: List[DirectiveItem]
class DepartmentTeacherRate(BaseModel):
teacher_id: Optional[str]
present_slots: int
total_slots: int
rate: float
class DepartmentStudentsAttendance(BaseModel):
total: int
present: int
absent_or_leave: int
class DepartmentOverview(BaseModel):
students_attendance: DepartmentStudentsAttendance
teacher_attendance_rates: List[DepartmentTeacherRate]
anomalies: List[Dict[str, Any]]
directives: List[Dict[str, Any]]
class CampusOverview(BaseModel):
total_students: int
present: int
leaves: int
directives: int
term_goals: List[Dict[str, Any]]
department_progress: List[Dict[str, Any]]
@router.get(
"/student/today",
summary="学生端:今日个人课表、出勤与操行",
description="需角色等级≥1且主绑定为学生;返回当天课表(共享节次按班级定位教室)、今日出勤记录与操行评语。"
, response_model=StudentTodaySummary
)
async def student_today(current_user: dict = Depends(require_role(1)), _b: dict = Depends(require_binding("student"))):
async def student_today(current_user: dict = Depends(require_role(1)), _b: dict = Depends(require_binding("student"))) -> StudentTodaySummary:
return await student_today_summary(current_user)
@router.get(
"/homeroom/current",
summary="班主任端:当前节次课程与地点、出勤率、请假、指示",
description="需角色等级≥2且主绑定为教师;按 head_teacher_person_id 定位所辖班,统计当前节次的课程与地点、节次出勤率、今日请假与部门指示。"
, response_model=HomeroomCurrentSummary
)
async def homeroom_current(current_user: dict = Depends(require_role(2)), _b: dict = Depends(require_binding("teacher"))):
async def homeroom_current(current_user: dict = Depends(require_role(2)), _b: dict = Depends(require_binding("teacher"))) -> HomeroomCurrentSummary:
return await homeroom_current_summary(current_user)
@router.get(
"/department/overview",
summary="中层端:教师节次出勤率、学生出勤、异常班级、指示",
description="需角色等级≥3;按今日工作日统计每位教师的节次出勤率、全校学生出勤聚合、异常班级(缺勤+请假占比>0.3)与近期部门/校园指示。"
, response_model=DepartmentOverview
)
async def department(current_user: dict = Depends(require_role(3))):
async def department(current_user: dict = Depends(require_role(3))) -> DepartmentOverview:
return await department_overview(current_user)
@router.get(
"/campus/overview",
summary="校级端:校园整体总览",
description="需角色等级≥4;返回学生总数、今日出勤、请假与指示数量等宏观数据。"
, response_model=CampusOverview
)
async def campus(current_user: dict = Depends(require_role(4))):
async def campus(current_user: dict = Depends(require_role(4))) -> CampusOverview:
return await campus_overview(current_user)
\ No newline at end of file
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from typing import List
from typing import List, Dict
from bson import ObjectId
from app.api.deps import require_edition_for_mode, require_role
from app.db.mongodb import db
......@@ -12,12 +12,26 @@ class PersonCreate(BaseModel):
name: str
type: str = Field(pattern="^(student|teacher|staff)$")
class BulkInsertResult(BaseModel):
inserted: int
class PersonOut(BaseModel):
_id: str
person_id: str
name: str
type: str
class PersonsListResponse(BaseModel):
items: List[PersonOut]
counts_by_type: Dict[str, int]
@router.post(
"/bulk",
summary="批量导入人物档案",
description="导入学生/教师/职员的基础人物信息(person_id/name/type)。",
response_model=BulkInsertResult,
)
async def bulk_create(persons: List[PersonCreate], current_user: dict = Depends(require_role(3))):
async def bulk_create(persons: List[PersonCreate], current_user: dict = Depends(require_role(3))) -> BulkInsertResult:
docs = [{"person_id": p.person_id, "name": p.name, "type": p.type} for p in persons]
if not docs:
raise HTTPException(status_code=400, detail="空数据")
......@@ -28,8 +42,9 @@ async def bulk_create(persons: List[PersonCreate], current_user: dict = Depends(
"",
summary="列出人物档案",
description="按 person_id 排序列出所有人物档案。",
response_model=PersonsListResponse,
)
async def list_persons(current_user: dict = Depends(require_role(3))):
async def list_persons(current_user: dict = Depends(require_role(3))) -> PersonsListResponse:
res = []
counts = {}
cursor = db.db.persons.find({}).sort("person_id", 1)
......
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from bson import ObjectId
from app.api.deps import require_edition_for_mode, require_role
from app.db.mongodb import db
......@@ -10,12 +11,24 @@ class AssignTeacherPayload(BaseModel):
lesson_id: str
teacher_person_id: str
class ActionResult(BaseModel):
success: bool
class ScheduleOut(BaseModel):
lesson_id: Optional[str]
weekday: Optional[int]
period: Optional[int]
course_name: Optional[str]
teacher_person_id: Optional[str]
classes: List[Dict[str, Any]]
@router.put(
"/assign-teacher",
summary="为节次设置任课教师人物",
description="将指定 lesson_id 的节次设置为 teacher_person_id(教师人物)。",
response_model=ActionResult,
)
async def assign_teacher(payload: AssignTeacherPayload, current_user: dict = Depends(require_role(3))):
async def assign_teacher(payload: AssignTeacherPayload, current_user: dict = Depends(require_role(3))) -> ActionResult:
res = await db.db.schedules.update_one({"lesson_id": payload.lesson_id}, {"$set": {"teacher_person_id": ObjectId(payload.teacher_person_id)}})
if res.matched_count == 0:
raise HTTPException(status_code=404, detail="节次不存在")
......@@ -25,8 +38,9 @@ async def assign_teacher(payload: AssignTeacherPayload, current_user: dict = Dep
"",
summary="列出课表节次",
description="按工作日与节次排序列出共享节次(含各班 location 与 teacher_person_id)。",
response_model=List[ScheduleOut],
)
async def list_schedules(current_user: dict = Depends(require_role(2))):
async def list_schedules(current_user: dict = Depends(require_role(2))) -> List[ScheduleOut]:
res = []
cursor = db.db.schedules.find({}).sort([("weekday", 1), ("period", 1)])
async for d in cursor:
......
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional, Dict, Any
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
......@@ -8,22 +9,31 @@ 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))):
class ActionResult(BaseModel):
success: bool
class StudentOut(BaseModel):
_id: str
student_id: Optional[str] = None
name: Optional[str] = None
class_id: Optional[str] = None
@router.post("/{student_id}/bind", response_model=ActionResult)
async def bind(student_id: str, payload: BindPayload, current_user: dict = Depends(require_role(2))) -> ActionResult:
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))):
@router.delete("/{student_id}/bind", response_model=ActionResult)
async def unbind(student_id: str, current_user: dict = Depends(require_role(2))) -> ActionResult:
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))):
@router.get("/me", response_model=StudentOut)
async def me(current_user: dict = Depends(require_role(1))) -> StudentOut:
s = await get_student_by_user(str(current_user["_id"]))
if not s:
raise HTTPException(status_code=404, detail="当前用户未绑定学生")
......
......@@ -14,12 +14,24 @@ class TeacherCreate(BaseModel):
roles: List[str]
account_id: Optional[str] = None
class BulkInsertResult(BaseModel):
inserted: int
class TeacherOut(BaseModel):
_id: str
person_id: str
teacher_id: str
department: str
roles: List[str]
account_id: Optional[str] = None
@router.post(
"/bulk",
summary="批量导入教师实体",
description="导入教师实体(person_id/teacher_id/department/roles),可选绑定 account_id。",
response_model=BulkInsertResult,
)
async def bulk_create(teachers: List[TeacherCreate], current_user: dict = Depends(require_role(3))):
async def bulk_create(teachers: List[TeacherCreate], current_user: dict = Depends(require_role(3))) -> BulkInsertResult:
docs = []
for t in teachers:
doc = {
......@@ -40,8 +52,9 @@ async def bulk_create(teachers: List[TeacherCreate], current_user: dict = Depend
"",
summary="列出教师实体",
description="按 teacher_id 排序列出所有教师实体(包含 person_id/account_id)。",
response_model=List[TeacherOut],
)
async def list_teachers(current_user: dict = Depends(require_role(3))):
async def list_teachers(current_user: dict = Depends(require_role(3))) -> List[TeacherOut]:
res = []
cursor = db.db.teachers.find({}).sort("teacher_id", 1)
async for d in cursor:
......
......@@ -16,4 +16,19 @@ class ConversationResponse(BaseModel):
id: str
messages: List[MessageResponse]
created_at: datetime
updated_at: datetime
\ No newline at end of file
updated_at: datetime
class ConversationDocOut(BaseModel):
_id: str
user_id: Optional[str] = None
messages: List[MessageResponse]
created_at: datetime
updated_at: datetime
class CreatedId(BaseModel):
id: str
class MessageSendResult(BaseModel):
contains_sensitive_words: bool
sensitive_words_found: List[str]
assistant_response: str
\ No newline at end of file
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