Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe44708fa6 |
@@ -1,39 +0,0 @@
|
||||
# wl_offer_wait_me
|
||||
|
||||
#### 介绍
|
||||
{**以下是 Gitee 平台说明,您可以替换此简介**
|
||||
Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台
|
||||
无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
|
||||
|
||||
#### 软件架构
|
||||
软件架构说明
|
||||
|
||||
|
||||
#### 安装教程
|
||||
|
||||
1. xxxx
|
||||
2. xxxx是第三方士大夫
|
||||
3. xxxx
|
||||
|
||||
#### 使用说明
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+33
-132
@@ -1,156 +1,57 @@
|
||||
# 顾问模块:独立接口,沿用原学生接口的依赖注入与返回方式。
|
||||
|
||||
# 顾问模块:独立接口,沿用原学生接口的依赖注入与返回方式。
|
||||
# FastAPI路由层,定义顾问管理相关接口,接收前端请求,参数校验,调用DAO层完成数据库操作
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
# 数据库会话依赖函数
|
||||
from database import get_db
|
||||
# 导入Pydantic模型:新增、更新、返回响应模型
|
||||
from scheme.wl_advisor_scheme import AdvisorCreate, AdvisorUpdate, AdvisorOut
|
||||
# 导入DAO层,封装数据库增删改查逻辑
|
||||
from dao import wl_advisor_dao
|
||||
|
||||
# 创建路由对象,统一前缀 /advisor,接口文档分组标签【顾问管理】
|
||||
from dao import wl_advisor_dao
|
||||
from database import get_db
|
||||
from scheme.wl_advisor_scheme import AdvisorCreate, AdvisorOut, AdvisorUpdate
|
||||
|
||||
router = APIRouter(prefix="/advisor", tags=["顾问管理"])
|
||||
|
||||
|
||||
def check_advisor_data(data):
|
||||
# 只取出前端实际传入的字段
|
||||
values = data.model_dump(exclude_unset=True)
|
||||
|
||||
# 如果传入了顾问姓名,则校验姓名
|
||||
if "adv_name" in values:
|
||||
# 姓名为空或者全空格,抛出异常
|
||||
if data.adv_name is None or not data.adv_name.strip():
|
||||
raise HTTPException(422, "顾问姓名不能为空")
|
||||
# 去除姓名前后空格
|
||||
data.adv_name = data.adv_name.strip()
|
||||
# 限制姓名最大长度50字符
|
||||
if len(data.adv_name) > 50:
|
||||
raise HTTPException(422, "顾问姓名不能超过50个字符")
|
||||
|
||||
# 性别不为空时,校验性别长度
|
||||
if data.adv_gender is not None and len(data.adv_gender) > 8:
|
||||
raise HTTPException(422, "性别不能超过8个字符")
|
||||
# 手机号不为空时,校验手机号长度
|
||||
if data.adv_phone is not None and len(data.adv_phone) > 20:
|
||||
raise HTTPException(422, "手机号不能超过20个字符")
|
||||
# 邮箱不为空时,校验邮箱长度
|
||||
if data.adv_email is not None and len(data.adv_email) > 64:
|
||||
raise HTTPException(422, "邮箱不能超过64个字符")
|
||||
# 地区不为空时,校验地区长度
|
||||
if data.adv_region is not None and len(data.adv_region) > 64:
|
||||
raise HTTPException(422, "区域不能超过64个字符")
|
||||
|
||||
|
||||
@router.get("/list", response_model=list[AdvisorOut])
|
||||
def list_advisors(
|
||||
adv_name: str = Query(None), # 查询参数:顾问姓名,模糊搜索,非必传
|
||||
adv_phone: str = Query(None), # 查询参数:顾问电话,模糊搜索,非必传
|
||||
adv_region: str = Query(None), # 查询参数:所属地区,模糊搜索,非必传
|
||||
adv_gender: str = Query(None), # 查询参数:性别,精确搜索,非必传
|
||||
page: int = Query(1), # 查询参数:当前页码,默认第1页
|
||||
size: int = Query(10), # 查询参数:每页条数,默认10条
|
||||
db: Session = Depends(get_db), # 依赖注入,自动获取数据库会话
|
||||
# 查询顾问列表(支持姓名/手机号/区域/性别模糊筛选 + 分页)
|
||||
# 注意:本路由必须写在 /{adv_id} 前面,否则 "/advisor/" 会被当成 adv_id 去解析
|
||||
@router.get("/", response_model=list[AdvisorOut])
|
||||
def query_list(
|
||||
adv_name: str = Query(None),
|
||||
adv_phone: str = Query(None),
|
||||
adv_region: str = Query(None),
|
||||
adv_gender: str = Query(None),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# 校验分页参数合法性
|
||||
if page < 1 or size < 1 or size > 100:
|
||||
raise HTTPException(422, "页码从1开始,每页条数为1到100")
|
||||
# 调用DAO层查询顾问列表,返回ORM对象列表
|
||||
return wl_advisor_dao.search_advisors(
|
||||
db, adv_name=adv_name, adv_phone=adv_phone,
|
||||
adv_region=adv_region, adv_gender=adv_gender, page=page, size=size,
|
||||
return wl_advisor_dao.list_advisors(
|
||||
db, adv_name, adv_phone, adv_region, adv_gender, skip, limit
|
||||
)
|
||||
|
||||
|
||||
@router.post("/add", response_model=AdvisorOut)
|
||||
def create_advisor(data: AdvisorCreate, db: Session = Depends(get_db)):
|
||||
|
||||
# 执行字段校验
|
||||
check_advisor_data(data)
|
||||
# 调用DAO层新增顾问
|
||||
return wl_advisor_dao.create_advisor(db, data)
|
||||
# 新增顾问
|
||||
@router.post("/", response_model=AdvisorOut, status_code=201)
|
||||
def add_advisor(body: AdvisorCreate, db: Session = Depends(get_db)):
|
||||
return wl_advisor_dao.create_advisor(db, body)
|
||||
|
||||
|
||||
# 根据编号查询单个顾问
|
||||
@router.get("/{adv_id}", response_model=AdvisorOut)
|
||||
def get_advisor(adv_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
# 校验ID必须大于0
|
||||
if adv_id <= 0:
|
||||
raise HTTPException(422, "顾问编号必须大于0")
|
||||
# DAO查询顾问
|
||||
def query_one(adv_id: int, db: Session = Depends(get_db)):
|
||||
advisor = wl_advisor_dao.get_advisor_by_id(db, adv_id)
|
||||
# 查不到抛出404
|
||||
if not advisor:
|
||||
raise HTTPException(404, "顾问不存在")
|
||||
raise HTTPException(status_code=404, detail="顾问不存在")
|
||||
return advisor
|
||||
|
||||
|
||||
# 查询顾问信息和该顾问名下的学生
|
||||
@router.get("/{adv_id}/students")
|
||||
def get_advisor_students(adv_id: int, db: Session = Depends(get_db)):
|
||||
if adv_id <= 0:
|
||||
raise HTTPException(422, "顾问编号必须大于0")
|
||||
advisor = wl_advisor_dao.get_advisor_with_students(db, adv_id)
|
||||
if not advisor:
|
||||
raise HTTPException(404, "顾问不存在")
|
||||
|
||||
student_list = []
|
||||
for student in advisor.student_list:
|
||||
student_list.append({
|
||||
"stu_no": student.stu_no,
|
||||
"stu_name": student.stu_name,
|
||||
"class_id": student.class_id,
|
||||
"advisor_id": student.advisor_id,
|
||||
"native_place": student.native_place,
|
||||
"graduate_school": student.graduate_school,
|
||||
"major": student.major,
|
||||
"in_time": student.in_time,
|
||||
"out_time": student.out_time,
|
||||
"edu": student.edu,
|
||||
"stu_age": student.stu_age,
|
||||
"stu_gender": student.stu_gender,
|
||||
})
|
||||
|
||||
return {
|
||||
"adv_id": advisor.adv_id,
|
||||
"adv_name": advisor.adv_name,
|
||||
"adv_gender": advisor.adv_gender,
|
||||
"adv_phone": advisor.adv_phone,
|
||||
"adv_email": advisor.adv_email,
|
||||
"adv_region": advisor.adv_region,
|
||||
"student_list": student_list,
|
||||
}
|
||||
|
||||
|
||||
# 修改顾问
|
||||
@router.put("/{adv_id}", response_model=AdvisorOut)
|
||||
def update_advisor(adv_id: int, data: AdvisorUpdate, db: Session = Depends(get_db)):
|
||||
|
||||
# 校验ID合法性
|
||||
if adv_id <= 0:
|
||||
raise HTTPException(422, "顾问编号必须大于0")
|
||||
# 校验提交的数据
|
||||
check_advisor_data(data)
|
||||
# 调用DAO层执行更新
|
||||
advisor = wl_advisor_dao.update_advisor(db, adv_id, data)
|
||||
# 找不到顾问返回404
|
||||
def edit_advisor(adv_id: int, body: AdvisorUpdate, db: Session = Depends(get_db)):
|
||||
advisor = wl_advisor_dao.update_advisor(db, adv_id, body)
|
||||
if not advisor:
|
||||
raise HTTPException(404, "顾问不存在")
|
||||
raise HTTPException(status_code=404, detail="顾问不存在")
|
||||
return advisor
|
||||
|
||||
|
||||
# 逻辑删除顾问
|
||||
@router.delete("/{adv_id}")
|
||||
def delete_advisor(adv_id: int, db: Session = Depends(get_db)):
|
||||
|
||||
# 校验ID合法性
|
||||
if adv_id <= 0:
|
||||
raise HTTPException(422, "顾问编号必须大于0")
|
||||
# 调用DAO逻辑删除,返回布尔值
|
||||
deleted = wl_advisor_dao.delete_advisor(db, adv_id)
|
||||
# 顾问不存在则抛出404
|
||||
if not deleted:
|
||||
raise HTTPException(404, "顾问不存在")
|
||||
# 返回成功信息
|
||||
return {"msg": "删除成功"}
|
||||
|
||||
def remove_advisor(adv_id: int, db: Session = Depends(get_db)):
|
||||
if not wl_advisor_dao.delete_advisor(db, adv_id):
|
||||
raise HTTPException(status_code=404, detail="顾问不存在")
|
||||
return {"code": 200, "msg": "删除成功"}
|
||||
|
||||
+1
-2
@@ -16,7 +16,7 @@ def query_list(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
company_name: Optional[str] = None,
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return EmpDAO.list_emps(db, skip, limit, company_name)
|
||||
|
||||
@@ -54,4 +54,3 @@ def remove_emp(stu_no: str, db: Session = Depends(get_db)):
|
||||
if not EmpDAO.delete_emp(db, stu_no):
|
||||
raise HTTPException(status_code=404, detail="就业信息不存在")
|
||||
return {"code": 200, "msg": "删除成功"}
|
||||
|
||||
|
||||
+2
-19
@@ -8,7 +8,7 @@ from dao.wl_teacher_dao import (
|
||||
list_teachers,
|
||||
update_teacher,
|
||||
delete_teacher_logic,
|
||||
generate_teacher_no, get_teacher_class_info,
|
||||
generate_teacher_no,
|
||||
)
|
||||
from database import get_db
|
||||
from scheme.wl_teacher_scheme import (
|
||||
@@ -75,21 +75,4 @@ def edit_teacher(
|
||||
# 逻辑删除教师
|
||||
@router.delete("/{t_id}")
|
||||
def remove_teacher(t_id: int, db: Session = Depends(get_db)):
|
||||
teacher = delete_teacher_logic(db, t_id)
|
||||
if not teacher:
|
||||
raise HTTPException(status_code=404, detail="老师不存在")
|
||||
return {"msg": "删除成功"}
|
||||
|
||||
|
||||
# 查询老师所带的班级ID
|
||||
@router.get("/{t_id}/class")
|
||||
def get_class_by_t_id(t_id: int, db: Session = Depends(get_db)):
|
||||
"""
|
||||
根据老师ID查询其负责的班级ID
|
||||
"""
|
||||
class_id = get_teacher_class_info(db, t_id)
|
||||
if class_id is None:
|
||||
# 如果返回 None,说明老师不存在或者没有分配班级
|
||||
raise HTTPException(status_code=404, detail="老师不存在或未分配班级")
|
||||
|
||||
return {"class_id": class_id}
|
||||
return delete_teacher_logic(db, t_id)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+37
-71
@@ -1,105 +1,71 @@
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
# 导入SQLAlchemy ORM数据库模型:顾问表实体
|
||||
|
||||
from model.wl_advisor_model import Advisor
|
||||
from model.wl_student_model import Student
|
||||
# 导入Pydantic请求模型:新增、更新顾问的入参校验模型
|
||||
from scheme.wl_advisor_scheme import AdvisorCreate, AdvisorUpdate
|
||||
|
||||
|
||||
# ---- 增 ----
|
||||
def create_advisor(db: Session, data: AdvisorCreate) -> Advisor:
|
||||
|
||||
# 将Pydantic入参转为字典,解包赋值给Advisor ORM模型,构建数据库实体对象
|
||||
advisor = Advisor(**data.model_dump())
|
||||
# 把对象加入数据库会话,此时还未写入数据库
|
||||
db.add(advisor)
|
||||
# 提交事务,真正执行INSERT语句,保存数据到数据库
|
||||
db_advisor = Advisor(**data.model_dump())
|
||||
db.add(db_advisor)
|
||||
db.commit()
|
||||
# 刷新对象,从数据库取回最新数据(如自动生成的主键adv_id)
|
||||
db.refresh(advisor)
|
||||
# 返回新增的顾问对象
|
||||
return advisor
|
||||
db.refresh(db_advisor)
|
||||
return db_advisor
|
||||
|
||||
|
||||
def search_advisors(db: Session, adv_name=None, adv_phone=None,
|
||||
adv_region=None, adv_gender=None, page=1, size=10):
|
||||
|
||||
# 基础查询:查询Advisor表,过滤掉已逻辑删除的数据(is_deleted=0代表未删除)
|
||||
# ---- 查(多条件 + 分页)----
|
||||
def list_advisors(db: Session, adv_name=None, adv_phone=None, adv_region=None,
|
||||
adv_gender=None, skip: int = 0, limit: int = 100):
|
||||
q = db.query(Advisor).filter(Advisor.is_deleted == 0)
|
||||
|
||||
# 如果传入姓名,添加模糊查询条件 %xxx% 包含匹配
|
||||
if adv_name:
|
||||
q = q.filter(Advisor.adv_name.like(f"%{adv_name}%"))
|
||||
# 如果传入电话,添加电话模糊查询
|
||||
if adv_phone:
|
||||
q = q.filter(Advisor.adv_phone.like(f"%{adv_phone}%"))
|
||||
# 如果传入地区,添加地区模糊查询
|
||||
if adv_region:
|
||||
q = q.filter(Advisor.adv_region.like(f"%{adv_region}%"))
|
||||
# 如果传入性别,性别使用精确匹配
|
||||
if adv_gender:
|
||||
q = q.filter(Advisor.adv_gender == adv_gender)
|
||||
# 后录进来的顾问排前面,方便前端看到最新数据
|
||||
return q.order_by(Advisor.adv_id.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
# 按顾问ID倒序排列,新数据排在前面
|
||||
q = q.order_by(Advisor.adv_id.desc())
|
||||
|
||||
# offset:跳过前面(page-1)*size条数据;limit:限制本次查询返回size条
|
||||
# 实现分页逻辑
|
||||
return q.offset((page - 1) * size).limit(size).all()
|
||||
|
||||
# 根据顾问ID查询单条顾问详情(过滤逻辑删除)
|
||||
# ---- 根据编号查单个顾问 ----
|
||||
def get_advisor_by_id(db: Session, adv_id: int) -> Advisor | None:
|
||||
|
||||
return db.query(Advisor).filter(
|
||||
Advisor.adv_id == adv_id, # ID匹配
|
||||
Advisor.is_deleted == 0, # 只查询未逻辑删除的数据
|
||||
).first() # first() 取第一条,无结果返回None
|
||||
|
||||
|
||||
# 查询顾问信息和该顾问名下的学生
|
||||
def get_advisor_with_students(db: Session, adv_id: int):
|
||||
advisor = get_advisor_by_id(db, adv_id)
|
||||
if not advisor:
|
||||
return None
|
||||
|
||||
# 学生表中的顾问外键字段名是 advisor_id
|
||||
student_list = db.query(Student).filter(
|
||||
Student.advisor_id == adv_id,
|
||||
Student.is_deleted == 0
|
||||
).all()
|
||||
advisor.student_list = student_list
|
||||
return advisor
|
||||
Advisor.adv_id == adv_id,
|
||||
Advisor.is_deleted == 0,
|
||||
).first()
|
||||
|
||||
|
||||
# ---- 改 ----
|
||||
def update_advisor(db: Session, adv_id: int, data: AdvisorUpdate) -> Advisor | None:
|
||||
# 根据ID查询顾问
|
||||
advisor = get_advisor_by_id(db, adv_id)
|
||||
# 判断顾问是否存在,不存在直接返回None
|
||||
if not advisor:
|
||||
db_advisor = get_advisor_by_id(db, adv_id)
|
||||
if not db_advisor:
|
||||
return None
|
||||
|
||||
# model_dump(exclude_unset=True) 只取前端实际传递过来的字段
|
||||
# 循环动态给ORM对象赋值,实现局部更新
|
||||
# exclude_unset:前端传了什么就改什么,没传的字段保持原值
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(advisor, k, v)
|
||||
|
||||
# 提交事务,保存修改
|
||||
setattr(db_advisor, k, v)
|
||||
db.commit()
|
||||
# 刷新对象,同步数据库最新数据
|
||||
db.refresh(advisor)
|
||||
# 返回更新后的顾问对象
|
||||
return advisor
|
||||
db.refresh(db_advisor)
|
||||
return db_advisor
|
||||
|
||||
# 逻辑删除顾问(不是DELETE语句,修改is_deleted标记)
|
||||
|
||||
# ---- 逻辑删除 ----
|
||||
def delete_advisor(db: Session, adv_id: int) -> bool:
|
||||
# 查询顾问
|
||||
advisor = get_advisor_by_id(db, adv_id)
|
||||
# 顾问不存在返回False
|
||||
if not advisor:
|
||||
db_advisor = get_advisor_by_id(db, adv_id)
|
||||
if not db_advisor:
|
||||
return False
|
||||
|
||||
# 逻辑删除:将删除标记置为1,表示已删除
|
||||
advisor.is_deleted = 1
|
||||
# 提交事务保存修改
|
||||
# 名下还挂着学生就不给删,处理方式和班级模块 delete_class 保持一致
|
||||
student = db.query(Student).filter(
|
||||
Student.advisor_id == adv_id,
|
||||
Student.is_deleted == 0,
|
||||
).first()
|
||||
if student:
|
||||
raise HTTPException(status_code=400, detail="该顾问名下还有学生,不能删除!")
|
||||
|
||||
db_advisor.is_deleted = 1
|
||||
db.commit()
|
||||
# 删除标记修改成功返回True
|
||||
return True
|
||||
return True
|
||||
|
||||
+9
-3
@@ -17,14 +17,18 @@ class EmpDAO:
|
||||
|
||||
@staticmethod
|
||||
def list_emps(db: Session, skip: int = 0, limit: int = 100, company_name: str = None):
|
||||
"""查询就业列表"""
|
||||
"""查询就业列表,顺带把学生姓名带上"""
|
||||
q = db.query(Emp).filter(Emp.is_deleted == 0)
|
||||
if company_name:
|
||||
q = q.filter(Emp.company_name.like(f"%{company_name}%"))
|
||||
|
||||
emp_list = q.offset(skip).limit(limit).all()
|
||||
for e in emp_list:
|
||||
e.stu_name = e.student.stu_name
|
||||
stu = db.query(Student).filter(
|
||||
Student.stu_no == e.stu_no,
|
||||
Student.is_deleted == 0,
|
||||
).first()
|
||||
e.stu_name = stu.stu_name if stu else None
|
||||
return emp_list
|
||||
|
||||
@staticmethod
|
||||
@@ -34,7 +38,9 @@ class EmpDAO:
|
||||
Emp.stu_no == stu_no,
|
||||
Emp.is_deleted == 0,
|
||||
).first()
|
||||
emp.stu_name =emp.student.stu_name
|
||||
if emp:
|
||||
stu = EmpDAO.get_student(db, stu_no)
|
||||
emp.stu_name = stu.stu_name if stu else None
|
||||
return emp
|
||||
|
||||
@staticmethod
|
||||
|
||||
+22
-41
@@ -1,4 +1,5 @@
|
||||
#统计分析 数据访问层(动态查询 + 聚合)
|
||||
from os.path import join
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import and_, case, func
|
||||
@@ -10,7 +11,7 @@ from model.wl_score_model import Score
|
||||
from model.wl_student_model import Student
|
||||
from scheme.wl_statistics_scheme import AgeCompareOp, SortOrder
|
||||
|
||||
# stu_gender 字段的实际取值
|
||||
# stu_gender 字段的实际取值,确认数据库里存的到底是什么后改这两行即可
|
||||
MALE_VALUE = "男"
|
||||
FEMALE_VALUE = "女"
|
||||
|
||||
@@ -33,14 +34,6 @@ class StatisticDao:
|
||||
@staticmethod
|
||||
def search_students_by_age(db: Session, op: AgeCompareOp,
|
||||
value: int, value2: Optional[int] = None):
|
||||
"""
|
||||
输入比较条件动态查询符合条件的学员信息
|
||||
:param db:数据库会话
|
||||
:param op:比较条件(如大于、小于、等于、区间等)
|
||||
:param value:对比值1
|
||||
:param value2:对比值2,选了between后的上界
|
||||
:return:
|
||||
"""
|
||||
if op == AgeCompareOp.between:
|
||||
if value2 is None:
|
||||
raise ValueError("区间查询(between)需要同时提供上界 value2")
|
||||
@@ -52,13 +45,12 @@ class StatisticDao:
|
||||
.filter(Student.is_deleted == 0, condition) \
|
||||
.order_by(Student.stu_age) \
|
||||
.all()
|
||||
|
||||
@staticmethod
|
||||
def statistic_classes_count(db: Session):
|
||||
"""每个班级的总人数 + 按性别细分的分布(含 0 人的空班级)"""
|
||||
result=[]
|
||||
for c in db.query(Class_).filter(Class_.is_deleted==0).all():
|
||||
students=[s for s in c.students if s.is_deleted == 0]# 班级里的学生,过滤逻辑删除后的
|
||||
students=[s for s in c.students if s.is_deleted == 0]
|
||||
result.append({
|
||||
"class_id":c.class_id,
|
||||
"total":len(students),
|
||||
@@ -88,12 +80,6 @@ class StatisticDao:
|
||||
|
||||
@staticmethod
|
||||
def query_score_by_line(db:Session,score_line:float):
|
||||
"""
|
||||
查询每次考试都在分数线以上学生信息
|
||||
:param db:
|
||||
:param score_line: 分数线
|
||||
:return:
|
||||
"""
|
||||
result=[]
|
||||
for s in db.query(Student).filter(Student.is_deleted==0).all():
|
||||
if all(sc.score>score_line for sc in s.scores):
|
||||
@@ -106,12 +92,6 @@ class StatisticDao:
|
||||
|
||||
@staticmethod
|
||||
def query_by_fail_count(db:Session,fail_count:int):
|
||||
"""
|
||||
查询有指定次数次以上不及格学生的信息
|
||||
:param db:
|
||||
:param fail_count: 指定次数
|
||||
:return:
|
||||
"""
|
||||
result=[]
|
||||
students=db.query(Student).filter(Student.is_deleted==0).all()
|
||||
for stu in students:
|
||||
@@ -149,12 +129,6 @@ class StatisticDao:
|
||||
|
||||
@staticmethod
|
||||
def get_info_by_rank(db:Session,rank:int):
|
||||
"""
|
||||
统计就业薪资排名TopN的学生信息
|
||||
:param db:
|
||||
:param rank: Top N 中的N
|
||||
:return:
|
||||
"""
|
||||
q=(db.query(
|
||||
Student.stu_name,
|
||||
Student.class_id,
|
||||
@@ -170,15 +144,15 @@ class StatisticDao:
|
||||
return q.all()
|
||||
@staticmethod
|
||||
def get_emp_time(db:Session):
|
||||
"""统计每个学生的就业时长(天)= offer下发时间 - 就业开放时间
|
||||
未就业(offer_time,和emp_open_time至少一个为null)
|
||||
"""每个学生的就业时长(天)= offer下发时间 - 就业开放时间
|
||||
"""
|
||||
emp_total_time = func.coalesce(
|
||||
# 未就业学生左外连接后 Emp 列为 NULL,DATEDIFF 也返回 NULL,统一兜成 0
|
||||
func.datediff(Emp.offer_time, Emp.emp_open_time),
|
||||
'', # 未进入就业阶段或者未拿到offer
|
||||
0,
|
||||
).label('emp_total_time')
|
||||
|
||||
# 每个学生都要统计,所以用外连接
|
||||
# 逻辑删除条件写在 ON 里,写进 where 会变成 INNER JOIN,未就业学生会被滤掉
|
||||
q=db.query(
|
||||
Student.stu_name,
|
||||
emp_total_time,
|
||||
@@ -190,15 +164,18 @@ class StatisticDao:
|
||||
|
||||
@staticmethod
|
||||
def get_class_avg_emp_time(db:Session, sort_order: SortOrder | None = None):
|
||||
"""
|
||||
统计每个班级平均就业时长
|
||||
:param db:
|
||||
:param sort_order:排序(可选)
|
||||
:return:
|
||||
"""每个班级的平均就业时长(天)= offer下发时间 - 就业开放时间
|
||||
|
||||
口径:
|
||||
- 以班级表为主表左外连接,保证没有任何就业学生的班级也出现在结果里
|
||||
- 只统计已进入就业阶段(emp_open_time 非空)且已拿到 offer(offer_time 非空)的学生
|
||||
- 该班可统计的人数为 0 时 AVG 返回 NULL,出参里 avg_emp_time 给 None,即"无就业学生"
|
||||
"""
|
||||
emp_time = func.datediff(Emp.offer_time, Emp.emp_open_time)
|
||||
# AVG 和 COUNT(表达式) 都会自动忽略 NULL,所以没 offer 时间的学生不参与分子也不占分母
|
||||
avg_emp_time = func.avg(emp_time).label("avg_emp_time")
|
||||
emp_stu_count = func.count(emp_time).label("emp_stu_count")# 统计有就业时长学生数量
|
||||
# 用 COUNT(表达式) 而不是 COUNT(*),它正好是平均值的分母
|
||||
emp_stu_count = func.count(emp_time).label("emp_stu_count")
|
||||
|
||||
q = db.query(
|
||||
Class_.class_id.label("class_id"),
|
||||
@@ -207,6 +184,7 @@ class StatisticDao:
|
||||
).select_from(Class_) \
|
||||
.outerjoin(
|
||||
Student,
|
||||
# 逻辑删除的过滤条件必须写在 ON 里,写进 where 会把空班级整行滤掉
|
||||
and_(Student.class_id == Class_.class_id, Student.is_deleted == 0),
|
||||
) \
|
||||
.outerjoin(
|
||||
@@ -220,8 +198,9 @@ class StatisticDao:
|
||||
) \
|
||||
.group_by(Class_.class_id)
|
||||
|
||||
|
||||
# 动态排序逻辑与 get_class_avg_score 保持一致
|
||||
if sort_order == SortOrder.desc:
|
||||
# MySQL 里 DESC 时 NULL 排最后,"无就业学生"的班级自然落到末尾
|
||||
q = q.order_by(avg_emp_time.desc())
|
||||
elif sort_order == SortOrder.asc:
|
||||
q = q.order_by(avg_emp_time.asc())
|
||||
@@ -231,7 +210,9 @@ class StatisticDao:
|
||||
return [
|
||||
{
|
||||
"class_id": r.class_id,
|
||||
"avg_emp_time": round(float(r.avg_emp_time), 1) if r.avg_emp_time is not None else None,
|
||||
# AVG 出来是 Decimal,转 float 并按天保留 1 位小数;None 表示该班无就业学生
|
||||
"avg_emp_time": round(float(r.avg_emp_time), 1)
|
||||
if r.avg_emp_time is not None else None,
|
||||
"emp_stu_count": r.emp_stu_count,
|
||||
}
|
||||
for r in q.all()
|
||||
|
||||
@@ -15,6 +15,7 @@ def generate_stu_no(db: Session, class_id: int, seq: int = None) -> str:
|
||||
if not cls or not cls.start_time:
|
||||
raise ValueError("班级不存在或未设置开班时间")
|
||||
date_str = cls.start_time.strftime("%Y%m%d")
|
||||
|
||||
# 直接写死一个日期字符串用于测试
|
||||
# date_str = "20260912"
|
||||
|
||||
|
||||
+6
-19
@@ -11,23 +11,16 @@ def generate_teacher_no():
|
||||
seq = random.randint(1, 999)
|
||||
return f"T{date_str}{seq:03d}"
|
||||
|
||||
def list_teachers(db: Session, page: int = 1, page_size: int = 10):
|
||||
skip = (page - 1) * page_size
|
||||
return (db.query(Teacher)
|
||||
.filter(Teacher.is_deleted == 0)
|
||||
.order_by(Teacher.t_id.desc())
|
||||
.offset(skip)
|
||||
.limit(page_size)
|
||||
.all())
|
||||
def list_teachers(db: Session):
|
||||
"""查询全部未删除教师列表"""
|
||||
return db.query(Teacher).filter(Teacher.is_deleted == 0).order_by(Teacher.t_id.desc()).all()
|
||||
|
||||
def get_teacher_id(db: Session, t_id: int):
|
||||
return (db.query(Teacher)
|
||||
.filter(Teacher.t_id == t_id, Teacher.is_deleted == 0)
|
||||
return (db.query(Teacher).filter(Teacher.t_id == t_id, Teacher.is_deleted == 0)
|
||||
.first() )
|
||||
|
||||
def get_teacher_no(db:Session, t_no: str):
|
||||
return(db.query(Teacher).
|
||||
filter(Teacher.t_no == t_no,Teacher.is_deleted == 0)
|
||||
return(db.query(Teacher).filter(Teacher.t_no == t_no,Teacher.is_deleted == 0)
|
||||
.first())
|
||||
|
||||
|
||||
@@ -69,10 +62,4 @@ def delete_teacher_logic(db: Session, t_id: int):
|
||||
return None
|
||||
db_obj.is_deleted = 1
|
||||
db.commit()
|
||||
return db_obj
|
||||
|
||||
def get_teacher_class_info(db: Session, t_id: int):
|
||||
teacher = get_teacher_id(db, t_id)
|
||||
if teacher:
|
||||
return teacher.class_id
|
||||
return None
|
||||
return db_obj
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -22,7 +22,8 @@ class Advisor(Base):
|
||||
adv_region = Column(String(64), comment="负责区域/招生渠道")
|
||||
is_deleted = Column(SmallInteger,nullable=False, default=0,server_default="0",
|
||||
index=True,comment="1=已删除",)
|
||||
|
||||
|
||||
#下面的内容临时注释,在main导入时,SQLAlchemy 在配置映射时就会报错,错误大概是:ArgumentError: Could not locate any relevant foreign key columns for primary join condition ...
|
||||
# students = relationship("Student",primaryjoin="and_(Advisor.id == stu_id, Student.is_deleted == 0)",
|
||||
# foreign_keys="stu_id",viewonly=True,lazy="selectin",)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from database import Base
|
||||
@@ -9,8 +9,8 @@ class Emp(Base):
|
||||
|
||||
# 学号做主键,同时做外键指向学生表,长度跟 Student.stu_no 保持一致
|
||||
stu_no = Column(String(20), ForeignKey('wl_student.stu_no'), primary_key=True)
|
||||
emp_open_time = Column(DateTime)
|
||||
offer_time = Column(DateTime)
|
||||
emp_open_time = Column(DateTime, default="2026-11-30 18:18:18")
|
||||
offer_time = Column(DateTime, default=func.now())
|
||||
company_name = Column(String(100), nullable=False)
|
||||
salary = Column(Integer, nullable=False, default=15000)
|
||||
|
||||
@@ -21,4 +21,3 @@ class Emp(Base):
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Emp(stu_no={self.stu_no}, company='{self.company_name}')>"
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class Teacher(Base):
|
||||
t_phone = Column(String(20), unique=True, nullable=True)
|
||||
t_email = Column(String(20), unique=False)
|
||||
#创建老师时必须指定一个存在的班级
|
||||
class_id = Column(Integer, ForeignKey("class_info.class_id"), nullable=True)
|
||||
class_id = Column(Integer, ForeignKey("class_info.class_id"), nullable=False)
|
||||
role = Column(String(50), nullable=False)
|
||||
is_deleted = Column(Integer, default=0)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16
-40
@@ -1,54 +1,30 @@
|
||||
# 顾问模块:请求和响应格式,写法与原学生模块一致。
|
||||
# 使用Pydantic定义接口入参、出参模型,用于FastAPI接口的数据校验与序列化
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# 创建顾问 请求模型:新增顾问时前端提交的数据结构
|
||||
# ---------- 请求模型 ----------
|
||||
# 顾问模块:入参格式,字段长度跟 model/wl_advisor_model.py 里的列宽保持一致
|
||||
class AdvisorCreate(BaseModel):
|
||||
# 顾问姓名(必填字段,创建时必须传入)
|
||||
adv_name: str
|
||||
# 顾问性别(选填,不传则为None)
|
||||
adv_gender: Optional[str] = None
|
||||
# 顾问联系电话(选填)
|
||||
adv_phone: Optional[str] = None
|
||||
# 顾问邮箱(选填)
|
||||
adv_email: Optional[str] = None
|
||||
# 顾问所属地区(选填)
|
||||
adv_region: Optional[str] = None
|
||||
adv_name: str = Field(..., max_length=50, description="顾问姓名")
|
||||
adv_gender: Optional[str] = Field(None, max_length=8, description="性别")
|
||||
adv_phone: Optional[str] = Field(None, max_length=20, description="手机号")
|
||||
adv_email: Optional[str] = Field(None, max_length=64, description="邮箱")
|
||||
adv_region: Optional[str] = Field(None, max_length=64, description="负责区域/招生渠道")
|
||||
|
||||
|
||||
# 更新顾问 请求模型:修改顾问信息时前端提交的数据结构
|
||||
# 所有字段全部可选,支持局部更新(只传需要修改的字段)
|
||||
class AdvisorUpdate(BaseModel):
|
||||
# 顾问姓名,可选更新
|
||||
adv_name: Optional[str] = None
|
||||
# 顾问性别,可选更新
|
||||
adv_gender: Optional[str] = None
|
||||
# 顾问电话,可选更新
|
||||
adv_phone: Optional[str] = None
|
||||
# 顾问邮箱,可选更新
|
||||
adv_email: Optional[str] = None
|
||||
# 顾问地区,可选更新
|
||||
adv_region: Optional[str] = None
|
||||
adv_name: Optional[str] = Field(None, max_length=50)
|
||||
adv_gender: Optional[str] = Field(None, max_length=8)
|
||||
adv_phone: Optional[str] = Field(None, max_length=20)
|
||||
adv_email: Optional[str] = Field(None, max_length=64)
|
||||
adv_region: Optional[str] = Field(None, max_length=64)
|
||||
|
||||
|
||||
# 顾问信息 响应模型:查询顾问信息,返回给前端的数据结构
|
||||
# ---------- 响应模型 ----------
|
||||
# is_deleted 是逻辑删除标记,不对外暴露
|
||||
class AdvisorOut(BaseModel):
|
||||
# 顾问主键ID,数据库自增主键
|
||||
adv_id: int
|
||||
# 顾问姓名
|
||||
adv_name: str
|
||||
# 顾问性别
|
||||
adv_gender: Optional[str] = None
|
||||
# 顾问联系电话
|
||||
adv_phone: Optional[str] = None
|
||||
# 顾问邮箱
|
||||
adv_email: Optional[str] = None
|
||||
# 顾问所属地区
|
||||
adv_region: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
# from_attributes = True:支持直接把SQLAlchemy ORM对象转为Pydantic模型
|
||||
# 作用:ORM查询出来的数据库实体,可以直接赋值给AdvisorOut,自动读取属性
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -8,7 +8,7 @@ class EmpCreate(BaseModel):
|
||||
emp_open_time: Optional[datetime] = Field(None, description="不传用表默认值")
|
||||
offer_time: Optional[datetime] = Field(None, description="不传用数据库当前时间")
|
||||
company_name: str = Field(..., max_length=100, description="公司名称")
|
||||
salary: Optional[int] = Field(15000, description="不传用默认值 15000")
|
||||
salary: Optional[int] = Field(None, description="不传用默认值 15000")
|
||||
|
||||
class EmpUpdate(BaseModel):
|
||||
emp_open_time: Optional[datetime] = None
|
||||
@@ -26,4 +26,3 @@ class EmpOut(BaseModel):
|
||||
salary: Optional[int] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict
|
||||
from model.wl_score_model import Score
|
||||
|
||||
|
||||
|
||||
# ---- 入参:动态查询的比较条件 ----
|
||||
class AgeCompareOp(str, Enum):
|
||||
"""年龄比较条件:大于/大于等于/小于/小于等于/等于/区间"""
|
||||
gt = "gt"
|
||||
@@ -25,7 +25,7 @@ class SortOrder(str, Enum):
|
||||
desc = "desc"
|
||||
|
||||
|
||||
|
||||
# ---- 出参:后续统计接口的聚合结果 DTO 放这里 ----
|
||||
class ClassGenderStatOut(BaseModel):
|
||||
"""班级多维度统计:总人数 + 男女分布"""
|
||||
class_id: Optional[int] = None
|
||||
@@ -49,7 +49,7 @@ class FailCountModel(BaseModel):
|
||||
|
||||
class ClassAvgScoreOut(BaseModel):
|
||||
"""每次考试每个班级的平均分"""
|
||||
# DAO 返回的是 SQLAlchemy Row 对象,开 from_attributes 属性名读取
|
||||
# DAO 返回的是 SQLAlchemy Row 对象,必须开 from_attributes 才能按属性名读取
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class_id:Optional[int]=None
|
||||
@@ -58,24 +58,26 @@ class ClassAvgScoreOut(BaseModel):
|
||||
|
||||
class EmpTopOut(BaseModel):
|
||||
"""就业薪资排名 Top N"""
|
||||
|
||||
# 同样是 Row 对象,必须开 from_attributes
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
stu_name:Optional[str]=None
|
||||
class_id:Optional[int]=None
|
||||
offer_time:Optional[datetime]=None
|
||||
company_name:Optional[str]=None
|
||||
# 接口是按薪资排序取前 N 名的,薪资也得一起给出来,
|
||||
# 否则前端只能看到"谁排前面"却看不出差了多少钱
|
||||
salary:Optional[int]=None
|
||||
|
||||
class ClassAvgEmpTimeOut(BaseModel):
|
||||
"""每个班级的平均就业时长"""
|
||||
class_id: Optional[int] = None
|
||||
# 单位:天,保留 1 位小数;为 None 表示该班无就业学生
|
||||
# 单位:天,保留 1 位小数;为 None 表示该班无就业学生,前端展示"无就业学生"
|
||||
avg_emp_time: Optional[float] = None
|
||||
# 参与统计的人数,为 0 表示该班无就业学生
|
||||
# 参与统计的人数(平均值的分母),为 0 同样表示该班无就业学生
|
||||
emp_stu_count: int = 0
|
||||
|
||||
class EmpTimeOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
stu_name:str=''
|
||||
emp_total_time:int|str=""# 单位为天,""表示没有开放简历或者没有拿到offer
|
||||
emp_total_time:int=0 # 单位为天
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
血灵很帅
|
||||
Reference in New Issue
Block a user