2 Commits
Author SHA1 Message Date
zzy ab5064fb21 新增demo演示文件 2026-09-21 19:38:32 +08:00
fyq 4b991d5824 添加majsoul网页和沃林学生管理系统源码 2026-09-21 19:27:07 +08:00
43 changed files with 3111 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
123
@@ -0,0 +1 @@
db/
+18
View File
@@ -0,0 +1,18 @@
# 构建上下文排除项,减小镜像体积
**/__pycache__/
**/*.pyc
**/*.pyo
# IDE / 版本控制
.codebuddy/
**/.git/
**/.idea/
**/.vscode/
# Docker 自身
Dockerfile
docker-compose.yml
.dockerignore
# 文档
*.md
+28
View File
@@ -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"]
View File
+51
View File
@@ -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)
+115
View File
@@ -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
@@ -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
+113
View File
@@ -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="学生不存在,请先创建学生基础信息")
+40
View File
@@ -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)
+113
View File
@@ -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} 已逻辑删除,数据仍保留在库中"}
+66
View File
@@ -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}
+125
View File
@@ -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
+178
View File
@@ -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
@@ -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的同学
@@ -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
+123
View File
@@ -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}"}
+18
View File
@@ -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
+104
View File
@@ -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} 已逻辑删除,数据仍保留在库中"}
@@ -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
+35
View File
@@ -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)
@@ -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
+62
View File
@@ -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)
@@ -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"]
@@ -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")
@@ -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"<Counselor(id={self.id}, name={self.name})>"
@@ -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()
+37
View File
@@ -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"<Scores(学生ID:{self.id}, 考核序次:{self.exam_round}, 成绩:{self.score})>"
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"<Scores(学生ID:{self.id}, 考核序次:{self.exam_round}, 成绩:{self.score})>"
@@ -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"<Student(no={self.id}, name={self.name}, class={self.class_name})>"
@@ -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"<Teacher(t_id={self.t_id}, name='{self.t_name}', subject='{self.t_subject}')>"
+19
View File
@@ -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
@@ -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的学生信息")
@@ -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
@@ -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
@@ -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)
+57
View File
@@ -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
+24
View File
@@ -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")
@@ -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
+160
View File
@@ -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()
+785
View File
@@ -0,0 +1,785 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>沃林学生管理系统</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<script src="https://unpkg.com/element-plus"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/echarts/dist/echarts.min.js"></script>
<style>
body { margin: 0; padding: 0; font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; background-color: #f0f2f5; }
#app { display: flex; height: 100vh; }
.sidebar { width: 220px; background-color: #001529; color: #fff; display: flex; flex-direction: column; }
.sidebar h2 { text-align: center; color: #fff; padding: 20px 0; margin: 0; font-size: 18px; border-bottom: 1px solid #1f2d3d; }
.sidebar .el-menu { border-right: none; flex: 1; }
.main-content { flex: 1; padding: 20px; overflow-y: auto; }
.header-title { font-size: 24px; margin-bottom: 20px; color: #303133; font-weight: 500; }
.search-bar { margin-bottom: 20px; display: flex; gap: 10px; flex-wrap: wrap; }
.table-container { background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
.chart-container { height: 400px; margin-top: 20px; }
.stat-card { text-align: center; padding: 20px; }
.stat-card .num { font-size: 32px; font-weight: bold; color: #409EFF; margin-bottom: 10px; }
.stat-card .label { color: #909399; font-size: 14px; }
</style>
</head>
<body>
<div id="app">
<!-- 侧边栏 -->
<div class="sidebar">
<h2>沃林学生管理系统</h2>
<el-menu :default-active="activeMenu" background-color="#001529" text-color="#fff" active-text-color="#409EFF" @select="handleMenuSelect">
<el-menu-item index="dashboard">📊 统计分析</el-menu-item>
<el-menu-item index="students">👨‍🎓 学生管理</el-menu-item>
<el-menu-item index="classes">🏫 班级管理</el-menu-item>
<el-menu-item index="teachers">👨‍🏫 教师管理</el-menu-item>
<el-menu-item index="counselors">💼 顾问管理</el-menu-item>
<el-menu-item index="scores">📝 成绩管理</el-menu-item>
<el-menu-item index="employments">💼 就业管理</el-menu-item>
</el-menu>
</div>
<!-- 主内容区 -->
<div class="main-content">
<!-- 统计分析 -->
<div v-if="activeMenu === 'dashboard'">
<div class="header-title">统计分析</div>
<el-row :gutter="20">
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="num">{{ stats.totalStudents }}</div>
<div class="label">学生总数</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="num">{{ stats.totalClasses }}</div>
<div class="label">班级总数</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="hover" class="stat-card">
<div class="num">{{ stats.totalTeachers }}</div>
<div class="label">教师总数</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<el-card shadow="hover">
<template #header><div class="card-header"><span>班级人数分布</span></div></template>
<div ref="classChart" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><div class="card-header"><span>就业薪资排名 (前5名)</span></div></template>
<div ref="salaryChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 学生管理 -->
<div v-if="activeMenu === 'students'">
<div class="header-title">学生管理</div>
<div class="table-container">
<div class="search-bar">
<el-input v-model="studentQuery.id" placeholder="学号" style="width: 150px;" clearable></el-input>
<el-input v-model="studentQuery.name" placeholder="姓名" style="width: 150px;" clearable></el-input>
<el-input v-model="studentQuery.class_name" placeholder="班级" style="width: 150px;" clearable></el-input>
<el-button type="primary" @click="fetchStudents">查询</el-button>
<el-button type="success" @click="openStudentDialog()">新增学生</el-button>
</div>
<el-table :data="studentList" border stripe v-loading="loading">
<el-table-column prop="id" label="学号" width="150"></el-table-column>
<el-table-column prop="name" label="姓名" width="100"></el-table-column>
<el-table-column prop="gender" label="性别" width="80"></el-table-column>
<el-table-column prop="class_name" label="班级" width="150"></el-table-column>
<el-table-column prop="age" label="年龄" width="80"></el-table-column>
<el-table-column prop="major" label="专业" width="150"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status === '已就业' ? 'success' : (scope.row.status === '未就业' ? 'danger' : 'warning')">{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button size="small" @click="openStudentDialog(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteStudent(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 班级管理 -->
<div v-if="activeMenu === 'classes'">
<div class="header-title">班级管理</div>
<div class="table-container">
<div class="search-bar">
<el-input v-model="classQuery.class_name" placeholder="班级名称" style="width: 200px;" clearable></el-input>
<el-button type="primary" @click="fetchClasses">查询</el-button>
<el-button type="success" @click="openClassDialog()">新增班级</el-button>
</div>
<el-table :data="classList" border stripe v-loading="loading">
<el-table-column prop="class_id" label="ID" width="80"></el-table-column>
<el-table-column prop="class_name" label="班级名称"></el-table-column>
<el-table-column prop="head_teacher_id" label="班主任ID" width="120"></el-table-column>
<el-table-column prop="counselor_id" label="顾问ID" width="120"></el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180"></el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="openClassDialog(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteClass(scope.row.class_id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 20px;" background layout="prev, pager, next" :total="classTotal" :page-size="classQuery.page_size" @current-change="handleClassPageChange"></el-pagination>
</div>
</div>
<!-- 教师管理 -->
<div v-if="activeMenu === 'teachers'">
<div class="header-title">教师管理</div>
<div class="table-container">
<div class="search-bar"><el-button type="success" @click="openTeacherDialog()">新增教师</el-button></div>
<el-table :data="teacherList" border stripe v-loading="loading">
<el-table-column prop="t_id" label="ID" width="80"></el-table-column>
<el-table-column prop="t_name" label="姓名" width="120"></el-table-column>
<el-table-column prop="t_gender" label="性别" width="80"></el-table-column>
<el-table-column prop="t_phone" label="电话" width="150"></el-table-column>
<el-table-column prop="t_subject" label="教授科目" width="150"></el-table-column>
<el-table-column prop="t_created" label="入职时间" width="150"></el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="openTeacherDialog(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteTeacher(scope.row.t_id, scope.row.t_name)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 20px;" background layout="prev, pager, next" :total="teacherTotal" :page-size="teacherQuery.size" @current-change="handleTeacherPageChange"></el-pagination>
</div>
</div>
<!-- 顾问管理 -->
<div v-if="activeMenu === 'counselors'">
<div class="header-title">顾问管理</div>
<div class="table-container">
<div class="search-bar"><el-button type="success" @click="openCounselorDialog()">新增顾问</el-button></div>
<el-table :data="counselorList" border stripe v-loading="loading">
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="c_time" label="入职时间"></el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="openCounselorDialog(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteCounselor(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 成绩管理 -->
<div v-if="activeMenu === 'scores'">
<div class="header-title">成绩管理</div>
<div class="table-container">
<div class="search-bar">
<el-input v-model="scoreQuery.id" placeholder="学号" style="width: 150px;" clearable></el-input>
<el-input v-model="scoreQuery.exam_round" placeholder="考核序次" style="width: 150px;" clearable></el-input>
<el-button type="primary" @click="fetchScores">查询</el-button>
<el-button type="success" @click="openScoreDialog()">录入成绩</el-button>
</div>
<el-table :data="scoreList" border stripe v-loading="loading">
<el-table-column prop="id" label="学号" width="150"></el-table-column>
<el-table-column prop="exam_round" label="考核序次" width="120"></el-table-column>
<el-table-column prop="score" label="成绩" width="100">
<template #default="scope">
<el-tag :type="scope.row.score < 60 ? 'danger' : 'success'">{{ scope.row.score }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="openScoreDialog(scope.row)">修改</el-button>
<el-button size="small" type="danger" @click="deleteScore(scope.row.id, scope.row.exam_round)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 就业管理 -->
<div v-if="activeMenu === 'employments'">
<div class="header-title">就业管理</div>
<div class="table-container">
<div class="search-bar">
<el-input v-model="employmentQuery.id" placeholder="学号" style="width: 150px;" clearable></el-input>
<el-input v-model="employmentQuery.name" placeholder="公司名称" style="width: 150px;" clearable></el-input>
<el-button type="primary" @click="fetchEmployments">查询</el-button>
</div>
<el-table :data="employmentList" border stripe v-loading="loading">
<el-table-column prop="id" label="学号" width="120"></el-table-column>
<el-table-column prop="name" label="姓名" width="100"></el-table-column>
<el-table-column prop="class_name" label="班级" width="150"></el-table-column>
<el-table-column prop="emp" label="就业公司" width="150"></el-table-column>
<el-table-column prop="sal" label="薪资" width="120"></el-table-column>
<el-table-column prop="offer_open" label="就业开放时间" width="180"></el-table-column>
<el-table-column prop="offer_review" label="Offer下发时间" width="180"></el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="scope">
<el-button size="small" @click="openEmploymentDialog(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- 弹窗:学生 -->
<el-dialog v-model="studentDialogVisible" :title="isStudentEdit ? '编辑学生' : '新增学生'" width="600px">
<el-form :model="studentForm" label-width="100px">
<el-form-item label="学号" v-if="!isStudentEdit">
<el-input v-model="studentForm.id" placeholder="请输入学号(如 2024001)" maxlength="20" show-word-limit clearable></el-input>
</el-form-item>
<el-form-item label="学号" v-else>
<el-input v-model="studentForm.id" disabled></el-input>
</el-form-item>
<el-form-item label="姓名"><el-input v-model="studentForm.name" placeholder="请输入姓名"></el-input></el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="studentForm.gender">
<el-radio label="男">男</el-radio><el-radio label="女">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="班级"><el-input v-model="studentForm.class_name" placeholder="请输入班级名称"></el-input></el-form-item>
<el-form-item label="年龄"><el-input-number v-model="studentForm.age" :min="1" :max="100"></el-input-number></el-form-item>
<el-form-item label="专业"><el-input v-model="studentForm.major" placeholder="请输入专业"></el-input></el-form-item>
<el-form-item label="状态">
<el-select v-model="studentForm.status" placeholder="请选择状态">
<el-option label="在读" value="在读"></el-option>
<el-option label="未就业" value="未就业"></el-option>
<el-option label="已就业" value="已就业"></el-option>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="studentDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveStudent">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 弹窗:班级 -->
<el-dialog v-model="classDialogVisible" :title="classForm.class_id ? '编辑班级' : '新增班级'" width="600px">
<el-form :model="classForm" label-width="100px">
<el-form-item label="班级名称"><el-input v-model="classForm.class_name" placeholder="请输入班级名称"></el-input></el-form-item>
<el-form-item label="班主任ID"><el-input-number v-model="classForm.head_teacher_id" :min="1"></el-input-number></el-form-item>
<el-form-item label="顾问ID"><el-input-number v-model="classForm.counselor_id" :min="1"></el-input-number></el-form-item>
<el-form-item label="开课时间"><el-date-picker v-model="classForm.create_time" type="date" placeholder="选择日期" value-format="YYYY-MM-DD"></el-date-picker></el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="classDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveClass">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 弹窗:教师 -->
<el-dialog v-model="teacherDialogVisible" :title="teacherForm.t_id ? '编辑教师' : '新增教师'" width="600px">
<el-form :model="teacherForm" label-width="100px">
<el-form-item label="姓名"><el-input v-model="teacherForm.t_name" placeholder="请输入姓名"></el-input></el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="teacherForm.t_gender">
<el-radio label="男">男</el-radio><el-radio label="女">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="电话"><el-input v-model="teacherForm.t_phone" placeholder="请输入电话"></el-input></el-form-item>
<el-form-item label="教授科目"><el-input v-model="teacherForm.t_subject" placeholder="请输入教授科目"></el-input></el-form-item>
<el-form-item label="入职时间"><el-date-picker v-model="teacherForm.t_created" type="date" placeholder="选择日期" value-format="YYYY-MM-DD"></el-date-picker></el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="teacherDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveTeacher">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 弹窗:顾问 -->
<el-dialog v-model="counselorDialogVisible" :title="counselorForm.id ? '编辑顾问' : '新增顾问'" width="600px">
<el-form :model="counselorForm" label-width="100px">
<el-form-item label="ID" v-if="!counselorForm.id"><el-input-number v-model="counselorForm.id" :min="1"></el-input-number></el-form-item>
<el-form-item label="姓名"><el-input v-model="counselorForm.name" placeholder="请输入姓名"></el-input></el-form-item>
<el-form-item label="入职时间"><el-date-picker v-model="counselorForm.c_time" type="date" placeholder="选择日期" value-format="YYYY-MM-DD"></el-date-picker></el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="counselorDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveCounselor">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 弹窗:成绩(修复学号无法输入) -->
<el-dialog v-model="scoreDialogVisible" :title="isScoreEdit ? '修改成绩' : '录入成绩'" width="600px">
<el-form :model="scoreForm" label-width="100px">
<el-form-item label="学号">
<el-input v-model="scoreForm.id" placeholder="请输入学号" :disabled="isScoreEdit"></el-input>
</el-form-item>
<el-form-item label="考核序次">
<el-input-number v-model="scoreForm.exam_round" :min="1" :max="10" :disabled="isScoreEdit"></el-input-number>
</el-form-item>
<el-form-item label="成绩">
<el-input-number v-model="scoreForm.score" :min="0" :max="100"></el-input-number>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="scoreDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveScore">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 弹窗:就业 -->
<el-dialog v-model="employmentDialogVisible" title="编辑就业信息" width="600px">
<el-form :model="employmentForm" label-width="120px">
<el-form-item label="学号"><el-input v-model="employmentForm.id" disabled></el-input></el-form-item>
<el-form-item label="就业公司"><el-input v-model="employmentForm.emp" placeholder="请输入公司名称"></el-input></el-form-item>
<el-form-item label="薪资"><el-input-number v-model="employmentForm.sal" :min="0" :precision="2"></el-input-number></el-form-item>
<el-form-item label="就业开放时间"><el-date-picker v-model="employmentForm.offer_open" type="datetime" placeholder="选择时间" value-format="YYYY-MM-DDTHH:mm:ss"></el-date-picker></el-form-item>
<el-form-item label="Offer下发时间"><el-date-picker v-model="employmentForm.offer_review" type="datetime" placeholder="选择时间" value-format="YYYY-MM-DDTHH:mm:ss"></el-date-picker></el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="employmentDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEmployment">确定</el-button>
</span>
</template>
</el-dialog>
</div>
<script>
const { createApp, ref, reactive, onMounted, nextTick } = Vue;
const { ElMessage, ElMessageBox } = ElementPlus;
const app = createApp({
setup() {
const activeMenu = ref('dashboard');
const loading = ref(false);
const baseUrl = 'http://127.0.0.1:8001';
// --- 统计分析 ---
const stats = reactive({ totalStudents: 0, totalClasses: 0, totalTeachers: 0 });
const classChart = ref(null);
const salaryChart = ref(null);
// --- 学生管理 ---
const studentList = ref([]);
const studentQuery = reactive({ id: '', name: '', class_name: '' });
const studentDialogVisible = ref(false);
const studentForm = reactive({ id: '', name: '', gender: '男', class_name: '', age: 18, major: '', status: '在读' });
const isStudentEdit = ref(false);
// --- 班级管理 ---
const classList = ref([]);
const classTotal = ref(0);
const classQuery = reactive({ class_name: '', page_num: 1, page_size: 10 });
const classDialogVisible = ref(false);
const classForm = reactive({ class_id: null, class_name: '', head_teacher_id: 1, counselor_id: 1, create_time: '' });
// --- 教师管理 ---
const teacherList = ref([]);
const teacherTotal = ref(0);
const teacherQuery = reactive({ page: 1, size: 10 });
const teacherDialogVisible = ref(false);
const teacherForm = reactive({ t_id: null, t_name: '', t_gender: '男', t_phone: '', t_subject: '', t_created: '' });
// --- 顾问管理 ---
const counselorList = ref([]);
const counselorDialogVisible = ref(false);
const counselorForm = reactive({ id: null, name: '', c_time: '' });
// --- 成绩管理 ---
const scoreList = ref([]);
const scoreQuery = reactive({ id: '', exam_round: '' });
const scoreDialogVisible = ref(false);
const scoreForm = reactive({ id: '', exam_round: 1, score: 0 });
const isScoreEdit = ref(false); // ★ 新增:区分录入与修改
// --- 就业管理 ---
const employmentList = ref([]);
const employmentQuery = reactive({ id: '', name: '' });
const employmentDialogVisible = ref(false);
const employmentForm = reactive({ id: '', emp: '', sal: 0, offer_open: '', offer_review: '' });
// --- 菜单切换 ---
const handleMenuSelect = (index) => {
activeMenu.value = index;
if (index === 'dashboard') fetchStats();
if (index === 'students') fetchStudents();
if (index === 'classes') fetchClasses();
if (index === 'teachers') fetchTeachers();
if (index === 'counselors') fetchCounselors();
if (index === 'scores') fetchScores();
if (index === 'employments') fetchEmployments();
};
// --- API 请求方法 ---
const fetchStats = async () => {
try {
const stuRes = await axios.get(`${baseUrl}/students`);
stats.totalStudents = stuRes.data.length;
const classRes = await axios.get(`${baseUrl}/api/classes?page_num=1&page_size=100`);
stats.totalClasses = classRes.data.total;
const teaRes = await axios.get(`${baseUrl}/teacher/list?page=1&size=100`);
stats.totalTeachers = teaRes.data.total;
nextTick(() => { renderClassChart(); renderSalaryChart(); });
} catch (e) { console.error(e); }
};
const renderClassChart = async () => {
try {
const res = await axios.get(`${baseUrl}/analysis/studnet_info/class`);
const data = res.data;
const chart = echarts.init(classChart.value);
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['总人数', '男', '女'] },
xAxis: { type: 'category', data: data.map(i => i.class_name) },
yAxis: { type: 'value' },
series: [
{ name: '总人数', type: 'bar', data: data.map(i => i['总人数']) },
{ name: '男', type: 'bar', data: data.map(i => i['男']) },
{ name: '女', type: 'bar', data: data.map(i => i['女']) }
]
});
} catch (e) { console.error(e); }
};
const renderSalaryChart = async () => {
try {
const res = await axios.get(`${baseUrl}/analysis/studnet_employments_salary?n=5`);
const data = res.data;
const chart = echarts.init(salaryChart.value);
chart.setOption({
tooltip: { trigger: 'item' },
xAxis: { type: 'category', data: data.map(i => i.student ? i.student.name : i.id) },
yAxis: { type: 'value' },
series: [{ data: data.map(i => i.sal), type: 'bar', color: '#67C23A' }]
});
} catch (e) { console.error(e); }
};
// ------- 学生 -------
const fetchStudents = async () => {
loading.value = true;
try {
const params = {};
if (studentQuery.id) params.id = studentQuery.id;
if (studentQuery.name) params.name = studentQuery.name;
if (studentQuery.class_name) params.class_name = studentQuery.class_name;
const res = await axios.get(`${baseUrl}/students`, { params });
studentList.value = res.data;
} catch (e) { ElMessage.error('获取学生列表失败'); }
loading.value = false;
};
const openStudentDialog = (row = null) => {
if (row) {
isStudentEdit.value = true;
Object.assign(studentForm, row);
} else {
isStudentEdit.value = false;
Object.assign(studentForm, { id: '', name: '', gender: '男', class_name: '', age: 18, major: '', status: '在读' });
}
studentDialogVisible.value = true;
};
const saveStudent = async () => {
if (!studentForm.id) { ElMessage.warning('请填写学号'); return; }
if (!studentForm.name) { ElMessage.warning('请填写姓名'); return; }
if (!studentForm.class_name) { ElMessage.warning('请填写班级'); return; }
try {
if (isStudentEdit.value) {
const { id, ...body } = studentForm;
await axios.put(`${baseUrl}/students/${id}`, body);
ElMessage.success('更新成功');
} else {
await axios.post(`${baseUrl}/students`, studentForm);
ElMessage.success('创建成功');
}
studentDialogVisible.value = false;
fetchStudents();
} catch (e) { ElMessage.error(e.response?.data?.detail || '操作失败'); }
};
const deleteStudent = async (id) => {
try {
await ElMessageBox.confirm(
'此操作将学生标记为已删除(逻辑删除),数据库记录仍会保留,且支持后续恢复。是否继续?',
'逻辑删除确认',
{ type: 'warning', confirmButtonText: '确定删除', cancelButtonText: '取消' }
);
await axios.delete(`${baseUrl}/students/${id}`);
ElMessage.success('学生已逻辑删除,数据仍保留在库中');
fetchStudents();
} catch (e) {
if (e !== 'cancel') ElMessage.error('删除失败');
}
};
// ------- 班级 -------
const fetchClasses = async () => {
loading.value = true;
try {
const res = await axios.get(`${baseUrl}/api/classes`, { params: classQuery });
classList.value = res.data.items;
classTotal.value = res.data.total;
} catch (e) { ElMessage.error('获取班级列表失败'); }
loading.value = false;
};
const handleClassPageChange = (val) => { classQuery.page_num = val; fetchClasses(); };
const openClassDialog = (row = null) => {
if (row) { Object.assign(classForm, row); }
else { Object.assign(classForm, { class_id: null, class_name: '', head_teacher_id: 1, counselor_id: 1, create_time: '' }); }
classDialogVisible.value = true;
};
const saveClass = async () => {
try {
if (classForm.class_id) {
await axios.put(`${baseUrl}/api/classes/${classForm.class_id}/base`, classForm);
ElMessage.success('更新成功');
} else {
await axios.post(`${baseUrl}/api/classes`, classForm);
ElMessage.success('创建成功');
}
classDialogVisible.value = false;
fetchClasses();
} catch (e) { ElMessage.error(e.response?.data?.detail || '操作失败'); }
};
const deleteClass = async (id) => {
try {
await ElMessageBox.confirm('确定要删除该班级吗?', '提示', { type: 'warning' });
await axios.put(`${baseUrl}/api/classes/batch-delete`, { class_ids: [id], deleted: true });
ElMessage.success('删除成功');
fetchClasses();
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败'); }
};
// ------- 教师 -------
const fetchTeachers = async () => {
loading.value = true;
try {
const res = await axios.get(`${baseUrl}/teacher/list`, { params: teacherQuery });
teacherList.value = res.data.list;
teacherTotal.value = res.data.total;
} catch (e) { ElMessage.error('获取教师列表失败'); }
loading.value = false;
};
const handleTeacherPageChange = (val) => { teacherQuery.page = val; fetchTeachers(); };
const openTeacherDialog = (row = null) => {
if (row) { Object.assign(teacherForm, row); }
else { Object.assign(teacherForm, { t_id: null, t_name: '', t_gender: '男', t_phone: '', t_subject: '', t_created: '' }); }
teacherDialogVisible.value = true;
};
const saveTeacher = async () => {
try {
if (teacherForm.t_id) {
await axios.put(`${baseUrl}/teacher/${teacherForm.t_id}`, teacherForm);
ElMessage.success('更新成功');
} else {
await axios.post(`${baseUrl}/teacher/teachers`, teacherForm);
ElMessage.success('创建成功');
}
teacherDialogVisible.value = false;
fetchTeachers();
} catch (e) { ElMessage.error(e.response?.data?.detail || '操作失败'); }
};
const deleteTeacher = async (t_id, t_name) => {
try {
await ElMessageBox.confirm('确定要删除该教师吗?', '提示', { type: 'warning' });
await axios.delete(`${baseUrl}/teacher/${t_id}`, { data: { t_id, t_name } });
ElMessage.success('删除成功');
fetchTeachers();
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败'); }
};
// ------- 顾问 -------
const fetchCounselors = async () => {
loading.value = true;
try {
counselorList.value = [
{ id: 1, name: '吴振玮', c_time: '2023-01-01' },
{ id: 2, name: '孙波', c_time: '2023-01-01' }
];
} catch (e) { ElMessage.error('获取顾问列表失败'); }
loading.value = false;
};
const openCounselorDialog = (row = null) => {
if (row) { Object.assign(counselorForm, row); }
else { Object.assign(counselorForm, { id: null, name: '', c_time: '' }); }
counselorDialogVisible.value = true;
};
const saveCounselor = async () => {
try {
if (counselorForm.id) {
await axios.put(`${baseUrl}/counselor/${counselorForm.id}`, counselorForm);
ElMessage.success('更新成功');
} else {
await axios.post(`${baseUrl}/counselor/counselors/`, counselorForm);
ElMessage.success('创建成功');
}
counselorDialogVisible.value = false;
fetchCounselors();
} catch (e) { ElMessage.error(e.response?.data?.detail || '操作失败'); }
};
const deleteCounselor = async (id) => {
try {
await ElMessageBox.confirm('确定要删除该顾问吗?', '提示', { type: 'warning' });
await axios.delete(`${baseUrl}/counselor/${id}`, { data: { id } });
ElMessage.success('删除成功');
fetchCounselors();
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败'); }
};
// ------- 成绩 -------
const fetchScores = async () => {
loading.value = true;
try {
if (scoreQuery.id) {
const res = await axios.get(`${baseUrl}/scores/query/id?id=${scoreQuery.id}`);
scoreList.value = res.data.msg || [];
} else if (scoreQuery.exam_round) {
const res = await axios.get(`${baseUrl}/scores/query/round?exam_round=${scoreQuery.exam_round}`);
scoreList.value = res.data.msg || [];
} else {
scoreList.value = [];
}
} catch (e) { ElMessage.error('获取成绩列表失败'); }
loading.value = false;
};
const openScoreDialog = (row = null) => {
if (row) {
// 编辑模式
isScoreEdit.value = true;
Object.assign(scoreForm, row);
} else {
// 录入模式
isScoreEdit.value = false;
Object.assign(scoreForm, { id: '', exam_round: 1, score: 0 });
}
scoreDialogVisible.value = true;
};
const saveScore = async () => {
if (!scoreForm.id) { ElMessage.warning('请输入学号'); return; }
if (!scoreForm.exam_round) { ElMessage.warning('请输入考核序次'); return; }
try {
if (isScoreEdit.value) {
await axios.put(`${baseUrl}/scores/update?id=${scoreForm.id}&exam_round=${scoreForm.exam_round}&score=${scoreForm.score}`);
ElMessage.success('修改成功');
} else {
await axios.post(`${baseUrl}/scores/add?id=${scoreForm.id}&exam_round=${scoreForm.exam_round}&score=${scoreForm.score}`);
ElMessage.success('录入成功');
}
scoreDialogVisible.value = false;
fetchScores();
} catch (e) { ElMessage.error(e.response?.data?.msg || '操作失败'); }
};
const deleteScore = async (id, exam_round) => {
try {
await ElMessageBox.confirm('确定要删除该成绩吗?', '提示', { type: 'warning' });
await axios.delete(`${baseUrl}/scores/delete?id=${id}&exam_round=${exam_round}`);
ElMessage.success('删除成功');
fetchScores();
} catch (e) { if (e !== 'cancel') ElMessage.error('删除失败'); }
};
// ------- 就业 -------
const fetchEmployments = async () => {
loading.value = true;
try {
const params = {};
if (employmentQuery.id) params.mid = employmentQuery.id;
if (employmentQuery.name) params.name = employmentQuery.name;
const res = await axios.get(`${baseUrl}/employment/query`, { params });
employmentList.value = res.data;
} catch (e) { ElMessage.error('获取就业列表失败'); }
loading.value = false;
};
const openEmploymentDialog = (row) => {
Object.assign(employmentForm, row);
employmentDialogVisible.value = true;
};
const saveEmployment = async () => {
try {
await axios.post(`${baseUrl}/employment/students/${employmentForm.id}`, {
offer_open: employmentForm.offer_open || null,
offer_review: employmentForm.offer_review || null,
emp: employmentForm.emp || null,
sal: employmentForm.sal || null
});
ElMessage.success('更新成功');
employmentDialogVisible.value = false;
fetchEmployments();
} catch (e) { ElMessage.error(e.response?.data?.detail || '操作失败'); }
};
onMounted(() => { fetchStats(); });
return {
activeMenu, handleMenuSelect, loading, stats, classChart, salaryChart,
// 学生
studentList, studentQuery, fetchStudents,
studentDialogVisible, studentForm, isStudentEdit,
openStudentDialog, saveStudent, deleteStudent,
// 班级
classList, classTotal, classQuery, fetchClasses, handleClassPageChange,
classDialogVisible, classForm, openClassDialog, saveClass, deleteClass,
// 教师
teacherList, teacherTotal, teacherQuery, fetchTeachers, handleTeacherPageChange,
teacherDialogVisible, teacherForm, openTeacherDialog, saveTeacher, deleteTeacher,
// 顾问
counselorList, fetchCounselors, counselorDialogVisible, counselorForm,
openCounselorDialog, saveCounselor, deleteCounselor,
// 成绩
scoreList, scoreQuery, fetchScores, scoreDialogVisible, scoreForm,
isScoreEdit, openScoreDialog, saveScore, deleteScore,
// 就业
employmentList, employmentQuery, fetchEmployments, employmentDialogVisible,
employmentForm, openEmploymentDialog, saveEmployment
};
}
});
app.use(ElementPlus);
app.mount('#app');
</script>
</body>
</html>