代码提交

This commit is contained in:
xwt
2026-09-21 19:03:31 +08:00
parent 34d656492c
commit 97986d9a1a
217 changed files with 12042 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
"""就业管理接口(需求 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)