diff --git a/.codebuddy/.gitignore b/.codebuddy/.gitignore new file mode 100644 index 0000000..9f4c740 --- /dev/null +++ b/.codebuddy/.gitignore @@ -0,0 +1 @@ +db/ \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..913e40d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +# 构建上下文排除项,减小镜像体积 +**/__pycache__/ +**/*.pyc +**/*.pyo + +# IDE / 版本控制 +.codebuddy/ +**/.git/ +**/.idea/ +**/.vscode/ + +# Docker 自身 +Dockerfile +docker-compose.yml +.dockerignore + +# 文档 +*.md diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b6b1ecf --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 已忽略包含查询文件的默认文件夹 +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..c3f1c77 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..808b041 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/沃林学生管理系统.iml b/.idea/沃林学生管理系统.iml new file mode 100644 index 0000000..822f348 --- /dev/null +++ b/.idea/沃林学生管理系统.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8d48324 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# ============================================ +# 沃林学生管理系统 (Docker) +# FastAPI + SQLAlchemy + PyMySQL -> MySQL 8 +# ============================================ +FROM python:3.11-slim + +# 运行环境:不生成 .pyc、日志实时输出、pip 不缓存 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +# 1) 先装依赖(利用层缓存,改代码不必重装) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt \ + -i https://pypi.tuna.tsinghua.edu.cn/simple + +# 2) 拷贝应用代码 +COPY . . + +# 3) static 为空目录时 COPY 不会生成,手动补上(main.py 挂载时需要) +RUN mkdir -p static + +EXPOSE 8001 + +# 4) 启动流程:建表 -> 灌种子数据(seed_data 有幂等检查,可重复执行) -> 启动服务 +CMD ["sh", "-c", "python -c 'from database import init_db; init_db()' && python seed_data.py && exec python main.py"] diff --git a/__pycache__/database.cpython-314.pyc b/__pycache__/database.cpython-314.pyc new file mode 100644 index 0000000..16e3041 Binary files /dev/null and b/__pycache__/database.cpython-314.pyc differ diff --git a/__pycache__/main.cpython-314.pyc b/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000..8fcfd39 Binary files /dev/null and b/__pycache__/main.cpython-314.pyc differ diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/__pycache__/__init__.cpython-314.pyc b/api/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..ce92dbf Binary files /dev/null and b/api/__pycache__/__init__.cpython-314.pyc differ diff --git a/api/__pycache__/analysis.cpython-314.pyc b/api/__pycache__/analysis.cpython-314.pyc new file mode 100644 index 0000000..b6ddf12 Binary files /dev/null and b/api/__pycache__/analysis.cpython-314.pyc differ diff --git a/api/__pycache__/class_api.cpython-314.pyc b/api/__pycache__/class_api.cpython-314.pyc new file mode 100644 index 0000000..73f1859 Binary files /dev/null and b/api/__pycache__/class_api.cpython-314.pyc differ diff --git a/api/__pycache__/counselor_api.cpython-314.pyc b/api/__pycache__/counselor_api.cpython-314.pyc new file mode 100644 index 0000000..4fd94de Binary files /dev/null and b/api/__pycache__/counselor_api.cpython-314.pyc differ diff --git a/api/__pycache__/employment.cpython-314.pyc b/api/__pycache__/employment.cpython-314.pyc new file mode 100644 index 0000000..f753288 Binary files /dev/null and b/api/__pycache__/employment.cpython-314.pyc differ diff --git a/api/__pycache__/score.cpython-314.pyc b/api/__pycache__/score.cpython-314.pyc new file mode 100644 index 0000000..b151eeb Binary files /dev/null and b/api/__pycache__/score.cpython-314.pyc differ diff --git a/api/__pycache__/student.cpython-314.pyc b/api/__pycache__/student.cpython-314.pyc new file mode 100644 index 0000000..c671198 Binary files /dev/null and b/api/__pycache__/student.cpython-314.pyc differ diff --git a/api/__pycache__/teachers.cpython-314.pyc b/api/__pycache__/teachers.cpython-314.pyc new file mode 100644 index 0000000..cb5cd88 Binary files /dev/null and b/api/__pycache__/teachers.cpython-314.pyc differ diff --git a/api/analysis.py b/api/analysis.py new file mode 100644 index 0000000..1645dad --- /dev/null +++ b/api/analysis.py @@ -0,0 +1,51 @@ +from fastapi import FastAPI,APIRouter,Depends +from schemas.analysis import (AnalysisStudentInfoAge,AnalysisStudent_score,AnalysisStudent_score60 +,AnalysisStudent_score_class,AnalysisStudent_employments_salary) +from dao.analysis import (student_info_age,student_info_class,student_score,student_score_class,student_employments_salary +,student_employments_time,student_employments_class_time,student_score60) +from database import get_db +from sqlalchemy.orm import Session + +router = APIRouter(prefix="/analysis", tags=["统计分析"]) + +#动态年龄范围查询:支持用户输入年龄阈值及比较条件(如大于、小于、等于、区间等),动态查询符合条件的学员信息 +@router.get("/studnet_info/age",summary="动态年龄范围查询") +async def student_info_age1(student:AnalysisStudentInfoAge = Depends(),db: Session = Depends(get_db)): + return student_info_age(db, student.min_age, student.max_age, student.operator) + +#多维度班级统计:统计每个班级的总人数,以及按性别(男、女)细分的人数分布 +@router.get("/studnet_info/class",summary="多维度班级统计") +async def student_info_class1(db: Session = Depends(get_db)): + return student_info_class(db) + +#查询每次考试成绩都在输入分数线(如80分)以上的学生的编号、姓名和成绩。 +@router.get("/studnet_score",summary="查询每次考试成绩都在输入分数线以上的学生") +async def student_score1(s_n:AnalysisStudent_score = Depends(),db: Session = Depends(get_db)): + return student_score(db,s_n.s_n) + +#查询有输入指定次数(如两次(包含两次))以上不及格的学生的姓名、班级和不及格成绩明细。 +@router.get("/studnet_score60",summary="查询有输入指定次数(如两次(包含两次))以上不及格的学生") +async def student_score60_1(n:AnalysisStudent_score60 = Depends(),db: Session = Depends(get_db)): + return student_score60(db,n.n) + +#统计每次考试每个班级的平均分,并支持按分数从高到低或从低到高动态排序。 +@router.get("/studnet_score_class",summary="统计每次考试每个班级的平均分") +async def student_score_class1(order:AnalysisStudent_score_class = Depends(),db: Session = Depends(get_db)): + return student_score_class(db,order.order) + +#查询查询就业薪资排名前n的学生 +@router.get("/studnet_employments_salary",summary="查询查询就业薪资排名前n的学生") +async def student_employments_salary1(n:AnalysisStudent_employments_salary = Depends(),db: Session = Depends(get_db)): + return student_employments_salary(db,n.n) + +#统计每个学生的就业时长 +@router.get("/studnet_employments_time",summary="每个学生的就业时长(天)") +async def student_employments_time1(db: Session = Depends(get_db)): + return student_employments_time(db) + +#统计每个班级的平均就业时长(仅统计进入就业阶段,即有就业开放时间的学生) +@router.get("/studnet_employments_class_time",summary="每个班级的平均就业时长(天)") +async def student_employments_class_time1(db: Session = Depends(get_db)): + return student_employments_class_time(db) + + diff --git a/api/class_api.py b/api/class_api.py new file mode 100644 index 0000000..a707d7b --- /dev/null +++ b/api/class_api.py @@ -0,0 +1,115 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import Optional + + +from database import get_db # 按你项目实际路径改 +from dao.class_dao import ClassDAO +from schemas.class_scheme import ( + ClassCreate, + ClassUpdateBase, + ClassHeadTeacherUpdate, + ClassTeachersUpdate, + ClassDelete, + ClassDetail, + ClassPageResult, + BatchDeleteResult, +) + +router = APIRouter(prefix="/api/classes", tags=["班级管理"]) + + +@router.post("", response_model=ClassDetail, summary="新增班级") +def create_class(req: ClassCreate, db: Session = Depends(get_db)): + try: + return ClassDAO.create(db, req) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + + +@router.get("", response_model=ClassPageResult, summary="分页查询班级列表") +def list_classes( + page_num: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100), + class_name: Optional[str] = None, + head_teacher_id: Optional[int] = None, + db: Session = Depends(get_db), +): + total, rows = ClassDAO.list_page( + db, page_num, page_size, class_name, head_teacher_id + ) + return ClassPageResult(total=total, items=rows) + + + +@router.put( + "/batch-delete", + response_model=BatchDeleteResult, + summary="批量逻辑删除/恢复班级", +) +def batch_delete(req: ClassDelete, db: Session = Depends(get_db)): + count = ClassDAO.batch_toggle_delete(db, req) + return BatchDeleteResult(affected_count=count) + + + +@router.get("/{class_id}", response_model=ClassDetail, summary="查询班级详情") +def get_class(class_id: int, db: Session = Depends(get_db)): + data = ClassDAO.get_by_id(db, class_id) + if not data: + raise HTTPException(status_code=404, detail="班级不存在或已删除") + return data + + + +@router.put( + "/{class_id}/base", + response_model=ClassDetail, + summary="更新班级基础信息", +) +def update_class_base( + class_id: int, req: ClassUpdateBase, db: Session = Depends(get_db) +): + try: + data = ClassDAO.update_base(db, class_id, req) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + if not data: + raise HTTPException(status_code=404, detail="班级不存在或已删除") + return data + + + +@router.put( + "/{class_id}/head-teacher", + response_model=ClassDetail, + summary="调整班主任", +) +def update_head_teacher( + class_id: int, req: ClassHeadTeacherUpdate, db: Session = Depends(get_db) +): + try: + data = ClassDAO.update_head_teacher(db, class_id, req) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + if not data: + raise HTTPException(status_code=404, detail="班级不存在或已删除") + return data + + +@router.put( + "/{class_id}/teachers", + response_model=ClassDetail, + summary="更新授课老师", +) +def update_teachers( + class_id: int, req: ClassTeachersUpdate, db: Session = Depends(get_db) +): + try: + data = ClassDAO.update_teachers(db, class_id, req) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + if not data: + raise HTTPException(status_code=404, detail="班级不存在或已删除") + return data \ No newline at end of file diff --git a/api/counselor_api.py b/api/counselor_api.py new file mode 100644 index 0000000..8fb887b --- /dev/null +++ b/api/counselor_api.py @@ -0,0 +1,68 @@ +from datetime import datetime + +from fastapi import HTTPException, APIRouter, Depends +from sqlalchemy.orm import Session + + +from dao.counselor_dao import add_counselor, query_counselor,\ + update_counselor, delete_counselor_id +from database import get_db +from schemas.counselor_scheme import CounselorCreate, DeleteCounselor, UpdateCounselor + +router = APIRouter( + prefix="/counselor", + tags=["顾问管理"] +) + +@router.post("/counselors/") +def counselor_create(cou: CounselorCreate, + db:Session=Depends(get_db)): + date_obj=(datetime.strptime(cou.c_time,"%Y-%m-%d")) + data= { + "id":cou.id, + "name":cou.name, + "c_time":date_obj.date() + } + add_counselor(db,data["id"], data["name"],data["c_time"]) + return {"message":"新增成功"} + +@router.get("/{id}") +def get_counselor(id:int, db:Session=Depends(get_db)): + cou = query_counselor(db, id) + if not cou: + raise HTTPException(404,detail="顾问不存在") + return {"data":cou} + +# @router.get("/counselors/{name}") +# async def counselor_get_name(name,db:Session=Depends(get_db)): +# res=query_counselor_name(db,name) +# return {"message":"根据名字查询成功","data":res} + +@router.delete("/{id}") +async def counselor_del_id(dele:DeleteCounselor, + db:Session=Depends(get_db)): + delete_counselor_id(db,dele.id) + return {"message":"根据id删除成功"} + +# @router.delete("/counselors/{name}") +# async def counselor_del_name(name:str, +# db:Session=Depends(get_db)): +# delete_counselor_name(db,name) +# return {"message":"根据姓名删除成功"} + +@router.put("/{id}") +async def update_tea(id:int,update:UpdateCounselor,db:Session=Depends(get_db)): + date_obj = datetime.strptime(update.c_time, "%Y-%m-%d") + res=update_counselor(db,id=id, + new_name=update.name, + new_c_time=date_obj + ) + if res.get("msg")=="没找到顾问信息": + raise HTTPException(404,detail="顾问不存在") + return {"msg":"修改成功","data":res} + + +# @router.put("/counselors/{name}") +# async def counselor_update_name(id:int,name:str,c_time:datetime,db:Session=Depends(get_db)): +# res=query_counselor_name(db,id,name,c_time) +# return res diff --git a/api/employment.py b/api/employment.py new file mode 100644 index 0000000..c4768ab --- /dev/null +++ b/api/employment.py @@ -0,0 +1,113 @@ +from fastapi import APIRouter,Depends, HTTPException,Query +from sqlalchemy.orm import Session +from database import get_db +from dao.employments import get_student_id,get_class_id,add_update_employment,get_student_multi_condition +from dao.student import get_student_by_id +from schemas.employments import Emp_Class,Emp_add_update,Emp_multi_Conditions +from typing import List,Optional +from dao.employments import delete_employment +router = APIRouter( + prefix="/employment", tags=["学生就业管理"] # 该路由下所有路径的前缀 +) + +# 1. GET /employment/students/{student_id} 获取单个学生就业信息 +@router.get("/students/{student_id}", response_model=Emp_Class, summary="获取单个学生就业信息") +def get_student_employment(student_id: str, db: Session = Depends(get_db)): + stu = get_student_by_id(db, student_id) + if not stu: + raise HTTPException(status_code=404, detail="学生不存在") + isbool = get_student_id(db, student_id) + if not isbool: + raise HTTPException(status_code=404, detail="学生不存在") + return Emp_Class( + id=stu.id, + name=stu.name, + class_name=stu.class_name, + offer_open=isbool.offer_open if isbool else None, + offer_review=isbool.offer_review if isbool else None, + emp=isbool.emp if isbool else None, + sal=isbool.sal if isbool else None, + is_deleted=isbool.is_deleted + ) + + +# 2. GET /employment/class/{class_id} 获取班级全部学生就业信息 +@router.get("/class/{class_id}", response_model=List[Emp_Class], summary="获取班级下所有学生就业信息") +def get_class_employment(class_id: str, db: Session = Depends(get_db)): + rows =get_class_id(db, class_id) + result_list = [] + for stu, isbool in rows: + if not stu: + raise HTTPException(status_code=404, detail="学生不存在") + if not isbool: + raise HTTPException(status_code=404, detail="学生不存在") + result_list.append(Emp_Class( + id=stu.id, + name=stu.name, + class_name=stu.class_name, + offer_open=isbool.offer_open if isbool else None, + offer_review=isbool.offer_review if isbool else None, + emp=isbool.emp if isbool else None, + sal=isbool.sal if isbool else None, + is_deleted=isbool.is_deleted + )) + return result_list + +# 3. POST /employment/students/{student_id} 添加/更新就业信息 +@router.post("/students/{student_id}", response_model=Emp_Class, summary="新增或更新学生就业信息") +def upsert_student_employment( + student_id: str, + emp_data: Emp_add_update, + db: Session = Depends(get_db) +): + stu = get_student_by_id(db, student_id) + if not stu: + raise HTTPException(status_code=404, detail="学生不存在,请先创建学生基础信息") + emp = add_update_employment(db, student_id, emp_data, stu) + + return Emp_Class( + id=stu.id, + name=stu.name, + class_name=stu.class_name, + offer_open=emp.offer_open, + offer_review=emp.offer_review, + emp=emp.emp, + sal=emp.sal, + is_deleted=emp.is_deleted + ) +#多条件查询 +@router.get("/query", response_model=List[Emp_Class], summary="多条件查询就业信息") +def multi_query_employment( + mid: Optional[str] = Query(default=None, title="学生id", description="主键"),#title是前端可以看到的输入框内容 + name: Optional[str] = Query(default=None, title="学生姓名"), + min_sal: Optional[float] = Query(default=None, ge=0, title="最低薪资"), + max_sal: Optional[float] = Query(default=None, ge=0, title="最高薪资"), + db: Session = Depends(get_db) +): + rows = get_student_multi_condition(db, mid, name, min_sal, max_sal) + result_list = [] + + for stu, isbool in rows: + result_list.append(Emp_Class( + id=stu.id, + name=stu.name, + class_name=stu.class_name, + offer_open=isbool.offer_open if isbool else None, + offer_review=isbool.offer_review if isbool else None, + emp=isbool.emp if isbool else None, + sal=isbool.sal if isbool else None, + is_deleted=isbool.is_deleted if isbool else False # ← 修复了 NoneType 报错 + )) + + return result_list + + +@router.post('/deleted',summary="逻辑删除学生就业信息") +def delete_emp( student_id:str,db: Session = Depends(get_db)): + delete_employment(db,student_id) + if delete_employment(db,student_id): + return f"学生id:{student_id}删除成功" + else: + raise HTTPException(status_code=404, detail="学生不存在,请先创建学生基础信息") + + diff --git a/api/score.py b/api/score.py new file mode 100644 index 0000000..083c4bb --- /dev/null +++ b/api/score.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from database import get_db +from dao.scores import add_score,query_id,query_er,query_sc,update_sc,delete_sc + +router = APIRouter(prefix="/scores", tags=["考核成绩管理"]) +@router.post("/add", summary="按考核序次录入考核成绩") #1.录入成绩的路由 +def add_score_api(id: str = Query(..., description="学生ID"), #用于校验学生ID是否存在 + exam_round: int = Query(..., description="考核序次"), #用于校验同一学生同一考核序次是否重复录入 + score: int = Query(..., ge=0, le=100, description="成绩(0-100)"), #录入的成绩 + db: Session = Depends(get_db)): + return add_score(db=db, id=id, exam_round=exam_round, score=score) + +@router.get("/query/id", summary="按学生ID查询成绩") #2. 按学生ID查询的路由 +def query_id_api(id: str = Query(..., description="学生ID"), + db: Session = Depends(get_db)): + return query_id(db=db, id=id) + +@router.get("/query/round", summary="按考核序次查询成绩") #3. 按考核序次查询的路由 +def query_er_api(exam_round: int = Query(..., description="考核序次"), + db: Session = Depends(get_db)): + return query_er(db=db, exam_round=exam_round) + +@router.get("/query/score", summary="按成绩查询记录") #4. 按成绩查询的路由 +def query_sc_api(score: int = Query(..., ge=0, le=100, description="成绩"), + db: Session = Depends(get_db)): + return query_sc(db=db, score=score) + +@router.put("/update", summary="根据学生ID和考核序次修改成绩") #5. 修改成绩的路由 +def update_sc_api(id: str = Query(..., description="学生ID"), #根据学生ID + exam_round: int = Query(..., description="考核序次"), #根据考核序次 + score: int = Query(..., ge=0, le=100, description="新的成绩"), #修改的成绩值 + db: Session = Depends(get_db)): + return update_sc(db=db, id=id, exam_round=exam_round, score=score) + +@router.delete("/delete", summary="根据学生ID和考核序次删除成绩") #6. 删除成绩(软删除) +def delete_sc_api(id: str = Query(..., description="学生ID"), #根据学生ID + exam_round: int = Query(..., description="考核序次"), #根据考核序次 + db: Session = Depends(get_db)): + return delete_sc(db=db, id=id, exam_round=exam_round) \ No newline at end of file diff --git a/api/student.py b/api/student.py new file mode 100644 index 0000000..593f43d --- /dev/null +++ b/api/student.py @@ -0,0 +1,113 @@ +from fastapi import APIRouter, Depends, HTTPException +from pymysql import IntegrityError +from sqlalchemy.orm import Session +from typing import List +from model.classes_model import Classes +from dao.student_dao import update_student +from database import get_db +from model.student_info import Student +from schemas.student_info import StudentCreate, StudentUpdate, StudentResponse,student_date + +router = APIRouter(prefix="/students", tags=["学生基本信息"]) + +# 1. 查询 +@router.get("", response_model=List[StudentResponse], summary="查询学生") +def list_students( + id: str = None, + name: str = None, + class_name: str = None, + db: Session = Depends(get_db) +): + query = db.query(Student).filter(Student.is_deleted == False) + if id: + query = query.filter(Student.id == id) + if name: + query = query.filter(Student.name.like(f"%{name}%")) + if class_name: + query = query.filter(Student.class_name == class_name) + return query.all() + + +# 2. 创建 +@router.post("", response_model=StudentResponse, summary="创建新学生") +def create_student(student: StudentCreate, db: Session = Depends(get_db)): + # 调用校验函数 如果入学日期存在,就把它变成字符串;如果不存在,就赋值为空 + enroll_str = student.enrollment_date.strftime("%Y-%m-%d") if student.enrollment_date else None + grad_str = student.graduation_date.strftime("%Y-%m-%d") if student.graduation_date else None + date_check_result = student_date(enroll_str, grad_str) + + #如果不存在,直接抛出异常中断请求 + if date_check_result != "时间校验通过": + raise HTTPException(status_code=400, detail=date_check_result) + # 检查学号是否已存在 + exist = db.query(Student).filter(Student.id == student.id).first() + if exist: + raise HTTPException(status_code=400, detail=f"学号 {student.id} 已存在") + + # 不存在的班级导致外键约束报错 + class_exists = db.query(Classes).filter( + Classes.class_name == student.class_name, + ).first() + if not class_exists: + raise HTTPException(status_code=400,detail=f"班级 '{student.class_name}' 不存在,请检查输入是否正确!") + + # 3. 构建数据库模型对象 一个个赋值 + db_student = Student( + id=student.id, + name=student.name, + gender=student.gender, + class_name=student.class_name, + age=student.age, + hometown=student.hometown, + graduate_school=student.graduate_school, + major=student.major, + education=student.education, + enrollment_date=student.enrollment_date, + graduation_date=student.graduation_date, + status=student.status, + is_deleted=False + ) + db.add(db_student) + db.commit() + db.refresh(db_student) + return db_student #返回响应 + + +# 3. 详情 +@router.get("/{id}", response_model=StudentResponse, summary="获取特定学生信息") +def get_student(id: str, db: Session = Depends(get_db)): + student = db.query(Student).filter( + Student.id == id, + Student.is_deleted == False + ).first() + if student is None: + raise HTTPException(status_code=404, detail="学生不存在或已删除") + return student + + +# 4. 更新 +@router.put("/{id}", response_model=StudentResponse, summary="更新学生信息") +def api_update_student(id: str, payload: StudentUpdate, db: Session = Depends(get_db)): + try: + student = update_student(db, id, payload) + if student is None: + raise HTTPException(status_code=404, detail="学生不存在或已删除") + return student + except IntegrityError: + db.rollback() + raise HTTPException(status_code=400, detail="更新失败:指定的班级不存在,请先创建对应班级!") + + + +# 5. 逻辑删除 +@router.delete("/{id}", summary="逻辑删除学生") +def delete_student(id: str, db: Session = Depends(get_db)): + student = db.query(Student).filter( + Student.id == id, + Student.is_deleted == False + ).first() + if student is None: + raise HTTPException(status_code=404, detail="学生不存在或已删除") + student.is_deleted = True + db.commit() + return {"message": f"学生 {id} 已逻辑删除,数据仍保留在库中"} \ No newline at end of file diff --git a/api/teachers.py b/api/teachers.py new file mode 100644 index 0000000..87f68ea --- /dev/null +++ b/api/teachers.py @@ -0,0 +1,66 @@ +from datetime import datetime + +from fastapi import APIRouter, HTTPException +from fastapi.params import Depends +from sqlalchemy.orm import Session + +from dao.teachers_dao import add_teachers, del_teachers, update_teachers, search_teachers, \ + search_teachers_by_id, get_teacher_classes +from database import get_db +from schemas.req import AddTeacher, DeleteTeacher, UpdateTeacher + +router=APIRouter(prefix="/teacher",tags=["老师管理"]) +@router.post("/teachers") +async def create_teacher(tea:AddTeacher,db:Session=Depends(get_db)): + date_obj=datetime.strptime(tea.t_created,"%Y-%m-%d") # 将字符串转时间 + data={ + "t_name":tea.t_name, + "t_gender":tea.t_gender, + "t_phone":tea.t_phone, + "t_subject":tea.t_subject, + "t_created":date_obj + } + res=add_teachers(db, data["t_name"],data["t_gender"],data["t_phone"],data["t_subject"],data["t_created"]) + return {"msg":"新增成功","data":res} + +@router.delete("/{t_id}") +def delete_teacher(dele:DeleteTeacher,db:Session=Depends(get_db)): + res=del_teachers(db,t_id=dele.t_id,t_name=dele.t_name) + if res.get("msg") == "没找到老师信息": + raise HTTPException(404, detail="老师不存在") + return {"msg": "删除成功"} + + +@router.put("/{t_id}") +async def update_tea(t_id:int,update:UpdateTeacher,db:Session=Depends(get_db)): + date_obj = datetime.strptime(update.t_created, "%Y-%m-%d") + res=update_teachers(db,t_id=t_id, + new_name=update.t_name, + new_gender=update.t_gender, + new_phone=update.t_phone, + new_subject=update.t_subject, + new_created=date_obj + ) + if res.get("msg")=="没找到老师信息": + raise HTTPException(404,detail="老师不存在") + return {"msg":"修改成功","data":res} + +@router.get("/list") +def list_teachers(db:Session=Depends(get_db),page:int=1,size:int=3): + res=search_teachers(db,page,size) + return res + + +@router.get("/{t_id}") +def get_one(t_id:int, db:Session=Depends(get_db)): + tea = search_teachers_by_id(db, t_id) + if not tea: + raise HTTPException(404,detail="老师不存在") + return {"data":tea} + +@router.get("/{t_id}/classes") +async def get_classes(t_id:int, db:Session=Depends(get_db)): + data=get_teacher_classes(db, t_id) + if not data: + raise HTTPException(404,detail="老师不存在") + return {"data":data} diff --git a/dao/__init__.py b/dao/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dao/__pycache__/__init__.cpython-314.pyc b/dao/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..e617e0b Binary files /dev/null and b/dao/__pycache__/__init__.cpython-314.pyc differ diff --git a/dao/__pycache__/analysis.cpython-314.pyc b/dao/__pycache__/analysis.cpython-314.pyc new file mode 100644 index 0000000..4875bda Binary files /dev/null and b/dao/__pycache__/analysis.cpython-314.pyc differ diff --git a/dao/__pycache__/class_dao.cpython-314.pyc b/dao/__pycache__/class_dao.cpython-314.pyc new file mode 100644 index 0000000..1e2d449 Binary files /dev/null and b/dao/__pycache__/class_dao.cpython-314.pyc differ diff --git a/dao/__pycache__/counselor_dao.cpython-314.pyc b/dao/__pycache__/counselor_dao.cpython-314.pyc new file mode 100644 index 0000000..0ba5e7e Binary files /dev/null and b/dao/__pycache__/counselor_dao.cpython-314.pyc differ diff --git a/dao/__pycache__/employments.cpython-314.pyc b/dao/__pycache__/employments.cpython-314.pyc new file mode 100644 index 0000000..c798212 Binary files /dev/null and b/dao/__pycache__/employments.cpython-314.pyc differ diff --git a/dao/__pycache__/scores.cpython-314.pyc b/dao/__pycache__/scores.cpython-314.pyc new file mode 100644 index 0000000..01790b4 Binary files /dev/null and b/dao/__pycache__/scores.cpython-314.pyc differ diff --git a/dao/__pycache__/student.cpython-314.pyc b/dao/__pycache__/student.cpython-314.pyc new file mode 100644 index 0000000..b6ec214 Binary files /dev/null and b/dao/__pycache__/student.cpython-314.pyc differ diff --git a/dao/__pycache__/student_dao.cpython-314.pyc b/dao/__pycache__/student_dao.cpython-314.pyc new file mode 100644 index 0000000..bb6b6df Binary files /dev/null and b/dao/__pycache__/student_dao.cpython-314.pyc differ diff --git a/dao/__pycache__/teachers_dao.cpython-314.pyc b/dao/__pycache__/teachers_dao.cpython-314.pyc new file mode 100644 index 0000000..f9e950d Binary files /dev/null and b/dao/__pycache__/teachers_dao.cpython-314.pyc differ diff --git a/dao/analysis.py b/dao/analysis.py new file mode 100644 index 0000000..cc83a45 --- /dev/null +++ b/dao/analysis.py @@ -0,0 +1,125 @@ +from fastapi import Query,Depends +from sqlalchemy import func, case +from model.student_info import Student +from model.employments import Employments +from model.scores import Scores +from sqlalchemy.orm import Session + +#student_info学生基本信息 +#动态年龄范围查询:支持用户输入年龄阈值及比较条件(如大于、小于、等于、区间等),动态查询符合条件的学员信息 +def student_info_age(db: Session,min_age:int ,max_age:int ,operator:str,): + if operator not in("大于","大于等于","小于","小于等于","区间"): + raise ValueError("输入比较格式不正确,请在规定范围内选择输入") + else: + q = db.query(Student) + if operator == "大于": + q = q.filter(Student.age > min_age) + elif operator == "大于等于": + q = q.filter(Student.age >= min_age) + elif operator == "小于": + q = q.filter(Student.age < min_age) + elif operator == "小于等于": + q = q.filter(Student.age <= min_age) + else: # 区间 + q = q.filter(Student.age >= min_age, Student.age <= max_age) + print(q.all()) + return q.all() + +#多维度班级统计:统计每个班级的总人数,以及按性别(男、女)细分的人数分布 +def student_info_class(db: Session): + s1 = (db.query(Student.class_name, + func.count(Student.id).label("总人数"), + func.sum(case((Student.gender=="男",1),else_=0)).label("男"), + func.sum(case((Student.gender=="女",1),else_=0)).label("女")) + .group_by(Student.class_name).all()) + + return [ + { + "class_name": row.class_name, + "总人数": row.总人数, + "男": row.男, + "女": row.女, + }for row in s1 + ] + + + + #student_score学生成绩 +#查询每次考试成绩都在输入分数线(如80分)以上的学生的编号、姓名和成绩。 +def student_score(db: Session,s_n : int): + s1 = db.query(Scores.id).group_by(Scores.id).having(func.min(Scores.score)>=s_n).all() + s1_ids = [row[0] for row in s1] + if not s1_ids: + return [] + s2 = (db.query(Student.id,Student.name,Scores.exam_round,Scores.score) + .join(Student, Scores.id == Student.id) + .filter(Scores.id.in_(s1_ids)).all()) + return [{"id": i.id, "name": i.name, "exam_round": i.exam_round, "score": i.score} + for i in s2] +#查询有输入指定次数(如两次(包含两次))以上不及格的学生的姓名、班级和不及格成绩明细。 +def student_score60(db: Session,n : int ): + s1 = (db.query(Scores.id).group_by(Scores.id) + .having(func.sum(case((Scores.score < 60, 1), else_=0)) >=n) + .all()) + s1_ids = [row[0] for row in s1] + if not s1_ids: + return [] + s2 = (db.query(Student.name,Student.class_name,Scores.exam_round,Scores.score) + .join(Student, Scores.id == Student.id) + .filter(Scores.id.in_(s1_ids)).all()) + return [{"id": i.name, "name": i.class_name, "exam_round": i.exam_round, "score": i.score} + for i in s2] +#统计每次考试每个班级的平均分,并支持按分数从高到低或从低到高动态排序。 +def student_score_class(db: Session,order: str = "desc"): + s1 = (db.query(Scores.exam_round, + Student.class_name, + func.avg(Scores.score).label("平均分"),).join(Student, Scores.id == Student.id) + .group_by(Student.class_name,Scores.exam_round)) + if order == "desc": + res = s1.order_by(func.avg(Scores.score).desc()).all() + else: + res = s1.order_by(func.avg(Scores.score).asc()).all() + return [row._asdict() for row in res] + + + #student_employments +#查询查询就业薪资排名前n的学生 +def student_employments_salary(db: Session,n:int): + s1 = db.query(Employments).order_by(Employments.sal.desc()).limit(n).all() + return s1 + +#统计每个学生的就业时长 +def student_employments_time(db: Session): + time = [] + all_ = db.query(Employments).all() + for employments in all_: + if employments.offer_review is None: + time.append({"name":employments.student.name, + "class_name": employments.student.class_name, + "status": "未就业"}) + elif employments.offer_open and employments.offer_review: + time.append({"name":employments.student.name, + "class_name": employments.student.class_name, + "time": (employments.offer_review-employments.offer_open).days}) + return time + +#统计每个班级的平均就业时长(仅统计进入就业阶段,即有就业开放时间的学生) +def student_employments_class_time(db: Session): + s1 = (db.query(Student.class_name, + func.avg( + func.unix_timestamp(Employments.offer_review) - + func.unix_timestamp(Employments.offer_open)) + .label("avg_time")) + .join(Student, Employments.id == Student.id) + .filter(Employments.offer_review.isnot(None)) + .group_by(Student.class_name).all()) + time = [] + for i in s1: + days = round(float(i.avg_time) / 86400, 2) if i.avg_time else 0 + time.append({ + "class_name": i.class_name, + "平均时长(天)": days + }) + return time + + diff --git a/dao/class_dao.py b/dao/class_dao.py new file mode 100644 index 0000000..a8bb657 --- /dev/null +++ b/dao/class_dao.py @@ -0,0 +1,178 @@ +from sqlalchemy.orm import Session +from typing import Optional, List, Tuple + +from model.classes_model import Classes +from model.teachers import Teacher +from schemas.class_scheme import ( + ClassCreate, + ClassUpdateBase, + ClassHeadTeacherUpdate, + ClassTeachersUpdate, + ClassDelete, +) + + +class ClassDAO: + + # ---------- 创建 ---------- + @staticmethod + def create(db: Session, req: ClassCreate) -> Classes: + # 1) 班级名唯一 + exists = ( + db.query(Classes) + .filter(Classes.class_name == req.class_name) + .first() + ) + if exists: + raise ValueError("班级名称已存在") + + # 2) 班主任存在 + head_teacher = ( + db.query(Teacher).filter(Teacher.t_id == req.head_teacher_id).first() + ) + if not head_teacher: + raise ValueError("班主任不存在") + + # 3) 授课老师全部存在 + teachers: List[Teacher] = [] + if req.teacher_ids: + teacher_ids = set(req.teacher_ids) + teachers = ( + db.query(Teacher).filter(Teacher.t_id.in_(teacher_ids)).all() + ) + if len(teachers) != len(teacher_ids): + raise ValueError("部分授课老师不存在") + + data = req.model_dump( + exclude={"teacher_ids"}, + exclude_none=True, + ) + db_class = Classes(**data) + db_class.teachers = teachers + + db.add(db_class) + db.commit() + db.refresh(db_class) + return db_class + + + @staticmethod + def get_by_id(db: Session, class_id: int) -> Optional[Classes]: + return ( + db.query(Classes) + .filter( + Classes.class_id == class_id, + Classes.class_delete == False, # noqa: E712 + ) + .first() + ) + + # ---------- 分页 ---------- + @staticmethod + def list_page( + db: Session, + page_num: int, + page_size: int, + class_name: Optional[str] = None, + head_teacher_id: Optional[int] = None, + ) -> Tuple[int, List[Classes]]: + query = db.query(Classes).filter(Classes.class_delete == False) # noqa: E712 + + if class_name: + query = query.filter(Classes.class_name.like(f"%{class_name}%")) + if head_teacher_id is not None: + query = query.filter(Classes.head_teacher_id == head_teacher_id) + + total = query.count() + rows = ( + query.order_by(Classes.class_id.desc()) + .offset((page_num - 1) * page_size) + .limit(page_size) + .all() + ) + return total, rows + + + @staticmethod + def update_base( + db: Session, class_id: int, req: ClassUpdateBase + ) -> Optional[Classes]: + db_class = ClassDAO.get_by_id(db, class_id) + if not db_class: + return None + + data = req.model_dump(exclude_unset=True, exclude_none=True) + + # 改班级名要查重 + new_name = data.get("class_name") + if new_name and new_name != db_class.class_name: + exists = ( + db.query(Classes) + .filter( + Classes.class_name == new_name, + Classes.class_id != class_id, + ) + .first() + ) + if exists: + raise ValueError("班级名称已存在") + + for k, v in data.items(): + setattr(db_class, k, v) + + db.commit() + db.refresh(db_class) + return db_class + + @staticmethod + def update_head_teacher( + db: Session, class_id: int, req: ClassHeadTeacherUpdate + ) -> Optional[Classes]: + db_class = ClassDAO.get_by_id(db, class_id) + if not db_class: + return None + + teacher = ( + db.query(Teacher).filter(Teacher.t_id == req.head_teacher_id).first() + ) + if not teacher: + raise ValueError("班主任不存在") + + db_class.head_teacher_id = req.head_teacher_id + db.commit() + db.refresh(db_class) + return db_class + + + @staticmethod + def update_teachers( + db: Session, class_id: int, req: ClassTeachersUpdate + ) -> Optional[Classes]: + db_class = ClassDAO.get_by_id(db, class_id) + if not db_class: + return None + + teacher_ids = set(req.teacher_ids) + teachers = ( + db.query(Teacher).filter(Teacher.t_id.in_(teacher_ids)).all() + ) if teacher_ids else [] + if len(teachers) != len(teacher_ids): + raise ValueError("部分授课老师不存在") + + db_class.teachers = teachers + db.commit() + db.refresh(db_class) + return db_class + + @staticmethod + def batch_toggle_delete(db: Session, req: ClassDelete) -> int: + count = ( + db.query(Classes) + .filter(Classes.class_id.in_(req.class_ids)) + .update( + {"class_delete": req.deleted}, + synchronize_session=False, + ) + ) + db.commit() + return count \ No newline at end of file diff --git a/dao/counselor_dao.py b/dao/counselor_dao.py new file mode 100644 index 0000000..520ce1f --- /dev/null +++ b/dao/counselor_dao.py @@ -0,0 +1,82 @@ +#创建方法 +from datetime import date + +from sqlalchemy.orm import sessionmaker, Session + +from model.counselor import Counselor + + +#增 +def add_counselor(db:Session,id: int,name:str,c_time:date): + c1 = Counselor( + id=id, + name=name, + c_time=c_time, + c_de=False + ) + try: + db.add(c1) + db.commit() + db.refresh(Counselor) + return "添加成功" + except Exception as e: + print(e) + +#删 +def delete_counselor_id(db:Session,id:int): + res=db.query(Counselor).filter(Counselor.id == id).first() + res.c_de=True + db.commit() + return "删除成功" + +def delete_counselor_name(db:Session,name:str): + db.query(Counselor).filter(Counselor.name==name).delete() + db.commit() + db.refresh(Counselor) + return "删除成功" + +#改,根据id修改 +def update_counselor(db:Session,id:int,new_name,new_c_time): + counselor=db.query(Counselor).filter(Counselor.id==id,Counselor.c_de==False).first() + if not counselor: + return {"msg":"没找到顾问信息"} + counselor.name=new_name + counselor.c_time=new_c_time + try: + db.commit() + db.refresh(counselor) + return {"msg":"修改成功"} + except Exception as e: + print(e) + return {"msg": "修改失败"} + +#改,根据名字修改 +def update_counselor_name(db:Session,id:int,name:str,c_time:date): + if Counselor.name==name: + Counselor.id==id + Counselor.c_time=c_time + db.commit() + db.refresh(Counselor) + return f"修改成功" + +#查,根据id查询 +#根据id查 +def query_counselor(db:Session,id:int): + counselor=db.query(Counselor).filter(Counselor.id==id,Counselor.c_de==False).first() + if not counselor: + return None + l2={ + "id":counselor.id, + "name":counselor.name, + "t_created":counselor.c_time.strftime("%Y-%m-%d") + } + return l2 + + +#根据名字查询 +def query_counselor_name(db:Session,name:str): + res=db.query(Counselor).filter(Counselor.name==name).first() + db.commit() + db.refresh(Counselor) + return res +#筛选低于60的同学 \ No newline at end of file diff --git a/dao/employments.py b/dao/employments.py new file mode 100644 index 0000000..410089f --- /dev/null +++ b/dao/employments.py @@ -0,0 +1,87 @@ +from typing import Optional +from sqlalchemy.orm import Session +from model.employments import Employments +from model.student_info import Student +from schemas.employments import Emp_Class,Emp_add_update +from sqlalchemy import and_ +from fastapi import HTTPException + +#根据学生id查看信息 +def get_student_id(db: Session, student_id: str): + return db.query(Employments).filter(and_(Employments.id == student_id,Employments.is_deleted==True)).first() +#根据学生id、公司名称、工资范围多条件查询 +def get_student_multi_condition( + db: Session, + id: Optional[str] = None, + name: Optional[str] = None, + min_sal: Optional[float] = None, + max_sal: Optional[float] = None, +): + # LEFT JOIN student + student_employment + q = db.query(Student, Employments)\ + .outerjoin(Employments, Student.id == Employments.id)\ + .filter(Employments.is_deleted==True) + # if min_sal>max_sal: + # raise HTTPException(status_code=404, detail="学生不存在,请先创建学生基础信息") + # 条件动态拼接 + if id: + q = q.filter(Student.id.like(f"%{id}%"))#like模糊查询 + if name: + q = q.filter(Employments.emp.like(f"%{name}%")) + if min_sal is not None: + q = q.filter(Employments.sal >= min_sal) + if max_sal is not None: + q = q.filter(Employments.sal <= max_sal) + return q.all() +#根据学生班级查看信息 +def get_class_id(db:Session,class_id:str): + return (db.query(Student,Employments) + .outerjoin(Student,Student.id==Employments.id))\ + .filter(Student.class_name==class_id).all() +#增加修改学生信息 +#首先判断是否是已存在的学生,如果不是就增加,反之就修改 +def add_update_employment(db: Session,student_id:str,emp_data: Emp_add_update,student:Student ): + emp = get_student_id(db, student_id) + if emp: + # 更新 + emp.offer_open = emp_data.offer_open + emp.offer_review = emp_data.offer_review + emp.emp = emp_data.emp + emp.sal = emp_data.sal + else: + # 新增 + stu = db.query(Student).filter(Student.id == student_id) + if stu: + emp = Employments( + id=student_id, + offer_open=emp_data.offer_open, + offer_review=emp_data.offer_review, + emp=emp_data.emp, + sal=emp_data.sal + ) + db.add(emp) + else: + raise HTTPException(status_code=404, detail="学生不存在,请先创建学生基础信息") + + # 业务状态更新 offer优先级更高 + if emp_data.offer_review is not None: + student.status = "已就业" + elif emp_data.offer_open is not None: + student.status = "就业开放" + db.commit() + db.refresh(emp) + return emp + +def delete_employment(db: Session,student_id:str ): + emp = get_student_id(db, student_id) + if emp: + # 删除 + emp.is_deleted = False + db.commit() + db.refresh(emp) + # else: + # raise HTTPException(status_code=404, detail="学生不存在,请先创建学生基础信息") + return 1 + + + diff --git a/dao/scores.py b/dao/scores.py new file mode 100644 index 0000000..5cbed77 --- /dev/null +++ b/dao/scores.py @@ -0,0 +1,123 @@ +from sqlalchemy.orm import Session + +from model.scores import Scores + +def add_score(db: Session, id: str, exam_round: int, score: int): #1.按考核序次录入考核成绩(包含字段:学生ID、考核序次、成绩) + try: + judgement_id = (db.query(Scores). + filter(Scores.id == id,Scores.is_deleted == False). + first()) # 通过外键class Student校验学生ID是否存在 + if not judgement_id: + return {"code": 404, "msg": f"学生ID:{id}不存在,请去录入学生信息!"} + + judgement_id_er = (db.query(Scores). + filter(Scores.id == id, + Scores.exam_round == exam_round, + Scores.is_deleted == False). + first()) # 本表校验同一学生、同一考核序次是否已录入 + if judgement_id_er: + return {"code": 400, "msg": f"学生ID:{id}第{exam_round}次考核成绩已存在,请勿重复录入!"} + + if score < 60: # 创建分数小于等于60分的记录并警告 + record = Scores(id=id, + exam_round=exam_round, + score=score, + is_warning=True) #写入学生ID,考核序次,成绩,标记警告 + db.add(record) + db.commit() + db.refresh(record) + return {"code": 200, "msg": f"学生ID:{id}第{exam_round}次考核成绩{score}分录入成功!成绩小于60分,需要多关注!"} + else: # 创建分数大于等于60分的记录 + record = Scores(id=id, + exam_round=exam_round, + score=score) #写入学生ID,考核序次,成绩 + db.add(record) + db.commit() + db.refresh(record) + return {"code": 200, "msg": f"学生ID:{id}第{exam_round}次考核成绩{score}分录入成功!"} + except Exception as e: + db.rollback() + return {"code": 500, "msg": f"考核成绩录入失败: {e}"} + + +def query_id(db: Session, id: str): #2.按学生ID过滤,成绩降序,考核序次升序,返回全部信息 + try: + query_id = (db.query(Scores). + filter(Scores.id == id,Scores.is_deleted == False). + order_by(Scores.score.desc(),Scores.exam_round.asc()). + all()) + if not query_id: + return {"code": 404, "msg": "未查询到相关结果"} + data = [{"id": o.id, "exam_round": o.exam_round, "score": o.score}for o in query_id] + return {"code": 200, "msg": data} + except Exception as e: + db.rollback() + return {"code": 500, "msg": f"未查询到相关结果:{e}"} + + +def query_er(db: Session, exam_round: int): #3.按考核序次过滤,成绩降序,学生ID升序,返回全部信息 + try: + query_er = (db.query(Scores). + filter(Scores.exam_round == exam_round,Scores.is_deleted == False). + order_by(Scores.score.desc(),Scores.id.asc()). + all()) + if not query_er: + return {"code": 404, "msg": "未查询到相关结果"} + data = [{"id": p.id, "exam_round": p.exam_round, "score": p.score}for p in query_er] + return {"code": 200, "msg": data} + except Exception as e: + db.rollback() + return {"code": 500, "msg": f"未查询到相关结果: {e}"} + +def query_sc(db: Session, score: int): #4.按成绩过滤,学生ID升序,考核序次升序,返回全部信息 + try: + query_sc = (db.query(Scores). + filter(Scores.score == score,Scores.is_deleted == False). + order_by(Scores.id.asc(),Scores.exam_round.asc()). + all()) + if not query_sc: + return {"code": 404, "msg": "未查询到相关结果"} + data = [{"id": q.id, "exam_round": q.exam_round, "score": q.score}for q in query_sc] + return {"code": 200, "msg": data} + except Exception as e: + db.rollback() + return {"code": 500, "msg": f"未查询到相关结果: {e}"} + + +def update_sc(db: Session, id: str, exam_round: int, score: int): #5.根据学生ID、考核序次修改成绩 + try: + update_sc = (db.query(Scores). + filter(Scores.id == id, + Scores.exam_round == exam_round, + Scores.is_deleted == False). + first()) + if not update_sc: + return {"code": 404, "msg": "该成绩记录不存在"} + update_sc.score = score + update_sc.is_warning = score < 60 + db.commit() + db.refresh(update_sc) + return {"code": 200, "msg": f"学生ID:{id} 第{exam_round}次成绩已修改为{score}分!"} + except Exception as e: + db.rollback() + return {"code": 500, "msg": f"考核成绩修改失败: {e}"} + + +def delete_sc(db: Session, id: str, exam_round: int): #6.根据学生ID、考核序次删除成绩 + try: + delete_sc = (db.query(Scores). + filter(Scores.id == id, + Scores.exam_round == exam_round, + Scores.is_deleted == False). + first()) + if not delete_sc: + return {"code": 404, "msg": "成绩记录不存在"} + delete_sc.is_deleted = True # 逻辑删除 + db.commit() + db.refresh(delete_sc) + return {"code": 200, "msg": f"学生ID:{id}第{exam_round}次成绩已删除!"} + except Exception as e: + db.rollback() + return {"code": 500, "msg": f"考核成绩删除失败: {e}"} + + diff --git a/dao/student.py b/dao/student.py new file mode 100644 index 0000000..f576282 --- /dev/null +++ b/dao/student.py @@ -0,0 +1,18 @@ +from sqlalchemy.orm import Session +from model.student_info import Student + +# 根据学生id查询学生 +def get_student_by_id(db: Session, student_id: str): + return db.query(Student).filter(Student.id == student_id).first() + +# 根据班级名称查询该班级全部学生 +def get_student_by_class(db: Session, class_name: str): + return db.query(Student).filter(Student.class_name == class_name).all() + +# 创建学生 +def create_student(db: Session, student_id: str, student_name: str, class_name: str): + new_stu = Student(id=student_id, name=student_name, class_name=class_name, status="待就业") + db.add(new_stu) + db.commit() + db.refresh(new_stu) + return new_stu \ No newline at end of file diff --git a/dao/student_dao.py b/dao/student_dao.py new file mode 100644 index 0000000..fb2f914 --- /dev/null +++ b/dao/student_dao.py @@ -0,0 +1,104 @@ + +from sqlalchemy.orm import Session +from model.student_info import Student +from schemas.student_info import StudentCreate, StudentUpdate,StudentDate, StudentResponse + + + +#查询 查询学生信息(按编号、姓名、班级条件筛选) +#orm动态查询 +def list_students(db: Session,id: str = None,name: str = None,class_name: str = None): + # 已逻辑删除的学生 + query = db.query(Student).filter(Student.is_deleted == False) + # 查询学生信息(支持按编号、姓名、班级等条件筛选)。 + if id: + query = query.filter(Student.id == id) + if name: + query = query.filter(Student.name.like(f"%{name}%")) + if class_name: + query = query.filter(Student.class_name == class_name) + # 执行查询并返回 + return query.all() + + +#2创建新的学生 +def create_student(db: Session,student: StudentCreate): + data = {} + #遍历 + for field_name in student.model_fields: + if field_name == "id": + val = student.id + elif field_name == "name": + val = student.name + elif field_name == "gender": + val = student.gender + elif field_name == "class_no": + val = student.class_name + elif field_name == "major": + val = student.major + elif field_name == "age": + val = student.age + elif field_name == "hometown": + val = student.hometown + else: + val = None + data[field_name] = val + # 赋值给ORM + db_student = Student( + id=data["id"], + name=data["name"], + gender=data["gender"], + class_name=data["class_name"], + major=data.get("major"), + age=data.get("age"), + hometown=data.get("hometown") + ) + db.add(db_student) + db.commit() + db.refresh(db_student) + return db_student + +# 获取特定学生信息 +# 严格按照需求文档:GET /students/{id},从URL路径中获取学生学号 +def get_student(db: Session,id: str): + # 查询:id字段等于url传过来的id + student = db.query(Student).filter(Student.id == id,).first() + if student is None: + return None + return student + + +# 更新学生信息 /students/{id} id为学号 +def update_student( db: Session,id: str, payload: StudentUpdate): + # 根据学号查询学生 + student = db.query(Student).filter(Student.id == id).first() + # 学生是否存在 + if student is None: + return None + if payload.name is not None: + student.name = payload.name + if payload.age is not None: + student.age = payload.age + if payload.class_name is not None: + student.class_name = payload.class_name + # 保存到数据库 + db.commit() + db.refresh(student) + # 返回更新后的学生 + return student + + +# 逻辑删除学生。逻辑上删除,实际上未删除 +def delete_student(db: Session,id: str): + # 根据学号查询 + student = db.query(Student).filter(Student.id == id, Student.is_deleted == False).first() + # 判断学生是否存在 + if student is None: + return None + # 逻辑删除:把is_deleted改成True,不是真的删掉这条记录 + student.is_deleted = True + db.commit() + return {"message": f"学生 {id} 已逻辑删除,数据仍保留在库中"} + + + diff --git a/dao/teachers_dao.py b/dao/teachers_dao.py new file mode 100644 index 0000000..4cc17bd --- /dev/null +++ b/dao/teachers_dao.py @@ -0,0 +1,141 @@ +from datetime import date + +from sqlalchemy.orm import sessionmaker, declarative_base, Session, Query, selectinload + +from model.teachers import Teacher + +#添加方法 +def add_teachers(db:Session ,t_name:str, t_gender:str, t_phone:str, t_subject:str,t_created:date): + t1=Teacher( + t_name=t_name, + t_gender=t_gender, + t_phone=t_phone, + t_subject=t_subject, + t_created=t_created, + t_delete=False + ) + try: + db.add(t1) + db.commit() + db.refresh(t1) + return {"msg":"添加成功"} + except Exception as e: + print(e) + return {"msg":"删除成功"} + + +#删除方法 +def del_teachers(db:Session,t_id:int,t_name:str): + teacher=db.query(Teacher).filter(Teacher.t_id==t_id, + Teacher.t_name==t_name, + Teacher.t_delete==False + ).first() + if not teacher: + return {"msg":"没找到老师信息"} + #逻辑删除 + try: + teacher.t_delete=True + db.commit() + return {"msg":"删除成功"} + except Exception as e: + print("删除异常",e) + return {"msg":"删除失败"} + +#修改老师 +def update_teachers(db:Session,t_id:int,new_name,new_gender,new_phone,new_subject,new_created): + teacher=db.query(Teacher).filter(Teacher.t_id==t_id,Teacher.t_delete==False).first() + if not teacher: + return {"msg":"没找到老师信息"} + teacher.t_name=new_name + teacher.t_gender=new_gender + teacher.t_phone=new_phone + teacher.t_subject=new_subject + teacher.t_created=new_created + try: + db.commit() + db.refresh(teacher) + return {"msg":"修改成功"} + except Exception as e: + print(e) + return {"msg": "修改失败"} + +# 查询所有老师信息 +# 分页 +def search_teachers(db:Session,page:int,size:int): + start = (page - 1) * size + teachers=db.query(Teacher.t_id, + Teacher.t_name, + Teacher.t_gender, + Teacher.t_phone, + Teacher.t_subject, + Teacher.t_created).filter(Teacher.t_delete==False) + teach=teachers.offset(start).limit(size).all() #跳过多少行,查询当前页数据 + total=teachers.count() + l1=[] + #将查出来的值存入列表 + for i in teach: + bb={ + "t_id":i[0], + "t_name":i[1], + "t_gender":i[2], + "t_phone":i[3], + "t_subject":i[4], + "t_created":i[5].strftime("%Y-%m-%d") # 将字符串转时间 + } + l1.append(bb) + return { + "total":total, + "page":page, + "size":size, + "list":l1 + } + + + +#查询单个老师信息 +#根据id查 +def search_teachers_by_id(db:Session,t_id:int): + teachers=db.query(Teacher).filter(Teacher.t_id==t_id,Teacher.t_delete==False).first() + if not teachers: + return None + l2={ + "t_id":teachers.t_id, + "t_name":teachers.t_name, + "t_gender":teachers.t_gender, + "t_phone":teachers.t_phone, + "t_subject":teachers.t_subject, + "t_created":teachers.t_created.strftime("%Y-%m-%d") + } + return l2 + +#查看带班信息 +def get_teacher_classes(db:Session,t_id:int): + teacher=(db.query(Teacher).filter(Teacher.t_id==t_id,Teacher.t_delete==False) + .options(selectinload(Teacher.classes)).first()) + if not teacher: + return None + + l3=[] + for i in teacher.classes: + l3.append({ + "class_id":i.class_id, + "class_name":i.class_name, + "create_time":i.create_time.strftime("%Y-%m-%d"), + }) + + res={ + "tid":teacher.t_id, + "t_id": teacher.t_id, + "t_name": teacher.t_name, + "t_gender": teacher.t_gender, + "t_phone": teacher.t_phone, + "t_subject": teacher.t_subject, + "t_created": teacher.t_created.strftime("%Y-%m-%d"), + "class_list": l3 + } + return res + + + + + diff --git a/database.py b/database.py new file mode 100644 index 0000000..1e5ff2a --- /dev/null +++ b/database.py @@ -0,0 +1,35 @@ +import os + +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base, sessionmaker + +# 优先读取环境变量(Docker 部署用);默认值与原硬编码配置一致,本地开发不受影响 +MYSQL_USER = os.getenv("MYSQL_USER", "root") +MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "123456") +MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost") +MYSQL_PORT = os.getenv("MYSQL_PORT", "3306") +MYSQL_DB = os.getenv("MYSQL_DB", "student_manager") + +db_url = f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DB}?charset=utf8mb4" +engine = create_engine(db_url, pool_size=5, pool_pre_ping=True) +Base = declarative_base() +SessionLocal = sessionmaker(bind=engine) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +def init_db(): + """导入所有模型并建表,避免任何入口文件漏导入""" + import model # noqa: F401 —— 触发 model/__init__.py 注册所有类 + Base.metadata.create_all(bind=engine) + + +def reset_db(): + """开发环境重建所有表""" + import model # noqa: F401 + Base.metadata.drop_all(bind=engine) + Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4dec57f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +# ============================================ +# 沃林学生管理系统 - Docker 编排 +# MySQL 8.0 虚拟镜像 + Backend (FastAPI) +# ============================================ +version: "3.9" + +services: + # ---------- MySQL 虚拟镜像 ---------- + mysql: + image: mysql:8.0 + container_name: wolin-sms-mysql + restart: unless-stopped + environment: + # 与 database.py 中的默认凭证保持一致 + MYSQL_ROOT_PASSWORD: "123456" + MYSQL_DATABASE: student_manager + TZ: Asia/Shanghai + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --default-authentication-plugin=mysql_native_password + ports: + # 宿主机 3307 -> 容器 3306 + # (用 3307 是为了避免与本机已安装的 MySQL 冲突) + - "3308:3306" + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p123456"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + networks: + - wolin-net + + # ---------- 后端应用 ---------- + backend: + build: + context: . + dockerfile: Dockerfile + container_name: wolin-sms-backend + restart: unless-stopped + depends_on: + mysql: + condition: service_healthy + environment: + # 容器内通过服务名 mysql 访问数据库(Docker 内置 DNS) + MYSQL_HOST: mysql + MYSQL_PORT: "3306" + MYSQL_USER: root + MYSQL_PASSWORD: "123456" + MYSQL_DB: student_manager + TZ: Asia/Shanghai + ports: + - "8001:8001" + networks: + - wolin-net + +networks: + wolin-net: + driver: bridge + +volumes: + mysql_data: + driver: local diff --git a/main.py b/main.py new file mode 100644 index 0000000..7d50830 --- /dev/null +++ b/main.py @@ -0,0 +1,62 @@ +import os +import uvicorn +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from database import engine, Base + +#导入模型 +from model.classes_model import Classes +from model.student_info import Student +from model.scores import Scores +from model.employments import Employments +from model.teachers import Teacher +from model.counselor import Counselor + +#导入路由 +from api import student as student_api +from api import score as score_api +from api import employment as employment_api +from api import analysis as analysis_api +from api import teachers as teachers_api +from api import class_api +from api import counselor_api + +#创建 FastAPI 应用 +app = FastAPI(title="学生管理系统API", version="1.0") + +#配置跨域 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +#注册路由 +app.include_router(student_api.router) +app.include_router(score_api.router) +app.include_router(employment_api.router) +app.include_router(analysis_api.router) +app.include_router(teachers_api.router) +app.include_router(class_api.router) +app.include_router(counselor_api.router) + +#挂载前端静态文件 +if not os.path.exists("static"): + os.makedirs("static") +app.mount("/ui", StaticFiles(directory="static", html=True), name="static") + + +@app.get("/") +def root(): + return {"message": "欢迎使用沃林学生管理系统,前端请访问 http://127.0.0.1:8001/ui/"} + +if __name__ == "__main__": + # 建表 + Base.metadata.create_all(bind=engine) + # host 必须为 0.0.0.0 才能在 Docker 容器外访问;本地运行 127.0.0.1:8001 依然有效 + host = os.getenv("HOST", "0.0.0.0") + port = int(os.getenv("PORT", "8001")) + uvicorn.run("main:app", host=host, port=port, reload=False) \ No newline at end of file diff --git a/model/__init__.py b/model/__init__.py new file mode 100644 index 0000000..e112641 --- /dev/null +++ b/model/__init__.py @@ -0,0 +1,6 @@ +from .classes_model import Classes +from .student_info import Student +from .scores import Scores +from .employments import Employments + +__all__ = ["Classes", "Student", "Scores", "Employments"] \ No newline at end of file diff --git a/model/__pycache__/__init__.cpython-314.pyc b/model/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..1032efd Binary files /dev/null and b/model/__pycache__/__init__.cpython-314.pyc differ diff --git a/model/__pycache__/classes_model.cpython-314.pyc b/model/__pycache__/classes_model.cpython-314.pyc new file mode 100644 index 0000000..3ac1e2b Binary files /dev/null and b/model/__pycache__/classes_model.cpython-314.pyc differ diff --git a/model/__pycache__/counselor.cpython-314.pyc b/model/__pycache__/counselor.cpython-314.pyc new file mode 100644 index 0000000..77b34a7 Binary files /dev/null and b/model/__pycache__/counselor.cpython-314.pyc differ diff --git a/model/__pycache__/employments.cpython-314.pyc b/model/__pycache__/employments.cpython-314.pyc new file mode 100644 index 0000000..7832be5 Binary files /dev/null and b/model/__pycache__/employments.cpython-314.pyc differ diff --git a/model/__pycache__/scores.cpython-314.pyc b/model/__pycache__/scores.cpython-314.pyc new file mode 100644 index 0000000..d9c5985 Binary files /dev/null and b/model/__pycache__/scores.cpython-314.pyc differ diff --git a/model/__pycache__/student_info.cpython-314.pyc b/model/__pycache__/student_info.cpython-314.pyc new file mode 100644 index 0000000..092ced2 Binary files /dev/null and b/model/__pycache__/student_info.cpython-314.pyc differ diff --git a/model/__pycache__/teachers.cpython-314.pyc b/model/__pycache__/teachers.cpython-314.pyc new file mode 100644 index 0000000..2632658 Binary files /dev/null and b/model/__pycache__/teachers.cpython-314.pyc differ diff --git a/model/classes_model.py b/model/classes_model.py new file mode 100644 index 0000000..eb62a59 --- /dev/null +++ b/model/classes_model.py @@ -0,0 +1,25 @@ +from sqlalchemy import Column, Integer, String, Boolean, Date, Table, ForeignKey +from sqlalchemy.orm import relationship +from database import Base + + + +teacher_classes = Table( + 'teacher_classes', + Base.metadata, + Column('teacher_id', ForeignKey('teacher.t_id'), primary_key=True), + Column('class_id', ForeignKey('classes.class_id'), primary_key=True) +) + +class Classes(Base): + __tablename__ = 'classes' + class_id = Column(Integer, primary_key=True,autoincrement=True) + head_teacher_id = Column(Integer, ForeignKey('teacher.t_id'), nullable=False) + create_time = Column(Date,nullable=False) + class_name = Column(String(50), nullable=False, unique=True) + class_delete = Column(Boolean, default=False) + counselor_id = Column(Integer, ForeignKey('counselor.id')) + + students = relationship('Student', back_populates='classes') + teachers = relationship("Teacher", secondary=teacher_classes, back_populates="classes") + counselor = relationship("Counselor", back_populates="classes") \ No newline at end of file diff --git a/model/counselor.py b/model/counselor.py new file mode 100644 index 0000000..f373d97 --- /dev/null +++ b/model/counselor.py @@ -0,0 +1,23 @@ +#创建顾问表 +from sqlalchemy import Column, Integer, String, Boolean, Date +from sqlalchemy.orm import relationship + +from database import Base + + + +class Counselor(Base): + __tablename__ = 'counselor' + id = Column(Integer, primary_key=True,autoincrement=True) + name = Column(String(100),nullable=False,unique=True) + c_time = Column(Date,nullable=False) #创建时间 + c_de = Column(Boolean,default=False) #逻辑判断 + + #关联班级表,一对多 + classes=relationship("Classes",back_populates="counselor") + + def __repr__(self): + return f"" + + + diff --git a/model/employments.py b/model/employments.py new file mode 100644 index 0000000..98b15d7 --- /dev/null +++ b/model/employments.py @@ -0,0 +1,25 @@ +from sqlalchemy import create_engine, Column, Float, String, DateTime, Boolean, ForeignKey +from sqlalchemy.orm import relationship + +from database import Base +# db_url='mysql+pymysql://root:123456@localhost:3306/student_manager' +# engine=create_engine(db_url,pool_size=5) +#学生就业表 +class Employments(Base): + __tablename__='employments' + id = Column(String(20), ForeignKey('students.id'), primary_key=True) #学生id + offer_open=Column(DateTime,nullable=True,comment='就业开放时间') #就业开放时间 + offer_review=Column(DateTime,nullable=True,comment='offer时间') #offer下发时间 + emp=Column(String(20),nullable=True,comment='就业公司名称') #就业公司名称 + sal = Column(Float, nullable=True, comment="就业薪资") #就业薪资 + is_deleted = Column(Boolean, default=False, comment="判断状态") #判断状态 + student = relationship("Student", back_populates="employment") + +# Base.metadata.drop_all(engine) +# Base.metadata.create_all(engine) +# SessionLocal=sessionmaker(bind=engine) +# # db=SessionLocal() +# Base.metadata.create_all(engine) +# db.close() + + diff --git a/model/scores.py b/model/scores.py new file mode 100644 index 0000000..2aa473a --- /dev/null +++ b/model/scores.py @@ -0,0 +1,37 @@ +# from sqlalchemy import Column, Integer, Boolean, ForeignKey,String +# from sqlalchemy.orm import relationship +# from database import Base +# class Scores(Base): # 学生考核成绩管理表模型 +# __tablename__ = "scores" +# scores_id = Column(Integer, primary_key=True, autoincrement=True) # 成绩自增长序号 是本表主键(1,2,3,4,5,6) +# id = Column(String(30), ForeignKey('student.id')) #学生ID 是Student表的外键(1,1,1,2,2,2) +# exam_round = Column(Integer, nullable=False) #考核序次 不唯一(1,2,3,1,2,3) +# score = Column(Integer) #成绩 不唯一 可None +# is_warning= Column(Boolean, default=False) #成绩红线预警 默认没预警 (高于60写入"True",低于60写入"False") +# is_deleted = Column(Boolean, default=False) #逻辑删除标记 默认没删除 (删除写入"True",不删除写入"False") +# +# student = relationship("Student", back_populates="scores") # 外键 多对一 关联Student +# def __repr__(self): # 魔法方法打印展示学生ID、对应序次的成绩 +# return f"" + +from sqlalchemy import Column, Integer, Boolean, ForeignKey, String +from sqlalchemy.orm import relationship +from database import Base + + +class Scores(Base): + __tablename__ = "scores" + + scores_id = Column(Integer, primary_key=True, autoincrement=True) + id = Column(String(20), ForeignKey('students.id')) # ← 关键修复 + exam_round = Column(Integer, nullable=False) + score = Column(Integer) + is_warning = Column(Boolean, default=False) + is_deleted = Column(Boolean, default=False) + + student = relationship("Student", back_populates="scores") + + def __repr__(self): + return f"" + + diff --git a/model/student_info.py b/model/student_info.py new file mode 100644 index 0000000..fa89b3d --- /dev/null +++ b/model/student_info.py @@ -0,0 +1,34 @@ +from sqlalchemy import Column, String, Integer, Date, Boolean, ForeignKey +from sqlalchemy.orm import relationship +from database import Base + + +class Student(Base): + __tablename__ = "students" + id = Column(String(20), primary_key=True, comment="学号")# 主键:学号 + + name = Column(String(50), nullable=False, comment="学生姓名") + gender = Column(String(10), nullable=False, comment="性别:男 / 女") + age = Column(Integer, comment="年龄") + hometown = Column(String(100), comment="籍贯") + graduate_school = Column(String(100), comment="毕业学校") + major = Column(String(50), comment="专业") + education = Column(String(30), comment="专科/本科/硕士") + enrollment_date = Column(Date, comment="入学时间") + graduation_date = Column(Date, comment="毕业时间") + status = Column(String(20), default="在读", comment="状态:没有毕业/未就业/已就业") + is_deleted = Column(Boolean, default=False, comment="逻辑删除标记") + # 外键 班级编号 + class_name = Column(String(20), ForeignKey("classes.class_name"), comment="所属班级编号") + + # 关系映射 学生和就业 一对一,一个学生对应一条就业信息 学生和成绩一对多,一个学生对应多条成绩 + scores = relationship("Scores", back_populates="student") # 一个学生多次考核成绩 + employment = relationship("Employments", back_populates="student", uselist=False) # 一对一就业信息 + classes = relationship("Classes", back_populates="students") + + def __repr__(self): + return f"" + + + + diff --git a/model/teachers.py b/model/teachers.py new file mode 100644 index 0000000..d8d1025 --- /dev/null +++ b/model/teachers.py @@ -0,0 +1,22 @@ +from sqlalchemy import Column, Integer, String, Boolean, Date +from sqlalchemy.orm import relationship + +from database import Base + + +class Teacher(Base): + __tablename__ = 'teacher' + t_id = Column(Integer, primary_key=True, autoincrement=True) #老师id + t_name = Column(String(50), nullable=False) #老师姓名 + t_gender = Column(String(50), nullable=False) #性别 + t_phone = Column(String(50), nullable=False) #手机号 + t_subject = Column(String(50), nullable=False) #教授科目 + t_created=Column(Date,nullable=False) #入职时间 + t_delete = Column(Boolean, default=False) #逻辑判断 + + + #关联表 + classes=relationship('Classes',secondary='teacher_classes',back_populates='teachers') + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..33599ff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +# ============================================ +# 沃林学生管理系统 - Python 依赖 +# Python 3.11+ +# ============================================ + +# Web 框架与服务器 +fastapi>=0.115.0 +uvicorn[standard]>=0.30.0 + +# ORM 与数据校验 +sqlalchemy>=2.0.0 +pydantic>=2.0.0 + +# 表单支持 +python-multipart>=0.0.5 + +# MySQL 驱动与认证加密(caching_sha2_password 需要) +pymysql>=1.1.0 +cryptography>=42.0.0 diff --git a/schemas/__init__.py b/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/schemas/__pycache__/__init__.cpython-314.pyc b/schemas/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..259b8bb Binary files /dev/null and b/schemas/__pycache__/__init__.cpython-314.pyc differ diff --git a/schemas/__pycache__/analysis.cpython-314.pyc b/schemas/__pycache__/analysis.cpython-314.pyc new file mode 100644 index 0000000..b0026f9 Binary files /dev/null and b/schemas/__pycache__/analysis.cpython-314.pyc differ diff --git a/schemas/__pycache__/class_scheme.cpython-314.pyc b/schemas/__pycache__/class_scheme.cpython-314.pyc new file mode 100644 index 0000000..be7246c Binary files /dev/null and b/schemas/__pycache__/class_scheme.cpython-314.pyc differ diff --git a/schemas/__pycache__/counselor_scheme.cpython-314.pyc b/schemas/__pycache__/counselor_scheme.cpython-314.pyc new file mode 100644 index 0000000..84ceff1 Binary files /dev/null and b/schemas/__pycache__/counselor_scheme.cpython-314.pyc differ diff --git a/schemas/__pycache__/employments.cpython-314.pyc b/schemas/__pycache__/employments.cpython-314.pyc new file mode 100644 index 0000000..b3f4364 Binary files /dev/null and b/schemas/__pycache__/employments.cpython-314.pyc differ diff --git a/schemas/__pycache__/req.cpython-314.pyc b/schemas/__pycache__/req.cpython-314.pyc new file mode 100644 index 0000000..ec527da Binary files /dev/null and b/schemas/__pycache__/req.cpython-314.pyc differ diff --git a/schemas/__pycache__/score.cpython-314.pyc b/schemas/__pycache__/score.cpython-314.pyc new file mode 100644 index 0000000..71ef8cd Binary files /dev/null and b/schemas/__pycache__/score.cpython-314.pyc differ diff --git a/schemas/__pycache__/student_info.cpython-314.pyc b/schemas/__pycache__/student_info.cpython-314.pyc new file mode 100644 index 0000000..565c624 Binary files /dev/null and b/schemas/__pycache__/student_info.cpython-314.pyc differ diff --git a/schemas/analysis.py b/schemas/analysis.py new file mode 100644 index 0000000..a848762 --- /dev/null +++ b/schemas/analysis.py @@ -0,0 +1,28 @@ +from datetime import date + +from pydantic import BaseModel, Field, ConfigDict +from fastapi import FastAPI + +#请求体模型 + +#student_info +#动态年龄范围查询:支持用户输入年龄阈值及比较条件(如大于、小于、等于、区间等),动态查询符合条件的学员信息 +class AnalysisStudentInfoAge(BaseModel): + min_age: int = Field(ge=1, description="查询的最小年龄,若不填则仅查询上限") + max_age: int = Field(le=100, description="查询的最大年龄,若不填则仅查询下限") + operator: str = Field("区间", descroption="大于,大于等于,小于,小于等于,区间") + +class AnalysisStudent_score(BaseModel): + s_n : int = Field(...,ge=1,le=100,description = "输入分数") + +class AnalysisStudent_score60(BaseModel): + n : int = Field(...,ge=0,descriptino = "输入不及格次数") + +class AnalysisStudent_score_class(BaseModel): + order : str = Field("desc", description="排序:输入asc则升序 / 输入desc则降序") + +class AnalysisStudent_employments_salary(BaseModel): + n : int = Field(10,ge = 1,description = "查询就业薪资排名前n的学生信息") + + + diff --git a/schemas/class_scheme.py b/schemas/class_scheme.py new file mode 100644 index 0000000..5d4143e --- /dev/null +++ b/schemas/class_scheme.py @@ -0,0 +1,55 @@ +from pydantic import BaseModel, Field, ConfigDict +from datetime import datetime,date +from typing import Optional, List + + +class ClassCreate(BaseModel): + class_name: str = Field(..., max_length=20, description="班级名称,唯一") + head_teacher_id: int = Field(..., ge=1, description="班主任id") + counselor_id: Optional[int] = Field(None, description="顾问id") + create_time: Optional[datetime] = Field(None, description="开课时间,不传默认当前时间") + teacher_ids: Optional[List[int]] = Field(None, description="绑定授课老师id列表") + + +class ClassUpdateBase(BaseModel): + class_name: Optional[str] = Field(None, max_length=20, description="班级名称") + create_time: Optional[date] = Field(None, description="开课时间,格式 YYYY-MM-DD,不传默认当前日期") + counselor_id: Optional[int] = Field(None, description="顾问id") + + +class ClassHeadTeacherUpdate(BaseModel): + head_teacher_id: int = Field(..., ge=1, description="新的班主任id") + + +class ClassTeachersUpdate(BaseModel): + teacher_ids: List[int] = Field(..., description="新老师id列表") + + +class ClassDelete(BaseModel): + class_ids: List[int] = Field(..., min_length=1, description="班级id列表") + deleted: bool = Field(..., description="True 逻辑删除,False 恢复") + + +class TeacherSimple(BaseModel): + model_config = ConfigDict(from_attributes=True) + t_id: int + t_name: str + +class ClassDetail(BaseModel): + model_config = ConfigDict(from_attributes=True) + class_id: int + class_name: str + head_teacher_id: int + counselor_id: Optional[int] = None + create_time: Optional[datetime] = None + class_delete: bool = False + teachers: List[TeacherSimple] = [] + + +class ClassPageResult(BaseModel): + total: int + items: List[ClassDetail] + + +class BatchDeleteResult(BaseModel): + affected_count: int \ No newline at end of file diff --git a/schemas/counselor_scheme.py b/schemas/counselor_scheme.py new file mode 100644 index 0000000..b26f3d5 --- /dev/null +++ b/schemas/counselor_scheme.py @@ -0,0 +1,47 @@ +#请求体参数验证 +from datetime import datetime + +from pydantic import BaseModel, Field, field_validator +from typing import Optional + + + +#创建顾问 请求体 +class CounselorCreate(BaseModel): + id : int + name : Optional[str] + c_time : Optional[str] + c_de : bool + #日期校验, + @field_validator("c_time") + @classmethod + def c_time_validator(cls, v:str): + try: + datetime.strptime(v, "%Y-%m-%d") + except ValueError: + raise ValueError("日期格式必须是 yyyy-MM-dd") + return v + +class DeleteCounselor(BaseModel): + id: int + +class UpdateCounselor(BaseModel): + name: str = Optional[str] + c_time: str = Optional[str] + + @field_validator('c_time') + @classmethod + def j_t_created(cls, v: str): + try: + c_created_date=datetime.strptime(v, '%Y-%m-%d') # 时间转字符串日期 + except ValueError: + raise ValueError('时间格式错误,请按照yyyy-MM-dd格式输入') + if c_created_date.date() > datetime.now().date(): + raise ValueError('入职时间不能大于未来时间') + return v + + + + + + diff --git a/schemas/employments.py b/schemas/employments.py new file mode 100644 index 0000000..964f532 --- /dev/null +++ b/schemas/employments.py @@ -0,0 +1,31 @@ +from datetime import datetime + +from pydantic import BaseModel, Field +from typing import Optional +import uvicorn + +#就业信息请求体 +class Emp_Class(BaseModel): + id:str # 学生id + name:str # 学生姓名 + class_name :str # 学生班级 + offer_open:Optional[datetime] # 就业开放时间 + offer_review:Optional[datetime] # offer下发时间 + emp:Optional[str]# 就业公司名称 + sal:Optional[float] # 就业薪资 + is_deleted:bool=True +#增加、修改请求体 +class Emp_add_update(BaseModel): + offer_open: datetime | None #等价Optional[datetime]=None + offer_review: datetime | None + emp: str | None + sal: float | None +#多条件参数查询 +class Emp_multi_Conditions(BaseModel): + id: Optional[str] = None + name: Optional[str] = None + min_sal: Optional[float] = None + max_sal: Optional[float] = None + +if __name__ == '__main__': + uvicorn.run("app", host="localhost", port=8000,reload=True) \ No newline at end of file diff --git a/schemas/req.py b/schemas/req.py new file mode 100644 index 0000000..209551f --- /dev/null +++ b/schemas/req.py @@ -0,0 +1,57 @@ + +from datetime import datetime + +from pydantic import BaseModel, Field, field_validator + + +#增加类 +class AddTeacher(BaseModel): + t_name:str= Field(...,min_length=1,max_length=20,description='老师姓名'), + t_gender:str=Field(...,min_length=1,max_length=10,description='性别') + t_phone:str=Field(...,max_length=15,description="手机号码最多15位") + t_subject:str=Field(...,min_length=1,max_length=50,description='教授科目') + t_created:str=Field(...,description='入职时间,格式为%Y-%m-%d') + + @field_validator('t_created') + def j_t_created(cls,v:str): + try: + t_created_date=datetime.strptime(v,'%Y-%m-%d') #时间转字符串日期 + except ValueError: + raise ValueError('时间格式错误,请按照yyyy-MM-dd格式输入') + + if t_created_date.date() > datetime.now().date(): + raise ValueError('入职时间不能大于未来时间') + return v + +#删除类 +class DeleteTeacher(BaseModel): + t_id:int=Field(...,gt=0,description='老师id'), + t_name:str=Field(...,min_length=1,max_length=20,description='老师姓名') + + +#修改类 +class UpdateTeacher(BaseModel): + t_name: str = Field(..., min_length=1, max_length=20, description='老师姓名') + t_gender: str = Field(..., min_length=1, max_length=10, description='性别') + t_phone: str = Field(..., max_length=15, description="手机号码最多15位") + t_subject: str = Field(..., min_length=1, max_length=50, description='教授科目') + t_created: str = Field(..., description='入职时间,格式为%Y-%m-%d') + + @field_validator('t_created') + def j_t_created(cls, v: str): + try: + t_created_date = datetime.strptime(v, '%Y-%m-%d') # 时间转字符串日期 + except ValueError: + raise ValueError('时间格式错误,请按照yyyy-MM-dd格式输入') + + if t_created_date.date() > datetime.now().date(): + raise ValueError('入职时间不能大于未来时间') + return v + + + + + + + + diff --git a/schemas/score.py b/schemas/score.py new file mode 100644 index 0000000..c7ded7f --- /dev/null +++ b/schemas/score.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, Field + +class AddScore(BaseModel): + id: str = Field(...,min_length=1,max_length=10,description="学生ID") + exam_round: int = Field(...,ge=1,le=10,description="考核序次1~10") + score: int = Field(...,ge=0,le=100,description="分数0~100") + +class QueryID(BaseModel): + id: str = Field(..., min_length=1, max_length=10, description="学生ID") + +class QueryExamRound(BaseModel): + exam_round: int = Field(..., ge=1, le=10, description="考核序次1~10") + +class QueryScore(BaseModel): + score: int = Field(..., ge=0, le=100, description="分数0~100") + +class UpdateScore(BaseModel): + id: str = Field(...,min_length=1,max_length=10,description="学生ID") + exam_round: int = Field(...,ge=1,le=10,description="考核序次1~10") + score: int = Field(...,ge=0,le=100,description="分数0~100") + +class DeleteScore(BaseModel): + id: str = Field(..., min_length=1, max_length=10, description="学生ID") + exam_round: int = Field(..., ge=1, le=10, description="考核序次1~10") \ No newline at end of file diff --git a/schemas/student_info.py b/schemas/student_info.py new file mode 100644 index 0000000..88198c9 --- /dev/null +++ b/schemas/student_info.py @@ -0,0 +1,98 @@ +# 校验时间,入学时间小于毕业时间, +from datetime import datetime +from datetime import date +from pydantic import BaseModel, field_validator, Field, ConfigDict + +# 请求体模型:两个日期都允许为空 在api.studend调用检测 +class StudentDate(BaseModel): + enrollment_date: str | None = Field(default=None, description="入学日期,格式为 yyyy-MM-dd,非必填") + graduation_date: str | None = Field(default=None, description="毕业日期,格式 yyyy-MM-dd,非必填") +# 日期校验函数 +def student_date(enrollment_date: str , graduation_date: str): + # 情况1:两个日期都没传,直接通过 + if not enrollment_date and not graduation_date: + return "时间校验通过" + + # 只传了入学日期或者毕业日期,只校验格式 + if enrollment_date and not graduation_date: + try: + datetime.strptime(enrollment_date, "%Y-%m-%d") + except ValueError: + return "入学日期格式错误,请按照 yyyy-MM-dd 输入" + return "时间校验通过" + elif graduation_date and not enrollment_date: + try: + datetime.strptime(graduation_date, "%Y-%m-%d") + except ValueError: + return "毕业日期格式错误,请按照 yyyy-MM-dd 输入" + return "时间校验通过" + + # 如果两个日期都传了,先分别校验格式,再校验先后顺序 + try: + enrollment_date = datetime.strptime(enrollment_date, "%Y-%m-%d") + except ValueError: + return "入学日期格式错误,请按照 yyyy-MM-dd 输入" + try: + graduation_date = datetime.strptime(graduation_date, "%Y-%m-%d") + except ValueError: + return "毕业日期格式错误,请按照 yyyy-MM-dd 输入" + # 核心业务判断:毕业时间不能早于入学时间 + if graduation_date < enrollment_date: + return "毕业时间必须晚于入学时间" + return "时间校验通过" + +#创建学生的请求体 ...(必填) +class StudentCreate(BaseModel): + id: str = Field(..., min_length=1, max_length=20, description="学号") + name: str = Field(..., min_length=1, max_length=50, description="学生姓名") + gender: str = Field(..., pattern=r"^(男|女)$", description="性别:男/女") + class_name: str = Field(..., min_length=1, max_length=20, description="所属班级编号") +#非必填 + age: int | None = Field(default=None, ge=1, le=100, description="年龄") + hometown: str | None = Field(default=None, max_length=100, description="籍贯") + graduate_school: str | None = Field(default=None, max_length=100, description="毕业院校") + major: str | None = Field(default=None, max_length=50, description="专业") + education: str | None = Field(default=None, max_length=30, description="学历:专科/本科/硕士") + enrollment_date: date | None = Field(default=None, description="入学时间,格式yyyy-MM-dd") + graduation_date: date | None = Field(default=None, description="毕业时间,格式yyyy-MM-dd") + status: str | None = Field(default="在读", description="状态:未毕业/进入就业/已就业") +# 更新修改学生 +class StudentUpdate(BaseModel): + name: str | None = Field(default=None, min_length=1, max_length=50, description="学生姓名") + class_name: str | None = Field(default=None, description="班级名称") + gender: str | None = Field(default=None, pattern=r"^(男|女)$", description="性别:男/女") + class_no: str | None = Field(default=None, max_length=20, description="所属班级编号") + age: int | None = Field(default=None, ge=1, le=100, description="年龄") + hometown: str | None = Field(default=None, max_length=100, description="籍贯") + graduate_school: str | None = Field(default=None, max_length=100, description="毕业院校") + major: str | None = Field(default=None, max_length=50, description="专业") + education: str | None = Field(default=None, max_length=30, description="学历") + enrollment_date: date | None = Field(default=None, description="入学时间") + graduation_date: date | None = Field(default=None, description="毕业时间") + status: str | None = Field(default=None, description="状态:未毕业/进入就业/已就业") + + + + +# 响应体 +class StudentResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) #支持读取SQLAlchemy ORM对象属性 + id: str + name: str + gender: str + class_name: str | None = None + age: int | None = None + hometown: str | None = None + graduate_school: str | None = None + major: str | None = None + education: str | None = None + enrollment_date: date | None = None + graduation_date: date | None = None + status: str | None = None + is_deleted: bool = False + + + + + + diff --git a/seed_data.py b/seed_data.py new file mode 100644 index 0000000..12807cd --- /dev/null +++ b/seed_data.py @@ -0,0 +1,160 @@ +from database import SessionLocal, engine, Base +from datetime import date, datetime + + +from model.teachers import Teacher +from model.counselor import Counselor +from model.classes_model import Classes +from model.student_info import Student +from model.scores import Scores +from model.employments import Employments + +# Base.metadata.drop_all(bind=engine) +# Base.metadata.create_all(bind=engine) + +# 教师数据 +teachers_data = [ + {"t_name": "周凌云", "t_gender": "男", "t_phone": "13800000001", "t_subject": "python", + "t_created": date(2023, 1, 1), "t_delete": False}, + {"t_name": "张显", "t_gender": "女", "t_phone": "2783263268242", "t_subject": "mysql", + "t_created": date(2023, 1, 1), "t_delete": False}, +] + +# 2. 顾问数据 +counselors_data = [ + {"name": "吴振玮", "c_time": date(2023, 1, 1), "c_de": False}, + {"name": "孙波", "c_time": date(2023, 1, 1), "c_de": False}, +] + +# 班级数据 +classes_data = [ + {"class_name": "沃林0803班", "head_teacher_name": "周凌云", "counselor_name": "吴振玮", "create_time": date(2023, 1, 1), "class_delete": False}, + {"class_name": "沃林0814班", "head_teacher_name": "张显", "counselor_name": "孙波", "create_time": date(2023, 1, 1), "class_delete": False}, + {"class_name": "沃林0824班", "head_teacher_name": "周凌云", "counselor_name": "孙波", "create_time": date(2023, 1, 1), "class_delete": False}, +] + +# 学生数据 +students_data = [ + {"id": "2024001", "name": "张伟", "gender": "男", "class_name": "沃林0803班", "age": 22, "hometown": "北京", + "graduate_school": "北京四中", "major": "计算机科学与技术", "education": "本科", + "enrollment_date": date(2022, 9, 1), "graduation_date": date(2026, 6, 30), "status": "未就业", "is_deleted": False}, + {"id": "2024002", "name": "王芳", "gender": "女", "class_name": "沃林0803班", "age": 23, "hometown": "上海", + "graduate_school": "上海中学", "major": "计算机科学与技术", "education": "硕士", + "enrollment_date": date(2022, 9, 1), "graduation_date": date(2026, 6, 30), "status": "在读", "is_deleted": False}, + {"id": "2024003", "name": "李娜", "gender": "女", "class_name": "沃林0824班", "age": 24, "hometown": "广州", + "graduate_school": "华南师范大学附属中学", "major": "计算机科学与技术", "education": "本科", + "enrollment_date": date(2022, 9, 1), "graduation_date": date(2026, 6, 30), "status": "未就业", "is_deleted": False}, + {"id": "2024004", "name": "刘洋", "gender": "男", "class_name": "沃林0824班", "age": 25, "hometown": "深圳", + "graduate_school": "深圳中学", "major": "计算机科学与技术", "education": "本科", + "enrollment_date": date(2022, 9, 1), "graduation_date": date(2026, 6, 30), "status": "已就业", "is_deleted": False}, + {"id": "2024005", "name": "陈静", "gender": "女", "class_name": "沃林0803班", "age": 26, "hometown": "杭州", + "graduate_school": "杭州第二中学", "major": "软件工程", "education": "本科", "enrollment_date": date(2022, 9, 1), + "graduation_date": date(2026, 6, 30), "status": "在读", "is_deleted": False}, + {"id": "2024006", "name": "杨强", "gender": "男", "class_name": "沃林0814班", "age": 27, "hometown": "成都", + "graduate_school": "成都七中", "major": "软件工程", "education": "本科", "enrollment_date": date(2022, 9, 1), + "graduation_date": date(2026, 6, 30), "status": "未就业", "is_deleted": False}, + {"id": "2024007", "name": "赵敏", "gender": "女", "class_name": "沃林0803班", "age": 28, "hometown": "武汉", + "graduate_school": "武汉外国语学校", "major": "人工智能", "education": "硕士", "enrollment_date": date(2022, 9, 1), + "graduation_date": date(2026, 6, 30), "status": "已就业", "is_deleted": False}, + {"id": "2024008", "name": "黄磊", "gender": "男", "class_name": "沃林0814班", "age": 29, "hometown": "南京", + "graduate_school": "南京师范大学附属中学", "major": "人工智能", "education": "本科", + "enrollment_date": date(2022, 9, 1), "graduation_date": date(2026, 6, 30), "status": "在读", "is_deleted": False}, + {"id": "2024009", "name": "周杰", "gender": "男", "class_name": "沃林0824班", "age": 30, "hometown": "西安", + "graduate_school": "西安高新第一中学", "major": "网络工程", "education": "硕士", + "enrollment_date": date(2022, 9, 1), "graduation_date": date(2026, 6, 30), "status": "在读", "is_deleted": False}, + {"id": "2024010", "name": "吴霞", "gender": "女", "class_name": "沃林0814班", "age": 31, "hometown": "重庆", + "graduate_school": "重庆巴蜀中学", "major": "网络工程", "education": "本科", "enrollment_date": date(2022, 9, 1), + "graduation_date": date(2026, 6, 30), "status": "已就业", "is_deleted": False}, +] + +# 5. 成绩数据 +scores_data = [ + {"id": "2024001", "exam_round": 1, "score": 85, "is_warning": False, "is_deleted": False}, + {"id": "2024002", "exam_round": 1, "score": 55, "is_warning": True, "is_deleted": False}, + {"id": "2024003", "exam_round": 1, "score": 92, "is_warning": False, "is_deleted": False}, + {"id": "2024004", "exam_round": 1, "score": 78, "is_warning": False, "is_deleted": False}, + {"id": "2024005", "exam_round": 1, "score": 45, "is_warning": True, "is_deleted": False}, +] + +#6. 就业数据 +employments_data = [ + {"id": "2024004", "offer_open": datetime(2026, 3, 1, 9, 0), "offer_review": datetime(2026, 3, 15, 14, 30), "emp": "腾讯科技", "sal": 25000.0, "is_deleted": False}, + {"id": "2024007", "offer_open": datetime(2026, 2, 10, 10, 0), "offer_review": datetime(2026, 2, 20, 16, 0), "emp": "阿里巴巴", "sal": 30000.0, "is_deleted": False}, + {"id": "2024010", "offer_open": datetime(2026, 4, 5, 9, 30), "offer_review": datetime(2026, 4, 12, 11, 0), "emp": "字节跳动", "sal": 28000.0, "is_deleted": False}, + {"id": "2024001", "offer_open": datetime(2026, 5, 1, 9, 0), "offer_review": None, "emp": None, "sal": None, "is_deleted": False}, + {"id": "2024002", "offer_open": None, "offer_review": None, "emp": None, "sal": None, "is_deleted": True}, +] + + +def insert_data(): + db = SessionLocal() + try: + # 插入教师 + print("正在写入教师数据...") + for t in teachers_data: + if not db.query(Teacher).filter(Teacher.t_name == t["t_name"]).first(): + db.add(Teacher(**t)) + db.commit() + + # 插入顾问 (Counselor) + print("正在写入顾问数据...") + for c in counselors_data: + if not db.query(Counselor).filter(Counselor.name == c["name"]).first(): + db.add(Counselor(**c)) + db.commit() + + # 3. 插入班级 (Classes) + print("正在写入班级数据...") + for cl in classes_data: + if not db.query(Classes).filter(Classes.class_name == cl["class_name"]).first(): + # 查找对应老师和顾问的自增 ID + t = db.query(Teacher).filter(Teacher.t_name == cl["head_teacher_name"]).first() + c = db.query(Counselor).filter(Counselor.name == cl["counselor_name"]).first() + if t and c: + # 注意:Classes模型有 teachers 多对多关系,这里我们只处理必需的 head_teacher_id 和 counselor_id + db.add(Classes( + class_name=cl["class_name"], + head_teacher_id=t.t_id, + counselor_id=c.id, + create_time=cl["create_time"], + class_delete=cl["class_delete"] + )) + else: + print(f" 找不到班级 {cl['class_name']} 对应的老师或顾问,跳过!") + db.commit() + + # --- 4. 插入学生 (Student) --- + print("正在写入学生数据...") + for s in students_data: + if not db.query(Student).filter(Student.id == s["id"]).first(): + db.add(Student(**s)) + db.commit() + + # 5. 插入成绩 (Scores) + print("正在写入成绩数据...") + for sc in scores_data: + if not db.query(Scores).filter(Scores.id == sc["id"], Scores.exam_round == sc["exam_round"]).first(): + db.add(Scores(**sc)) + db.commit() + + # 6. 插入就业信息 (Employments) + print("正在写入就业数据...") + for emp in employments_data: + if not db.query(Employments).filter(Employments.id == emp["id"]).first(): + db.add(Employments(**emp)) + db.commit() + + # 最终统计 + print(f"\n 数据初始化完成!") + print(f"班级: {db.query(Classes).count()} 个 | 学生: {db.query(Student).count()} 名") + print(f"成绩: {db.query(Scores).count()} 条 | 就业: {db.query(Employments).count()} 条") + + except Exception as e: + db.rollback() + print(f"\n 插入失败,已回滚:{e}") + finally: + db.close() + + +if __name__ == "__main__": + insert_data() \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..d0db3c3 --- /dev/null +++ b/static/index.html @@ -0,0 +1,785 @@ + + + + + + 沃林学生管理系统 + + + + + + + + + + + + 沃林学生管理系统 + + 📊 统计分析 + 👨🎓 学生管理 + 🏫 班级管理 + 👨🏫 教师管理 + 💼 顾问管理 + 📝 成绩管理 + 💼 就业管理 + + + + + + + + 统计分析 + + + + {{ stats.totalStudents }} + 学生总数 + + + + + {{ stats.totalClasses }} + 班级总数 + + + + + {{ stats.totalTeachers }} + 教师总数 + + + + + + + 班级人数分布 + + + + + + 就业薪资排名 (前5名) + + + + + + + + + 学生管理 + + + + + + 查询 + 新增学生 + + + + + + + + + + + {{ scope.row.status }} + + + + + 编辑 + 删除 + + + + + + + + + 班级管理 + + + + 查询 + 新增班级 + + + + + + + + + + 编辑 + 删除 + + + + + + + + + + 教师管理 + + 新增教师 + + + + + + + + + + 编辑 + 删除 + + + + + + + + + + 顾问管理 + + 新增顾问 + + + + + + + 编辑 + 删除 + + + + + + + + + 成绩管理 + + + + + 查询 + 录入成绩 + + + + + + + {{ scope.row.score }} + + + + + 修改 + 删除 + + + + + + + + + 就业管理 + + + + + 查询 + + + + + + + + + + + + 编辑 + + + + + + + + + + + + + + + + + + + + 男女 + + + + + + + + + + + + + + + + 取消 + 确定 + + + + + + + + + + + + + + + 取消 + 确定 + + + + + + + + + + + 男女 + + + + + + + + + 取消 + 确定 + + + + + + + + + + + + + + 取消 + 确定 + + + + + + + + + + + + + + + + + + + + 取消 + 确定 + + + + + + + + + + + + + + + + 取消 + 确定 + + + + + + + + \ No newline at end of file