diff --git a/api/wl_emp_api.py b/api/wl_emp_api.py new file mode 100644 index 0000000..8981ddb --- /dev/null +++ b/api/wl_emp_api.py @@ -0,0 +1,56 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from dao.wl_emp_dao import EmpDAO +from database import get_db +from scheme.wl_emp_scheme import EmpCreate, EmpOut, EmpUpdate + +router = APIRouter(prefix="/emp", tags=["就业管理"]) + + +# 查询就业列表 +@router.get("/", response_model=list[EmpOut]) +def query_list( + skip: int = 0, + limit: int = 100, + company_name: Optional[str] = None, + db: Session = Depends(get_db), +): + return EmpDAO.list_emps(db, skip, limit, company_name) + + +# 根据学号查询就业信息 +@router.get("/{stu_no}", response_model=EmpOut) +def query_one(stu_no: str, db: Session = Depends(get_db)): + emp = EmpDAO.get_emp(db, stu_no) + if not emp: + raise HTTPException(status_code=404, detail="就业信息不存在") + return emp + + +# 新增就业信息 +@router.post("/", response_model=EmpOut, status_code=201) +def add_emp(body: EmpCreate, db: Session = Depends(get_db)): + try: + return EmpDAO.create_emp(db, body) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +# 修改就业信息 +@router.put("/{stu_no}", response_model=EmpOut) +def edit_emp(stu_no: str, body: EmpUpdate, db: Session = Depends(get_db)): + emp = EmpDAO.update_emp(db, stu_no, body) + if not emp: + raise HTTPException(status_code=404, detail="就业信息不存在") + return emp + + +# 逻辑删除就业信息 +@router.delete("/{stu_no}") +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": "删除成功"} diff --git a/dao/wl_emp_dao.py b/dao/wl_emp_dao.py new file mode 100644 index 0000000..6be5b52 --- /dev/null +++ b/dao/wl_emp_dao.py @@ -0,0 +1,99 @@ +from sqlalchemy.orm import Session + +from model.wl_emp_model import Emp +from model.wl_student_model import Student +from scheme.wl_emp_scheme import EmpCreate, EmpUpdate + + +class EmpDAO: + + @staticmethod + def get_student(db: Session, stu_no: str): + """按学号查未删除的学生""" + return db.query(Student).filter( + Student.stu_no == stu_no, + Student.is_deleted == 0, + ).first() + + @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: + 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 + def get_emp(db: Session, stu_no: str): + """按学号查就业信息""" + emp = db.query(Emp).filter( + Emp.stu_no == stu_no, + Emp.is_deleted == 0, + ).first() + if emp: + stu = EmpDAO.get_student(db, stu_no) + emp.stu_name = stu.stu_name if stu else None + return emp + + @staticmethod + def create_emp(db: Session, emp_data: EmpCreate): + """新增就业信息;如果这个学生之前被删过,就把那条记录复活""" + # stu_no 是主键,已删除的记录也占着这个主键,所以查的时候要连已删除的一起查 + exist = db.query(Emp).filter(Emp.stu_no == emp_data.stu_no).first() + if exist and exist.is_deleted == 0: + raise ValueError("该学生的就业信息已存在") + + # exclude_none:没传的字段不写进去,交给数据库默认值 + data = emp_data.model_dump(exclude_none=True) + stu = EmpDAO.get_student(db, emp_data.stu_no) + + if exist: + # 复活旧记录 + for k, v in data.items(): + setattr(exist, k, v) + exist.is_deleted = 0 + db.commit() + db.refresh(exist) + exist.stu_name = stu.stu_name if stu else None + return exist + + db_emp = Emp(**data) + db.add(db_emp) + db.commit() + db.refresh(db_emp) + db_emp.stu_name = stu.stu_name if stu else None + return db_emp + + @staticmethod + def update_emp(db: Session, stu_no: str, emp_data: EmpUpdate): + """修改就业信息""" + db_emp = EmpDAO.get_emp(db, stu_no) + if not db_emp: + return None + + update_dict = emp_data.model_dump(exclude_unset=True) + for k, v in update_dict.items(): + setattr(db_emp, k, v) + + db.commit() + db.refresh(db_emp) + return db_emp + + @staticmethod + def delete_emp(db: Session, stu_no: str): + """逻辑删除,修改 is_deleted=1""" + db_emp = EmpDAO.get_emp(db, stu_no) + if not db_emp: + return None + + db_emp.is_deleted = 1 + db.commit() + return db_emp diff --git a/dao/wl_statistics_dao.py b/dao/wl_statistics_dao.py index 04b112f..4a3d820 100644 --- a/dao/wl_statistics_dao.py +++ b/dao/wl_statistics_dao.py @@ -136,7 +136,7 @@ class StatisticDao: Emp.company_name ).select_from(Student) \ .join(Student.emp)) \ - .filter(Student.is_deleted==0) \ + .filter(Student.is_deleted==0, Emp.is_deleted==0) \ .order_by(Emp.salary.desc())\ .limit(rank) @@ -151,11 +151,12 @@ class StatisticDao: 0, ).label('emp_total_time') + # 逻辑删除条件写在 ON 里,写进 where 会变成 INNER JOIN,未就业学生会被滤掉 q=db.query( Student.stu_name, emp_total_time, ).select_from(Student) \ - .outerjoin(Student.emp) \ + .outerjoin(Emp, and_(Student.stu_no == Emp.stu_no, Emp.is_deleted == 0)) \ .filter(Student.is_deleted==0) return q.all() @@ -188,7 +189,8 @@ class StatisticDao: .outerjoin( Emp, and_( - Emp.stu_id == Student.stu_id, + Emp.stu_no == Student.stu_no, + Emp.is_deleted == 0, Emp.emp_open_time.isnot(None), Emp.offer_time.isnot(None), ), diff --git a/main.py b/main.py index 56965fe..94a271b 100644 --- a/main.py +++ b/main.py @@ -1,9 +1,11 @@ #项目初始化入口 from fastapi import FastAPI -# 周学灵、熊浩钦、圣国伟、刘盼、彭少的路由 -from api import wl_student_api,wl_class_api,wl_score_api,wl_teacher_api,statistics +# 周学灵、熊浩钦、圣国伟、刘盼、彭少、赵康宁的路由 +from api import wl_student_api,wl_class_api,wl_score_api,wl_teacher_api,statistics,wl_emp_api from database import Base, engine -from model import wl_student_model,wl_class_model,wl_advisor_model,wl_score_model,wl_teacher_model +# wl_emp_model 必须显式导入:否则 wl_emp 表只能靠 statistics 的传递导入进 metadata, +# 一旦统计路由被拿掉,create_all 会漏建 wl_emp,Student.emp 关系随即报错 +from model import wl_student_model,wl_class_model,wl_advisor_model,wl_score_model,wl_teacher_model,wl_emp_model Base.metadata.drop_all(engine) Base.metadata.create_all(bind=engine) @@ -14,6 +16,7 @@ app.include_router(wl_class_api.router) app.include_router(wl_score_api.router) app.include_router(wl_teacher_api.router) app.include_router(statistics.router) +app.include_router(wl_emp_api.router) if __name__ == "__main__": diff --git a/model/wl_emp_model.py b/model/wl_emp_model.py index 82f8fd3..6d26ac6 100644 --- a/model/wl_emp_model.py +++ b/model/wl_emp_model.py @@ -1,5 +1,5 @@ -from sqlalchemy import * -from sqlalchemy.orm import * +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, func +from sqlalchemy.orm import relationship from database import Base @@ -7,15 +7,17 @@ from database import Base class Emp(Base): __tablename__ = "wl_emp" - stu_id = Column(Integer,ForeignKey('wl_student.stu_id'), primary_key=True, autoincrement=True) + # 学号做主键,同时做外键指向学生表,长度跟 Student.stu_no 保持一致 + stu_no = Column(String(20), ForeignKey('wl_student.stu_no'), primary_key=True) 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) - # Student.emp 用的是 back_populates="student",这个反向属性必须保留,否则 mapper 配置失败 student = relationship("Student", back_populates="emp") - def __repr__(self): - pass + # 逻辑删除:0 正常 1 已删除 + is_deleted = Column(Integer, default=0, comment='逻辑删除: 0正常 1删除') + def __repr__(self): + return f"" diff --git a/model/wl_student_model.py b/model/wl_student_model.py index 8789196..354ea18 100644 --- a/model/wl_student_model.py +++ b/model/wl_student_model.py @@ -24,7 +24,7 @@ class Student(Base): class_ = relationship("Class_", back_populates="students") scores=relationship("Score",back_populates="student") - emp=relationship("Emp",back_populates="student") + emp = relationship("Emp", back_populates="student") # advisor = relationship("Advisor", back_populates="students") is_deleted = Column(Integer, default=0, comment='逻辑删除: 0正常 1删除') diff --git a/scheme/wl_emp_scheme.py b/scheme/wl_emp_scheme.py new file mode 100644 index 0000000..e21376a --- /dev/null +++ b/scheme/wl_emp_scheme.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field, ConfigDict +from datetime import datetime +from typing import Optional + +# ---------- 请求模型 ---------- +class EmpCreate(BaseModel): + stu_no: str = Field(..., max_length=20, description="学号") + 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(None, description="不传用默认值 15000") + +class EmpUpdate(BaseModel): + emp_open_time: Optional[datetime] = None + offer_time: Optional[datetime] = None + company_name: Optional[str] = Field(None, max_length=100) + salary: Optional[int] = None + +# ---------- 响应模型 ---------- +class EmpOut(BaseModel): + stu_no: str + stu_name: Optional[str] = None + emp_open_time: Optional[datetime] = None + offer_time: Optional[datetime] = None + company_name: Optional[str] = None + salary: Optional[int] = None + + model_config = ConfigDict(from_attributes=True) diff --git a/scheme/wl_score_scheme.py b/scheme/wl_score_scheme.py index a19ab59..5dc7146 100644 --- a/scheme/wl_score_scheme.py +++ b/scheme/wl_score_scheme.py @@ -30,18 +30,6 @@ class ScoreResp(ScoreCreate): class Config: from_attributes = True -# ----------------就业---------------- -class EmploymentCreate(BaseModel): - stu_id: int - emp_open_time: date - offer_time: Optional[date] = None - company_name: Optional[str] = None - salary: Optional[float] = None - -class EmploymentResp(EmploymentCreate): - class Config: - from_attributes = True - # ----------------班级---------------- class ClassCreate(BaseModel): start_time: Optional[date] = None