claude_sms_test

This commit is contained in:
jianqi
2026-09-18 16:53:21 +08:00
commit b9311bdb1e
46 changed files with 4225 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# api/__init__.py
# 聚合入口:导入所有路由模块,便于统一导出(与 main.py 的导入保持一致)
from api import statistics_api, stu_score_api, cls_mgmt_api, employ_api, stu_api, teacher_api, advisor_api
__all__ = [
"statistics_api",
"stu_score_api",
"cls_mgmt_api",
"employ_api",
"stu_api",
"teacher_api",
"advisor_api",
]
+59
View File
@@ -0,0 +1,59 @@
# api/advisor_api.py
# 顾问信息管理模块:接口层只负责 HTTP 出入参,查库/校验/事务交给 dao.advisor_dao
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from dao import advisor_dao
from database import get_db
from schemas.advisor_schema import AdvisorCreate, AdvisorUpdate, AdvisorOut
router = APIRouter()
@router.get("", summary="分页查询顾问列表")
def list_advisors(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量"),
name: Optional[str] = Query(None, description="按姓名模糊查询"),
db: Session = Depends(get_db),
):
total, items = advisor_dao.list_advisors(db, page=page, size=size, name=name)
return {
"total": total,
"page": page,
"size": size,
"items": [AdvisorOut.model_validate(x) for x in items],
}
@router.get("/{adv_id}", summary="查询单个顾问详情")
def get_advisor(adv_id: str, db: Session = Depends(get_db)):
"""查询顾问详情,附带其负责的学生编号与人数。"""
advisor = advisor_dao.assert_advisor_alive(db, adv_id, "顾问不存在或已删除")
data = AdvisorOut.model_validate(advisor).model_dump()
student_ids = [s.id for s in advisor.students if not s.is_deleted]
data["student_ids"] = student_ids
data["student_count"] = len(student_ids)
return data
@router.post("", response_model=AdvisorOut, status_code=201, summary="新增顾问")
def create_advisor(payload: AdvisorCreate, db: Session = Depends(get_db)):
"""新增顾问(编号唯一,由 DAO 校验)。"""
return advisor_dao.create_advisor(db, payload.model_dump())
@router.put("/{adv_id}", response_model=AdvisorOut, summary="更新顾问信息")
def update_advisor(adv_id: str, payload: AdvisorUpdate, db: Session = Depends(get_db)):
"""按传入字段局部更新顾问信息。"""
return advisor_dao.update_advisor(db, adv_id, payload.model_dump(exclude_unset=True))
@router.delete("/{adv_id}", summary="删除顾问(软删除)")
def delete_advisor(adv_id: str, db: Session = Depends(get_db)):
"""软删除顾问。"""
advisor_dao.soft_delete_advisor(db, adv_id)
return {"message": "删除成功", "id": adv_id}
+66
View File
@@ -0,0 +1,66 @@
# api/cls_mgmt_api.py
# 班级管理模块:接口层只负责 HTTP 出入参,查库/校验/事务交给 dao.cls_mgmt_dao
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from dao import cls_mgmt_dao
from database import get_db
from schemas.cls_mgmt_schema import ClsMgmtCreate, ClsMgmtUpdate, ClsMgmtOut
router = APIRouter()
def _to_detail(cls) -> dict:
"""把班级实体组装成详情响应:基础字段 + 班主任/主讲老师姓名 + 在班人数。"""
data = ClsMgmtOut.model_validate(cls).model_dump()
data["head_teacher_name"] = cls.head_teacher.name if cls.head_teacher else None
data["lecturer_name"] = cls.lecturer.name if cls.lecturer else None
data["student_count"] = sum(1 for s in cls.stu if not s.is_deleted)
return data
@router.get("", summary="分页查询班级列表")
def list_classes(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量"),
head_tea_id: Optional[str] = Query(None, description="按班主任id过滤"),
lecturer_id: Optional[str] = Query(None, description="按主讲老师id过滤"),
db: Session = Depends(get_db),
):
total, items = cls_mgmt_dao.list_classes(
db, page=page, size=size, head_tea_id=head_tea_id, lecturer_id=lecturer_id
)
return {
"total": total,
"page": page,
"size": size,
"items": [ClsMgmtOut.model_validate(x) for x in items],
}
@router.get("/{cls_id}", summary="查询单个班级详情")
def get_class(cls_id: str, db: Session = Depends(get_db)):
"""查询班级详情,附带班主任/主讲老师姓名与在班学生数。"""
return _to_detail(cls_mgmt_dao.assert_cls_alive(db, cls_id, "班级不存在或已删除"))
@router.post("", response_model=ClsMgmtOut, status_code=201, summary="新增班级")
def create_class(payload: ClsMgmtCreate, db: Session = Depends(get_db)):
"""新增班级(编号唯一、两位教师必须存在,由 DAO 校验)。"""
return cls_mgmt_dao.create_class(db, payload.model_dump())
@router.put("/{cls_id}", response_model=ClsMgmtOut, summary="更新班级信息")
def update_class(cls_id: str, payload: ClsMgmtUpdate, db: Session = Depends(get_db)):
"""按传入字段局部更新班级信息。"""
return cls_mgmt_dao.update_class(db, cls_id, payload.model_dump(exclude_unset=True))
@router.delete("/{cls_id}", summary="删除班级(软删除)")
def delete_class(cls_id: str, db: Session = Depends(get_db)):
"""软删除班级。"""
cls_mgmt_dao.soft_delete_class(db, cls_id)
return {"message": "删除成功", "id": cls_id}
+60
View File
@@ -0,0 +1,60 @@
# api/employ_api.py
# 学生就业管理模块:接口层只负责 HTTP 出入参,查库/校验/事务交给 dao.employ_dao
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from dao import employ_dao
from database import get_db
from schemas.employ_schema import EmployCreate, EmployUpdate, EmployOut
router = APIRouter()
@router.get("", summary="分页查询就业记录")
def list_employ(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量"),
emp_company: Optional[str] = Query(None, description="按公司名称模糊查询"),
employed: Optional[bool] = Query(None, description="是否已就业(true/false)"),
db: Session = Depends(get_db),
):
total, items = employ_dao.list_employ(
db, page=page, size=size, emp_company=emp_company, employed=employed
)
return {
"total": total,
"page": page,
"size": size,
"items": [EmployOut.model_validate(x) for x in items],
}
@router.get("/{stu_id}", summary="查询单个学生的就业信息")
def get_employ(stu_id: str, db: Session = Depends(get_db)):
"""查询就业详情,附带学生姓名。"""
emp = employ_dao.assert_employ_alive(db, stu_id)
data = EmployOut.model_validate(emp).model_dump()
data["student_name"] = emp.stu.name if emp.stu else None
return data
@router.post("", response_model=EmployOut, status_code=201, summary="新增就业记录")
def create_employ(payload: EmployCreate, db: Session = Depends(get_db)):
"""新增就业记录(学生必须存在,且一对一不可重复,由 DAO 校验)。"""
return employ_dao.create_employ(db, payload.model_dump())
@router.put("/{stu_id}", response_model=EmployOut, summary="更新就业信息")
def update_employ(stu_id: str, payload: EmployUpdate, db: Session = Depends(get_db)):
"""按传入字段局部更新就业信息。"""
return employ_dao.update_employ(db, stu_id, payload.model_dump(exclude_unset=True))
@router.delete("/{stu_id}", summary="删除就业记录(软删除)")
def delete_employ(stu_id: str, db: Session = Depends(get_db)):
"""软删除就业记录。"""
employ_dao.soft_delete_employ(db, stu_id)
return {"message": "删除成功", "stu_id": stu_id}
+61
View File
@@ -0,0 +1,61 @@
# api/statistics_api.py
# 统计分析模块(动态查询与综合能力锻炼):
# 聚合 / 分组 / 关联 / 动态查询条件全部沉在 dao.statistics_dao,这里只暴露接口。
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from dao import statistics_dao
from database import get_db
router = APIRouter()
@router.get("/overview", summary="系统总览统计")
def overview(db: Session = Depends(get_db)):
"""统计学生、班级、教师、顾问、已就业人数及就业率。"""
return statistics_dao.overview(db)
@router.get("/student/gender", summary="学生性别分布")
def student_gender_distribution(db: Session = Depends(get_db)):
return statistics_dao.student_gender_distribution(db)
@router.get("/student/education", summary="学生学历分布")
def student_education_distribution(db: Session = Depends(get_db)):
return statistics_dao.student_education_distribution(db)
@router.get("/student/state", summary="学生就读状态分布")
def student_state_distribution(db: Session = Depends(get_db)):
return statistics_dao.student_state_distribution(db)
@router.get("/student/hometown", summary="学生生源地分布(Top N)")
def student_hometown_distribution(
limit: int = Query(10, ge=1, le=100, description="返回前 N 个生源地"),
db: Session = Depends(get_db),
):
return statistics_dao.student_hometown_distribution(db, limit=limit)
@router.get("/score/summary", summary="成绩综合统计(平均/最高/最低/及格率)")
def score_summary(
cls_id: Optional[str] = Query(None, description="按班级过滤(不传则统计全部)"),
db: Session = Depends(get_db),
):
"""统计平均分、最高分、最低分、及格人数与及格率,可动态按班级过滤(关联 stu_info)。"""
return statistics_dao.score_summary(db, cls_id=cls_id)
@router.get("/score/distribution", summary="成绩等级分布")
def score_level_distribution(db: Session = Depends(get_db)):
return statistics_dao.score_level_distribution(db)
@router.get("/employ/summary", summary="就业综合统计(就业率/薪资)")
def employ_summary(db: Session = Depends(get_db)):
return statistics_dao.employ_summary(db)
+100
View File
@@ -0,0 +1,100 @@
# api/stu_api.py
# 学生信息管理模块:接口层只负责 HTTP 出入参与响应组装,查库/校验/事务交给 dao.stu_dao
# 业务异常(NotFoundError / ConflictError)由 main.py 注册的全局处理器统一转成状态码
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from dao import stu_dao
from database import get_db
from schemas.stu_schema import StuInfoCreate, StuInfoUpdate, StuInfoOut
router = APIRouter()
def _to_detail(stu) -> dict:
"""把学生实体组装成详情响应:基础字段 + 班级 / 顾问 / 成绩 / 就业信息。"""
data = StuInfoOut.model_validate(stu).model_dump()
# 班级信息
data["cls"] = None
if stu.cls:
data["cls"] = {
"id": stu.cls.id,
"cls_start_date": stu.cls.cls_start_date.isoformat() if stu.cls.cls_start_date else None,
}
# 顾问信息
data["advisor"] = {"id": stu.adv.id, "name": stu.adv.name} if stu.adv else None
# 成绩列表(过滤已删除)
data["scores"] = [
{
"exam_attempt": s.exam_attempt,
"exam_score": s.exam_score,
"score_level": s.score_level,
}
for s in stu.score
if not s.is_deleted
]
# 就业信息(一对一)
data["employ"] = None
if stu.stu_emp_mgmt and not stu.stu_emp_mgmt.is_deleted:
e = stu.stu_emp_mgmt
data["employ"] = {
"emp_company": e.emp_company,
"salary": e.salary,
"emp_open_time": e.emp_open_time.isoformat() if e.emp_open_time else None,
"send_offer_time": e.send_offer_time.isoformat() if e.send_offer_time else None,
}
return data
@router.get("", summary="分页查询学生列表")
def list_students(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量"),
name: Optional[str] = Query(None, description="按姓名模糊查询"),
cls_id: Optional[str] = Query(None, description="按班级编号过滤"),
state: Optional[str] = Query(None, description="按状态过滤"),
db: Session = Depends(get_db),
):
"""查询未删除的学生,支持分页和按姓名/班级/状态过滤。"""
total, items = stu_dao.list_students(
db, page=page, size=size, name=name, cls_id=cls_id, state=state
)
return {
"total": total,
"page": page,
"size": size,
"items": [StuInfoOut.model_validate(x) for x in items],
}
@router.get("/{stu_id}", summary="查询单个学生详情")
def get_student(stu_id: str, db: Session = Depends(get_db)):
"""查询学生详情,并附带班级、顾问、成绩与就业信息。"""
return _to_detail(stu_dao.assert_stu_alive(db, stu_id, "学生不存在或已删除"))
@router.post("", response_model=StuInfoOut, status_code=201, summary="新增学生")
def create_student(payload: StuInfoCreate, db: Session = Depends(get_db)):
"""新增学生(学号唯一、班级与顾问必须存在,由 DAO 校验)。"""
return stu_dao.create_student(db, payload.model_dump())
@router.put("/{stu_id}", response_model=StuInfoOut, summary="更新学生信息")
def update_student(stu_id: str, payload: StuInfoUpdate, db: Session = Depends(get_db)):
"""按传入字段局部更新学生信息。"""
return stu_dao.update_student(db, stu_id, payload.model_dump(exclude_unset=True))
@router.delete("/{stu_id}", summary="删除学生(软删除)")
def delete_student(stu_id: str, db: Session = Depends(get_db)):
"""软删除学生:仅置 is_deleted 标记,数据保留。"""
stu_dao.soft_delete_student(db, stu_id)
return {"message": "删除成功", "id": stu_id}
+62
View File
@@ -0,0 +1,62 @@
# api/stu_score_api.py
# 学生成绩管理模块:接口层只负责 HTTP 出入参,等级判定与查库交给 dao.stu_score_dao
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from dao import stu_score_dao
from database import get_db
from schemas.stu_score_schema import StuScoreCreate, StuScoreUpdate, StuScoreOut
router = APIRouter()
@router.get("", summary="分页查询成绩列表")
def list_scores(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量"),
stu_id: Optional[str] = Query(None, description="按学生编号过滤"),
score_level: Optional[str] = Query(None, description="按成绩等级过滤"),
db: Session = Depends(get_db),
):
total, items = stu_score_dao.list_scores(
db, page=page, size=size, stu_id=stu_id, score_level=score_level
)
return {
"total": total,
"page": page,
"size": size,
"items": [StuScoreOut.model_validate(x) for x in items],
}
@router.get("/student/{stu_id}", summary="查询某学生的全部成绩")
def get_student_scores(stu_id: str, db: Session = Depends(get_db)):
scores = stu_score_dao.list_scores_by_student(db, stu_id)
return {"stu_id": stu_id, "items": [StuScoreOut.model_validate(s) for s in scores]}
@router.get("/{score_id}", response_model=StuScoreOut, summary="查询单条成绩记录")
def get_score(score_id: int, db: Session = Depends(get_db)):
return stu_score_dao.assert_score_alive(db, score_id)
@router.post("", response_model=StuScoreOut, status_code=201, summary="新增成绩记录")
def create_score(payload: StuScoreCreate, db: Session = Depends(get_db)):
"""新增成绩:未显式指定等级时,由 DAO 按分数自动判定。"""
return stu_score_dao.create_score(db, payload.model_dump())
@router.put("/{score_id}", response_model=StuScoreOut, summary="更新成绩记录")
def update_score(score_id: int, payload: StuScoreUpdate, db: Session = Depends(get_db)):
"""更新成绩:只改分数没改等级时,由 DAO 自动重判等级。"""
return stu_score_dao.update_score(db, score_id, payload.model_dump(exclude_unset=True))
@router.delete("/{score_id}", summary="删除成绩记录(软删除)")
def delete_score(score_id: int, db: Session = Depends(get_db)):
"""软删除成绩记录。"""
stu_score_dao.soft_delete_score(db, score_id)
return {"message": "删除成功", "id": score_id}
+59
View File
@@ -0,0 +1,59 @@
# api/teacher_api.py
# 教师信息管理模块:接口层只负责 HTTP 出入参,查库/校验/事务交给 dao.teacher_dao
from typing import Optional
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from dao import teacher_dao
from database import get_db
from schemas.teacher_schema import TeacherCreate, TeacherUpdate, TeacherOut
router = APIRouter()
@router.get("", summary="分页查询教师列表")
def list_teachers(
page: int = Query(1, ge=1, description="页码"),
size: int = Query(10, ge=1, le=100, description="每页数量"),
name: Optional[str] = Query(None, description="按姓名模糊查询"),
type: Optional[str] = Query(None, description="按职位过滤"),
db: Session = Depends(get_db),
):
total, items = teacher_dao.list_teachers(db, page=page, size=size, name=name, type=type)
return {
"total": total,
"page": page,
"size": size,
"items": [TeacherOut.model_validate(x) for x in items],
}
@router.get("/{tea_id}", summary="查询单个教师详情")
def get_teacher(tea_id: str, db: Session = Depends(get_db)):
"""查询教师详情,附带其担任班主任/主讲老师的班级编号。"""
teacher = teacher_dao.assert_teacher_alive(db, tea_id, "教师不存在或已删除")
data = TeacherOut.model_validate(teacher).model_dump()
data["head_classes"] = [c.id for c in teacher.head_classes if not c.is_deleted]
data["lecturer_classes"] = [c.id for c in teacher.lecturer_classes if not c.is_deleted]
return data
@router.post("", response_model=TeacherOut, status_code=201, summary="新增教师")
def create_teacher(payload: TeacherCreate, db: Session = Depends(get_db)):
"""新增教师(编号与电话唯一,由 DAO 校验)。"""
return teacher_dao.create_teacher(db, payload.model_dump())
@router.put("/{tea_id}", response_model=TeacherOut, summary="更新教师信息")
def update_teacher(tea_id: str, payload: TeacherUpdate, db: Session = Depends(get_db)):
"""按传入字段局部更新教师信息,改电话时由 DAO 重新校验唯一。"""
return teacher_dao.update_teacher(db, tea_id, payload.model_dump(exclude_unset=True))
@router.delete("/{tea_id}", summary="删除教师(软删除)")
def delete_teacher(tea_id: str, db: Session = Depends(get_db)):
"""软删除教师。"""
teacher_dao.soft_delete_teacher(db, tea_id)
return {"message": "删除成功", "id": tea_id}