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 对象转换 + + +