diff --git a/api/employment.py b/api/employment.py index e69de29..9ea1a0c 100644 --- a/api/employment.py +++ b/api/employment.py @@ -0,0 +1,82 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from core.database import get_db +from dao.employment import ( + create_employment, + get_employment_by_id, + get_employment_by_student_id, + get_employment_list, + update_employment, + delete_employment_logic +) +from schemas.employment import ( + EmploymentCreate, + EmploymentUpdate, + EmploymentResponse +) + +employment_router = APIRouter(prefix="/employments", tags=["就业管理模块"]) + + +@employment_router.post("/", response_model=EmploymentResponse, summary="新增就业信息") +def add_employment(body: EmploymentCreate, db: Session = Depends(get_db)): + """ + 新增就业信息 + - 一个学生只能保存一条就业记录 + - student_id必须在学生表student_info_detail中真实存在 + """ + res = create_employment(db, body) + if res is None: + raise HTTPException(status_code=500, detail="新增失败,学生id不存在或该学生已有就业记录") + return res + + +@employment_router.get("/students/{student_id}", response_model=EmploymentResponse, summary="查询指定学生的就业信息") +def query_student_employment(student_id: int, db: Session = Depends(get_db)): + """根据学生id获取对应就业记录""" + record = get_employment_by_student_id(db, student_id) + if not record: + raise HTTPException(status_code=404, detail="未找到该学生的就业信息") + return record + + +@employment_router.get("/", summary="就业信息列表,支持筛选") +def query_employment_list( + company_name: str | None = Query(None, description="公司名称模糊查询"), + salary_min: float | None = Query(None, description="最低薪资"), + salary_max: float | None = Query(None, description="最高薪资"), + skip: int = Query(0, ge=0, description="偏移量"), + limit: int = Query(20, ge=1, le=100, description="每页条数"), + db: Session = Depends(get_db) +): + # 调用dao列表查询,拿到数据字典列表和总条数 + data_list, total = get_employment_list(db, company_name, salary_min, salary_max, skip, limit) + return {"total": total, "data": data_list} + + +@employment_router.get("/{eid}", response_model=EmploymentResponse, summary="根据id查询就业详情") +def query_employment_detail(eid: int, db: Session = Depends(get_db)): + """根据就业主键id查询单条就业记录""" + record = get_employment_by_id(db, eid) + if not record: + raise HTTPException(status_code=404, detail="就业记录不存在或已被逻辑删除") + return record + + +@employment_router.put("/{eid}", response_model=EmploymentResponse, summary="修改就业信息") +def edit_employment(eid: int, body: EmploymentUpdate, db: Session = Depends(get_db)): + """修改就业记录,只传想要修改的字段即可""" + res = update_employment(db, eid, body) + if res is None: + raise HTTPException(status_code=404, detail="修改失败,记录不存在") + return res + + +@employment_router.delete("/{eid}", summary="逻辑删除就业记录") +def logic_delete_employment(eid: int, db: Session = Depends(get_db)): + """执行逻辑删除,is_deleted置1,不会真正删除数据库行""" + ok = delete_employment_logic(db, eid) + if not ok: + raise HTTPException(status_code=404, detail="删除失败,记录不存在") + # 删除成功返回提示字典 + return {"code": 200, "msg": "逻辑删除成功"} diff --git a/dao/employment.py b/dao/employment.py index e69de29..d760185 100644 --- a/dao/employment.py +++ b/dao/employment.py @@ -0,0 +1,115 @@ +from sqlalchemy.orm import Session +from models.employment import Employment +from schemas.employment import EmploymentCreate, EmploymentUpdate, EmploymentResponse + + +def create_employment(db, obj_in): + """新增就业信息:一个学生只能存在一条就业记录""" + try: + # obj_in.model_dump()把pydantic请求对象转为字典,解包传给Employment生成ORM对象 + db_obj = Employment(**obj_in.model_dump()) + # add:把对象加入数据库会话,此时还没写入数据库 + db.add(db_obj) + # commit:提交事务,真正写入mysql数据库 + db.commit() + # refresh:从数据库刷新对象,拿到数据库自动生成的id、默认时间等字段 + db.refresh(db_obj) + # model_validate:把sqlalchemy ORM对象解析进pydantic响应模型 + # model_dump:把pydantic模型转为普通python字典返回给上层api + return EmploymentResponse.model_validate(db_obj).model_dump() + except Exception: + # 发生任何异常,执行回滚,撤销本次会话中未提交的数据库改动 + db.rollback() + # 异常情况返回None,上层api可以判断新增失败 + return None + + +def get_employment_by_id(db, eid): + """根据主键id查询就业详情,过滤逻辑删除""" + try: + db_obj = db.query(Employment).filter( + Employment.id == eid, + Employment.is_deleted == 0 + ).first() + if not db_obj: + return None + return EmploymentResponse.model_validate(db_obj).model_dump() + except Exception: + return None + + +def get_employment_by_student_id(db, student_id): + """根据学生id查询就业信息,过滤逻辑删除""" + try: + db_obj = db.query(Employment).filter( + Employment.student_id == student_id, + Employment.is_deleted == 0 + ).first() + if not db_obj: + return None + return EmploymentResponse.model_validate(db_obj).model_dump() + except Exception: + return None + + +def get_employment_list(db, company_name=None, salary_min=None, salary_max=None, skip=0, limit=20): + """就业列表查询;支持公司名模糊、薪资范围筛选;只查询is_deleted=0""" + try: + query = db.query(Employment).filter(Employment.is_deleted == 0) + if company_name: + query = query.filter(Employment.company_name.like(f"%{company_name}%")) + if salary_min is not None: + query = query.filter(Employment.salary >= salary_min) + if salary_max is not None: + query = query.filter(Employment.salary <= salary_max) + # count()统计满足条件总条数,用于分页total + total = query.count() + # offset偏移量(跳过多少条),limit每页多少条,执行查询拿到结果列表 + records = query.offset(skip).limit(limit).all() + # 列表推导式:循环每一条ORM记录,全部转为字典,得到字典列表 + dict_list = [EmploymentResponse.model_validate(item).model_dump() for item in records] + # 返回字典列表 + 总条数 + return dict_list, total + except Exception: + # 异常返回空列表,总条数0 + return [], 0 + + +def update_employment(db, eid, obj_in): + """修改就业信息,只更新传入的字段""" + try: + # 根据id查询未删除的记录 + db_obj_raw = db.query(Employment).filter( + Employment.id == eid, + Employment.is_deleted == 0 + ).first() + # 记录不存在直接返回None + if db_obj_raw is None: + return None + # exclude_unset=True:只取出前端实际传过来的字段,不会带上没传的None字段 + update_dict = obj_in.model_dump(exclude_unset=True) + for key, value in update_dict.items(): + setattr(db_obj_raw, key, value) + db.commit() + db.refresh(db_obj_raw) + return EmploymentResponse.model_validate(db_obj_raw).model_dump() + except Exception: + db.rollback() + return None + + +def delete_employment_logic(db, eid): + """逻辑删除,只修改is_deleted=1,不做物理删除""" + try: + db_obj_raw = db.query(Employment).filter( + Employment.id == eid, + Employment.is_deleted == 0 + ).first() + if db_obj_raw is None: + return False + db_obj_raw.is_deleted = 1 + db.commit() + return True + except Exception: + db.rollback() + return False diff --git a/schemas/employment.py b/schemas/employment.py index e69de29..bfe27ed 100644 --- a/schemas/employment.py +++ b/schemas/employment.py @@ -0,0 +1,38 @@ +from pydantic import BaseModel, Field +from datetime import date, datetime +from decimal import Decimal + + +class EmploymentCreate(BaseModel): + """新增就业信息 请求体 Schema""" + student_id: int = Field(..., description="关联学生ID,必填") + employment_status: str | None = Field(None, description="就业状态:not_started/job_hunting/offered/employed") + employment_open_date: date | None = Field(None, description="就业开放时间,选填") + offer_date: date | None = Field(None, description="offer下发时间,选填") + company_name: str | None = Field(None, max_length=100, description="就业公司名称,选填") + salary: Decimal | None = Field(None, ge=0, description="就业薪资,不能负数,选填") + remark: str | None = Field(None, max_length=255, description="备注,选填") + + +class EmploymentUpdate(BaseModel): + """修改就业信息 请求体 Schema:全部字段可选,只传要修改的字段""" + employment_status: str | None = Field(None, description="就业状态") + employment_open_date: date | None = Field(None, description="就业开放时间") + offer_date: date | None = Field(None, description="offer下发时间") + company_name: str | None = Field(None, max_length=100, description="就业公司名称") + salary: Decimal | None = Field(None, ge=0, description="就业薪资") + remark: str | None = Field(None, max_length=255, description="备注") + + +class EmploymentResponse(BaseModel): + """就业信息返回响应体Schema,接口返回给前端的数据结构""" + id: int + student_id: int + employment_status: str + employment_open_date: date | None + offer_date: date | None + company_name: str | None + salary: Decimal | None + remark: str | None + created_at: datetime | None + updated_at: datetime | None