Merge remote-tracking branch 'origin/master'

This commit is contained in:
aiapple
2026-09-14 17:23:49 +08:00
4 changed files with 250 additions and 94 deletions
+131 -32
View File
@@ -1,57 +1,156 @@
# 顾问模块:独立接口,沿用原学生接口的依赖注入与返回方式。
# 顾问模块:独立接口,沿用原学生接口的依赖注入与返回方式。
# FastAPI路由层,定义顾问管理相关接口,接收前端请求,参数校验,调用DAO层完成数据库操作
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from dao import wl_advisor_dao
# 数据库会话依赖函数
from database import get_db
from scheme.wl_advisor_scheme import AdvisorCreate, AdvisorOut, AdvisorUpdate
# 导入Pydantic模型:新增、更新、返回响应模型
from scheme.wl_advisor_scheme import AdvisorCreate, AdvisorUpdate, AdvisorOut
# 导入DAO层,封装数据库增删改查逻辑
from dao import wl_advisor_dao
# 创建路由对象,统一前缀 /advisor,接口文档分组标签【顾问管理】
router = APIRouter(prefix="/advisor", tags=["顾问管理"])
# 查询顾问列表(支持姓名/手机号/区域/性别模糊筛选 + 分页)
# 注意:本路由必须写在 /{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),
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), # 依赖注入,自动获取数据库会话
):
return wl_advisor_dao.list_advisors(
db, adv_name, adv_phone, adv_region, adv_gender, skip, limit
# 校验分页参数合法性
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,
)
# 新增顾问
@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.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.get("/{adv_id}", response_model=AdvisorOut)
def query_one(adv_id: int, db: Session = Depends(get_db)):
def get_advisor(adv_id: int, db: Session = Depends(get_db)):
# 校验ID必须大于0
if adv_id <= 0:
raise HTTPException(422, "顾问编号必须大于0")
# DAO查询顾问
advisor = wl_advisor_dao.get_advisor_by_id(db, adv_id)
# 查不到抛出404
if not advisor:
raise HTTPException(status_code=404, detail="顾问不存在")
raise HTTPException(404, "顾问不存在")
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 edit_advisor(adv_id: int, body: AdvisorUpdate, db: Session = Depends(get_db)):
advisor = wl_advisor_dao.update_advisor(db, adv_id, body)
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
if not advisor:
raise HTTPException(status_code=404, detail="顾问不存在")
raise HTTPException(404, "顾问不存在")
return advisor
# 逻辑删除顾问
@router.delete("/{adv_id}")
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": "删除成功"}
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": "删除成功"}
+72 -38
View File
@@ -1,71 +1,105 @@
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:
db_advisor = Advisor(**data.model_dump())
db.add(db_advisor)
# 将Pydantic入参转为字典,解包赋值给Advisor ORM模型,构建数据库实体对象
advisor = Advisor(**data.model_dump())
# 把对象加入数据库会话,此时还未写入数据库
db.add(advisor)
# 提交事务,真正执行INSERT语句,保存数据到数据库
db.commit()
db.refresh(db_advisor)
return db_advisor
# 刷新对象,从数据库取回最新数据(如自动生成的主键adv_id)
db.refresh(advisor)
# 返回新增的顾问对象
return advisor
# ---- 查(多条件 + 分页)----
def list_advisors(db: Session, adv_name=None, adv_phone=None, adv_region=None,
adv_gender=None, skip: int = 0, limit: int = 100):
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代表未删除)
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,
Advisor.is_deleted == 0,
).first()
Advisor.adv_id == adv_id, # ID匹配
Advisor.is_deleted == 0, # 只查询未逻辑删除的数据
).first() # first() 取第一条,无结果返回None
# ---- 改 ----
def update_advisor(db: Session, adv_id: int, data: AdvisorUpdate) -> Advisor | None:
db_advisor = get_advisor_by_id(db, adv_id)
if not db_advisor:
# 查询顾问信息和该顾问名下的学生
def get_advisor_with_students(db: Session, adv_id: int):
advisor = get_advisor_by_id(db, adv_id)
if not advisor:
return None
# exclude_unset:前端传了什么就改什么,没传的字段保持原值
# 学生表中的顾问外键字段名是 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
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:
return None
# model_dump(exclude_unset=True) 只取前端实际传递过来的字段
# 循环动态给ORM对象赋值,实现局部更新
for k, v in data.model_dump(exclude_unset=True).items():
setattr(db_advisor, k, v)
setattr(advisor, k, v)
# 提交事务,保存修改
db.commit()
db.refresh(db_advisor)
return db_advisor
# 刷新对象,同步数据库最新数据
db.refresh(advisor)
# 返回更新后的顾问对象
return advisor
# ---- 逻辑删除 ----
# 逻辑删除顾问(不是DELETE语句,修改is_deleted标记)
def delete_advisor(db: Session, adv_id: int) -> bool:
db_advisor = get_advisor_by_id(db, adv_id)
if not db_advisor:
# 查询顾问
advisor = get_advisor_by_id(db, adv_id)
# 顾问不存在返回False
if not advisor:
return False
# 名下还挂着学生就不给删,处理方式和班级模块 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
# 逻辑删除:将删除标记置为1,表示已删除
advisor.is_deleted = 1
# 提交事务保存修改
db.commit()
return True
# 删除标记修改成功返回True
return True
+2 -3
View File
@@ -22,8 +22,7 @@ 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",)
+45 -21
View File
@@ -1,30 +1,54 @@
from pydantic import BaseModel, Field, ConfigDict
# 顾问模块:请求和响应格式,写法与原学生模块一致。
# 使用Pydantic定义接口入参、出参模型,用于FastAPI接口的数据校验与序列化
from pydantic import BaseModel
from typing import Optional
# ---------- 请求模型 ----------
# 顾问模块:入参格式,字段长度跟 model/wl_advisor_model.py 里的列宽保持一致
# 创建顾问 请求模型:新增顾问时前端提交的数据结构
class AdvisorCreate(BaseModel):
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] = 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):
adv_id: int
# 顾问姓名(必填字段,创建时必须传入)
adv_name: str
# 顾问性别(选填,不传则为None)
adv_gender: Optional[str] = None
# 顾问联系电话(选填)
adv_phone: Optional[str] = None
# 顾问邮箱(选填)
adv_email: Optional[str] = None
# 顾问所属地区(选填)
adv_region: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
# 更新顾问 请求模型:修改顾问信息时前端提交的数据结构
# 所有字段全部可选,支持局部更新(只传需要修改的字段)
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
# 顾问信息 响应模型:查询顾问信息,返回给前端的数据结构
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