学生信息管理系统

This commit is contained in:
倾音节
2026-09-21 19:14:15 +08:00
commit e1491ead63
110 changed files with 1922 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
.venv
venv
__pycache__
*.pyc
*.pyo
.git
.idea
.vscode
*.sqlite3
*.db
.env
+10
View File
@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 已忽略包含查询文件的默认文件夹
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="~\Desktop\sqlalchemy_fastapi_demo\.venv" project-jdk-type="Python SDK" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/sqlalchemy_fastapi_demo.iml" filepath="$PROJECT_DIR$/.idea/sqlalchemy_fastapi_demo.iml" />
</modules>
</component>
</project>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="~\Desktop\sqlalchemy_fastapi_demo\.venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PackageRequirementsSettings" />
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
</component>
<component name="ReSTService" />
<component name="TestRunnerService" />
</module>
+21
View File
@@ -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"]
View File
+132
View File
@@ -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":"删除成功"}
+50
View File
@@ -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}
+73
View File
@@ -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
@@ -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": "删除成功"}
+106
View File
@@ -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)
+108
View File
@@ -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
+81
View File
@@ -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
+56
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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
+66
View File
@@ -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`,删了的数据还在数据库里,只是查不到 —— 这就是逻辑删除。
+175
View File
@@ -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
+110
View File
@@ -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]
+115
View File
@@ -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
@@ -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()
+24
View File
@@ -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 = 无论接口成功还是报错,最后一定关闭会话,释放数据库连接
+49
View File
@@ -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,
)
+18
View File
@@ -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)
+14
View File
@@ -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")
+23
View File
@@ -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")
# 同样:一个班级下有很多老师
@@ -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)
+41
View File
@@ -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")
+16
View File
@@ -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"<Teacher(teacher_id={self.teacher_id},class_id={self.class_id},teacher_name='{self.teacher_name}',job_name='{self.job_name}')>"
+5
View File
@@ -0,0 +1,5 @@
fastapi>=0.100.0
uvicorn>=0.23.0
sqlalchemy>=2.0.0
pymysql>=1.1.0
pydantic>=2.0.0
+15
View File
@@ -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之间')

Some files were not shown because too many files have changed in this diff Show More