Files
test/app/api/employment_api.py
T
2026-09-21 19:03:31 +08:00

187 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""就业管理接口(需求 2.3、4.3)。
需求里给的示例路径是 /employment/students/{id}、/employment/class/{id},
这里在同一套语义下改成标准 REST 写法(资源在复数、动作用方法表达),
同时对老路径做了兼容别名,见文件末尾。
"""
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Query
from app.core.deps import DbSession, ReadAccount, WriteAccount
from app.core.exceptions import BusinessError
from app.core.response import Resp, ok
from app.dao.employment_dao import EmploymentDao
from app.dao.student_dao import StudentDao
from app.schema.common import PageResult
from app.schema.employment_schema import (
EmploymentCreate,
EmploymentOut,
EmploymentRegisterResult,
EmploymentUpdate,
)
from app.service.employment_service import EmploymentService
router = APIRouter(prefix="/employments", tags=["2.3 学生就业管理"])
legacy_router = APIRouter(prefix="/employment", tags=["2.3 学生就业管理(需求示例路径)"], include_in_schema=False)
@router.get("", response_model=Resp[PageResult[EmploymentOut]], summary="就业信息列表(按编号/公司/薪资区间多条件查询)")
def list_employments(
db: DbSession,
_: ReadAccount,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=200)] = 10,
stu_id: Annotated[int | None, Query(description="学生ID")] = None,
class_id: Annotated[int | None, Query(description="班级ID")] = None,
company: Annotated[str | None, Query(description="公司名称,模糊匹配")] = None,
salary_min: Annotated[float | None, Query(ge=0, description="薪资下限")] = None,
salary_max: Annotated[float | None, Query(ge=0, description="薪资上限")] = None,
has_offer: Annotated[bool | None, Query(description="是否已下发 offer")] = None,
has_open: Annotated[bool | None, Query(description="是否已开放就业")] = None,
keyword: Annotated[str | None, Query(description="姓名/学号/公司/城市")] = None,
order_by: Annotated[str, Query(description="id/salary/open_date/offer_date/company/student_name")] = "id",
order: Annotated[str, Query(pattern="^(asc|desc)$")] = "desc",
):
if salary_min is not None and salary_max is not None and salary_min > salary_max:
salary_min, salary_max = salary_max, salary_min # 顺序写反了自动纠正
stmt = EmploymentDao.build_stmt(
stu_id=stu_id, class_id=class_id, company=company, salary_min=salary_min,
salary_max=salary_max, has_offer=has_offer, has_open=has_open,
keyword=keyword, order_by=order_by, order=order,
)
items, total, page, pages = EmploymentDao.paginate(db, stmt, page, page_size)
return ok({
"total": total, "page": page, "page_size": page_size, "pages": pages,
"items": [EmploymentOut.model_validate(e) for e in items],
})
def _status_msg(action: str, changed: bool, before: str | None, after: str | None) -> str:
if not changed:
return action
return f"{action},学生状态:{before} → {after}"
@router.post("", response_model=Resp[EmploymentRegisterResult], summary="登记就业信息(事务内同步学生状态)")
def register_employment(db: DbSession, _: WriteAccount, payload: EmploymentCreate):
employment, created, changed, before, after = EmploymentService.register(db, payload)
db.commit()
db.refresh(employment)
msg = _status_msg("就业信息登记成功" if created else "就业信息已更新", changed, before, after)
return ok(
{
"employment": EmploymentOut.model_validate(employment),
"created": created,
"status_changed": changed,
"from_status_text": before,
"to_status_text": after,
"msg": msg,
},
msg=msg,
)
@router.get("/student/{stu_id}", response_model=Resp[EmploymentOut], summary="查询某个学生的就业信息")
def employment_of_student(db: DbSession, _: ReadAccount, stu_id: int):
StudentDao.get_or_404(db, stu_id, "学生")
employment = EmploymentDao.get_by_stu_id(db, stu_id)
if employment is None:
return Resp.fail("该学生还没有登记就业信息", 404)
return ok(EmploymentOut.model_validate(employment))
@router.get("/class/{class_id}", summary="查询某个班级的就业情况(含班级就业率)")
def employment_of_class(db: DbSession, _: ReadAccount, class_id: int):
from app.dao.clazz_dao import ClazzDao
klass = ClazzDao.get_or_404(db, class_id, "班级")
stmt = EmploymentDao.build_stmt(class_id=class_id, order_by="salary", order="desc")
items = EmploymentDao.all(db, stmt)
students_total = len([s for s in StudentDao.all(db, StudentDao.build_stmt(class_id=class_id, order_by="id", order="asc"))])
offered = [e for e in items if e.offer_date]
salaries = [float(e.salary) for e in items if e.salary is not None]
return ok({
"class_id": klass.id,
"class_name": klass.name,
"student_total": students_total,
"open_count": len([e for e in items if e.open_date]),
"offer_count": len(offered),
"employment_rate": round(len(offered) / students_total * 100, 1) if students_total else 0.0,
"avg_salary": round(sum(salaries) / len(salaries), 2) if salaries else None,
"items": [EmploymentOut.model_validate(e) for e in items],
})
@router.put("/{employment_id}", response_model=Resp[EmploymentRegisterResult], summary="修改就业信息")
def update_employment(db: DbSession, _: WriteAccount, employment_id: int, payload: EmploymentUpdate):
employment = EmploymentDao.get_or_404(db, employment_id, "就业记录")
employment, changed, before, after = EmploymentService.update(db, employment, payload)
db.commit()
db.refresh(employment)
msg = _status_msg("修改成功", changed, before, after)
return ok(
{
"employment": EmploymentOut.model_validate(employment),
"created": False,
"status_changed": changed,
"from_status_text": before,
"to_status_text": after,
"msg": msg,
},
msg=msg,
)
@router.delete("/{employment_id}", summary="删除就业信息(学生状态回退到在读)")
def delete_employment(db: DbSession, _: WriteAccount, employment_id: int):
employment = EmploymentDao.get_or_404(db, employment_id, "就业记录")
changed, before, after = EmploymentService.delete(db, employment)
db.commit()
msg = _status_msg("已删除就业记录", changed, before, after)
return ok(
{"status_changed": changed, "from_status_text": before, "to_status_text": after, "msg": msg},
msg=msg,
)
# ----------------------------------------------------------------- 兼容老路径
@legacy_router.get("/students/{stu_id}", summary="[兼容] 获取学生就业信息")
def legacy_get_student_employment(db: DbSession, account: ReadAccount, stu_id: int):
return employment_of_student(db, account, stu_id)
@legacy_router.post("/students/{stu_id}", summary="[兼容] 添加/更新就业信息")
def legacy_add_student_employment(db: DbSession, account: WriteAccount, stu_id: int, payload: EmploymentUpdate):
body = payload.model_dump(exclude_unset=True)
body["stu_id"] = stu_id
try:
create_payload = EmploymentCreate(**body)
except Exception as exc: # noqa: BLE001
raise BusinessError(
"登记就业至少需要 就业开放时间 / offer 时间 / 公司 / 薪资 中的一项"
) from exc
employment, created, changed, before, after = EmploymentService.register(db, create_payload)
db.commit()
db.refresh(employment)
msg = _status_msg("登记成功" if created else "已更新", changed, before, after)
return ok(
{
"employment": EmploymentOut.model_validate(employment),
"created": created,
"status_changed": changed,
"from_status_text": before,
"to_status_text": after,
},
msg=msg,
)
@legacy_router.get("/class/{class_id}", summary="[兼容] 获取班级学生就业信息")
def legacy_class_employment(db: DbSession, account: ReadAccount, class_id: int):
return employment_of_class(db, account, class_id)