From 53afb2897b88500d360f747a4de646bc8e93441b Mon Sep 17 00:00:00 2001 From: qinghu762-afk Date: Mon, 14 Sep 2026 13:32:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/wl_advisor_api.py | 123 ++++++++++++++++++++++++++---------- dao/wl_advisor_dao.py | 94 ++++++++++++++++----------- scheme/wl_advisor_scheme.py | 67 ++++++++++++++------ 3 files changed, 194 insertions(+), 90 deletions(-) diff --git a/api/wl_advisor_api.py b/api/wl_advisor_api.py index d98734c..bdbda29 100644 --- a/api/wl_advisor_api.py +++ b/api/wl_advisor_api.py @@ -1,57 +1,116 @@ +# 顾问模块:独立接口,沿用原学生接口的依赖注入与返回方式。 +# 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.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": "删除成功"} diff --git a/dao/wl_advisor_dao.py b/dao/wl_advisor_dao.py index 0032933..71f1f2d 100644 --- a/dao/wl_advisor_dao.py +++ b/dao/wl_advisor_dao.py @@ -1,71 +1,91 @@ -from fastapi import HTTPException +# 顾问模块DAO层:直接操作 advisor 表,沿用原学生 DAO 的写法。 +# DAO(Data Access Object) 数据访问层,封装数据库增删改查逻辑,供上层API调用 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: + # 根据ID查询顾问 + advisor = get_advisor_by_id(db, adv_id) + # 判断顾问是否存在,不存在直接返回None + if not advisor: return None - # exclude_unset:前端传了什么就改什么,没传的字段保持原值 + + # 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() + # 删除标记修改成功返回True return True diff --git a/scheme/wl_advisor_scheme.py b/scheme/wl_advisor_scheme.py index 1f0e020..3d255bc 100644 --- a/scheme/wl_advisor_scheme.py +++ b/scheme/wl_advisor_scheme.py @@ -1,30 +1,55 @@ -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 +