commit e1491ead63764aa650c3f54eeb039203ebd5d9f8
Author: 倾音节 <17608307+sansanbusan@user.noreply.gitee.com>
Date: Mon Sep 21 19:14:15 2026 +0800
学生信息管理系统
diff --git a/sqlalchemy_fastapi_demo/.dockerignore b/sqlalchemy_fastapi_demo/.dockerignore
new file mode 100644
index 0000000..01b463a
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/.dockerignore
@@ -0,0 +1,11 @@
+.venv
+venv
+__pycache__
+*.pyc
+*.pyo
+.git
+.idea
+.vscode
+*.sqlite3
+*.db
+.env
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/.idea/.gitignore b/sqlalchemy_fastapi_demo/.idea/.gitignore
new file mode 100644
index 0000000..f6906f2
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/.idea/.gitignore
@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/sqlalchemy_fastapi_demo/.idea/inspectionProfiles/profiles_settings.xml b/sqlalchemy_fastapi_demo/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/.idea/misc.xml b/sqlalchemy_fastapi_demo/.idea/misc.xml
new file mode 100644
index 0000000..959f330
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/.idea/modules.xml b/sqlalchemy_fastapi_demo/.idea/modules.xml
new file mode 100644
index 0000000..724d6c7
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/.idea/sqlalchemy_fastapi_demo.iml b/sqlalchemy_fastapi_demo/.idea/sqlalchemy_fastapi_demo.iml
new file mode 100644
index 0000000..afc0918
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/.idea/sqlalchemy_fastapi_demo.iml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/Dockerfile b/sqlalchemy_fastapi_demo/Dockerfile
new file mode 100644
index 0000000..ce91c5b
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/Dockerfile
@@ -0,0 +1,21 @@
+# 1. 使用官方的 Python 轻量级基础镜像
+FROM python:3.11-slim
+
+# 2. 设置容器内的工作目录
+WORKDIR /app
+
+# 3. 复制依赖文件到工作目录
+COPY requirements.txt .
+
+# 4. 安装依赖 (使用清华源可加速,不需要可去掉 -i 参数)
+RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+
+# 5. 复制当前目录下的所有代码到容器工作目录
+COPY . .
+
+# 6. 暴露 FastAPI 默认端口 (根据你的项目实际端口调整)
+EXPOSE 8000
+
+# 7. 启动命令。假设你的入口文件是 main.py,FastAPI 实例名为 app
+# 注意:必须是 0.0.0.0,不能是 127.0.0.1,否则容器外无法访问
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/__init__.py b/sqlalchemy_fastapi_demo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sqlalchemy_fastapi_demo/__pycache__/__init__.cpython-310.pyc b/sqlalchemy_fastapi_demo/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..8e85fdd
Binary files /dev/null and b/sqlalchemy_fastapi_demo/__pycache__/__init__.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/__pycache__/__init__.cpython-312.pyc b/sqlalchemy_fastapi_demo/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..2156533
Binary files /dev/null and b/sqlalchemy_fastapi_demo/__pycache__/__init__.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/__pycache__/database.cpython-310.pyc b/sqlalchemy_fastapi_demo/__pycache__/database.cpython-310.pyc
new file mode 100644
index 0000000..2b4f693
Binary files /dev/null and b/sqlalchemy_fastapi_demo/__pycache__/database.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/__pycache__/database.cpython-312.pyc b/sqlalchemy_fastapi_demo/__pycache__/database.cpython-312.pyc
new file mode 100644
index 0000000..ce53fe4
Binary files /dev/null and b/sqlalchemy_fastapi_demo/__pycache__/database.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/__pycache__/main.cpython-310.pyc b/sqlalchemy_fastapi_demo/__pycache__/main.cpython-310.pyc
new file mode 100644
index 0000000..3490d0f
Binary files /dev/null and b/sqlalchemy_fastapi_demo/__pycache__/main.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/__pycache__/main.cpython-312.pyc b/sqlalchemy_fastapi_demo/__pycache__/main.cpython-312.pyc
new file mode 100644
index 0000000..bed3378
Binary files /dev/null and b/sqlalchemy_fastapi_demo/__pycache__/main.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/Score.py b/sqlalchemy_fastapi_demo/api/Score.py
new file mode 100644
index 0000000..134bc10
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/api/Score.py
@@ -0,0 +1,132 @@
+
+from fastapi import Query, Depends, HTTPException, APIRouter
+from sqlalchemy.orm import Session
+from dao.Score import ScoreDAO
+from database import get_db
+from scheme.Score import ScoreCreate, ScoreUpdate
+
+app_score = APIRouter()
+
+# 查询所有成绩
+@app_score.get("/",summary='查询所有成绩')
+async def get_scores(
+ skip: int = Query(0, ge=0, description="跳过的记录数"),
+ limit: int = Query(100, ge=1, le=200, description="返回的最大记录数"),
+ db: Session = Depends(get_db)
+):
+ scores=ScoreDAO.get_all(db, skip=skip, limit=limit)
+ return scores
+
+# 查询单个数据
+@app_score.get("/{stu_id}/{exam_id}",summary='查询单个成绩')
+async def get_score(
+ stu_id: int,
+ exam_id: int,
+ db: Session = Depends(get_db)
+):
+ score=ScoreDAO.get_by_id(db, stu_id,exam_id)
+ if not score:
+ raise HTTPException(status_code=404, detail="学生不存在")
+ return score
+
+# 创建学生
+@app_score.post("/",summary='创建学生成绩')
+async def create_scores(
+ score:ScoreCreate,
+ db: Session = Depends(get_db)
+):
+ # 检查学生在不在学生表
+ existing_foreign_key=ScoreDAO.inspect_stu_id_unq(db, score.stu_id)
+ if not existing_foreign_key:
+ raise HTTPException(status_code=404, detail="该学生不存在")
+ if existing_foreign_key.is_deleted == 1:
+ raise HTTPException(status_code=404, detail="该学生不存在")
+ # 检查成绩是否已被占用
+ existing_score=ScoreDAO.get_by_id(db, score.stu_id,score.exam_id)
+ if existing_score:
+ raise HTTPException(status_code=400, detail="学生成绩已存在,请勿重复添加")
+ new_score=ScoreDAO.post_score(db,score)
+ return new_score
+
+# 修改
+@app_score.put("/{stu_id}/{exam_id}",summary='修改信息')
+async def update_scores(
+ stu_id: int,
+ exam_id: int,
+ score:ScoreUpdate,
+ db: Session = Depends(get_db)
+):
+ # 只允许修改成绩,学生id和考试次序不允许修改,SQLAlchemy不能直接更新联合主键本身
+ # 检查成绩是否已存在
+ existing_foreign_key = ScoreDAO.inspect_stu_id_unq(db, score.stu_id)
+ if not existing_foreign_key:
+ raise HTTPException(status_code=404, detail="该学生不存在")
+ existing_score=ScoreDAO.get_by_id(db, stu_id,exam_id)
+ if not existing_score:
+ raise HTTPException(status_code=404, detail="学生成绩不存在")
+ # 直接调用DAO更新,只更新score字段
+ b = ScoreDAO.put_score(db, stu_id, exam_id, score)
+ return b
+
+# 修改成绩:支持修改 stu_id、exam_id、score;原理:删除旧记录,新增新记录
+ # stu_id、exam_id是联合主键。MySQL不允许update 直接修改主键字段,SQLAlchemy
+ # 底层执行会抛数据库异常。所以采用「删旧、建新」的方案模拟修改
+
+ # 风险大
+ # 现在代码:删旧和新增是两次
+ # db.commit()。
+ # 如果删旧成功,新增中途崩掉,会出现旧数据没了,新数据没加上,数据丢失。
+
+ # 数据库这条记录实际上是新行,不是原来那一行。
+ # 如果别的表有外键引用这条成绩记录,这个方案会出问题(旧记录删掉,关联跟着没了)。
+# @app_score.put("/{stu_id}/{exam_id}",summary='修改学生成绩(支持修改学生ID、考核序次、分数)')
+# async def update_scores(
+# stu_id: int,
+# exam_id: int,
+# score:ScoreUpdate,
+# db: Session = Depends(get_db)
+# ):
+# # 1.找到旧记录
+# old_score = ScoreDAO.get_by_id(db, stu_id, exam_id)
+# if not old_score:
+# raise HTTPException(status_code=404, detail="待修改的成绩记录不存在")
+# # 2.确定新值:前端没传的字段,沿用旧记录的值
+# new_stu = score.stu_id if score.stu_id is not None else old_score.stu_id
+# new_exam_id = score.exam_id if score.exam_id is not None else old_score.exam_id
+# new_score_val = score.score if score.score is not None else old_score.score
+# # 3.校验新stu_id学生是否存在
+# if new_stu != old_score.stu_id:
+# stu_obj = ScoreDAO.inspect_stu_id_unq(db, new_stu)
+# if not stu_obj:
+# raise HTTPException(status_code=404, detail="目标学生不存在")
+# # 4.如果新主键组合和旧的不一样,要检查新组合是否冲突
+# if (new_stu, new_exam_id) != (old_score.stu_id, old_score.exam_id):
+# conflict = ScoreDAO.get_by_id(db, new_stu, new_exam_id)
+# if conflict:
+# raise HTTPException(status_code=400, detail="新【学生ID+考核序次】组合已存在,不能使用")
+# # 核心逻辑:删除旧记录,新增一条全新记录
+# # 删除旧数据
+# ScoreDAO.delete_score(db, stu_id, exam_id)
+# # 组装成ScoreCreate对象,调用新增DAO
+# from scheme.Score import ScoreCreate
+# new_create_data = ScoreCreate(
+# stu_id=new_stu,
+# exam_id=new_exam_id,
+# score=new_score_val
+# )
+# new_db_score = ScoreDAO.post_score(db, new_create_data)
+# return new_db_score
+
+
+
+# 删除
+@app_score.delete("/{stu_id}/{exam_id}",summary='删除学生成绩')
+async def delete_scores(
+ stu_id: int,
+ exam_id: int,
+ db: Session = Depends(get_db)
+):
+ score=ScoreDAO.delete_score(db, stu_id,exam_id)
+ if not score:
+ raise HTTPException(status_code=404, detail="学生成绩不存在")
+ return {"msg":"删除成功"}
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/api/__init__.py b/sqlalchemy_fastapi_demo/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/Score.cpython-310.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/Score.cpython-310.pyc
new file mode 100644
index 0000000..fcfb952
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/Score.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/Score.cpython-312.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/Score.cpython-312.pyc
new file mode 100644
index 0000000..ebb333c
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/Score.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/__init__.cpython-310.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..f8898a4
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/__init__.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/__init__.cpython-312.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..e4ee2a3
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/__init__.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/advisors.cpython-310.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/advisors.cpython-310.pyc
new file mode 100644
index 0000000..ec1265a
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/advisors.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/advisors.cpython-312.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/advisors.cpython-312.pyc
new file mode 100644
index 0000000..5253574
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/advisors.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/c_lass.cpython-310.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/c_lass.cpython-310.pyc
new file mode 100644
index 0000000..e6f1bd0
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/c_lass.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/c_lass.cpython-312.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/c_lass.cpython-312.pyc
new file mode 100644
index 0000000..4a40239
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/c_lass.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/employment_api.cpython-310.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/employment_api.cpython-310.pyc
new file mode 100644
index 0000000..7f5d32d
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/employment_api.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/employment_api.cpython-312.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/employment_api.cpython-312.pyc
new file mode 100644
index 0000000..652cfa2
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/employment_api.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/statistics.cpython-310.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/statistics.cpython-310.pyc
new file mode 100644
index 0000000..ab56b07
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/statistics.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/students.cpython-310.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/students.cpython-310.pyc
new file mode 100644
index 0000000..de1ed86
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/students.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/students.cpython-312.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/students.cpython-312.pyc
new file mode 100644
index 0000000..900f83c
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/students.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/teachers.cpython-310.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/teachers.cpython-310.pyc
new file mode 100644
index 0000000..b05e847
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/teachers.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/__pycache__/teachers.cpython-312.pyc b/sqlalchemy_fastapi_demo/api/__pycache__/teachers.cpython-312.pyc
new file mode 100644
index 0000000..41b26da
Binary files /dev/null and b/sqlalchemy_fastapi_demo/api/__pycache__/teachers.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/api/advisors.py b/sqlalchemy_fastapi_demo/api/advisors.py
new file mode 100644
index 0000000..2c5effc
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/api/advisors.py
@@ -0,0 +1,50 @@
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+# 下面三行,坚决把前面的 sqlalchemy_fastapi_demo. 删掉!
+from database import get_db
+from scheme.advisors import AdvisorIn, AdvisorOut
+from dao.advisor import service, crud
+
+router = APIRouter()
+@router.get("/read",response_model=AdvisorOut)
+def read_advisor(advisor_id:int|None=None,advisor_name:str|None=None,db:Session=Depends(get_db)):
+ r_advisor=crud.search_advisor(db=db,advisor_id=advisor_id,advisor_name=advisor_name)
+ if r_advisor is not None:
+ return r_advisor
+ raise HTTPException(status_code=404,detail="顾问老师不存在")
+@router.get("/list",response_model=list[AdvisorOut])
+def list_advisor(advisor_name:str|None=None,db:Session=Depends(get_db)):
+ return crud.list_advisors(db=db,advisor_name=advisor_name)
+ # r_advisor=crud.list_advisors(db=db,advisor_name=advisor_name,phone=phone)
+ # if r_advisor:
+ # return r_advisor
+ # raise HTTPException(status_code=404,detail="顾问老师不存在")
+@router.post("/create",response_model=AdvisorOut)
+def create_advisor(advisor_in:AdvisorIn,db:Session=Depends(get_db)):
+ c_advisor,error=service.create_advisor(db=db,advisor_id=advisor_in.advisor_id,advisor_name=advisor_in.advisor_name)
+ if error == "DUPLICATE":
+ raise HTTPException(status_code=409, detail="该顾问ID已被占用")
+ if error is not None:
+ # 兜底:Service 新增了返回码但这里没接住,不要静默当成成功
+ raise HTTPException(status_code=500, detail="未处理的业务结果: " + error)
+ return c_advisor
+
+
+@router.put("/update",response_model=AdvisorOut)
+def update_advisor(advisor_in:AdvisorIn,db:Session=Depends(get_db)):
+ u_advisor,error=service.update_advisor(db=db,advisor_id=advisor_in.advisor_id,advisor_in=advisor_in)
+ if error == "NOT_FOUND":
+ raise HTTPException(status_code=404,detail="更新失败:顾问不存在")
+ if error is not None:
+ raise HTTPException(status_code=500, detail="未处理的业务结果: " + error)
+ return u_advisor
+@router.delete("/delete/{advisor_id}")
+def delete_advisor(advisor_id:int,db:Session=Depends(get_db)):
+ d_advisor,error=service.delete_advisor(db=db,advisor_id=advisor_id)
+ if error == "NOT_FOUND":
+ raise HTTPException(status_code=404,detail="顾问老师不存在")
+ if error == "HAS_STUDENTS":
+ raise HTTPException(status_code=409, detail="顾问老师下还有学生")
+ if error is not None:
+ raise HTTPException(status_code=500, detail="未处理的业务结果: " + error)
+ return {"message":"删除成功","delete_id":d_advisor}
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/api/c_lass.py b/sqlalchemy_fastapi_demo/api/c_lass.py
new file mode 100644
index 0000000..f505990
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/api/c_lass.py
@@ -0,0 +1,73 @@
+# # api/c_lass.py
+# # 本文件定义班级的所有 API 路由(Controller 层)
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+# APIRouter=创建路由,Depends=依赖注入,HTTPException=错误响应,Query=查询参数校验
+from sqlalchemy.orm import Session # 会话类型
+from database import get_db # 导入会话生成器
+from dao.c_lass import create_c_lass, get_c_lass, get_all_c_lass, update_c_lass, delete_c_lass
+# 把DAO层的5个函数都导入进来,接口里直接调用
+from scheme.c_lass import ClassCreate, ClassUpdate, ClassResponse
+# 路由对象,具体前缀(/c_lass)在 main.py 统一注册
+router = APIRouter()# 创建本模块的路由对象,main.py会把它注册进应用
+
+
+# ---------- 1. 新增班级 POST ----------
+# 完整地址:POST /c_lass/add
+@router.post("/add", response_model=ClassResponse, summary="新增班级")
+# 装饰器:注册一个POST接口,路径/add;response_model=返回时按ClassResponse格式化
+def add_c_lass(class_data: ClassCreate, db: Session = Depends(get_db)):
+ # class_data: ClassCreate = FastAPI自动校验请求体并转成对象;db = FastAPI自动注入会话
+ # 先查重:如果这个班级ID已经存在(包括逻辑删除的),进入if
+ if get_c_lass(db, class_data.class_id):
+ raise HTTPException(status_code=400, detail="该班级ID已存在,不能重复添加")
+ return create_c_lass(db, class_data)# 通过查重,调DAO层真正插入,返回结果
+
+
+# ---------- 2. 根据id查询单个班级 GET ----------
+# 完整地址:GET /c_lass/{class_id}
+@router.get("/{class_id}", response_model=ClassResponse, summary="根据id查询班级")
+# GET接口,路径里带班级编号,如 GET /c_lass/101
+def get_class_by_id(class_id: int, db: Session = Depends(get_db)): # 调DAO查询
+ db_class = get_c_lass(db, class_id)
+ if not db_class:
+ raise HTTPException(status_code=404, detail="该班级不存在") # 查不到就返回404,带提示信息
+ return db_class
+
+
+# ---------- 3. 分页查询所有班级 GET ----------
+# 完整地址:GET /c_lass/?skip=0&limit=10
+@router.get("/", response_model=list[ClassResponse], summary="分页查询班级列表")
+# GET根路径,如 GET /c_lass/?skip=0&limit=10;response_model是列表
+def get_class_list(
+ skip: int = Query(0, ge=0, description="跳过的记录数"), # 查询参数skip,默认0,必须≥0
+ limit: int = Query(10, ge=1, le=200, description="每页最大记录数"), # 查询参数limit,默认10,范围1~200
+ db: Session = Depends(get_db),
+):
+ return get_all_c_lass(db, skip, limit) # 调DAO分页查询
+
+
+# ---------- 4. 修改班级 PUT ----------
+# 完整地址:PUT /c_lass/{class_id}
+@router.put("/{class_id}/", response_model=ClassResponse, summary="修改班级信息")
+def update_class(class_id: int, update_data: ClassUpdate, db: Session = Depends(get_db)):
+# def update_class(class_id: int, start_time: date, db: Session = Depends(get_db)):
+
+ if not get_c_lass(db, class_id): # 先确认要改的班级存在(存在且没删)
+ raise HTTPException(status_code=404, detail="要修改的班级不存在")
+ return update_c_lass(db, class_id, update_data) # 调DAO执行局部更新
+
+
+# ---------- 5. 删除班级 DELETE ----------
+# 完整地址:DELETE /c_lass/{class_id}
+@router.delete("/{class_id}", summary="删除班级")
+def delete_class(class_id: int, db: Session = Depends(get_db)):
+ if not delete_c_lass(db, class_id):
+ raise HTTPException(status_code=404, detail="该班级不存在")
+ # 删除失败(查不到)返回404
+ return {"message": "删除成功"}
+
+
+
+
+#API 层只做三件事—— 接请求、校验 / 查重、调 DAO,自己不写 SQL
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/api/employment_api.py b/sqlalchemy_fastapi_demo/api/employment_api.py
new file mode 100644
index 0000000..3eed738
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/api/employment_api.py
@@ -0,0 +1,127 @@
+# api/employment_api.py
+# 本文件定义学生就业信息相关的所有 API 路由(Controller 层)
+from typing import List, Optional
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy.orm import Session
+
+# 注意:下面的导入全部去掉了 sqlalchemy_fastapi_demo. 前缀!
+from database import get_db
+from dao.employment import EmploymentBaseDAO, EmploymentOfferDAO
+from model.employment import EmploymentBase, EmploymentOffer
+from scheme.employment import (
+ EmploymentOfferUpdate,
+ EmploymentBaseUpdate,
+ EmploymentBaseCreate,
+ EmploymentOfferCreate,
+ EmploymentQuery,
+ EmploymentOfferQueryResponse,
+ EmploymentBaseQueryResponse
+)
+
+router = APIRouter()
+
+#-----------多条件查询就业协议信息-----------
+@router.get("/offer/search/",response_model=List[EmploymentOfferQueryResponse],summary="多条件查询就业协议信息")
+def search_offer(
+ stu_id: Optional[int] = Query(None, description="学生学号"),
+ offer_id: Optional[int] = Query(None, description="offer编号,需同时提供学生编号"),
+ skip: int = Query(0, ge=0, description="跳过的记录条数"),
+ limit: int = Query(10, ge=1, le=200, description="返回最大记录数"),
+ db: Session = Depends(get_db)
+):
+ #捕获异常
+ try:
+ data_list = EmploymentOfferDAO.different_choice_query(
+ db=db, stu_id=stu_id, offer_id=offer_id, skip=skip, limit=limit
+ )
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ return data_list
+#-----------多条件查询就业基础信息-----------
+@router.get("/base/search/",response_model=List[EmploymentBaseQueryResponse],summary="多条件查询就业基础信息")
+def search_base(
+ query: EmploymentQuery = Depends(),
+ skip: int = Query(0, ge=0, description="跳过的记录条数"),
+ limit: int = Query(10, ge=1, le=200, description="返回最大记录数"),
+ db: Session = Depends(get_db)
+):
+ # 多条件筛选就业基础信息 + 分页
+ if query.min_salary is not None and query.max_salary is not None and query.min_salary > query.max_salary:
+ raise HTTPException(status_code=400, detail="最低薪资不能大于最高薪资")
+
+ data_list = EmploymentBaseDAO.different_choice_query(
+ db=db,
+ stu_id=query.stu_id,
+ company_name=query.company_name,
+ min_salary=query.min_salary,
+ max_salary=query.max_salary,
+ skip=skip,
+ limit=limit
+ )
+ return data_list
+#-----------添加就业基础信息-----------
+@router.post("/base/add/",response_model=EmploymentBaseQueryResponse,summary="添加就业基础信息")
+def add_base(obj: EmploymentBaseCreate, db: Session = Depends(get_db)):
+ items = EmploymentBaseDAO.get_by_stu_id(db, obj.stu_id)
+ if items:
+ raise HTTPException(status_code=400, detail="该学生的就业基础信息已存在,不可重复新增")
+
+ new_obj = EmploymentBase(
+ stu_id=obj.stu_id,
+ employment_open_time=obj.employment_open_time,
+ job_time=obj.job_time,
+ company_name=obj.company_name,
+ salary=obj.salary,
+ is_deleted=0
+ )
+ res = EmploymentBaseDAO.create(db, new_obj)
+ return res
+#-----------添加就业协议信息-----------
+@router.post("/offer/add/",response_model=EmploymentOfferQueryResponse,summary="添加就业协议信息")
+def add_offer(obj: EmploymentOfferCreate, db: Session = Depends(get_db)):
+ new_obj = EmploymentOffer(
+ stu_id=obj.stu_id,
+ offer_id=obj.offer_id,
+ offer_time=obj.offer_time,
+ is_deleted=0
+ )
+ res = EmploymentOfferDAO.create(db, new_obj)
+ return res
+#-----------修改就业基础信息-----------
+@router.put("/base/{stu_id}/",response_model=EmploymentBaseQueryResponse,summary="修改就业基础信息")
+def update_base(stu_id: int, obj: EmploymentBaseUpdate, db: Session = Depends(get_db)):
+ update_data = obj.model_dump(exclude_unset=True)
+ try:
+ res = EmploymentBaseDAO.update(db, stu_id, update_data)
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ if not res:
+ raise HTTPException(status_code=404, detail="未找到该学生就业基础信息")
+ return res
+#-----------修改就业协议信息-----------
+@router.put("/offer/{offer_id}", response_model=EmploymentOfferQueryResponse,summary="修改就业协议信息")
+def update_offer(stu_id:int,offer_id: int, obj: EmploymentOfferUpdate, db: Session = Depends(get_db)):
+ # offer 表主键是 (stu_id, offer_id),定位记录需要两者
+ try:
+ res = EmploymentOfferDAO.update(db, stu_id, offer_id, obj.offer_time)
+ except ValueError as e:
+ # 捕获校验异常,返回标准400错误给前端
+ raise HTTPException(status_code=400, detail=str(e))
+ if not res:
+ raise HTTPException(status_code=404, detail="未找到该就业协议记录")
+ return res
+#-----------逻辑删除就业基础信息-----------
+@router.delete("/base/{stu_id}/",summary="删除就业基础信息")
+def delete_base(stu_id: int, db: Session = Depends(get_db)):
+ ok = EmploymentBaseDAO.delete(db, stu_id)
+ if not ok:
+ raise HTTPException(status_code=404, detail="记录不存在或已经删除")
+ return {"code": 200, "msg": "删除成功"}
+#-----------逻辑删除就业协议信息-----------
+@router.delete("/offer/{stu_id}/{offer_id}",summary="修改就业协议信息")
+def delete_offer(stu_id: int, offer_id: int, db: Session = Depends(get_db)):
+ ok = EmploymentOfferDAO.delete(db, stu_id, offer_id)
+ if not ok:
+ raise HTTPException(status_code=404, detail="记录不存在或已经删除")
+ return {"code": 200, "msg": "删除成功"}
diff --git a/sqlalchemy_fastapi_demo/api/statistics.py b/sqlalchemy_fastapi_demo/api/statistics.py
new file mode 100644
index 0000000..998ffb6
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/api/statistics.py
@@ -0,0 +1,106 @@
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy.orm import Session
+from sqlalchemy import func
+
+# 注意下面这些导入,全都没有 sqlalchemy_fastapi_demo. 前缀!
+from scheme.statistics import StudentAge, ClassCount, ScoreCount, ScoreAvg, Employment, EmploymentOff
+from database import get_db
+from model.students import Student
+from model.Score import Score
+from model.employment import EmploymentBase, EmploymentOffer
+from model.c_lass import Classinfo
+from dao.statistics import (
+ students_age, class_statistics, score_above, score_fail,
+ avg_scores, top_salary, stu_every, class_avg
+)
+
+router = APIRouter()
+# app = FastAPI(title="统计接口") # 这行如果不需要可以注释掉
+
+# -动态年龄范围查询**:支持用户输入年龄阈值及比较条件(如大于、小于、等于、区间等),动态查询符合条件的学员信息。
+@router.get("/students/age")
+def api_students_age(op: str = Query(..., description=">, <, =, >=, <=, between"),
+ age_value: int = Query(None),age_min: int = Query(None),
+ age_max: int = Query(None),db: Session = Depends(get_db)):
+ try:
+ rows = students_age(db, op, age_value, age_min, age_max)
+ except ValueError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ return [{"stu_id": s.stu_id,
+ "stu_name": s.stu_name,
+ "age": s.age,
+ "gender": s.gender,
+ "class_id": s.class_id,} for s in rows]
+
+# 多维度班级统计**:统计每个班级的总人数,以及按性别(男、女)细分的人数分布。
+@router.get("/classes/statistics")
+def api_class_statistics(db: Session = Depends(get_db)):
+ return class_statistics(db)
+
+
+# - 查询每次考试成绩都在输入分数线(如80分)以上的学生的编号、姓名和成绩。
+@router.get("/scores/above")
+def api_score_above(
+ score_input: int = Query(..., ge=0, le=100),
+ db: Session = Depends(get_db)):
+ rows = score_above(db, score_input)
+ return [{"stu_id": i[0], "stu_name": i[1], "exam_order": i[2], "score": i[3]} for i in rows]
+
+# - 查询有输入指定次数(如两次)以上不及格的学生的姓名、班级和不及格成绩明细。
+@router.get("/scores/fail")
+def api_score_fail(
+ fail_times: int = Query(..., ge=1),
+ fail_score: int = Query(60, ge=0, le=100),
+ db: Session = Depends(get_db)):
+ rows = score_fail(db, fail_times, fail_score)
+ return [{"stu_name": i[0], "class_id": i[1], "exam_order": i[2], "score": i[3]} for i in rows]
+
+# 统计每次考试每个班级的平均分,并支持按分数从高到低或从低到高动态排序
+@router.get("/scores/avg")
+def api_avg_scores(order: str = Query("desc", pattern="^(asc|desc)$"),
+ db: Session = Depends(get_db)):
+ a = avg_scores(db, order)
+ return [{"exam_id": i.exam_id,
+ "class_id": i.class_id,
+ "avg_score": round(float(i.avg_score), 2)} for i in a]
+
+#就业统计
+# - 统计就业薪资排名 Top N(动态输入 N)的学生的姓名、班级、就业时间和就业公司。
+@router.get("/employment/top")
+def api_top_salary(n: int = Query(5, ge=1), db: Session = Depends(get_db)):
+ return top_salary(db, n)
+
+# - 统计每个学生的就业时长(计算公式:offer下发时间 - 就业开放时间)。
+@router.get("/employment/every")
+def api_stu_every(db: Session = Depends(get_db)):
+ return stu_every(db)
+
+# 每个班级的平均就业时长
+@router.get("/employment/class_avg")
+def api_class_duration(db: Session = Depends(get_db)):
+ return class_avg(db)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sqlalchemy_fastapi_demo/api/students.py b/sqlalchemy_fastapi_demo/api/students.py
new file mode 100644
index 0000000..26398c8
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/api/students.py
@@ -0,0 +1,108 @@
+# api/users.py
+# 本文件定义学生相关的所有 API 路由(Controller 层)
+
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy.orm import Session
+from typing import List, Optional
+
+# 注意下面这三行,坚决把开头 的 sqlalchemy_fastapi_demo. 删掉!
+from database import get_db
+from dao.students_dao import StudentDAO
+from scheme.students import StudentCreate, StudentUpdate, StudentResponse
+router = APIRouter(prefix="/students",tags=["学生信息管理模块"])
+
+# 增加学生信息接口
+@router.post("/",response_model=StudentResponse)
+def create_student_api(
+ student_in: StudentCreate,
+ db: Session = Depends(get_db)
+):
+ # 1. 校验外键:班级和顾问是否存在(与更新逻辑保持一致)
+ if student_in.advisor_id is not None:
+ advisor_exists = StudentDAO.inspect_advisor_id_unq(db, student_in.advisor_id)
+ if not advisor_exists:
+ raise HTTPException(status_code=400, detail="该顾问不存在")
+
+ if student_in.class_id is not None:
+ class_exists = StudentDAO.inspect_class_id_unq(db, student_in.class_id)
+ if not class_exists:
+ raise HTTPException(status_code=400, detail="该班级不存在")
+
+ # 2. 校验通过后,直接调用 DAO 新增学生
+ student = StudentDAO.create_student(db=db, obj_in=student_in)
+ return student
+
+# 查询所有学生接口
+@router.get("/all", response_model=List[StudentResponse])
+async def get_all_api(
+ skip: int = Query(0, ge=0, description="跳过的记录数"),
+ limit: int = Query(100, ge=1, le=200, description="返回的最大记录数"),
+ db: Session = Depends(get_db) # 依赖注入获得数据库会话
+):
+ students = StudentDAO.get_students_all(db, skip=skip, limit=limit)
+ return students # FastAPI 自动根据 response_model 转换为 JSON
+
+# 多条件查询接口
+@router.get("/search",response_model=List[StudentResponse])
+async def get_multi_condition_api(
+ stu_id: Optional[int] = Query(None, description="学生编号"),
+ stu_name: Optional[str] = Query(None, description="学生姓名(支持模糊查询)"),
+ class_id: Optional[int] = Query(None, description="班级编号"),
+ skip: int = 0,
+ limit: int = 100,
+ db: Session = Depends(get_db)
+):
+ student = StudentDAO.query_multi_condition(
+ db = db,
+ stu_id = stu_id,
+ stu_name = stu_name,
+ class_id = class_id,
+ skip = skip,
+ limit = limit )
+ return student
+
+# # 根据id查询单个学生接口
+# @router.get("/{stu_id}", response_model=StudentResponse)
+# async def get_student_by_id_api(
+# stu_id: int,
+# db: Session = Depends(get_db)
+# ):
+# student = StudentDAO.get_student_by_id(db=db, stu_id=stu_id)
+# if not student:
+# raise HTTPException(status_code=404, detail="用户不存在")
+# return student
+
+# 更改学生信息接口
+@router.put("/{stu_id}",response_model=StudentResponse)
+async def update_student_api(
+ stu_id: int,
+ stu_update: StudentUpdate,
+ db: Session = Depends(get_db)
+):
+ """更新学生信息,支持部分字段更新"""
+ # 检查学生是否存在
+ existing = StudentDAO.get_student_by_id(db, stu_id)
+ if not existing:
+ raise HTTPException(status_code=404, detail="学生不存在")
+ # 校验外键(班级和顾问是否存在)
+ if stu_update.advisor_id is not None:
+ advisor_exists = StudentDAO.inspect_advisor_id_unq(db, stu_update.advisor_id)
+ if not advisor_exists:
+ raise HTTPException(status_code=400, detail="该顾问不存在")
+ if stu_update.class_id is not None:
+ class_exists = StudentDAO.inspect_class_id_unq(db, stu_update.class_id)
+ if not class_exists:
+ raise HTTPException(status_code=400, detail="该班级不存在")
+ # 直接调用 DAO 更新,学号不可改等逻辑已在 DAO 层处理
+ student = StudentDAO.update_student(db = db , stu_id = stu_id , obj = stu_update)
+ return student
+
+# 逻辑删除接口:不返回完整学生实体,返回简单提示
+@router.delete("/{stu_id}",status_code=204)
+async def delete_student_api(
+ stu_id: int,
+ db: Session = Depends(get_db)
+):
+ StudentDAO.delete_student(db, stu_id)
+ # 返回 None 表示 204 状态码(无内容)
+ return None
diff --git a/sqlalchemy_fastapi_demo/api/teachers.py b/sqlalchemy_fastapi_demo/api/teachers.py
new file mode 100644
index 0000000..f02b9d7
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/api/teachers.py
@@ -0,0 +1,81 @@
+from typing import List
+from fastapi import APIRouter, Depends, Query, HTTPException
+from sqlalchemy.orm import Session
+
+# 坚决把下面这三行开头的 sqlalchemy_fastapi_demo. 全删掉!
+from scheme.teachers import TeacherAdd, TeacherResponse, TeacherUpdate
+from database import get_db
+from dao.teachers_dao import TeacherDAO
+router=APIRouter()
+# ---------- 创建新教师 ----------
+@router.post("/teacher/add",response_model=TeacherResponse)
+async def add_teacher(teacher:TeacherAdd,db:Session=Depends(get_db)):
+ existing_foreign_key=TeacherDAO.inspect_class_id_unq(db,teacher.class_id)
+ if existing_foreign_key:# 外键校验
+ existing_teacher_id=TeacherDAO.inspect_teacher_id_unq(db,teacher.teacher_id)
+ if existing_teacher_id:# 主键校验
+ raise HTTPException(status_code=409, detail="主键教师ID已被占用,包括软删除") # 资源冲突
+ return TeacherDAO.add_teacher(db,teacher)
+ else:
+ raise HTTPException(status_code=404, detail="班级ID不存在,数据库无数据,或已被软删除") # 关联资源不存在
+# ---------- 软删除教师 ----------
+@router.delete("/teacher/delete", status_code=204)
+async def light_delete_teacher(teacher_id:int=Query(...,ge=1,description='输入想要删除的教师ID'),
+ db:Session=Depends(get_db)):
+ """
+ 删除用户,成功返回 204 No Content
+ """
+ success = TeacherDAO.delete_light(db, teacher_id)
+ if not success: # 目标删除教师ID不存在(被硬删除或软删除)
+ raise HTTPException(status_code=404, detail="教师ID不存在") # 关联资源不存在
+ # 返回 None 表示目标教师ID已删除
+ return None
+# ---------- 更新教师信息 ----------
+@router.put("/teacher/update",response_model=TeacherResponse)
+async def update_teacher(
+ teacher_data: TeacherUpdate,
+ teacher_id:int=Query(...,ge=1,description='输入教师想要更新的教师ID'),
+ db: Session = Depends(get_db)
+):
+ """
+ 更新用户信息(只更新传入的字段,也就是非None)
+ """
+ # 检查教师ID是否存在
+ existing_teacher_id = TeacherDAO.inspect_teacher_id_unq(db,teacher_id)
+ if (not existing_teacher_id) or (existing_teacher_id.is_deleted==1):
+ raise HTTPException(status_code=404, detail="教师ID不存在,已被硬删除或软删除") # 关联资源不存在
+ # 更新班级ID
+ if teacher_data.class_id is not None:
+ # 校验外键,校验班级ID是否在Class表存在
+ existing_foreign_key=TeacherDAO.inspect_class_id_unq(db,teacher_data.class_id)
+ if not existing_foreign_key:
+ raise HTTPException(status_code=404, detail="班级ID不存在,已被硬删除或软删除") # 关联资源不存在
+ updated = TeacherDAO.update(db, teacher_id, teacher_data)
+ return updated
+ updated = TeacherDAO.update(db, teacher_id, teacher_data)
+ return updated
+# ---------- 查询所有教师(分页) ----------
+@router.get("/teacher/total_query",response_model=List[TeacherResponse])
+async def get_teachers(
+ skip: int = Query(0, ge=0, description="跳过的记录数"),
+ limit: int = Query(100, ge=1, le=200, description="返回的最大记录数"),
+ db: Session = Depends(get_db) # 依赖注入获得数据库会话
+):
+ """
+ 获取用户列表,支持分页,从skip+1条开始返回
+ """
+ users = TeacherDAO.get_all(db, skip=skip, limit=limit)
+ return users # FastAPI 自动根据 response_model 转换为 JSON
+# ---------- 根据 ID 查询单个教师 ----------
+@router.get("/teacher/single_query", response_model=TeacherResponse)
+async def get_teacher(
+ teacher_id: int,
+ db: Session = Depends(get_db)
+):
+ """
+ 根据用户 ID 获取详细信息,不显示软删除
+ """
+ teacher = TeacherDAO.inspect_teacher_id_unq(db, teacher_id)
+ if (not teacher) or teacher.is_deleted==1:
+ raise HTTPException(status_code=404, detail="教师ID不存在") # 关联资源不存在
+ return teacher
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/dao/Score.py b/sqlalchemy_fastapi_demo/dao/Score.py
new file mode 100644
index 0000000..d279d41
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/dao/Score.py
@@ -0,0 +1,56 @@
+from model.Score import Score
+from model.students import Student
+from sqlalchemy.orm import Session
+from scheme.Score import ScoreCreate,ScoreUpdate
+
+class ScoreDAO:
+ """用户数据访问对象,所有方法均为静态方法,方便调用"""
+
+ @staticmethod
+ def inspect_stu_id_unq(db:Session,stu_id:int):
+ return db.query(Student).filter(Student.stu_id == stu_id,Student.is_deleted==0).first()
+
+
+ # 查所有数据
+ @staticmethod
+ def get_all(db:Session,skip:int=0,limit:int=100):
+ return db.query(Score).offset(skip).limit(limit).all()
+
+ # 查询单个数据
+ @staticmethod
+ def get_by_id(db:Session,stu_id:int,exam_id:int):
+ return db.query(Score).filter(Score.stu_id == stu_id,Score.exam_id==exam_id).first()
+
+ # 添加数据
+ @staticmethod
+ def post_score(db:Session,score_create:ScoreCreate):
+ # 将 Pydantic 模型转为字典,并解包构建 SQLAlchemy 模型实例
+ db_score=Score(**score_create.model_dump())
+ db.add(db_score) #添加到会话
+ db.commit() #提交事务,此时会执行 INSERT,并自动填充自增字段
+ db.refresh(db_score) #刷新对象,获取数据库生成的默认值(如 created_at)
+ return db_score
+
+ # 更改数据
+ @staticmethod
+ def put_score(db:Session,score_id:int,exam_id:int,score_update:ScoreUpdate):
+ db_score=ScoreDAO.get_by_id(db,score_id,exam_id)
+ if not db_score:
+ return None
+ # 只更新客户端显式传入的字段(exclude_unset=True 排除未设置的字段)
+ put_score_data = score_update.model_dump(exclude_unset=True)
+ for k,v in put_score_data.items():
+ setattr(db_score,k,v) # 动态设置属性
+ db.commit()
+ db.refresh(db_score)
+ return db_score
+
+ # 删除
+ @staticmethod
+ def delete_score(db:Session,score_id:int,exam_id:int):
+ db_score=ScoreDAO.get_by_id(db,score_id,exam_id)
+ if not db_score:
+ return False
+ db.delete(db_score)
+ db.commit()
+ return True
diff --git a/sqlalchemy_fastapi_demo/dao/__init__.py b/sqlalchemy_fastapi_demo/dao/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/Score.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/Score.cpython-310.pyc
new file mode 100644
index 0000000..6ffff66
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/Score.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/Score.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/Score.cpython-312.pyc
new file mode 100644
index 0000000..2d69e92
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/Score.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/__init__.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..d0943ac
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/__init__.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/__init__.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..1213990
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/__init__.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/c_lass.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/c_lass.cpython-310.pyc
new file mode 100644
index 0000000..3e12ff5
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/c_lass.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/c_lass.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/c_lass.cpython-312.pyc
new file mode 100644
index 0000000..e687613
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/c_lass.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/employment.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/employment.cpython-310.pyc
new file mode 100644
index 0000000..2909daf
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/employment.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/employment.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/employment.cpython-312.pyc
new file mode 100644
index 0000000..f92ffd7
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/employment.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/statistics.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/statistics.cpython-310.pyc
new file mode 100644
index 0000000..16396bf
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/statistics.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/students_dao.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/students_dao.cpython-310.pyc
new file mode 100644
index 0000000..cf1b157
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/students_dao.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/students_dao.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/students_dao.cpython-312.pyc
new file mode 100644
index 0000000..ae9aaef
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/students_dao.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/teachers_dao.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/teachers_dao.cpython-310.pyc
new file mode 100644
index 0000000..1e9f9e3
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/teachers_dao.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/__pycache__/teachers_dao.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/__pycache__/teachers_dao.cpython-312.pyc
new file mode 100644
index 0000000..917e515
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/__pycache__/teachers_dao.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/.___init__.py b/sqlalchemy_fastapi_demo/dao/advisor/.___init__.py
new file mode 100644
index 0000000..c30a4b1
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/.___init__.py differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/._crud.py b/sqlalchemy_fastapi_demo/dao/advisor/._crud.py
new file mode 100644
index 0000000..b56f038
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/._crud.py differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/._service.py b/sqlalchemy_fastapi_demo/dao/advisor/._service.py
new file mode 100644
index 0000000..b56f038
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/._service.py differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/__init__.py b/sqlalchemy_fastapi_demo/dao/advisor/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/__init__.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000..d58d5e7
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/__init__.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/__init__.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..7029330
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/__init__.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/crud.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/crud.cpython-310.pyc
new file mode 100644
index 0000000..a9af3db
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/crud.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/crud.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/crud.cpython-312.pyc
new file mode 100644
index 0000000..e5cb895
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/crud.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/service.cpython-310.pyc b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/service.cpython-310.pyc
new file mode 100644
index 0000000..6c0833e
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/service.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/service.cpython-312.pyc b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/service.cpython-312.pyc
new file mode 100644
index 0000000..795e8f6
Binary files /dev/null and b/sqlalchemy_fastapi_demo/dao/advisor/__pycache__/service.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/crud.py b/sqlalchemy_fastapi_demo/dao/advisor/crud.py
new file mode 100644
index 0000000..f48ee0b
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/dao/advisor/crud.py
@@ -0,0 +1,56 @@
+from sqlalchemy.orm import Session
+from model.advisors import Advisor
+from model.students import Student
+
+def create_advisor(db:Session,advisor_id:int,advisor_name:str):
+ advisor = Advisor(advisor_id=advisor_id,advisor_name=advisor_name)
+ db.add(advisor)
+ db.commit()
+ db.refresh(advisor)
+ return advisor
+
+
+def search_advisor(db:Session,advisor_id:int|None=None,advisor_name:str|None=None):
+ if advisor_id is None and advisor_name is None:
+ return None
+ query=db.query(Advisor)
+ if advisor_id is not None:
+ query = query.filter(Advisor.advisor_id == advisor_id)
+ if advisor_name is not None:
+ query = query.filter(Advisor.advisor_name==advisor_name)
+ advisor = query.first()
+ return advisor
+
+
+def list_advisors(db: Session,advisor_name: str|None =None):
+ query = db.query(Advisor)
+ if advisor_name is not None:
+ query = query.filter(Advisor.advisor_name.like(f"%{advisor_name}%"))
+ return query.all()
+
+
+def list_students_of_advisor(db: Session, advisor_id: int):
+ """查这个顾问名下还没被逻辑删除的学生"""
+ return db.query(Student).filter(
+ Student.is_deleted == 0,
+ Student.advisor_id == advisor_id,
+ ).all()
+
+
+def update_advisor(db:Session,advisor_id:int,advisor_name:str|None=None):
+ r_advisor = search_advisor(db=db,advisor_id=advisor_id)
+ if r_advisor is None:
+ return None
+ if advisor_name is not None:
+ r_advisor.advisor_name = advisor_name
+ db.commit()
+ return r_advisor
+
+
+def delete_advisor(db:Session,advisor_id:int):
+ d_advisor = search_advisor(db=db,advisor_id=advisor_id)
+ if d_advisor is None:
+ return None
+ db.delete(d_advisor)
+ db.commit()
+ return advisor_id
diff --git a/sqlalchemy_fastapi_demo/dao/advisor/service.py b/sqlalchemy_fastapi_demo/dao/advisor/service.py
new file mode 100644
index 0000000..bdffd80
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/dao/advisor/service.py
@@ -0,0 +1,41 @@
+from sqlalchemy.orm import Session
+from sqlalchemy.exc import IntegrityError
+from dao.advisor import crud
+from scheme.advisors import AdvisorIn
+
+# 成功 (数据, None);失败 (None, "错误码")。
+
+def create_advisor(db:Session,advisor_id:int,advisor_name:str):
+ exist=crud.search_advisor(db,advisor_id=advisor_id)
+ if exist is not None:
+ return None,"DUPLICATE" #ID主键已被占用
+ try:
+ advisor = crud.create_advisor(db,advisor_id,advisor_name)
+ except IntegrityError:
+ db.rollback()
+ return None, "DUPLICATE"
+ return advisor, None
+
+
+def update_advisor(db:Session,advisor_id:int,advisor_in:AdvisorIn):
+ exist = crud.search_advisor(db,advisor_id=advisor_id)
+ if exist is None:
+ return None, "NOT_FOUND"
+ u_advisor=crud.update_advisor(db,advisor_id=advisor_id,advisor_name=advisor_in.advisor_name)
+ if u_advisor is None:
+ return None, "NOT_FOUND" # 查过之后被并发删掉了
+ return u_advisor,None
+
+
+#删除成功返回Advisor ID,删除失败返回None。顾问如连接学生,则拒绝删除。
+def delete_advisor(db: Session, advisor_id: int):
+ exist = crud.search_advisor(db=db, advisor_id=advisor_id)
+ if exist is None:
+ return None, "NOT_FOUND"
+ # 查一下这个顾问名下有没有学生
+ if crud.list_students_of_advisor(db, advisor_id):
+ return None, "HAS_STUDENTS"
+ result = crud.delete_advisor(db=db, advisor_id=advisor_id)
+ if result is None:
+ return None, "NOT_FOUND" # 查过之后被并发删掉了
+ return result, None
diff --git a/sqlalchemy_fastapi_demo/dao/c_lass.py b/sqlalchemy_fastapi_demo/dao/c_lass.py
new file mode 100644
index 0000000..524820c
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/dao/c_lass.py
@@ -0,0 +1,66 @@
+# # dao/c_lass.py 数据访问层,直接操作数据库
+# # 本文件封装对 Classinfo表的所有数据库操作(增、删、改、查)
+
+from sqlalchemy.orm import Session # 会话类型,标注db参数
+from model.c_lass import Classinfo # # 导入ORM模型
+from scheme.c_lass import ClassCreate, ClassUpdate # 导入入参模型
+from typing import Optional, List # 类型注解用:Optional=可空,List=列表
+
+# 新增班级
+def create_c_lass(db: Session, obj: ClassCreate):# 新增函数:接收会话db和校验后的数据obj,返回Classinfo对象
+ db_obj = Classinfo(class_id=obj.class_id, start_time=obj.start_time)
+ # 把前端传来的数据组装成一个ORM对象(相当于内存里的一条新记录)
+ db.add(db_obj) # 把对象加入会话
+ db.commit() # 提交事务:真正把INSERT语句发给MySQL执行,写进数据库
+ db.refresh(db_obj) # 从数据库重新查一遍这条记录,把数据库自动生成的值(如默认值)刷新回对象
+ return db_obj # 返回这个对象,方便API层序列化成JSON返回前端
+
+
+# 根据 id 查询单个班级:只查询 is_deleted=0 未删除
+def get_c_lass(db: Session, class_id: int) -> Optional[Classinfo]:
+ # 按ID查单个班级;Optional = 可能查不到(返回None)
+ return db.query(Classinfo).filter(
+ Classinfo.class_id == class_id,# 条件1:班级编号等于传入的id
+ Classinfo.is_deleted == 0 # 条件2:没被逻辑删除(只查0)
+ ).first()
+# .first() = 取第一条;查不到就返回None,不会报错
+
+# 分页查询所有班级:过滤已经逻辑删除的数据
+def get_all_c_lass(db: Session, skip: int = 0, limit: int = 100) -> List[Classinfo]:
+ # 分页查所有班级:skip=跳过几条,limit=最多取几条
+ return db.query(Classinfo)\
+ .filter(Classinfo.is_deleted == 0)\
+ .offset(skip).limit(limit).all() # # 只查没删的; 跳过skip条,取limit条,返回列表
+
+
+# 修改班级(局部更新:传了哪个字段就改哪个)
+def update_c_lass(db: Session, class_id: int, update_obj: ClassUpdate):
+ # 修改函数:按id找到班级,把传进来的字段改掉
+ db_class = get_c_lass(db, class_id)
+
+ # 只取前端真正传了的字段
+ update_data = update_obj.model_dump(exclude_unset=True)
+ # 把前端传的修改数据转成字典;exclude_unset=True = 只保留前端真正传了的字段
+ # (没传的字段不出现,就不会被误改成None——就是"局部更新")
+ for key, value in update_data.items():# 循环:把字典里每个字段的值,赋给ORM对象对应属性(内存里改好)
+ setattr(db_class, key, value)
+ db.commit() # 提交:把改动写进数据库
+ db.refresh(db_class) # 从数据库重新查询这条记录,把数据库里的最新值刷新到 Python 对象上,让内存里的对象和数据库保持一致。
+ return db_class # 返回改好的对象
+
+
+# 逻辑删除:不再db.delete,设置is_deleted=1
+def delete_c_lass(db: Session, class_id: int) -> bool:
+ # 删除函数:不是物理删除(不删数据库行),是打标记
+ db_class = get_c_lass(db, class_id) # 先查这条班级
+ if not db_class:
+ return False # 查不到返回False,API层返回404
+ # 打删除标记
+ db_class.is_deleted = 1 # 把逻辑删除标记改成1(=已删除
+ db.commit()
+ return True # 返回True表示删除成功
+
+
+#DAO 层是所有 SQL 的家,API 层不写任何 SQL,只调这里的函数。
+# 查询统一过滤 `is_deleted == 0`,删了的数据还在数据库里,只是查不到 —— 这就是逻辑删除。
+
diff --git a/sqlalchemy_fastapi_demo/dao/employment.py b/sqlalchemy_fastapi_demo/dao/employment.py
new file mode 100644
index 0000000..8fd2b32
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/dao/employment.py
@@ -0,0 +1,175 @@
+from fastapi import HTTPException
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, Date
+from model.employment import EmploymentBase,EmploymentOffer
+from model.students import Student
+# ========== 就业基础信息 DAO ==========
+class EmploymentBaseDAO:
+ @staticmethod
+ #根据学生编号查询
+ def get_by_stu_id(db: Session, stu_id: int):
+ record = db.query(EmploymentBase).filter(and_(
+ EmploymentBase.stu_id == stu_id,
+ EmploymentBase.is_deleted == 0
+ )).first()
+ return record
+ @staticmethod
+ #多条件查询
+ def different_choice_query(
+ db: Session,
+ stu_id: int = None,
+ company_name: str = None,
+ min_salary: float = None,
+ max_salary: float = None,
+ skip: int = 0,
+ limit: int = 10
+ ):
+ # 基础查询 + 过滤未删除
+ query = db.query(EmploymentBase).filter(EmploymentBase.is_deleted == 0)
+
+ # 动态拼接条件
+ #学生id查询
+ if stu_id is not None:
+ query = query.filter(EmploymentBase.stu_id == stu_id)
+ #公司名称查询
+ if company_name is not None and company_name.strip() != "":
+ # 模糊匹配,%代表通配符
+ query = query.filter(EmploymentBase.company_name.like(f"%{company_name}%"))
+ #工资范围查询
+ if min_salary is not None:
+ query = query.filter(EmploymentBase.salary >= min_salary)
+ if max_salary is not None:
+ query = query.filter(EmploymentBase.salary <= max_salary)
+
+ # 分页
+ result = query.offset(skip).limit(limit).all()
+ return result
+
+ @staticmethod
+ #新增学生就业基础信息
+ def create(db: Session, obj: EmploymentBase):
+
+ student = db.query(Student).filter(
+ Student.stu_id == obj.stu_id,
+ Student.is_deleted == 0
+ ).first()
+ if not student:
+ raise HTTPException(status_code=400, detail="学生不存在或已逻辑删除,不能添加就业信息")
+ db.add(obj)
+ db.commit()
+ db.refresh(obj)
+ return obj
+
+ @staticmethod
+ #更新就业基础信息
+ def update(db: Session, stu_id: int, update_data: dict):
+ record = EmploymentBaseDAO.get_by_stu_id(db, stu_id)
+ if not record:
+ return None
+
+ if "company_name" in update_data:
+ record.company_name = update_data["company_name"]
+ if "salary" in update_data:
+ record.salary = update_data["salary"]
+ if "employment_open_time" in update_data:
+ record.employment_open_time = update_data["employment_open_time"]
+ if "job_time" in update_data:
+ record.job_time = update_data["job_time"]
+ # =========== 统一时间校验 ===========
+ final_emp_open = record.employment_open_time
+ final_job = record.job_time
+ # 两个时间都不为空才校验
+ if final_emp_open and final_job:
+ if final_job < final_emp_open:
+ raise ValueError("job时间不能早于就业开放时间")
+
+ db.commit()
+ db.refresh(record)
+ return record
+
+ @staticmethod
+ #根据学生编号对就业基础表进行逻辑删除
+ def delete(db: Session, stu_id: int):
+ record = EmploymentBaseDAO.get_by_stu_id(db, stu_id)
+ if not record:
+ return False
+ record.is_deleted = 1
+ db.commit()
+ return True
+
+# ========== 就业Offer协议 DAO ==========
+class EmploymentOfferDAO:
+
+ @staticmethod
+ #添加就业协议记录
+ def create(db: Session, obj_1: EmploymentOffer,obj_2:EmploymentBase):
+ # 时间校验
+ if obj_1.offer_time < obj_2.employment_open_time:
+ raise ValueError("offer时间不能早于就业开放时间")
+ db.add(obj_1)
+ db.commit()
+ db.refresh(obj_1)
+ return obj_1
+
+ @staticmethod
+ #修改就业协议表
+ def update(db: Session, stu_id: int, offer_id: int, offer_time:Date):
+ """修改offer"""
+ record_list = EmploymentOfferDAO.different_choice_query(db, stu_id, offer_id)
+ if not record_list:
+ return None
+ record = record_list[0]
+ #查询就业基础信息,拿到就业开放时间准备做时间校验
+ base_record = db.query(EmploymentBase).filter(
+ EmploymentBase.stu_id == stu_id,
+ EmploymentBase.is_deleted == 0
+ ).first()
+ #避免就业基础信息被逻辑删除了而就业基础协议记录还存在
+ if not base_record:
+ raise ValueError("未找到该学生就业基础信息")
+ #时间校验
+ if offer_time is not None:
+ if offer_time < base_record.employment_open_time:
+ raise ValueError("offer时间不能早于就业开放时间")
+ record.offer_time = offer_time
+
+ db.commit()
+ db.refresh(record)
+ return record
+
+ @staticmethod
+ #根据协议编号对就业协议表进行逻辑删除
+ def delete(db: Session, stu_id: int,offer_id: int):
+ """offer逻辑删除"""
+ record_list = EmploymentOfferDAO.different_choice_query(db, stu_id,offer_id)
+ if not record_list:
+ return False
+ record = record_list[0]
+ record.is_deleted = 1
+ db.commit()
+ return True
+
+ @staticmethod
+ #多条件查询就业协议表
+ def different_choice_query(
+ db: Session,
+ stu_id: int =None ,
+ offer_id: int = None,
+ skip: int = 0,
+ limit: int = 10
+ ):
+ #基础查询+过滤未删除
+ query = db.query(EmploymentOffer).filter(EmploymentOffer.is_deleted == 0)
+
+ #动态拼接条件
+ #学生id查询(选填)
+ if stu_id is not None:
+ query = query.filter(EmploymentOffer.stu_id == stu_id)
+ # 同时传了offer编号 → 查单个(联合主键两个都查)
+ if offer_id is not None:
+ query = query.filter(EmploymentOffer.offer_id == offer_id)
+ elif stu_id is None and offer_id is not None:
+ raise ValueError("不能单独查询offer编号,必须同时提供学生编号")
+ # 分页
+ result = query.offset(skip).limit(limit).all()
+ return result
diff --git a/sqlalchemy_fastapi_demo/dao/statistics.py b/sqlalchemy_fastapi_demo/dao/statistics.py
new file mode 100644
index 0000000..588c5ce
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/dao/statistics.py
@@ -0,0 +1,110 @@
+from sqlalchemy import func, case
+from model.students import Student
+from model.Score import Score
+from model.employment import EmploymentBase,EmploymentOffer
+from model.c_lass import Classinfo
+
+def students_age(db,op:str,
+ age_value: int = None,
+ age_min: int = None,
+ age_max: int = None):
+
+ a=db.query(Student).filter(Student.is_deleted == 0)
+ if op == ">":
+ a=a.filter(Student.age > age_value)
+ elif op == "<":
+ a=a.filter(Student.age < age_value)
+ elif op == "=":
+ a=a.filter(Student.age == age_value)
+ elif op == ">=":
+ a=a.filter(Student.age >= age_value)
+ elif op == "<=":
+ a=a.filter(Student.age <= age_value)
+ elif op == "between":
+ if age_min is None or age_max is None:
+ raise ValueError("区间查询必须提供 age_min和age_max")
+ a=a.filter(Student.age.between(age_min, age_max))
+ else:
+ raise ValueError("请使用 >, <, =, >=, <=, between")
+ return a.all()
+# 多维度班级统计**:统计每个班级的总人数,以及按性别(男、女)细分的人数分布。
+def class_statistics(db):
+ a = (db.query(Student.class_id,func.count(Student.stu_id).label("total"),
+ func.sum(case((Student.gender == "男", 1), else_=0)).label("male"),
+ func.sum(case((Student.gender == "女", 1), else_=0)
+ ).label("female"))
+ .filter(Student.is_deleted==0).group_by(Student.class_id).all())
+ list1 = []
+ for i in a:
+ list1.append({"class_id": i.class_id,"total": i.total,"male": i.male,"female": i.female})
+ return list1
+
+# - 查询每次考试成绩都在输入分数线(如80分)以上的学生的编号、姓名和成绩。
+def score_above(db,score_input):
+ result = []
+ for stu in db.query(Student).filter(Student.is_deleted == 0).all():
+ if stu.score and all(s.score >= score_input for s in stu.score):
+ for s in stu.score:
+ result.append((stu.stu_id, stu.stu_name, s.exam_id, s.score))
+ return result
+# - 查询有输入指定次数(如两次)以上不及格的学生的姓名、班级和不及格成绩明细。
+def score_fail(db, fail_times, fail_score=60):
+ result = []
+ for stu in db.query(Student).filter(Student.is_deleted == 0).all():
+ fails = [s for s in stu.score if s.score < fail_score]
+ if len(fails) >= fail_times:
+ for s in fails:
+ result.append((stu.stu_name, stu.class_id, s.exam_order, s.score))
+ return result
+
+# - 统计每次考试每个班级的平均分,并支持按分数从高到低或从低到高动态排序。
+def avg_scores(db, order="desc"):
+ avg = func.avg(Score.score).label("avg_score")
+ a = (db.query(Score.exam_id,Student.class_id,avg)
+ .join(Score.student).filter(Student.is_deleted == 0)
+ .group_by(Score.exam_id,Student.class_id))
+ a = a.order_by(avg.desc() if order == "desc" else avg.asc())
+ return a.all()
+
+# - 统计就业薪资排名 Top N(动态输入 N)的学生的姓名、班级、就业时间和就业公司。
+def top_salary(db, n):
+ a = (db.query(Student.stu_name,
+ Student.class_id,
+ EmploymentBase.job_time,
+ EmploymentBase.company_name,
+ EmploymentBase.salary).join(Student, EmploymentBase.stu_id == Student.stu_id)
+ .filter(EmploymentBase.is_deleted == 0, Student.is_deleted == 0)
+ .order_by(EmploymentBase.salary.desc()).offset(n-1).limit(1).first())
+ if not a:
+ return None
+ return {"name": a.stu_name,"class_id": a.class_id,"job_time": str(a.job_time) if a.job_time else None,
+ "company": a.company_name,"salary": a.salary}
+
+# - 统计每个学生的就业时长(计算公式:offer下发时间 - 就业开放时间)。
+def stu_every(db):
+ a = (db.query(Student.stu_id,
+ Student.stu_name,
+ EmploymentBase.employment_open_time,
+ EmploymentOffer.offer_time)
+ .join(EmploymentBase, EmploymentBase.stu_id == Student.stu_id)
+ .join(EmploymentOffer, EmploymentOffer.stu_id == Student.stu_id)
+ .filter(Student.is_deleted == 0,EmploymentBase.is_deleted == 0,
+ EmploymentOffer.is_deleted == 0).all())
+ return [{"stu_id": sid,"name": name,
+ "days": (offer_t - open_t).days if open_t and offer_t else None}
+ for sid, name, open_t, offer_t in a]
+
+# - 统计每个班级的平均就业时长(仅统计进入就业阶段,即有就业开放时间的学生)。
+def class_avg(db):
+ days = func.datediff(EmploymentOffer.offer_time,
+ EmploymentBase.employment_open_time).label("days")
+ a = (db.query(Student.class_id,
+ func.avg(days).label("avg_days"),
+ func.count().label("count"))
+ .join(EmploymentBase, EmploymentBase.stu_id == Student.stu_id)
+ .join(EmploymentOffer, EmploymentOffer.stu_id == Student.stu_id)
+ .filter(Student.is_deleted == 0,
+ EmploymentBase.is_deleted == 0,
+ EmploymentOffer.is_deleted == 0,
+ EmploymentBase.employment_open_time.isnot(None)).group_by(Student.class_id).all())
+ return [{"class_id": i.class_id, "avg_days": round(float(i.avg_days), 2)} for i in a]
diff --git a/sqlalchemy_fastapi_demo/dao/students_dao.py b/sqlalchemy_fastapi_demo/dao/students_dao.py
new file mode 100644
index 0000000..4862bc8
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/dao/students_dao.py
@@ -0,0 +1,115 @@
+# dao/students_dao.py
+# 本文件封装对 model 表的所有数据库操作(增、删、改、查)
+
+
+from sqlalchemy.orm import Session
+from fastapi import HTTPException
+from model.c_lass import Classinfo
+from model.advisors import Advisor
+from model.students import Student
+from scheme.students import StudentCreate, StudentUpdate
+from typing import Optional, List
+
+
+class StudentDAO:
+
+ @staticmethod
+ def inspect_class_id_unq(db: Session, class_id: int):
+ """校验班级是否存在:去 c_lass 表查"""
+ return db.query(Classinfo).filter(Classinfo.class_id == class_id).first()
+
+ @staticmethod
+ def inspect_advisor_id_unq(db: Session, advisor_id: int):
+ """校验顾问是否存在:去 advisors 表查"""
+ return db.query(Advisor).filter(Advisor.advisor_id == advisor_id).first()
+
+ @staticmethod
+ def create_student(db: Session, obj_in: StudentCreate):
+ """新增学生,检查学号唯一性"""
+ exist = db.query(Student).filter(Student.stu_id == obj_in.stu_id).first()
+ # 只按 stu_id 判断,且包含逻辑删除的记录
+ if exist:
+ if exist.is_deleted == 1:
+ raise HTTPException(status_code=400, detail="该学号已被占用(处于逻辑删除状态),请先恢复")
+ raise HTTPException(status_code=400, detail="该学生已存在")
+ db_obj = Student(**obj_in.model_dump())
+ try:
+ db.add(db_obj)
+ db.commit()
+ db.refresh(db_obj)
+ except Exception:
+ db.rollback()
+ raise
+ return db_obj
+
+ @staticmethod
+ def get_students_all(db: Session, skip: int = 0, limit: int = 100):
+ """分页查询全部学生"""
+ return db.query(Student).filter(Student.is_deleted == 0).offset(skip).limit(limit).all()
+
+ @staticmethod
+ def get_student_by_id(db: Session, stu_id: int):
+ """根据学生编号单条查询"""
+ return db.query(Student).filter(Student.is_deleted == 0,
+ Student.stu_id == stu_id).first()
+
+ @staticmethod
+ def query_multi_condition(
+ db: Session,
+ stu_id: Optional[int] = None,
+ stu_name: Optional[str] = None,
+ class_id: Optional[int] = None,
+ skip: int = 0,
+ limit: int = 100
+ ) :
+ q = db.query(Student).filter(Student.is_deleted == 0)
+ """多条件选择性查询"""
+ # 只有参数不为None的时候,才加上查询条件
+ if stu_id is not None:
+ q = q.filter(Student.stu_id == stu_id)
+ if stu_name is not None:
+ q = q.filter(Student.stu_name.like(f"%{stu_name}%"))
+ if class_id is not None:
+ q = q.filter(Student.class_id == class_id)
+
+ q = q.offset(skip).limit(limit)
+ return q.all()
+
+ @staticmethod
+ def update_student(db: Session,stu_id:int, obj: StudentUpdate):
+ db_obj = db.query(Student).filter(Student.stu_id == stu_id,
+ Student.is_deleted == 0).first()
+ if not db_obj:
+ raise HTTPException(status_code=404, detail="学生数据不存在或已删除")
+
+ update_data = obj.model_dump(exclude_unset=True)
+ for field, value in update_data.items():
+ setattr(db_obj, field, value)
+ try:
+ db.commit()
+ db.refresh(db_obj)
+ except Exception:
+ db.rollback()
+ raise
+ return db_obj
+
+ @staticmethod
+ def delete_student(db: Session, stu_id: int):
+ db_obj = db.query(Student).filter(Student.stu_id == stu_id,
+ Student.is_deleted == 0).first()
+ if not db_obj:
+ raise HTTPException(status_code=404, detail="学生数据不存在或已删除")
+
+ db_obj.is_deleted = 1
+ try:
+ db.commit()
+ db.refresh(db_obj)
+ except Exception:
+ db.rollback()
+ raise
+ return True # 返回布尔值,而不是 dict
+
+
+
+
+
diff --git a/sqlalchemy_fastapi_demo/dao/teachers_dao.py b/sqlalchemy_fastapi_demo/dao/teachers_dao.py
new file mode 100644
index 0000000..15c0baf
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/dao/teachers_dao.py
@@ -0,0 +1,67 @@
+# dao/teachers_dao.py
+# 本文件封装对 Teacher 表的所有数据库操作(增、删、改、查)
+from sqlalchemy.orm import Session
+from scheme.teachers import TeacherAdd, TeacherUpdate
+from model.teachers import Teacher
+from model.c_lass import Classinfo
+
+class TeacherDAO:
+ @staticmethod
+ def add_teacher(db:Session,teacher_data:TeacherAdd): # 前端返回的teacher_data是1个Pydantic对象
+ db_teacher=Teacher(**(teacher_data.model_dump()))
+ db.add(db_teacher)
+ db.commit()
+ db.refresh(db_teacher)
+ return db_teacher
+ @staticmethod
+ def inspect_teacher_id_unq(db: Session, teacher_id: int):
+ """
+ 根据教师ID获取教师对象(用于主键唯一性检查,查询结果包含被软删除的对象)
+ """
+ return db.query(Teacher).filter(Teacher.teacher_id == teacher_id).first()
+ @staticmethod
+ def inspect_class_id_unq(db: Session, class_id: int):
+ """
+ 根据班级ID获取班级对象(用于外键检查,查询结果排除被软删除对象,即不包括is_deleted=1的对象)
+ """
+ return db.query(Classinfo).filter(Classinfo.class_id == class_id,Classinfo.is_deleted==0).first()
+ @staticmethod
+ def delete_light(db: Session, teacher_id: int):
+ """
+ 删除教师
+ :return: True 表示删除成功,False 表示教师ID不存在,已被软删除或硬删除
+ """
+ db_teacher = TeacherDAO.inspect_teacher_id_unq(db, teacher_id)
+ if (not db_teacher) or db_teacher.is_deleted==1:
+ return False
+ db_teacher.is_deleted=1 # 标记为1逻辑删除
+ db.commit() # 提交事务
+ return True
+
+ @staticmethod
+ def update(db: Session,teacher_id:int,teacher_data: TeacherUpdate):
+ """
+ 更新用户信息(只更新传入的非空字段)
+ :param db: 数据库会话
+ :param teacher_data: 包含要更新字段的 Pydantic 模型
+ :return: 更新后的 User 对象,如果用户不存在则返回 None
+ """
+ # 只更新客户端显式传入的字段(exclude_unset=True 排除未设置的字段,不排除设置了None的字段)
+ update_data = teacher_data.model_dump(exclude_unset=True) # 返回要更新字段的字典
+ db_teacher=TeacherDAO.inspect_teacher_id_unq(db, teacher_id)
+ for key, value in update_data.items():
+ setattr(db_teacher, key, value) # 动态设置属性
+ db.commit() # 提交事务
+ db.refresh(db_teacher) # 刷新对象,获取 onupdate 时间等
+ return db_teacher
+ @staticmethod
+ def get_all(db: Session, skip: int = 0, limit: int = 100):
+ """
+ 获取所有用户(支持分页,不显示软删除)
+ :param db: 数据库会话
+ :param skip: 偏移量(跳过前 skip 条,从skip+1条开始返回)
+ :param limit: 最大返回条数
+ :return: 用老师对象列表
+ """
+ return db.query(Teacher).filter(Teacher.is_deleted==0).offset(skip).limit(limit).all()
+
diff --git a/sqlalchemy_fastapi_demo/database.py b/sqlalchemy_fastapi_demo/database.py
new file mode 100644
index 0000000..e5425fb
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/database.py
@@ -0,0 +1,24 @@
+from sqlalchemy import create_engine
+from sqlalchemy.orm import declarative_base, sessionmaker
+
+# 1. 配置数据库连接地址(注释掉 SQLite,直接使用 MySQL)
+# SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
+SQLALCHEMY_DATABASE_URL = "mysql+pymysql://root:123456@host.docker.internal:3306/0914tw"
+
+# 2. 创建 engine(数据库引擎)
+# 注意:MySQL 不需要 connect_args={"check_same_thread": False},这行要去掉
+engine = create_engine(SQLALCHEMY_DATABASE_URL)
+
+# 3. 创建 Base(ORM 基类)
+Base = declarative_base()
+
+# 4. 创建 SessionLocal(会话工厂,必须在 engine 最终确定后再绑定)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
+
+# 提供数据库会话,接口通过 Depends(get_db) 自动获取
+def get_db():
+ db = SessionLocal() # 创建一个新的数据库会话
+ try:
+ yield db # yield = 把会话"借出去"给接口函数用(执行到这里暂停,等接口用完)
+ finally:
+ db.close() # finally = 无论接口成功还是报错,最后一定关闭会话,释放数据库连接
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/main.py b/sqlalchemy_fastapi_demo/main.py
new file mode 100644
index 0000000..eeb4880
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/main.py
@@ -0,0 +1,49 @@
+from api import statistics
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from api import Score
+from api import employment_api # 导入 employment_api 子路由
+from api import teachers
+from api.advisors import router as advisor_router
+from api.c_lass import router as class_router
+from api.students import router as student_router
+# 从刚才创建的 database.py 中导入 Base 和 engine
+from database import Base, engine
+
+# 现在这行代码就不会报错了
+Base.metadata.create_all(bind=engine)
+
+
+# 创建 FastAPI 实例
+app = FastAPI(
+ title="FastAPI + SQLAlchemy 分层架构",
+ description="学生信息管理系统 - 班级模块",
+ version="1.0.0",
+)
+
+# 注册班级路由,统一加前缀 /c_lass
+app.include_router(class_router, prefix="/c_lass", tags=["班级信息管理模块"])
+# ⑫ 把班级路由注册进应用,统一加前缀 /c_lass
+# 所以API层里 @router.post("/add") 的完整地址是 POST /c_lass/add
+app.include_router(employment_api.router, prefix="/api/employment_api", tags=["就业模块管理"])
+app.include_router(Score.app_score, prefix="/api/score", tags=["成绩管理"])
+app.include_router(student_router,prefix="/api/student", tags=["学生信息管理"])
+app.include_router(advisor_router, prefix="/advisors", tags=["顾问老师模块"])
+app.include_router(teachers.router, prefix="/api/teachers", tags=["老师模块管理"])
+app.include_router(statistics.router,prefix="/api/statistics",tags=["统计模块"])
+# 根路径
+# @app.get("/")
+# async def root():
+# return {"message": "欢迎访问!请访问 /docs 查看接口文档。"}
+
+
+# 直接运行此文件时启动 uvicorn
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(
+ "main:app",
+ host="127.0.0.1", # 改成本机地址
+ port=8004,
+ reload=True,
+ )
diff --git a/sqlalchemy_fastapi_demo/model/Score.py b/sqlalchemy_fastapi_demo/model/Score.py
new file mode 100644
index 0000000..8983c9d
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/model/Score.py
@@ -0,0 +1,18 @@
+from sqlalchemy import Column, Integer,ForeignKey
+from sqlalchemy.orm import relationship
+from database import Base
+
+
+# 成绩表表模型
+class Score(Base):
+ __tablename__ = 'score'
+ # 联合主键stu_id,exam_order
+ # 外键关联student
+ stu_id=Column(Integer,ForeignKey('student.stu_id'),primary_key=True)
+ # 声明关联student
+ student=relationship('Student',back_populates='score')
+ exam_id=Column(Integer,primary_key=True)
+ score=Column(Integer,nullable=False)
+
+
+
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/Score.cpython-310.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/Score.cpython-310.pyc
new file mode 100644
index 0000000..3ca8c52
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/Score.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/Score.cpython-312.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/Score.cpython-312.pyc
new file mode 100644
index 0000000..913657d
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/Score.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/advisors.cpython-310.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/advisors.cpython-310.pyc
new file mode 100644
index 0000000..f8056c9
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/advisors.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/advisors.cpython-312.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/advisors.cpython-312.pyc
new file mode 100644
index 0000000..de89e76
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/advisors.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/c_lass.cpython-310.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/c_lass.cpython-310.pyc
new file mode 100644
index 0000000..342dcde
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/c_lass.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/c_lass.cpython-312.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/c_lass.cpython-312.pyc
new file mode 100644
index 0000000..ec05cf2
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/c_lass.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/employment.cpython-310.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/employment.cpython-310.pyc
new file mode 100644
index 0000000..3267ea8
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/employment.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/employment.cpython-312.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/employment.cpython-312.pyc
new file mode 100644
index 0000000..a33b66d
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/employment.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/students.cpython-310.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/students.cpython-310.pyc
new file mode 100644
index 0000000..4b6ca43
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/students.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/students.cpython-312.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/students.cpython-312.pyc
new file mode 100644
index 0000000..e70f839
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/students.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/teachers.cpython-310.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/teachers.cpython-310.pyc
new file mode 100644
index 0000000..93e975a
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/teachers.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/__pycache__/teachers.cpython-312.pyc b/sqlalchemy_fastapi_demo/model/__pycache__/teachers.cpython-312.pyc
new file mode 100644
index 0000000..ce57440
Binary files /dev/null and b/sqlalchemy_fastapi_demo/model/__pycache__/teachers.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/model/advisors.py b/sqlalchemy_fastapi_demo/model/advisors.py
new file mode 100644
index 0000000..4e4d1ac
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/model/advisors.py
@@ -0,0 +1,14 @@
+from datetime import datetime
+from sqlalchemy import Column, Integer, String, DateTime
+from sqlalchemy.orm import relationship
+
+from database import Base
+#创建顾问老师表模型。
+class Advisor(Base):
+ __tablename__ = 'advisor'
+ advisor_id = Column(Integer, primary_key=True,autoincrement=False)
+ advisor_name = Column(String(50),nullable=False)
+ #引入时间模块,得到创建时时间。特别注意这里,datetime点now不能有括号,有括号时,就在创建这个表类的时候,就锚定了时间。
+ #不写括号的话,就是当这个生成实例的时候,也就是具体往表里面注入内容的时候,才会生成一个对应的时间。
+ # 反向关系:Student.advisor 的对端(属性名必须是 students,与 Student.advisor 的 back_populates 一致)
+ students = relationship("Student", back_populates="advisor")
diff --git a/sqlalchemy_fastapi_demo/model/c_lass.py b/sqlalchemy_fastapi_demo/model/c_lass.py
new file mode 100644
index 0000000..7189683
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/model/c_lass.py
@@ -0,0 +1,23 @@
+
+
+from sqlalchemy import Column, Integer, Date # 导入"列"和字段类型(整数、日期)
+from sqlalchemy.orm import relationship # 导入"关系"函数,用来定义表间关联
+from database import Base # 导入database.py里的Base基类
+
+
+class Classinfo(Base):# 定义ORM模型类,继承Base,SQLAlchemy才知道它对应一张表
+ # 表名必须和建表语句一致:c_lass
+ __tablename__ = "c_lass" # 指定表名,必须和MySQL里的表名一模一样,否则连不上表
+
+ class_id = Column(Integer, primary_key=True, index=True) # 主键列:整数类型;index=True表示建索引,按这个字段查会更快
+ start_time = Column(Date, nullable=False) # 开班日期:日期类型;nullable=False = 不能为空(数据库层面强制)
+ # 逻辑删除:0未删除,1已删除,默认0
+ is_deleted = Column(Integer, nullable=False, default=0)
+
+ # 反向关系:Student.my_class / Teacher.classes 的对端
+ students = relationship("Student", back_populates="my_class")
+ # 声明关系:一个班级下有很多学生(一对多)。"Student"是字符串,指向学生模型
+ # back_populates="my_class":和Student模型里的my_class属性互相呼应
+ teachers = relationship("Teacher", back_populates="classes")
+ # 同样:一个班级下有很多老师
+
diff --git a/sqlalchemy_fastapi_demo/model/employment.py b/sqlalchemy_fastapi_demo/model/employment.py
new file mode 100644
index 0000000..9c09520
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/model/employment.py
@@ -0,0 +1,20 @@
+#导入创造引擎、定义类模块、数据类型模块、连接符模块
+from sqlalchemy import Column, Integer, String, Date, Float, ForeignKey
+#导入基类
+from database import Base
+#定义就业基础模块
+class EmploymentBase(Base):
+ __tablename__="employment_base"
+ stu_id=Column(Integer,ForeignKey("student.stu_id"),primary_key=True)
+ employment_open_time=Column(Date)
+ job_time=Column(Date)
+ company_name=Column(String(100))
+ salary=Column(Float)
+ is_deleted=Column(Integer,default=0)
+#定义就业协议模块
+class EmploymentOffer(Base):
+ __tablename__="employment_offer"
+ stu_id=Column(Integer,primary_key=True)
+ offer_id=Column(Integer,primary_key=True)
+ offer_time=Column(Date)
+ is_deleted=Column(Integer,default=0)
diff --git a/sqlalchemy_fastapi_demo/model/students.py b/sqlalchemy_fastapi_demo/model/students.py
new file mode 100644
index 0000000..a2d2af0
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/model/students.py
@@ -0,0 +1,41 @@
+# model/users.py
+# 本文件定义 User 表的结构,映射到 MySQL 数据库
+
+from sqlalchemy import Column, Integer, String, Float, ForeignKey,DateTime,Date
+from sqlalchemy.orm import declarative_base,relationship
+from sqlalchemy.sql import func
+from database import Base
+
+
+# model 类,对应 student表
+class Student(Base):
+ __tablename__ = "student" #指定表名
+
+ stu_id = Column(Integer, primary_key = True) #主键,学生编号
+ stu_name = Column(String(10),nullable = False) #学生名字,非空约束
+ native_place = Column(String(30),nullable = False) #籍贯,非空
+ graduate_school = Column(String(50),nullable = False) #毕业院校
+ major = Column(String(20),nullable = False) #专业
+ enroll_time = Column(Date, comment = "入学时间",nullable = False)
+ graduate_time = Column(Date, comment = "毕业时间",nullable = False)
+ education = Column(String(10),nullable = False) #学历,非空
+ age = Column(Integer,nullable = False) #年龄
+ gender = Column(String(10),nullable = False) #性别
+ # employment_open_time = Column(Date,nullable = False) #简历开放时间
+ # company_name = Column(String(100),nullable = False) #公司名称
+ # salary = Column(Float,nullable = False) #薪资
+ # # 逻辑删除标记
+ is_deleted = Column(Integer, nullable=False,default=0, comment="0正常;1逻辑删除")
+ # 外键:关联 C_lass 的 id
+ class_id = Column(Integer, ForeignKey("c_lass.class_id"), nullable=False)
+ # 关系属性:关联 Classinfo
+ my_class = relationship("Classinfo", back_populates="students")
+ # 外键:关联Advisor的id
+ advisor_id = Column(Integer,ForeignKey("advisor.advisor_id"),nullable=False)
+ # 关系属性:关联Advisor
+ advisor = relationship("Advisor", back_populates="students")
+ # 关系属性:关联成绩(Score.student 的对端)
+ score = relationship("Score", back_populates="student")
+
+
+
diff --git a/sqlalchemy_fastapi_demo/model/teachers.py b/sqlalchemy_fastapi_demo/model/teachers.py
new file mode 100644
index 0000000..e3c2962
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/model/teachers.py
@@ -0,0 +1,16 @@
+# 与数据库的表进行关联
+# 导入database文件,这里已经创建基类
+from sqlalchemy import Column, Integer, ForeignKey, String
+from sqlalchemy.orm import relationship
+from database import Base
+# 关联数据库中的teacher表
+class Teacher(Base):
+ __tablename__ = 'teacher'
+ teacher_id = Column(Integer, primary_key=True)
+ class_id = Column(Integer,ForeignKey('c_lass.class_id'),nullable=False)
+ teacher_name = Column(String(50),nullable=False)
+ job_name=Column(String(50),nullable=False,comment='主讲、班主任、助教')
+ is_deleted = Column(Integer, default=0, nullable=False)
+ classes=relationship("Classinfo",back_populates="teachers")
+ def __repr__(self):
+ return f""
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/requirements.txt b/sqlalchemy_fastapi_demo/requirements.txt
new file mode 100644
index 0000000..593df71
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/requirements.txt
@@ -0,0 +1,5 @@
+fastapi>=0.100.0
+uvicorn>=0.23.0
+sqlalchemy>=2.0.0
+pymysql>=1.1.0
+pydantic>=2.0.0
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/scheme/Score.py b/sqlalchemy_fastapi_demo/scheme/Score.py
new file mode 100644
index 0000000..a436e5a
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/scheme/Score.py
@@ -0,0 +1,15 @@
+# 请求体
+from pydantic import BaseModel, Field
+
+
+
+class ScoreCreate(BaseModel):
+ stu_id: int=Field(...,ge=1,description='学生id必须大于等于1')
+ exam_id: int=Field(...,ge=1,description='考核序次必须大于等于1')
+ score: int=Field(...,ge=0,le=100,description='成绩必须在0-100之间')
+
+
+class ScoreUpdate(BaseModel):
+ # stu_id: int = Field(None, ge=1, description='学生id必须大于等于1')
+ # exam_id: int = Field(None, ge=1, description='考核序次必须大于等于1')
+ score: int = Field(None, ge=0, le=100, description='成绩必须在0-100之间')
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/Score.cpython-310.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/Score.cpython-310.pyc
new file mode 100644
index 0000000..9fac3f5
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/Score.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/Score.cpython-312.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/Score.cpython-312.pyc
new file mode 100644
index 0000000..fddb390
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/Score.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/advisors.cpython-310.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/advisors.cpython-310.pyc
new file mode 100644
index 0000000..bce6916
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/advisors.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/advisors.cpython-312.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/advisors.cpython-312.pyc
new file mode 100644
index 0000000..52917d5
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/advisors.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/c_lass.cpython-310.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/c_lass.cpython-310.pyc
new file mode 100644
index 0000000..ac41dd4
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/c_lass.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/c_lass.cpython-312.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/c_lass.cpython-312.pyc
new file mode 100644
index 0000000..7c5fff1
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/c_lass.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/employment.cpython-310.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/employment.cpython-310.pyc
new file mode 100644
index 0000000..43a80c5
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/employment.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/employment.cpython-312.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/employment.cpython-312.pyc
new file mode 100644
index 0000000..20c0609
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/employment.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/statistics.cpython-310.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/statistics.cpython-310.pyc
new file mode 100644
index 0000000..91bf2c8
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/statistics.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/students.cpython-310.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/students.cpython-310.pyc
new file mode 100644
index 0000000..959d48d
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/students.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/students.cpython-312.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/students.cpython-312.pyc
new file mode 100644
index 0000000..827f45e
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/students.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/teachers.cpython-310.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/teachers.cpython-310.pyc
new file mode 100644
index 0000000..a082938
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/teachers.cpython-310.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/__pycache__/teachers.cpython-312.pyc b/sqlalchemy_fastapi_demo/scheme/__pycache__/teachers.cpython-312.pyc
new file mode 100644
index 0000000..5735913
Binary files /dev/null and b/sqlalchemy_fastapi_demo/scheme/__pycache__/teachers.cpython-312.pyc differ
diff --git a/sqlalchemy_fastapi_demo/scheme/advisors.py b/sqlalchemy_fastapi_demo/scheme/advisors.py
new file mode 100644
index 0000000..281c9bb
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/scheme/advisors.py
@@ -0,0 +1,12 @@
+from datetime import datetime
+from pydantic import BaseModel,Field
+#引入Pydantic数据校验。
+class AdvisorIn(BaseModel):
+ advisor_id: int=Field(...,ge=1,description="顾问ID")
+ advisor_name: str=Field(...,min_length=2,max_length=8,description="姓名为2~8个字符")
+
+class AdvisorOut(BaseModel):
+ advisor_id: int
+ advisor_name: str
+ class Config:
+ from_attributes = True
diff --git a/sqlalchemy_fastapi_demo/scheme/c_lass.py b/sqlalchemy_fastapi_demo/scheme/c_lass.py
new file mode 100644
index 0000000..3f4d28e
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/scheme/c_lass.py
@@ -0,0 +1,34 @@
+
+
+
+from pydantic import BaseModel, Field, ConfigDict # BaseModel=模型基类,Field=字段规则,ConfigDict=模型配置
+from datetime import date # Python的日期类型(只有年月日)
+from typing import Optional # Optional[X] 表示"可以是X,也可以是None"
+
+# ---------------- 请求模型 ----------------
+# 新增班级:class_id、start_time 必填
+# 请求模型 = 前端传过来的数据长什么样,FastAPI自动帮你校验
+class ClassCreate(BaseModel):# 新增班级的入参模型:POST新增时,请求体必须符合这个结构
+ class_id: int = Field(..., ge=1, description="班级编号,正整数,不能重复")
+ start_time: date = Field(..., description="开班日期,格式 yyyy-MM-dd")
+# description:这行字会显示在Swagger文档里,方便前端看
+
+# 修改班级:字段可选,前端传哪个改哪个(局部更新)
+class ClassUpdate(BaseModel):
+ # class_id: int = Field( ge=1, description="班级编号,可选") 被注释掉 = 不允许修改班级编号
+ start_time: Optional[date] = Field(None, description="开班日期,格式 yyyy-MM-dd")
+ # Optional[date] = 可以传日期也可以不传;Field(None) = 不传时默认值是None
+
+
+# ---------------- 响应模型 ----------------
+# 响应模型:后端返回给前端的数据结构,from_attributes 允许直接从 ORM (Classinfo实例)的属性取值,不用手动一个个赋值
+class ClassResponse(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+ class_id: int # 返回字段:班级编号
+ start_time: date# 返回字段:开班日期
+
+
+
+
+#逻辑:**三个模型管三种场景**—— 新增(必填)、修改(可选)、响应(输出)。
+# 前端传错类型 / 缺字段,FastAPI 直接返回 422,不会把脏数据送进数据库。
diff --git a/sqlalchemy_fastapi_demo/scheme/employment.py b/sqlalchemy_fastapi_demo/scheme/employment.py
new file mode 100644
index 0000000..98fcc0b
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/scheme/employment.py
@@ -0,0 +1,65 @@
+# 就业开放时间和offer下发时间校验
+# 导入基类、过滤条件、校验器
+from pydantic import BaseModel, Field, model_validator, field_validator
+# 导入时间模块
+from datetime import date, datetime
+# 导入可选类型、列表类型
+from typing import Optional, List
+
+# ---------- 请求模型 ----------
+
+# 修改就业记录
+class EmploymentOfferUpdate(BaseModel):
+ offer_time: Optional[date] = None
+
+
+# 修改就业基础
+class EmploymentBaseUpdate(BaseModel):
+ employment_open_time: Optional[date] = None
+ job_time: Optional[date] = None
+ company_name: Optional[str] = None
+ salary: Optional[float] = None
+
+# 添加就业基础
+class EmploymentBaseCreate(BaseModel):
+ stu_id: int = Field(..., description="学生编号")
+ job_time: Optional[date] = Field(None, description="实际去就职时间")
+ employment_open_time: date = Field(description="就业开放时间:年-月-日")
+ company_name: str = Field(max_length=100, description="就业公司")
+ salary: float = Field(..., gt=0, description="就业薪资,保留2位小数")
+
+# 添加就业记录
+class EmploymentOfferCreate(BaseModel):
+ stu_id: int = Field(..., description="学生编号")
+ offer_id: int = Field(..., description="offer编号")
+ offer_time: date = Field(description="offer下发时间:年-月-日")
+
+# 多条件组合查询
+class EmploymentQuery(BaseModel):
+ stu_id: Optional[int] = None
+ company_name: Optional[str] = Field(None, max_length=100, description="公司名称")
+ min_salary: Optional[float] = None
+ max_salary: Optional[float] = None
+
+
+# ---------- 响应模型 ----------
+# 就业记录查询响应
+class EmploymentOfferQueryResponse(BaseModel):
+ stu_id: int
+ offer_id: int
+ offer_time: date
+
+ class Config:
+ from_attributes = True
+
+
+# 就业基础查询响应
+class EmploymentBaseQueryResponse(BaseModel):
+ stu_id: int
+ employment_open_time: Optional[date] = None
+ job_time: Optional[date] = None
+ company_name: Optional[str] = None
+ salary: Optional[float] = None
+
+ class Config:
+ from_attributes = True
diff --git a/sqlalchemy_fastapi_demo/scheme/statistics.py b/sqlalchemy_fastapi_demo/scheme/statistics.py
new file mode 100644
index 0000000..fa6bd6e
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/scheme/statistics.py
@@ -0,0 +1,36 @@
+from pydantic import BaseModel, Field, model_validator
+
+class StudentAge(BaseModel):
+ age_star: int | None = Field(None, gt=0)
+ age_end: int | None = Field(None, gt=0)
+ age_value: int | None = Field(None, gt=0)
+
+ @model_validator(mode="after")
+ def check_age(self):
+ if self.age_value is not None:
+ if self.age_star is not None or self.age_end is not None:
+ raise ValueError('age_value和age_star/age_end区间不能同时存在')
+ else:
+ if self.age_star is None or self.age_end is None:
+ raise ValueError('区间需填写完整')
+ if self.age_star is not None and self.age_end is not None:
+ if self.age_star > self.age_end:
+ raise ValueError('输入起始值不能大于终止值')
+ return self
+
+class ClassCount(BaseModel):
+ class_id: int = Field(..., gt=0)
+ gender: str | None = Field(None, description="可填男/女")
+
+class ScoreCount(BaseModel):
+ score: int = Field(..., ge=0, le=100)
+ num: int | None = Field(None, gt=0)
+
+class ScoreAvg(BaseModel):
+ exam_order: int = Field(...)
+
+class Employment(BaseModel):
+ top: int = Field(...)
+
+class EmploymentOff(BaseModel):
+ class_id: int = Field(...)
\ No newline at end of file
diff --git a/sqlalchemy_fastapi_demo/scheme/students.py b/sqlalchemy_fastapi_demo/scheme/students.py
new file mode 100644
index 0000000..3e1e07a
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/scheme/students.py
@@ -0,0 +1,58 @@
+# scheme/student.py
+from pydantic import BaseModel, Field,model_validator
+from datetime import datetime, date
+from typing import Optional
+
+# ---------- 请求体模型 ----------
+class StudentCreate(BaseModel):
+ stu_id: int = Field(..., ge = 1)
+ class_id: int = Field(..., ge = 1)
+ advisor_id: int = Field(..., ge=1, description="顾问老师id,对应 advisors.advisor_id")
+ stu_name: str = Field(..., min_length=1, max_length=10)
+ native_place: str = Field(..., min_length=1, max_length=30)
+ graduate_school: str = Field(..., min_length=1, max_length=50)
+ education: str = Field(..., min_length=1, max_length=10)
+ major: str = Field(..., min_length=1, max_length=20)
+ age: int = Field(..., ge = 18, le =40)
+ gender: str = Field(..., min_length=1, max_length=20)
+ graduate_time:date= Field(...)
+ enroll_time:date= Field(...)
+
+ # @model_validator(mode="after")
+ # def check_time_order(self):
+ # enroll = self.enroll_time
+ # graduate = self.graduate_time
+ #
+ # # 两个时间都传入时才校验
+ # if enroll and graduate:
+ # if enroll >= graduate:
+ # raise ValueError("入学时间必须早于毕业时间")
+
+class StudentUpdate(BaseModel):
+ class_id: Optional[int] = Field(None)
+ advisor_id: Optional[int] = Field(None, ge=1)
+ stu_name: Optional[str] = Field(None, min_length=1, max_length=10)
+ native_place: Optional[str] = Field(None, min_length=1, max_length=30)
+ graduate_school: Optional[str] = Field(None, min_length=1, max_length=50)
+ education: Optional[str] = Field(None, min_length=1, max_length=10)
+ major: Optional[str] = Field(None, min_length=1, max_length=20)
+ age: Optional[int] = Field(None, ge = 18, le =40)
+ gender: Optional[str] = Field(None, min_length=1, max_length=20)
+
+# ----------响应体模型-------------
+class StudentResponse(BaseModel):
+ stu_id: int
+ class_id: int # 数据库非空,这里必须是 int
+ stu_name: str
+ native_place: str
+ graduate_school: Optional[str]
+ education: str
+ major: Optional[str]
+ # 敏感字段已在此模型中脱敏
+
+ class Config:
+ from_attributes = True #支持orm对象转换,让pydantic可以读取ORM数据库对象(SQLAlchemy模型)
+
+
+
+
diff --git a/sqlalchemy_fastapi_demo/scheme/teachers.py b/sqlalchemy_fastapi_demo/scheme/teachers.py
new file mode 100644
index 0000000..45a5120
--- /dev/null
+++ b/sqlalchemy_fastapi_demo/scheme/teachers.py
@@ -0,0 +1,52 @@
+# 正确
+from pydantic import Field
+
+from datetime import datetime
+from typing import Optional
+
+from pydantic import BaseModel, field_validator
+
+
+# --------------------请求体模型-----------------------------
+# 用于增加1行数据# 加了软删除标签
+class TeacherAdd(BaseModel):
+ teacher_id:int=Field(...,ge=1,description='教师ID是整数类型')
+ class_id:int=Field(...,ge=1,description='班级ID是整数类型')
+ teacher_name:str=Field(...,description='教师姓名')
+ job_name:str=Field(...,min_length=2,description='主讲老师、班主任、助教')
+ is_deleted:int=Field(0,ge=0,le=0,description='只能输入0,0代表未删除,1代表已经被软删除')
+
+ @field_validator("job_name")
+ @classmethod
+ def validate_job_name(cls, v):
+ """校验 job_name 必须是 主讲/班主任/助教 之一"""
+ allowed = ["主讲老师", "班主任", "助教"]
+ if v not in allowed:
+ raise ValueError(f"job_name 必须是 {allowed} 之一,当前值: {v}")
+ return v
+# 用于更新数据
+class TeacherUpdate(BaseModel):
+ class_id: Optional[int] = Field(None, ge=1, description='班级ID是整数类型') # ge=1 只对非 None 的值生效
+ teacher_name: Optional[str] = Field(None, description='教师姓名')
+ job_name: Optional[str] = Field(None,min_length=2, description='主讲老师、班主任、助教')
+
+ @field_validator("job_name")
+ @classmethod
+ def validate_job_name(cls, v):
+ """校验 job_name 必须是 主讲/班主任/助教 之一"""
+ allowed = ["主讲老师", "班主任", "助教"]
+ if v not in allowed:
+ raise ValueError(f"job_name 必须是 {allowed} 之一,当前值: {v}")
+ return v
+# --------------------- 响应模型 ----------------------------
+class TeacherResponse(BaseModel):
+ teacher_id: int
+ class_id: int
+ teacher_name: str
+ job_name: str
+
+ class Config: # 告诉 Pydantic:"可以从任意对象的属性中读取数据,而不只是从字典中读取。"
+ from_attributes = True # 支持 ORM 对象转换
+
+
+