学生信息管理系统
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
.venv
|
||||
venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
*.sqlite3
|
||||
*.db
|
||||
.env
|
||||
Generated
+10
@@ -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>
|
||||
Generated
+4
@@ -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
@@ -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>
|
||||
@@ -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"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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":"删除成功"}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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}
|
||||
@@ -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": "删除成功"}
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
|
||||
@@ -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`,删了的数据还在数据库里,只是查不到 —— 这就是逻辑删除。
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = 无论接口成功还是报错,最后一定关闭会话,释放数据库连接
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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}')>"
|
||||
@@ -0,0 +1,5 @@
|
||||
fastapi>=0.100.0
|
||||
uvicorn>=0.23.0
|
||||
sqlalchemy>=2.0.0
|
||||
pymysql>=1.1.0
|
||||
pydantic>=2.0.0
|
||||
@@ -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之间')
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user