2 Commits
Author SHA1 Message Date
wangtian ec53b7dc46 Merge pull request '就业管理模块' (#2) from pzz into master
Reviewed-on: #2
2026-09-22 17:01:44 +08:00
wangtian b3d6d85b25 Merge pull request '班级模块' (#1) from wch into master
Reviewed-on: #1
2026-09-22 16:46:25 +08:00
29 changed files with 51 additions and 1911 deletions
-15
View File
@@ -1,15 +0,0 @@
from database import engine
from sqlalchemy import text
conn = engine.connect()
tables = conn.execute(text("SHOW TABLES")).fetchall()
print('Tables:', tables)
for t in tables:
tname = t[0]
rows = conn.execute(text(f"SHOW COLUMNS FROM `{tname}` WHERE Field = 'is_deleted'")).fetchall()
if rows:
vals = conn.execute(text(f"SELECT is_deleted, COUNT(*) FROM `{tname}` GROUP BY is_deleted")).fetchall()
print(f" {tname}: type={rows[0][1]}, values={vals}")
conn.close()
+1 -1
View File
@@ -2,7 +2,7 @@ import uuid
from fastapi import APIRouter, Depends, HTTPException,Query
from dao.class_dao import update_class_dao, add_class_dao, get_class_dao
from database import get_db
from model.class_model import StudentClassIntermediate
from model.class_model import StudentClassIntermediate,Student_info
from schema.class_request import ClassRequest, ClassResponse, TransferRequest
from dao.class_dao import Student_delect_Dao
from datetime import datetime
-29
View File
@@ -1,29 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from schema.course_request import CourseRequest, CourseResponse
from database import get_db
from dao.course_dao import (
add_course_dao, get_all_courses_dao, get_course_by_id_dao
)
course_api = APIRouter()
@course_api.post('', response_model=CourseResponse, summary='添加课程')
def add_course(course: CourseRequest, db=Depends(get_db)):
d = course.model_dump()
r = add_course_dao(o=d, db=db)
if not r:
raise HTTPException(status_code=500, detail='添加失败!')
return CourseResponse(totals=1, data=d)
@course_api.get('/{course_id}', summary='按编号查课程')
def get_course(course_id: str, db=Depends(get_db)):
c = get_course_by_id_dao(course_id=course_id, db=db)
if not c:
raise HTTPException(status_code=404, detail='课程不存在')
return {
'course_id': c.course_id,
'course_name': c.course_name,
'teacher_id': c.teacher_id,
}
-106
View File
@@ -1,106 +0,0 @@
# 导入类型提示 List,用于指定响应模型为列表
from typing import List
# 导入 FastAPI 的 APIRouter(路由)、Depends(依赖注入)、HTTPException(HTTP 异常)
from fastapi import APIRouter, Depends, HTTPException
# 从 dao.region_sql 导入数据库操作函数,并为添加、修改、删除起了别名,避免与路由函数重名
from dao.region_sql import (
get_region_part,
add_region as dao_add_resp,
update_region as dao_update_resp,
delete_region as dao_delete_resp,
)
# 导入数据库会话依赖,用于每个请求获取独立的 Session
from database import get_db
# 导入 Pydantic 模型:地区响应体、地区添加请求体、通用操作响应体、地区修改请求体
from schema.stu_info_region_schema import (
region_resp,
region_req,
region_add_resp,
region_update_req,
)
# 创建地区模块的路由实例
region_api = APIRouter()
# -------------------- 查询地区(支持按省份过滤) --------------------
@region_api.get('/region', response_model=List[region_resp], summary='查询')
def get_regions_part(province: str = None, gs=Depends(get_db)):
"""
根据省份查询地区信息。
- province 为可选参数,不传则返回全部地区。
- 返回列表,每个元素符合 region_resp 模型。
"""
try:
# 调用 dao 层查询函数,传入数据库会话和省份过滤条件
data = get_region_part(gs, province)
return data
except Exception as e:
# 捕获异常,返回 500 错误,详情为字符串化的异常信息
raise HTTPException(status_code=500, detail=str(e))
# -------------------- 添加地区 --------------------
@region_api.post('/region', response_model=region_add_resp, summary='添加')
def add_region(rreq: region_req, gs=Depends(get_db)):
"""
新增一条地区记录。
- 请求体必须符合 region_req 模型。
- 返回统一的 region_add_resp(code + message)。
"""
try:
# 将 Pydantic 模型转为字典,便于传给 dao 层
data = rreq.model_dump()
# 调用 dao 层添加函数(别名为 dao_add_resp)
dao_add_resp(gs, data)
return region_add_resp(code=200, message='数据已添加')
except Exception as e:
# 注意:此处状态码写成了 200,通常异常应返回 500。
# 建议改为 status_code=500。
raise HTTPException(status_code=200, detail=str(e))
# -------------------- 修改地区 --------------------
@region_api.put('/region', response_model=region_add_resp, summary='修改')
def update_region(code: str, rreq: region_update_req, gs=Depends(get_db)):
"""
根据地区编码更新地区信息。
- code 为查询参数,指定要修改的地区编码。
- 请求体为 region_update_req,字段均为可选,只更新传入的字段。
- 返回统一的 region_add_resp。
"""
try:
# exclude_unset=True:只获取客户端显式传入的字段,未传的字段不参与更新
data = rreq.model_dump(exclude_unset=True)
# 调用 dao 层更新函数(别名为 dao_update_resp),传入编码和待更新数据
res = dao_update_resp(gs, code, data)
if not res:
# 没有更新任何行,说明编码不存在或数据未变化
raise HTTPException(status_code=500, detail='没有更新!')
# res 可能是 dao 返回的消息字符串,直接作为 message 返回
return region_add_resp(code=200, message=res)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# -------------------- 删除地区 --------------------
@region_api.delete('/region', response_model=region_add_resp, summary='删除')
def delete_region(code: str, gs=Depends(get_db)):
"""
根据地区编码删除地区记录。
- code 为查询参数,指定要删除的地区编码。
- 返回统一的 region_add_resp。
"""
try:
# 调用 dao 层删除函数(别名为 dao_delete_resp),传入编码
res = dao_delete_resp(gs, code)
if not res:
# 没有删除任何行,可能编码不存在
raise HTTPException(status_code=500, detail='没有删除')
return region_add_resp(code=200, message='删除成功')
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
-70
View File
@@ -1,70 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from schema.score_request import ScoreRequest, ScoreUpdateRequest, ScoreResponse
from database import get_db
from dao.score_dao import (
calc_level, calc_pass,
add_score_dao, update_score_dao, delete_score_dao, get_scores_dao
)
score_api = APIRouter()
# ========== 1. 添加成绩(按考核序次录入) ==========
@score_api.post('', response_model=ScoreResponse, summary='添加成绩')
def add_score(score: ScoreRequest, db=Depends(get_db)):
d = score.model_dump()
d['score_level'] = calc_level(d['score']) # ← 调 DAO 的工具函数
d['is_pass'] = calc_pass(d['score'])
r = add_score_dao(o=d, db=db)
if not r:
raise HTTPException(
status_code=500,
detail='添加失败!'
)
return ScoreResponse(totals=1, data=d)
# ========== 2. 查询成绩(动态条件 + 分页) ==========
@score_api.get('', summary='查询成绩(支持多条件+分页)')
def get_scores(
score_id: int = None,
student_id: str = None,
course_id: str = None,
teacher_id: str = None,
exam_order: int = None,
page: int = 1,
page_size: int = 10,
db=Depends(get_db)
):
r = get_scores_dao(
score_id=score_id, student_id=student_id, course_id=course_id,
teacher_id=teacher_id, exam_order=exam_order,
page=page, page_size=page_size, db=db
)
if r:
return r
raise HTTPException(status_code=404, detail='成绩不存在!')
# ========== 3. 修改成绩 ==========
@score_api.put('/{score_id}', summary='修改成绩')
def update_score(score_id: int, score: ScoreUpdateRequest, db=Depends(get_db)):
d = score.model_dump(exclude_unset=True)
if 'score' in d:
d['score_level'] = calc_level(d['score'])
d['is_pass'] = calc_pass(d['score'])
r = update_score_dao(score_id=score_id, update_data=d, db=db)
if not r:
raise HTTPException(status_code=500, detail='没有更新!')
return {'code': 200, 'totals': r, 'detail': '更新成功'}
# ========== 4. 删除成绩(逻辑删除) ==========
@score_api.delete('/{score_id}', summary='删除成绩(逻辑删除)')
def delete_score(score_id: int, db=Depends(get_db)):
r = delete_score_dao(score_id=score_id, db=db)
if not r:
raise HTTPException(status_code=500, detail='删除异常,稍后操作!')
return {'code': 200, 'totals': r, 'detail': '删除成功'}
-126
View File
@@ -1,126 +0,0 @@
from fastapi import APIRouter, Depends, Query
from database import get_db
from dao.statistics_dao import StatisticsDao
from schema.statistics_schema import (
ResponseModel, ResponseModel1, ResponseModel2, ResponseModel3,
ResponseModel4, ResponseModel5, ResponseModel6, ClassStatResponse
)
from math import ceil
BasicInformationAPI = APIRouter(tags=['统计分析'])
@BasicInformationAPI.get('/basic-information', summary='查询年龄区间内的学员信息')
def get_students_by_age_range_api(
n: int = Query(..., description='页码,从1开始', ge=1),
m: int = Query(..., description='每页条数', ge=1),
min_age: int = Query(..., description='最小年龄', ge=0),
max_age: int = Query(..., description='最大年龄', le=100),
db = Depends(get_db)):
if min_age > max_age:
return ResponseModel(code=400,
message='最小年龄不能大于最大年龄',
total=0,
total_pages=0,
data=[])
req, total = StatisticsDao.get_students_by_age_range_dao(n, m, min_age, max_age, db)
return ResponseModel(code=200,
message='查询成功',
total=total,
total_pages=ceil(total / m) if total > 0 else 0,
data=req)
@BasicInformationAPI.get('/basic-information/{class_id}', summary='统计每个班级的人数以及男生/女生的人数')
def get_students_by_class_id_api(class_id: str,
db = Depends(get_db)):
total_count, male_count, female_count = StatisticsDao.get_students_by_class_id_dao(class_id, db)
return ClassStatResponse(class_id=class_id,
total_count=total_count,
male_count=male_count,
female_count=female_count)
@BasicInformationAPI.get('/scores', summary='在某个分数段的学生')
def get_students_by_score_api(
n: int = Query(..., description='页码,从1开始', ge=1),
m: int = Query(..., description='每页条数', ge=1),
score: float = Query(ge=0, le=100),
db = Depends(get_db)):
if score < 0 or score > 100:
return ResponseModel1(code=400,
message='分数需要在0~100!',
total=0,
total_pages=0,
data=[])
req, total = StatisticsDao.get_students_by_score_dao(n, m, score, db)
return ResponseModel1(code=200,
message='查询成功',
total=total,
total_pages=ceil(total / m) if total > 0 else 0,
data=req)
@BasicInformationAPI.get('/scores_no_pass', summary='查询有n门成绩不合格的学生')
def get_students_by_no_pass_api(
n: int = Query(..., description='页码,从1开始', ge=1),
m: int = Query(..., description='每页条数', ge=1),
fail_count: int = Query(..., description='不及格门数', ge=1),
db = Depends(get_db)):
req, total = StatisticsDao.get_student_by_no_pass_dao(n, m, fail_count, db)
return ResponseModel2(code=200,
message='查询成功',
total=total,
total_pages=ceil(total / m) if total > 0 else 0,
data=req)
@BasicInformationAPI.get('/scores_avg', summary='统计每次考试每个班级的平均分(从高到低排序)')
def get_class_exam_avg_api(
n: int = Query(..., description='页码,从1开始', ge=1),
m: int = Query(..., description='每页条数', ge=1),
db = Depends(get_db)):
req, total = StatisticsDao.get_class_exam_avg_score_dao(n, m, db)
return ResponseModel3(code=200,
message='查询成功',
total=total,
total_pages=ceil(total / m) if total > 0 else 0,
data=req)
@BasicInformationAPI.get('/salary_top', summary='查询就业薪资最高的前m名学生')
def get_salary_top_api(
m: int = Query(5, description='查询前m名', ge=1),
db = Depends(get_db)):
req = StatisticsDao.get_salary_top_dao(m, db)
return ResponseModel4(code=200,
message='查询成功',
data=req)
@BasicInformationAPI.get('/time_size', summary='统计每个学生的就业时长(offer下发时间-就业开放时间)')
def get_time_size_api(
n: int = Query(..., description='页码,从1开始', ge=1),
m: int = Query(..., description='每页条数', ge=1),
db = Depends(get_db)):
req, total = StatisticsDao.get_time_size_dao(n, m, db)
return ResponseModel5(code=200,
message='查询成功',
total=total,
total_pages=ceil(total / m) if total > 0 else 0,
data=req)
@BasicInformationAPI.get('/class_avg_time_size', summary='统计每个班级的平均就业时长(只统计进入就业阶段的学生)')
def get_class_avg_time_size_api(
n: int = Query(..., description='页码,从1开始', ge=1),
m: int = Query(..., description='每页条数', ge=1),
db = Depends(get_db)):
req, total = StatisticsDao.get_class_avg_time_size_dao(n, m, db)
return ResponseModel6(code=200,
message='查询成功',
total=total,
total_pages=ceil(total / m) if total > 0 else 0,
data=req)
-101
View File
@@ -1,101 +0,0 @@
# 导入 FastAPI 的 APIRouter(用于组织路由)、Depends(依赖注入)、HTTPException(抛出 HTTP 异常)
from fastapi import APIRouter, Depends, HTTPException
# 从 dao 层导入数据库操作函数,并对修改和删除函数起了别名,避免与路由函数重名
from dao.student_info_sql import (
get_part_student_info,
add_student_info,
update_student_info as update_info,
delete_student_info as delete_info,
)
# 导入数据库会话依赖,用于每个请求获取独立的 Session
from database import get_db
# 导入 Pydantic 模型:请求体、响应体、通用操作响应体
from schema.stu_info_region_schema import stu_info_req, stu_info_resp, stu_info_add_resp
# 创建学生信息模块的路由实例
student_info_api = APIRouter()
# -------------------- 查询学生信息(支持条件过滤) --------------------
@student_info_api.get('/student', response_model=list[stu_info_resp], summary='查询')
def get_student_info_part(
student_id: str = None,
student_name: str = None,
gender: str = None,
gs=Depends(get_db)
):
"""
根据学号、姓名、性别查询学生信息。
- 所有查询参数均为可选,不传则返回全部未删除记录。
- 返回列表,每个元素符合 stu_info_resp 模型。
"""
try:
# 调用 dao 层查询函数,传入数据库会话和过滤条件
data = get_part_student_info(gs, student_id, student_name, gender)
return data
except Exception as err:
# 捕获异常,返回 500 错误,详情为原始异常信息
raise HTTPException(status_code=500, detail=err)
# -------------------- 添加学生信息 --------------------
@student_info_api.post('/student', response_model=stu_info_add_resp, summary='添加')
def add_student_infomation(stu_info: stu_info_req, gs=Depends(get_db)):
"""
新增一条学生记录。
- 请求体必须符合 stu_info_req 模型。
- 返回统一的 stu_info_add_resp(code + message)。
"""
try:
# 将 Pydantic 模型转为字典,便于传给 dao 层
data = stu_info.model_dump()
# 调用 dao 层添加函数
res = add_student_info(gs, data)
if not res:
# 如果 dao 返回假值(如 None、空字符串),说明添加失败
raise HTTPException(status_code=500, detail='添加失败')
return stu_info_add_resp(code=200, message='添加成功')
except Exception as e:
# 将异常信息转为字符串,避免序列化问题
raise HTTPException(status_code=500, detail=str(e))
# -------------------- 修改学生信息 --------------------
@student_info_api.put('/student', response_model=stu_info_add_resp, summary='修改')
def update_student_info(stu_info: stu_info_req, gs=Depends(get_db)):
"""
根据学号更新学生信息。
- 请求体必须包含完整的 stu_info_req(包括 student_id)。
- 返回统一的 stu_info_add_resp。
"""
try:
data = stu_info.model_dump()
# 调用 dao 层更新函数(已别名为 update_info)
res = update_info(gs, data)
if not res:
# 没有更新任何行,说明学号不存在或数据未变化
raise HTTPException(status_code=500, detail='没有更新!')
return stu_info_add_resp(code=200, message='更新成功')
except Exception as err:
raise HTTPException(status_code=500, detail=str(err))
# -------------------- 删除学生信息(逻辑删除) --------------------
@student_info_api.delete('/student', response_model=stu_info_add_resp, summary='删除')
def delete_student_info(sid: str, gs=Depends(get_db)):
"""
根据学号删除学生记录(通常为逻辑删除)。
- sid 为查询参数,返回统一的 stu_info_add_resp。
"""
try:
# 调用 dao 层删除函数(已别名为 delete_info),传入学号
res = delete_info(gs, sid)
if not res:
# 没有删除任何行,可能学号不存在
raise HTTPException(status_code=500, detail='没有删除')
return stu_info_add_resp(code=200, message='删除成功')
except Exception as err:
raise HTTPException(status_code=500, detail=str(err))
-41
View File
@@ -1,41 +0,0 @@
from fastapi import APIRouter,Depends,HTTPException
from schema.teacher_schema import TeacherRequest,TeacherResponse,TeacherUpdateRequest
from database import get_db
from dao.teacher_dao import add_teacher_dao,update_teacher_dao,get_teacher_dao,delete_teacher_dao
TeacherAPI = APIRouter()
@TeacherAPI.post('/teachers',response_model=TeacherResponse,summary='新增老师')
def add_teachers(teacher:TeacherRequest,db=Depends(get_db)):
d = teacher.model_dump()
r = add_teacher_dao(t=d,db=db)
if not r:
raise HTTPException(status_code=400,detail='教师编号已存在或新增失败!')
return teacher
@TeacherAPI.put('/teachers/{teacher_id}',summary='更新老师')
def update_teachers(teacher_id:str,teacher:TeacherUpdateRequest,db=Depends(get_db)):
d = teacher.model_dump(exclude_unset=True)
r = update_teacher_dao(teacher_id=teacher_id,data=d,db=db) #返回更新的行数
if not r:
raise HTTPException(status_code=404,detail='老师不存在或未更新!')
return {'code':200,'totals':r,'detail':'更新成功'}
@TeacherAPI.get('/teachers',response_model=list[TeacherResponse],summary='查询老师')
def get_teachers(teacher_id:str|None=None,
teacher_name:str|None=None,
db=Depends(get_db)):
r = get_teacher_dao(teacher_id=teacher_id,teacher_name=teacher_name,db=db)
if not r:
raise HTTPException(status_code=404,detail='该老师不存在!')
return r
@TeacherAPI.delete('/teachers/{teacher_id}',summary='删除老师')
def delete_teachers(teacher_id:str|None = None,db=Depends(get_db)):
r = delete_teacher_dao(teacher_id,db)
if not r:
raise HTTPException(status_code=404,detail='老师不存在或删除失败!')
return {'code':200,'totals':r,'detail':'删除成功!'}
-25
View File
@@ -1,25 +0,0 @@
from fastapi import APIRouter,Depends,HTTPException
from schema.teacher_schema import TeacherCourseRequest,TeacherCourseResponse
from database import get_db
from dao.teacher_course_dao import add_teacher_course_dao,get_teacher_course_dao
TeacherCourseAPI = APIRouter()
@TeacherCourseAPI.post('/teacher_courses',response_model=TeacherCourseResponse,summary='新增老师课程')
def add_teacher_courses(teachercourse:TeacherCourseRequest,db=Depends(get_db)):
d = teachercourse.model_dump()
r = add_teacher_course_dao(t=d,db=db)
if not r:
raise HTTPException(status_code=400,detail='新增课程失败!')
return d
@TeacherCourseAPI.get('/teacher_courses',response_model=list[TeacherCourseResponse],summary='查询老师课程')
def get_teacher_courses(teacher_id:str,
db=Depends(get_db)):
r = get_teacher_course_dao(teacher_id=teacher_id,db=db)
if not r:
raise HTTPException(status_code=404,detail='该老师不存在!')
return r
-24
View File
@@ -1,24 +0,0 @@
from model.course_model import Course
def add_course_dao(o, db):
try:
c = Course(**o)
db.add(c)
except:
db.rollback()
return False
else:
db.commit()
return True
def get_all_courses_dao(db):
return db.query(Course).filter(Course.is_deleted == 0).all()
def get_course_by_id_dao(course_id, db):
return db.query(Course).filter(
Course.course_id == course_id,
Course.is_deleted == 0
).first()
-110
View File
@@ -1,110 +0,0 @@
# 导入 SQLAlchemy 的 Session 类,用于类型提示
from sqlalchemy.orm import Session
# 导入地区维度表模型
from model.student_info_region_model import Region_dimension
# ==================== 查询地区 ====================
def get_region_part(session: Session, province: str):
"""
根据省份查询地区信息。
:param session: 数据库会话
:param province: 省份名称,若为空则查询全部
:return: Region_dimension 对象列表
"""
try:
# 如果传入了省份(非空),则按省份过滤
if province:
data = session.query(Region_dimension).filter(
Region_dimension.province == province
).all()
else:
# 否则查询所有地区
data = session.query(Region_dimension).all()
return data
except Exception as e:
# 出现异常直接向上抛出,由 API 层统一处理
raise e
# ==================== 添加地区 ====================
def add_region(session: Session, d1: dict):
"""
新增一条地区记录。
:param session: 数据库会话
:param d1: 地区信息字典,需包含 code、province、city、district
:return: 成功时返回字符串 '添加成功'
"""
try:
# 先检查该地区编码是否已存在
data = session.query(Region_dimension).filter(
Region_dimension.code == d1["code"]
).first()
# 如果已存在,抛出 ValueError(注意:这里没有带错误信息,调用方需自行处理)
if data:
raise ValueError
# 根据字典创建 ORM 对象
obj = Region_dimension(**d1)
session.add(obj) # 加入会话
session.commit() # 提交事务,写入数据库
session.refresh(obj) # 刷新对象,获取数据库生成的默认值(如自增 id)
return '添加成功'
except Exception as e:
# 出现异常时回滚事务,避免脏数据
session.rollback()
raise e
# ==================== 修改地区 ====================
def update_region(session: Session, code, d1: dict):
"""
根据地区编码更新地区信息(部分字段更新)。
:param session: 数据库会话
:param code: 要修改的地区编码
:param d1: 待更新的字段字典
:return: 成功返回 '修改成功';未找到记录返回 None
"""
try:
# 使用 query.update() 批量更新,返回受影响行数
rows = session.query(Region_dimension).filter(
Region_dimension.code == code
).update(d1)
# 如果受影响行数为 0,说明编码不存在,无记录可更新
if rows == 0:
return None
session.commit()
return '修改成功'
except Exception as e:
# 异常时回滚
session.rollback()
raise e
# ==================== 删除地区 ====================
def delete_region(session: Session, code):
"""
根据地区编码删除地区记录(物理删除)。
:param session: 数据库会话
:param code: 要删除的地区编码
:return: 成功返回 '删除成功';未找到记录返回 None
"""
try:
# 使用 query.delete() 物理删除,返回受影响行数
rows = session.query(Region_dimension).filter(
Region_dimension.code == code
).delete()
# 如果受影响行数为 0,说明编码不存在
if rows == 0:
return None
session.commit()
return '删除成功'
except Exception as e:
# 异常时回滚
session.rollback()
raise e
-98
View File
@@ -1,98 +0,0 @@
from model.score_model import ScoreRecord
# ========== 工具函数:算等级 ==========
def calc_level(score):
if score is None: return None
if score >= 95: return 'A+'
if score >= 90: return 'A'
if score >= 80: return 'B'
if score >= 70: return 'C'
if score >= 60: return 'D'
return 'E'
# ========== 工具函数:算及格 ==========
def calc_pass(score):
if score is None: return None
return 1 if score >= 60 else 0
# ========== 1. 添加成绩 ==========
def add_score_dao(o, db):
try:
# 【业务校验】该学生该课程该序次是否已存在
existing = db.query(ScoreRecord).filter(
ScoreRecord.student_id == o['student_id'],
ScoreRecord.course_id == o['course_id'],
ScoreRecord.exam_order == o['exam_order'],
ScoreRecord.is_deleted == 0
).all()
if existing:
raise ValueError('该学生该课程该序次成绩已存在')
o1 = ScoreRecord(**o)
db.add(o1)
except:
db.rollback()
return False
else:
db.commit()
return True
# ========== 2. 修改成绩 ==========
def update_score_dao(score_id, update_data, db):
try:
rows = db.query(ScoreRecord).filter(
ScoreRecord.score_id == score_id,
ScoreRecord.is_deleted == 0
).update(update_data)
except:
db.rollback()
return False
else:
db.commit()
return rows
# ========== 3. 查询成绩(动态条件 + 分页) ==========
def get_scores_dao(score_id, student_id, course_id, teacher_id,
exam_order, page, page_size, db):
q = db.query(ScoreRecord).filter(ScoreRecord.is_deleted == 0)
if score_id:
q = q.filter(ScoreRecord.score_id == score_id)
if student_id:
q = q.filter(ScoreRecord.student_id == student_id)
if course_id:
q = q.filter(ScoreRecord.course_id == course_id)
if teacher_id:
q = q.filter(ScoreRecord.teacher_id == teacher_id)
if exam_order:
q = q.filter(ScoreRecord.exam_order == exam_order)
r = q.order_by(ScoreRecord.exam_order.asc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
if r:
return [item.to_dict() for item in r]
return []
# ========== 4. 删除成绩 ==========
def delete_score_dao(score_id, db):
try:
rows = db.query(ScoreRecord).filter(
ScoreRecord.score_id == score_id,
ScoreRecord.is_deleted == 0
).update({'is_deleted': 1})
except:
db.rollback()
rows = 0
else:
db.commit()
finally:
return rows
-222
View File
@@ -1,222 +0,0 @@
from model.student_info_region_model import Student_info
from model.score_model import ScoreRecord
from model.class_model import Class_info
from model.employment_model import Employment
from sqlalchemy import func, text
GENDER_MAP = {0: '女', 1: '男'}
STATUS_MAP = {0: '在读', 1: '休学', 2: '退学', 3: '毕业', 4: '结业'}
EDUCATION_MAP = {1: '大专', 2: '本科', 3: '硕士研究生', 4: '博士研究生'}
class StatisticsDao:
@staticmethod
def get_students_by_age_range_dao(n: int, m: int, min_age: int, max_age: int, db):
try:
age = func.timestampdiff(text('YEAR'), Student_info.birthday, func.curdate())
q = db.query(Student_info). \
filter(Student_info.is_deleted == '0'). \
filter(age >= min_age). \
filter(age <= max_age)
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
data = [
{
"student_id": s.student_id,
"student_name": s.student_name,
"gender": GENDER_MAP.get(s.gender, str(s.gender)),
"id_card": s.id_card,
"birthday": s.birthday,
"ethnicity": s.ethnicity,
"region_id": s.region_id,
"phone": s.phone,
"major": s.major,
"class_id": s.class_id,
"enrollment_date": s.enrollment_date,
"graduation_date": s.graduation_date,
"student_status": STATUS_MAP.get(s.student_status, str(s.student_status)),
"education_level": EDUCATION_MAP.get(s.education_level, str(s.education_level)),
}
for s in req
]
return data, total
@staticmethod
def get_students_by_class_id_dao(class_id: str, db):
try:
q = db.query(Student_info).filter(Student_info.class_id == class_id). \
filter(Student_info.is_deleted == '0')
total_count = q.count()
male_count = q.filter(Student_info.gender == 1).count()
female_count = q.filter(Student_info.gender == 0).count()
except Exception as e:
db.rollback()
raise e
else:
return total_count, male_count, female_count
@staticmethod
def get_students_by_score_dao(n: int, m: int, score: float, db):
try:
q = (db.query(
Student_info.student_name,
Student_info.student_id,
func.min(ScoreRecord.score).label('score')
).join(ScoreRecord, Student_info.student_id == ScoreRecord.student_id)
.filter(Student_info.is_deleted == '0')
.filter(ScoreRecord.is_deleted == 0)
.group_by(Student_info.student_id, Student_info.student_name)
.having(func.min(ScoreRecord.score) > score))
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
return [{"student_name": i.student_name,
'student_id': i.student_id,
"score": float(i.score) if i.score is not None else None} for i in req], total
@staticmethod
def get_student_by_no_pass_dao(n: int, m: int, fail_count: int, db):
try:
q = (db.query(
Student_info.student_id,
Student_info.student_name,
func.count(ScoreRecord.score_id).label('fail_count')
).join(ScoreRecord, Student_info.student_id == ScoreRecord.student_id)
.filter(Student_info.is_deleted == '0')
.filter(ScoreRecord.is_deleted == 0)
.filter(ScoreRecord.is_pass == 0)
.group_by(Student_info.student_id, Student_info.student_name)
.having(func.count(ScoreRecord.score_id) >= fail_count))
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
return [{"student_id": i.student_id,
"student_name": i.student_name,
"fail_count": i.fail_count} for i in req], total
@staticmethod
def get_class_exam_avg_score_dao(n: int, m: int, db):
try:
q = (db.query(
ScoreRecord.course_id,
Student_info.class_id,
Class_info.class_name,
func.avg(func.ifnull(ScoreRecord.score, 0)).label('avg_score')
).join(Student_info, ScoreRecord.student_id == Student_info.student_id)
.join(Class_info, Student_info.class_id == Class_info.class_id)
.filter(Student_info.is_deleted == '0')
.filter(ScoreRecord.is_deleted == 0)
.filter(Class_info.is_deleted == False)
.group_by(ScoreRecord.course_id,
Student_info.class_id,
Class_info.class_name)
.order_by(func.avg(func.ifnull(ScoreRecord.score, 0)).desc()))
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
return [{"course_id": i.course_id,
"class_id": i.class_id,
"class_name": i.class_name,
"avg_score": round(float(i.avg_score), 2)} for i in req], total
@staticmethod
def get_salary_top_dao(m: int, db):
try:
sq = (db.query(
Employment.student_id,
func.max(Employment.salary).label('max_salary')
).filter(Employment.is_deleted == 0)
.group_by(Employment.student_id)
.order_by(func.max(Employment.salary).desc())
.limit(m)
.subquery())
q = (db.query(
Student_info.student_name,
Class_info.class_name,
Employment.offer_date,
Employment.company_name,
Employment.salary
).join(Employment, Student_info.student_id == Employment.student_id)
.join(Class_info, Student_info.class_id == Class_info.class_id)
.join(sq,
(Employment.student_id == sq.c.student_id) &
(Employment.salary == sq.c.max_salary))
.filter(Student_info.is_deleted == '0')
.filter(Class_info.is_deleted == False)
.filter(Employment.is_deleted == 0))
req = q.all()
except Exception as e:
db.rollback()
raise e
else:
return [{"student_name": i.student_name,
"class_name": i.class_name,
"offer_date": i.offer_date,
"company_name": i.company_name,
"salary": float(i.salary) if i.salary is not None else None} for i in req]
@staticmethod
def get_time_size_dao(n: int, m: int, db):
try:
q = (db.query(
Student_info.student_id,
Student_info.student_name,
Employment.offer_date,
Employment.resume_open_date,
func.datediff(Employment.offer_date, Employment.resume_open_date).label('time_size')
).join(Employment, Student_info.student_id == Employment.student_id)
.filter(Student_info.is_deleted == '0')
.filter(Employment.is_deleted == 0)
.filter(Employment.resume_open_date.isnot(None)))
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
return [{"student_id": i.student_id,
"student_name": i.student_name,
"offer_date": i.offer_date,
"resume_open_date": i.resume_open_date,
"time_size": i.time_size} for i in req], total
@staticmethod
def get_class_avg_time_size_dao(n: int, m: int, db):
try:
q = (db.query(
Class_info.class_id,
Class_info.class_name,
func.avg(func.datediff(Employment.offer_date, Employment.resume_open_date)).label('avg_time_size')
).join(Student_info, Class_info.class_id == Student_info.class_id)
.join(Employment, Student_info.student_id == Employment.student_id)
.filter(Class_info.is_deleted == False)
.filter(Student_info.is_deleted == '0')
.filter(Employment.is_deleted == 0)
.filter(Employment.resume_open_date.isnot(None))
.group_by(Class_info.class_id, Class_info.class_name))
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
return [{"class_id": i.class_id,
"class_name": i.class_name,
"avg_time_size": round(float(i.avg_time_size), 2)} for i in req], total
-153
View File
@@ -1,153 +0,0 @@
# 导入 SQLAlchemy 的 Session 类,用于类型提示
from sqlalchemy.orm import Session
# 导入学生信息模型和性别枚举
from model.student_info_region_model import Student_info, GenderEnum
# 导入完整性错误异常,用于捕获外键/唯一约束等数据库错误
from sqlalchemy.exc import IntegrityError
# ==================== 条件查询学生信息 ====================
def get_part_student_info(session: Session, student_id: str, student_name: str, gender: str):
"""
根据学号、姓名、性别查询学生信息(均为可选条件)。
:param session: 数据库会话
:param student_id: 学号,精确匹配
:param student_name: 姓名,精确匹配
:param gender: 性别,前端传“男/女”,内部转为“1/0”
:return: 学生对象列表
"""
# 前端传来的性别为 男/女,与数据库的枚举值('0'/'1')不符,需要转换
# 注意:若 gender 为 None(不筛选),此处会被错误地转为 '0',导致只查女生。
# 建议调用方在不需要筛选性别时不要传该参数,或修改此段逻辑。
if gender == '男':
gender = '1'
else:
gender = '0'
try:
# 创建基础查询
query = session.query(Student_info)
# 过滤逻辑删除:只查询未删除的记录(is_deleted = '0')
query = query.filter(Student_info.is_deleted == '0')
# 如果传入学号,则按学号精确过滤
if student_id is not None:
query = query.filter(Student_info.student_id == student_id)
# 如果传入姓名,则按姓名精确过滤
# 若需模糊查询,可将 == 改为 like(f"%{student_name}%")
if student_name is not None:
query = query.filter(Student_info.student_name == student_name)
# 如果传入性别,则按性别过滤(已转为 '0'/'1',再转为枚举)
if gender is not None:
# 把字符串转成枚举,SQLAlchemy 才能正确比较
query = query.filter(Student_info.gender == GenderEnum(gender))
# 返回所有匹配记录
return query.all()
except Exception as e:
# 出现异常直接向上抛出,由 API 层统一处理
raise e
# ==================== 添加学生信息 ====================
def add_student_info(session: Session, data: dict):
"""
新增一条学生记录。
:param session: 数据库会话
:param data: 学生信息字典,键需与模型字段对应
:return: 成功时返回字符串 '添加成功'
"""
try:
# 先检查学号是否已存在
rows = session.query(Student_info).filter(
Student_info.student_id == data['student_id']
).first()
# 如果已存在,返回 ValueError 类(注意:这里返回的是类,不是实例,也不是抛出异常)
# 调用方如果做 `if not res` 判断,ValueError 类是真值,可能导致逻辑错误。
# 建议改为 `raise ValueError("学号已存在")`
if rows is not None:
return ValueError
# 根据字典创建 ORM 对象
obj = Student_info(**data)
session.add(obj) # 加入会话
session.commit() # 提交事务,写入数据库
return '添加成功'
except Exception as e:
# 出现异常时回滚事务,避免脏数据
session.rollback()
raise e
# ==================== 修改学生信息 ====================
def update_student_info(session: Session, data: dict):
"""
根据学号更新学生信息(全量更新)。
:param session: 数据库会话
:param data: 包含 student_id 及待更新字段的字典
:return: 成功返回 '修改成功';未找到记录返回 None
"""
try:
# 使用 query.update() 批量更新,返回受影响行数
rows = session.query(Student_info).filter(
Student_info.student_id == data['student_id']
).update(data)
# 如果受影响行数为 0,说明学号不存在,无记录可更新
if rows == 0:
return None
session.commit()
return '修改成功'
except IntegrityError as e:
# 捕获数据库完整性错误(外键、唯一约束等),回滚并翻译为友好提示
session.rollback()
err_msg = str(e.orig) # 获取底层 MySQL 错误信息
if 'fk_student_region' in err_msg:
# 外键 fk_student_region 失败:region_id 在地区表中不存在
raise ValueError(
f"region_id '{data.get('region_id')}' 在地区表中不存在,请先维护 region_dimension"
)
elif 'id_card' in err_msg:
# 身份证号唯一约束冲突
raise ValueError(f"身份证号 '{data.get('id_card')}' 已被其他学生占用")
else:
# 其他完整性错误
raise ValueError(f"数据完整性错误: {err_msg}")
except Exception as e:
# 其他异常同样回滚并抛出
session.rollback()
raise e
# ==================== 删除学生信息(逻辑删除) ====================
def delete_student_info(session: Session, student_id: str):
"""
根据学号进行逻辑删除(将 is_deleted 置为 '1')。
:param session: 数据库会话
:param student_id: 学号
:return: 成功返回 '修改成功';未找到记录返回 None
"""
try:
# 使用 update 将 is_deleted 改为 '1',实现逻辑删除
rows = session.query(Student_info).filter(
Student_info.student_id == student_id
).update({'is_deleted': '1'})
# 没有匹配记录,返回 None
if rows == 0:
return None
session.commit()
return '修改成功'
except Exception as e:
# 异常时回滚
session.rollback()
raise e
-24
View File
@@ -1,24 +0,0 @@
from model.teacher_model import TeacherCourse
def add_teacher_course_dao(t,db):
try: #判断是都已存在
r = db.query(TeacherCourse).filter(TeacherCourse.teacher_id==t['teacher_id']).first()
if r:
return None #已存在,交给上层判断
obj = TeacherCourse(**t)
db.add(obj)
db.commit()
db.refresh(obj) #拿回自增id,create_time等
return obj
except Exception:
db.rollback()
return False
def get_teacher_course_dao(teacher_id=None,db=None):
q = db.query(TeacherCourse)
if teacher_id:
q = q.filter(TeacherCourse.teacher_id==teacher_id)
return q.all() #返回ORM对象列表
-53
View File
@@ -1,53 +0,0 @@
from model.teacher_model import Teacher
def add_teacher_dao(t,db):
try: #判断是都已存在
r = db.query(Teacher).filter(Teacher.teacher_id==t['teacher_id']).first()
if r:
return None #已存在,交给上层判断
obj = Teacher(**t)
db.add(obj)
db.commit()
db.refresh(obj) #拿回自增id,create_time等
return obj
except Exception:
db.rollback()
return False
def update_teacher_dao(teacher_id,data,db): #data:新增的内容
try: #数据更新有返回值,返回的是影响的行数
rows = db.query(Teacher).filter(Teacher.teacher_id == teacher_id).update(data)
db.commit()
return rows
except Exception:
db.rollback()
return False
def get_teacher_dao(teacher_id=None,teacher_name=None,db=None):
q = db.query(Teacher)
if teacher_id:
q = q.filter(Teacher.teacher_id==teacher_id)
if teacher_name:
q = q.filter(Teacher.teacher_name.like (f'%{teacher_name}%'))
return q.all() #返回ORM对象列表
def delete_teacher_dao(teacher_id,db):
try: #删除有返回值,删除的行数
rows = (db.query(Teacher)
.filter(Teacher.teacher_id == teacher_id)
.delete())
db.commit()
return rows
except Exception:
db.rollback()
return 0
-48
View File
@@ -1,48 +0,0 @@
# 导入 Python 标准库 enum,用于定义枚举类型
import enum
# ==================== 性别枚举 ====================
class GenderEnum(str, enum.Enum):
"""
性别枚举:继承 str 和 enum.Enum,使成员值可直接作为字符串使用。
与数据库 enum('0','1') 对应。
"""
female = "0" # 女
male = "1" # 男
# ==================== 学籍状态枚举 ====================
class StudentStatusEnum(str, enum.Enum):
"""
学籍状态枚举,与数据库 enum('0','1','2','3','4') 对应。
"""
studying = "0" # 在读
suspended = "1" # 休学
quit = "2" # 退学
graduated = "3" # 毕业
completed = "4" # 结业
# ==================== 学历层次枚举 ====================
class EducationLevelEnum(str, enum.Enum):
"""
学历层次枚举,与数据库 enum('1','2','3','4') 对应。
"""
college = "1" # 大专
bachelor = "2" # 本科
master = "3" # 硕士研究生
doctor = "4" # 博士研究生
# ==================== 逻辑删除枚举 ====================
class IsDeletedEnum(str, enum.Enum):
"""
逻辑删除标志枚举。
注意:此处成员值为整数 0 和 1,而其他枚举均为字符串。
数据库列定义为 enum('0','1')(字符串),因此建议将此处改为:
no = "0"
yes = "1"
以保持类型一致,避免 SQLAlchemy 绑定参数时出现类型不匹配问题。
"""
no = "0" # 未删除
yes = "1" # 已删除
-21
View File
@@ -3,13 +3,6 @@ from database import engine, Base
Base.metadata.create_all(engine)
from api.calss import class_api
from api.employment_api import app as employment_router
from api.student_info_api import student_info_api
from api.region_api import region_api
from api.teacher_api import TeacherAPI
from api.teacher_course_api import TeacherCourseAPI
from api.course_api import course_api
from api.score_api import score_api
from api.statistics_api import BasicInformationAPI
from starlette.middleware.cors import CORSMiddleware
@@ -24,20 +17,6 @@ app.add_middleware(
)
app.include_router(class_api,tags=['班级接口'],prefix='/stutent')
app.include_router(student_info_api,tags=['学生信息'])
app.include_router(region_api,tags=['地区信息'])
app.include_router(TeacherAPI,tags=['老师信息'])
app.include_router(TeacherCourseAPI,tags=['老师课程信息'])
app.include_router(score_api,tags=['成绩信息'],prefix='/score')
app.include_router(course_api,tags=['课程信息'],prefix='/course')
app.include_router(BasicInformationAPI)
app.include_router(employment_router)
+50 -1
View File
@@ -70,4 +70,53 @@ class StudentClassIntermediate(Base):
student_id =Column(String(20))
is_deleted= Column(Integer)
create_time= Column(DateTime)
update_time= Column(DateTime)
update_time= Column(DateTime)
class GenderEnum(str, enum.Enum):
FEMALE = '0'
MALE = '1'
class StudentStatusEnum(str, enum.Enum):
NORMAL = '0' # 正常
SUSPENDED = '1' # 休学
DROPPED = '2' # 退学
GRADUATED = '3' # 毕业
OTHER = '4' # 其他
class EducationLevelEnum(str, enum.Enum):
COLLEGE = '1' # 大专
UNDERGRADUATE = '2' # 本科
MASTER = '3' # 硕士
DOCTOR = '4' # 博士
class IsDeletedEnum(str, enum.Enum):
ACTIVE = '0' # 未删除
DELETED = '1' # 已删除
# 3. 定义学生模型类
class Student_info(Base):
__tablename__ = "student_info"
student_id = Column(String(20),primary_key=True, comment='学号')
student_name = Column(String(50), nullable=False, comment='学生姓名')
gender = Column(Enum(GenderEnum), nullable=False, comment='性别: 0=女, 1=男')
id_card = Column(String(18), nullable=False, comment='身份证号码,收紧至18位')
birthday = Column(Date, nullable=True, comment='出生日期')
ethnicity = Column(String(20), nullable=True, comment='民族')
region_id = Column(String(12), nullable=True, comment='籍贯地区编码,外键关联地区维度表(region_dimension)')
phone = Column(String(11), nullable=True, comment='手机号码,收紧至11位')
graduation_school = Column(String(20), nullable=True, comment='毕业学校')
major = Column(String(20), nullable=False, comment='就读专业')
class_id = Column(String(20), nullable=False, index=True, comment='所在班级,外键关联班级表(class_info)')
enrollment_date = Column(Date, nullable=True, comment='入学日期')
graduation_date = Column(Date, nullable=True, comment='毕业日期')
student_status = Column(Enum(StudentStatusEnum), nullable=False, default=StudentStatusEnum.NORMAL,
comment='学籍状态')
education_level = Column(Enum(EducationLevelEnum), nullable=False,
comment='学历层次:1=大专, 2=本科, 3=硕士研究生, 4=博士研究生')
is_deleted = Column(Enum(IsDeletedEnum), nullable=False, default=IsDeletedEnum.ACTIVE,
comment='逻辑删除:0=未删除, 1=已删除')
create_time = Column(DateTime, nullable=False, server_default=func.now(), comment='创建时间')
update_time = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now(), comment='更新时间')
-13
View File
@@ -1,13 +0,0 @@
from database import DATETIME, Base, Column, Integer, String
from datetime import datetime
class Course(Base):
__tablename__ = 'student_course'
course_id = Column(String(20), primary_key=True, comment='课程编号')
course_name = Column(String(50), nullable=False, comment='课程名称')
teacher_id = Column(String(20), nullable=True, comment='授课教师编号')
is_deleted = Column(Integer, nullable=False, default=0, comment='0未删1已删')
create_time = Column(DATETIME, default=datetime.now, nullable=False)
update_time = Column(DATETIME, default=datetime.now, onupdate=datetime.now, nullable=False)
-79
View File
@@ -1,79 +0,0 @@
from database import DATETIME, Base, Column, Integer, String, Numeric, Date
from datetime import datetime
from sqlalchemy import ForeignKey
# 考试类型映射
EXAM_TYPE_MAP = {
1: '期中',
2: '期末',
3: '月考',
4: '模拟',
}
class ScoreRecord(Base):
__tablename__ = 'student_score'
score_id = Column(Integer, primary_key=True, autoincrement=True,
comment='成绩记录自增主键')
student_id = Column(
String(20),
# ForeignKey("student_info.student_id", ondelete="RESTRICT"), # 等组员表定稿再加
nullable=False,
comment='学号'
)
exam_order = Column(
Integer,
nullable=False,
default=1,
comment='考核序次(第几次)'
)
course_id = Column(
String(20),
# ForeignKey("student_course.course_id", ondelete="RESTRICT"),
nullable=False,
comment='课程编号'
)
teacher_id = Column(
String(20),
# ForeignKey("teacher_info.teacher_id", ondelete="RESTRICT"), # 等组员表定稿再加
nullable=False,comment='教师编号'
)
exam_type = Column(Integer, nullable=False, comment='1期中 2期末 3月考 4模拟')
score = Column(Numeric(5, 2), nullable=True, comment='分数0~100,缺考NULL')
score_level = Column(String(2), nullable=True, comment='等级A+~E')
is_pass = Column(Integer, nullable=True, default=0, comment='1及格 0不及格')
exam_date = Column(Date, nullable=False, comment='考试日期')
remark = Column(String(200), nullable=True, comment='缺考,缓考')
is_deleted = Column(Integer, nullable=False, default=0, comment='0未删 1已删')
create_time = Column(DATETIME, default=datetime.now, nullable=False)
update_time = Column(DATETIME, default=datetime.now,
onupdate=datetime.now, nullable=False)
# ========== 实例方法:转字典 ==========
def to_dict(self):
return {
'score_id': self.score_id,
'student_id': self.student_id,
'course_id': self.course_id,
'teacher_id': self.teacher_id,
'exam_order': self.exam_order,
'exam_type': self.exam_type,
'exam_type_name': EXAM_TYPE_MAP.get(self.exam_type, '未知'),
'score': float(self.score) if self.score is not None else None,
'score_level': self.score_level,
'is_pass': self.is_pass,
'exam_date': str(self.exam_date),
'remark': self.remark,
}
-118
View File
@@ -1,118 +0,0 @@
from sqlalchemy import *
from database import DATETIME,Base,Column,Integer,String
class StudentInfo(Base):
"""学生基本信息表"""
__tablename__ = 'student_info'
student_id = Column(String(20), primary_key=True, comment='学生id')
student_name = Column(String(50), nullable=False, comment='学生姓名')
gender = Column(Enum('男', '女'), nullable=False, comment='性别')
id_card = Column(String(18), nullable=False, comment='身份证号')
birthday = Column(Date, nullable=False, comment='出生日期')
ethnicity = Column(String(20), nullable=False, comment='民族')
region_id = Column(String(20), ForeignKey('region.regio_id'),nullable=False, comment='籍贯编码,连接地区表')
phone = Column(String(11), nullable=False, comment='手机号')
major = Column(String(20), nullable=False, comment='就读专业')
class_id = Column(String(20), nullable=False, comment='班级,连接班级表')
enrollment_date = Column(Date, nullable=False, comment='入学日期')
graduation_date = Column(Date, nullable=False, comment='毕业日期')
student_status = Column(Enum('在读', '休学', '退学', '毕业'), nullable=False, comment='学籍状态')
education_level = Column(Enum('大专', '本科', '硕士研究生', '博士研究生'), nullable=False, comment='学历')
is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除,0=未删除, 1=已删除')
create_time = Column(Date, nullable=False, comment='创建时间')
update_time = Column(Date, nullable=False, comment='更新时间')
class Region(Base):
"""地区维度表"""
__tablename__ = 'region'
regio_id = Column(String(50), primary_key=True, comment='地区编码')
province = Column(String(50), nullable=False, comment='省/直辖市')
city = Column(String(50), nullable=False, comment='市')
district = Column(String(50), nullable=False, comment='区,县')
is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除')
create_time = Column(DATETIME, nullable=False, comment='创建时间')
update_time = Column(DATETIME, nullable=False, comment='更新时间')
class EmploymentInfo(Base):
"""学生就业信息表"""
__tablename__ = 'employment_info'
emp_id = Column(String(50), primary_key=True, comment='就业记录ID')
student_id = Column(String(20), ForeignKey('student_info.student_id'), nullable=False,
comment='学号,连接学生表')
regio_id = Column(String(50), ForeignKey('region.regio_id'), nullable=False, comment='就业地区编码,连接地区表')
job_name = Column(String(50), nullable=False, comment='就业岗位名称')
company_name = Column(String(50), nullable=False, comment='就业公司名称')
salary = Column(Float, nullable=False, comment='就业月薪(元)')
offer_date = Column(Date, nullable=False, comment='offer下发/签约时间')
part_time = Column(Date, nullable=False, comment='就业时间')
resume_open_date = Column(Date, nullable=True, comment='开放简历时间')
employment_status = Column(Enum('1', '2', '3', '4'), nullable=False,
comment='就业状态:1=未就业, 2,=已就业,3=升学, 4=灵活就业')
is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除')
create_time = Column(DATETIME, nullable=False, comment='创建时间')
update_time = Column(DATETIME, nullable=False, comment='更新时间')
class StudentScore(Base):
"""学生考核成绩表"""
__tablename__ = 'student_score'
score_id = Column(Integer, primary_key=True, autoincrement=True, comment='成绩记录自增主键')
student_id = Column(String(20), ForeignKey('student_info.student_id'),nullable=False,
comment='学号,连接学生表')
course_id = Column(String(20), ForeignKey('course_info.course_id'),nullable=False, comment='课程编号,连接课程表')
exam_type = Column(Enum('期中', '期末', '月考', '周考', '模拟考'), nullable=False,
comment='考试类型:1=期中, 2=期末, 3=月考, 4=周考,5=模拟考')
score = Column(Float, nullable=True, comment='考试分数0~100,缺考为NULL')
score_level = Column(String(10), nullable=False, comment='成绩等级')
is_pass = Column(Enum('0', '1'), nullable=False, comment='是否及格:1=及格, 0=不及格(可由score>=60判断)')
exam_date = Column(Date, nullable=False, comment='考试日期')
exam_category = Column(Enum('首考', '重考', '补考'), nullable=False, comment='考试分类:1=首考, 2=补考, 3=重考')
is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除')
create_time = Column(DATETIME, nullable=False, comment='创建时间')
update_time = Column(DATETIME, nullable=False, comment='更新时间')
class ClassInfo(Base):
"""班级信息表"""
__tablename__ = 'class_info'
class_id = Column(String(50), primary_key=True, comment='班级业务编码')
class_name = Column(String(50), nullable=False, comment='班级名称')
grade_year = Column(Date, nullable=False, comment='入学年级')
status = Column(Enum('1', '2', '3'), nullable=False, comment='班级状态:1=在读 2=已毕业 3=已停用')
tags = Column(String(100), nullable=False, comment='存储多维标签,例如:["重点班", "科技特长", "2026届"]')
is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除')
create_time = Column(DATETIME, nullable=False, comment='创建时间')
update_time = Column(DATETIME, nullable=False, comment='更新时间')
class TeacherInfo(Base):
"""教师基本信息表"""
__tablename__ = 'teacher_info'
teacher_id = Column(String(20), primary_key=True, comment='教师编号')
teacher_name = Column(String(20), nullable=False, comment='教师姓名')
gender = Column(Enum('男', '女'), nullable=False, comment='性别')
birthday = Column(Date, nullable=False, comment='出生日期')
region_id = Column(String(50), ForeignKey('region.regio_id'), nullable=False, comment='籍贯地区编码')
email = Column(String(50), nullable=False, comment='邮箱')
phone = Column(String(11), nullable=False, comment='手机号码')
graduation_school = Column(String(50), nullable=False, comment='毕业学校')
teacher_rank = Column(Enum('1', '2', '3', '4'), nullable=False, comment='职称:1=初级, 2=中级, 3=高级, 4=特级')
is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除')
create_time = Column(DATETIME, nullable=False, comment='创建时间')
update_time = Column(DATETIME, nullable=False, comment='更新时间')
class CourseInfo(Base):
"""课程基础信息"""
__tablename__ = 'course_info'
course_id = Column(String(30), primary_key=True, comment='课程编号')
course_name = Column(String(30), nullable=False, comment='课程名称')
course_type = Column(Enum('1', '2', '3'), nullable=False, comment='课程类型:1 必修课 2 选修课 3 实训课')
is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除')
create_time = Column(DATETIME, nullable=False, comment='创建时间')
update_time = Column(DATETIME, nullable=False, comment='更新时间')
-95
View File
@@ -1,95 +0,0 @@
# 从 database 模块导入 Base(ORM 基类)和 engine(数据库引擎,别名为 db_engine)
from database import Base, engine as db_engine
# 从 sqlalchemy 导入 Enum,并别名为 SAEnum,用于定义数据库枚举列
from sqlalchemy import Enum as SAEnum, Integer, String
# 从 sqlalchemy 导入常用组件:inspect(检查表是否存在)、Column(列)、
# VARCHAR/BIGINT/Date/DateTime(字段类型)、func(SQL 函数,如 now())
from sqlalchemy import inspect, Column, VARCHAR, BIGINT, Date, DateTime, func
# 从自定义枚举模块导入学生状态、性别、学历层次、逻辑删除枚举
from enums import StudentStatusEnum, GenderEnum, EducationLevelEnum, IsDeletedEnum
def _sa_enum(e):
"""
统一让 SQLAlchemy 存枚举的 value 而不是 name。
默认 SQLAlchemy 的 Enum 存的是枚举成员名(如 'male'),
这里通过 values_callable 指定存枚举的值(如 '1'、'0')。
"""
return SAEnum(e, values_callable=lambda x: [m.value for m in x])
# ======================== 学生 ==========================
# 学生信息表模型,继承 Base(declarative_base 生成的基类)
class Student_info(Base):
__tablename__ = 'student_info' # 数据库表名
# 表参数:指定字符集和排序规则为 utf8mb4
__table_args__ = {'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_general_ci'}
# 学号,主键,非空,长度 20
student_id = Column(VARCHAR(20), primary_key=True, nullable=False, comment='学号,收紧长度')
# 学生姓名,非空,长度 50
student_name = Column(VARCHAR(50), nullable=False, comment='学生姓名')
# 性别,枚举类型(存 value:'0'=女,'1'=男),非空
gender = Column(Integer, nullable=False, comment='性别:0=女,1=男')
# 身份证号码,唯一,非空,长度 18
id_card = Column(VARCHAR(18), unique=True, nullable=False, comment='身份证号码,收紧至18位')
# 出生日期,可空
birthday = Column(Date, nullable=True, comment='出生日期')
# 民族,可空,长度 20
ethnicity = Column(VARCHAR(20), nullable=True, comment='民族')
# 籍贯地区编码,可空,带索引(注释说外键关联 region_dimension,但模型里未定义 ForeignKey)
region_id = Column(VARCHAR(12), nullable=True, index=True,
comment='籍贯地区编码,外键关联地区维度表(region_dimension)')
# 手机号码,可空,长度 11
phone = Column(VARCHAR(11), nullable=True, comment='手机号码,收紧至11位')
# 毕业学校,可空,长度 20
graduation_school = Column(VARCHAR(20), nullable=True, comment='毕业学校')
# 就读专业,非空,长度 20
major = Column(VARCHAR(20), nullable=False, comment='就读专业')
# 所在班级,非空,长度 20,带索引(注释说外键关联 class_info,但模型里未定义 ForeignKey)
class_id = Column(VARCHAR(20), nullable=False, index=True, comment='所在班级,外键关联班级表(class_info)')
# 入学日期,可空
enrollment_date = Column(Date, nullable=True, comment='入学日期')
# 毕业日期,可空
graduation_date = Column(Date, nullable=True, comment='毕业日期')
# 学籍状态,枚举,非空:0=在读,1=休学,2=退学,3=毕业,4=结业
student_status = Column(Integer, nullable=False,comment='学籍状态:0=在读,1=休学,2=退学,3=毕业,4=结业')
# 学历层次,枚举,非空:1=大专,2=本科,3=硕士研究生,4=博士研究生
education_level = Column(Integer, nullable=False,comment='学历层次:1=大专,2=本科,3=硕士研究生,4=博士研究生')
# 逻辑删除标志,枚举,非空,默认值为 IsDeletedEnum.no('0'=未删除)
is_deleted = Column(Integer, default=IsDeletedEnum.no, nullable=False,comment='逻辑删除:0=未删除,1=已删除')
# 创建时间,非空,服务器默认值为当前时间(数据库端生成)
create_time = Column(DateTime, nullable=False, server_default=func.now(), comment='创建时间')
# 更新时间,非空,服务器默认值为当前时间,更新时自动刷新为当前时间
update_time = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now(),
comment='更新时间')
# ======================== 地区 ==========================
# 地区维度表模型
class Region_dimension(Base):
__tablename__ = 'region_dimension' # 表名
# 表参数:utf8mb4 字符集
__table_args__ = {'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_general_ci'}
# 自增主键,BIGINT 类型
id = Column(BIGINT, primary_key=True, autoincrement=True, comment='自增主键')
# 地区编码,唯一,非空,长度 12
code = Column(VARCHAR(12), unique=True, nullable=False, comment='地区编码')
# 省 / 直辖市,非空
province = Column(VARCHAR, nullable=False, comment='省 / 直辖市')
# 市,非空
city = Column(VARCHAR, nullable=False, comment='市')
# 区 / 县,非空
district = Column(VARCHAR, nullable=False, comment='区 / 县')
# 判断表是否存在,不存在则创建表
# 检查 student_info 表是否存在
if not inspect(db_engine).has_table('student_info'):
# 创建所有继承 Base 的表(此处会创建 student_info 和 region_dimension)
Base.metadata.create_all(db_engine)
# 检查 region_dimension 表是否存在
if not inspect(db_engine).has_table('region_dimension'):
Base.metadata.create_all(db_engine)
-40
View File
@@ -1,40 +0,0 @@
from database import DATETIME,Base,engine
from datetime import datetime
from sqlalchemy import Column,String,Date,Integer
class Teacher(Base):
__tablename__ = 'teacher_info'
id = Column(Integer,primary_key=True,autoincrement=True,comment='自增主键id')
teacher_id = Column(String(20),nullable=False,unique=True,comment='教师编号')
teacher_name = Column(String(50),nullable=False,comment='老师姓名')
gender = Column(Integer,nullable=False,comment='性别:0=女,1=男')
birthday = Column(Date,nullable=True,comment='出生年月')
native_region_code = Column(String(12),nullable=True,comment='籍贯地区编码')
email = Column(String(100),nullable=True,comment='邮箱')
phone = Column(String(11),nullable=False,comment='手机号')
graduation_school = Column(String(20),nullable=True,comment='毕业院校')
is_deleted = Column(Integer,nullable=False,default=0,comment='逻辑删除:0=未删除,1=已删除')
create_time = Column(DATETIME,nullable=False,default=datetime.now,comment='创建时间')
update_time = Column(DATETIME,default=datetime.now,onupdate=datetime.now,comment='更新时间')
class TeacherCourse(Base):
__tablename__ = 'teacher_course'
id = Column(Integer,primary_key=True,autoincrement=True,comment='记录ID')
teacher_id = Column(String(20),nullable=False,comment='教师编号')
course_id = Column(String(20),nullable=False,comment='任课科目编号')
is_deleted = Column(Integer,nullable=False,default=0,comment='逻辑删除:0=未删除,1=已删除')
create_time = Column(DATETIME,nullable=False,default=datetime.now,comment='创建时间')
update_time = Column(DATETIME,nullable=False,default=datetime.now,onupdate=datetime.now,comment='更新时间')
if __name__ == "__main__":
Base.metadata.create_all(engine)
print("数据表创建完成!")
-15
View File
@@ -1,15 +0,0 @@
from pydantic import BaseModel, Field
from typing import Optional
class CourseRequest(BaseModel):
course_id: str = Field(..., description='课程编号')
course_name: str = Field(..., description='课程名称')
teacher_id: Optional[str] = Field(None, description='授课教师编号')
class CourseResponse(BaseModel):
code: int = 200
detail: str = 'OK'
totals: int = 0
data: str | dict | tuple | list
-29
View File
@@ -1,29 +0,0 @@
from pydantic import BaseModel, Field
from datetime import date
from typing import Optional
class ScoreRequest(BaseModel):
student_id: str = Field(...,min_length=1, max_length=20,description="学号")
course_id: str = Field(..., min_length=1, max_length=20,description="课程编号")
teacher_id: str = Field(..., min_length=1, max_length=20,description="教师编号")
exam_order: int = Field(1,ge=1, description="考核序次(第几次考核)")
exam_type: int = Field(...,ge=1, le=4,description="考试类型:1=期中,2=期末,3=月考,4=模拟")
score: Optional[float] = Field(None,ge=0, le=100,description="分数 0~100,缺考填 null")
exam_date: date = Field(..., description="考试日期(YYYY-MM-DD)")
remark: Optional[str] = Field(None, description="备注,例如:缺考、缓考")
class ScoreUpdateRequest(BaseModel):
score: Optional[float] = Field(
None,
ge=0, le=100,
description="新分数 0~100"
)
remark: Optional[str] = Field(None, description="新备注")
class ScoreResponse(BaseModel):
code: int = 200
detail: str = 'OK'
totals: int = 0
data: str | dict | tuple | list
-125
View File
@@ -1,125 +0,0 @@
from pydantic import BaseModel, ConfigDict
from datetime import date
from typing import List, Optional
class StudentInfo1(BaseModel):
model_config = ConfigDict(from_attributes=True)
student_id: str
student_name: str
gender: str
id_card: str
birthday: Optional[date] = None
ethnicity: Optional[str] = None
region_id: Optional[str] = None
phone: Optional[str] = None
major: str
class_id: str
enrollment_date: Optional[date] = None
graduation_date: Optional[date] = None
student_status: str
education_level: str
class ResponseModel(BaseModel):
code: int
message: str = 'ok'
total: int
total_pages: int
data: List[StudentInfo1]
class ClassStatResponse(BaseModel):
code: int = 200
message: str = 'ok'
class_id: str
total_count: int
male_count: int
female_count: int
class StudentInfo2(BaseModel):
student_id: str
student_name: str
score: Optional[float] = None
class ResponseModel1(BaseModel):
code: int
message: str = 'ok'
total: int
total_pages: int
data: List[StudentInfo2]
class StudentInfo3(BaseModel):
student_id: str
student_name: str
fail_count: int
class ResponseModel2(BaseModel):
code: int
message: str = 'ok'
total: int
total_pages: int
data: List[StudentInfo3]
class ClassExamAvg(BaseModel):
course_id: str
class_id: str
class_name: str
avg_score: float
class ResponseModel3(BaseModel):
code: int
message: str = 'ok'
total: int
total_pages: int
data: List[ClassExamAvg]
class SalaryTop(BaseModel):
student_name: str
class_name: str
company_name: str
offer_date: Optional[date] = None
salary: Optional[float] = None
class ResponseModel4(BaseModel):
code: int
message: str = 'ok'
data: List[SalaryTop]
class TimeSize(BaseModel):
student_id: str
student_name: str
offer_date: Optional[date] = None
resume_open_date: Optional[date] = None
time_size: Optional[int] = None
class ResponseModel5(BaseModel):
code: int
message: str = 'ok'
total: int
total_pages: int
data: List[TimeSize]
class ClassAvgTimeSize(BaseModel):
class_id: str
class_name: str
avg_time_size: float
class ResponseModel6(BaseModel):
code: int
message: str = 'ok'
total: int
total_pages: int
data: List[ClassAvgTimeSize]
-84
View File
@@ -1,84 +0,0 @@
# 从 pydantic 导入 BaseModel(模型基类)和 ConfigDict(模型配置)
from pydantic import BaseModel, ConfigDict
# 导入日期类型,用于 birthday、enrollment_date 等字段
from datetime import date
# 导入 Optional,用于声明可选字段(可为 None)
from typing import Optional
# 从自定义枚举模块导入学生状态、性别、学历层次枚举
from enums import StudentStatusEnum, GenderEnum, EducationLevelEnum
# Optional 类型层面的声明,告诉 Pydantic:这个字段可以是 None
# ======================== 学生 ==========================
# 学生信息请求体:用于新增/修改学生时的入参校验
class stu_info_req(BaseModel):
student_id: str # 学号
student_name: str # 学生姓名
gender: GenderEnum # 性别(枚举:0=女,1=男)
id_card: str # 身份证号码
birthday: date # 出生日期
ethnicity: str # 民族
region_id: str # 籍贯地区编码
phone: str # 手机号码
graduation_school: str # 毕业学校
major: str # 就读专业
class_id: str # 所在班级
enrollment_date: date # 入学日期
graduation_date: date # 毕业日期
student_status: StudentStatusEnum # 学籍状态(枚举:0=在读,1=休学,2=退学,3=毕业,4=结业)
education_level: EducationLevelEnum # 学历层次(枚举:1=大专,2=本科,3=硕士,4=博士)
# 学生信息响应体:用于查询学生时的出参序列化
class stu_info_resp(BaseModel):
student_id: str # 学号
student_name: str # 学生姓名
gender: GenderEnum # 性别
id_card: str # 身份证号码
birthday: Optional[date] = None # 出生日期(可空)
ethnicity: Optional[str] = None # 民族(可空)
region_id: Optional[str] = None # 籍贯地区编码(可空)
phone: Optional[str] = None # 手机号码(可空)
graduation_school: Optional[str] = None # 毕业学校(可空)
major: str # 就读专业
class_id: str # 所在班级
enrollment_date: Optional[date] = None # 入学日期(可空)
graduation_date: Optional[date] = None # 毕业日期(可空)
student_status: StudentStatusEnum # 学籍状态
education_level: EducationLevelEnum # 学历层次
# 配置:允许从 ORM 对象属性中读取数据(配合 from_attributes=True)
model_config = ConfigDict(from_attributes=True)
# 学生信息增删改操作的统一响应体
class stu_info_add_resp(BaseModel):
code: int # 状态码,200 表示成功
message: Optional[str] = None # 提示信息(可空)
# ======================== 地区 ==========================
# 地区维度表 添加请求体:用于新增地区时的入参校验
class region_req(BaseModel):
code: str # 地区编码
province: str # 省 / 直辖市
city: str # 市
district: str # 区 / 县
# 地区维度表 修改请求体:所有字段均可选,code 不在修改范围内
class region_update_req(BaseModel):
province: Optional[str] = None # 省 / 直辖市(可选)
city: Optional[str] = None # 市(可选)
district: Optional[str] = None # 区 / 县(可选)
# 地区维度 响应体:用于查询地区时的出参序列化
class region_resp(BaseModel):
code: str # 地区编码
province: str # 省 / 直辖市
city: str # 市
district: str # 区 / 县
# 配置:允许从 ORM 对象属性中读取数据
model_config = ConfigDict(from_attributes=True)
# 地区增删改操作的统一响应体
class region_add_resp(BaseModel):
code: int # 状态码,200 表示成功
message: Optional[str] = None # 提示信息(可空)
-45
View File
@@ -1,45 +0,0 @@
from pydantic import BaseModel
from datetime import date
from typing import Literal
class TeacherRequest(BaseModel): #Teacher请求体模型
teacher_id: str
teacher_name: str
gender: Literal[1,0]
birthday: date|None = None
native_region_code: str|None = None
email: str|None = None
phone: str
graduation_school: str|None = None
class TeacherUpdateRequest(BaseModel):
teacher_name: str|None = None
gender: Literal[1,0]|None = None
birthday: date | None = None
native_region_code: str | None = None
email: str | None = None
phone: str
graduation_school: str | None = None
class TeacherResponse(BaseModel): #Teacher响应体模型
teacher_id: str
teacher_name: str
gender: Literal[1,0]
birthday: date|None = None
native_region_code: str|None = None
email: str|None = None
phone: str
graduation_school: str|None = None
class TeacherCourseRequest(BaseModel): #TeacherCourse请求体模型
teacher_id: str|None = None
course_id: str|None = None
class TeacherCourseResponse(BaseModel): #老师带班信息及课程表响应体模型
teacher_id: str|None = None
course_id: str|None = None
model_config = {"from_attributes": True} #可以直接读取ORM模型对象,不用手动转字典