Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2774f5f50a | ||
|
|
f19c1bfa27 |
+1
-1
@@ -2,7 +2,7 @@ import uuid
|
||||
from fastapi import APIRouter, Depends, HTTPException,Query
|
||||
from dao.class_dao import update_class_dao, add_class_dao, get_class_dao
|
||||
from database import get_db
|
||||
from model.class_model import StudentClassIntermediate,Student_info
|
||||
from model.class_model import StudentClassIntermediate
|
||||
from schema.class_request import ClassRequest, ClassResponse, TransferRequest
|
||||
from dao.class_dao import Student_delect_Dao
|
||||
from datetime import datetime
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from dao import employment_dao
|
||||
from database import get_db
|
||||
from schema.common import ApiResponse
|
||||
from schema.employment_schema import (
|
||||
ClassEmploymentStats,
|
||||
EmploymentOut,
|
||||
EmploymentUpdate,
|
||||
EmploymentUpsert,
|
||||
SalaryMarketComparison,
|
||||
)
|
||||
|
||||
app = APIRouter(prefix="/employment", tags=["就业管理"])
|
||||
|
||||
|
||||
# ==================== 基础 CRUD ====================
|
||||
|
||||
@app.get("/list", response_model=ApiResponse[list[EmploymentOut]], summary="获取就业记录列表")
|
||||
def list_employment(
|
||||
student_id: str | None = Query(None, description="学生ID"),
|
||||
company_name: str | None = Query(None, description="公司名称(模糊)"),
|
||||
salary_min: float | None = Query(None, description="最低月薪"),
|
||||
salary_max: float | None = Query(None, description="最高月薪"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return ApiResponse(data=employment_dao.list_employment(db, {
|
||||
"student_id": student_id, "company_name": company_name,
|
||||
"salary_min": salary_min, "salary_max": salary_max,
|
||||
}))
|
||||
|
||||
@app.get("/student/{student_id}", response_model=ApiResponse[list[EmploymentOut]], summary="获取指定学生全部就业记录")
|
||||
def list_student_employment(student_id: str, db: Session = Depends(get_db)):
|
||||
data = employment_dao.list_by_student(db, student_id)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="该学生暂无就业记录")
|
||||
return ApiResponse(data=data)
|
||||
|
||||
@app.get("/class/{class_id}", response_model=ApiResponse[list[EmploymentOut]], summary="获取班级全部就业记录")
|
||||
def list_class_employment(class_id: str, db: Session = Depends(get_db)):
|
||||
return ApiResponse(data=employment_dao.list_by_class(db, class_id))
|
||||
|
||||
@app.post("/students/{student_id}", response_model=ApiResponse[EmploymentOut], summary="新增学生就业信息")
|
||||
def create_employment(student_id: str, payload: EmploymentUpsert, db: Session = Depends(get_db)):
|
||||
data_dict = payload.model_dump(exclude_unset=True, exclude_none=True)
|
||||
return ApiResponse(data=employment_dao.create_by_student(db, student_id, data_dict))
|
||||
|
||||
@app.put("/{emp_id}", response_model=ApiResponse[EmploymentOut], summary="修改就业信息")
|
||||
def update_employment(emp_id: str, payload: EmploymentUpdate, db: Session = Depends(get_db)):
|
||||
updates = payload.model_dump(exclude_unset=True, exclude_none=True)
|
||||
data = employment_dao.update_by_emp_id(db, emp_id, updates)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="就业记录不存在")
|
||||
return ApiResponse(data=data)
|
||||
|
||||
@app.delete("/{emp_id}", response_model=ApiResponse, summary="删除就业信息")
|
||||
def delete_employment(emp_id: str, db: Session = Depends(get_db)):
|
||||
if not employment_dao.delete_by_emp_id(db, emp_id):
|
||||
raise HTTPException(status_code=404, detail="就业记录不存在")
|
||||
return ApiResponse()
|
||||
|
||||
|
||||
# ==================== 新增功能点 ====================
|
||||
|
||||
@app.get("/stats/class/{class_id}", response_model=ApiResponse[ClassEmploymentStats], summary="班级就业统计仪表盘")
|
||||
def get_class_stats(class_id: str, db: Session = Depends(get_db)):
|
||||
return ApiResponse(data=employment_dao.get_class_employment_stats(db, class_id))
|
||||
|
||||
@app.get("/warnings", response_model=ApiResponse[list[dict]], summary="就业跟进预警")
|
||||
def get_warnings(
|
||||
days: int = Query(30, description="简历开放后多少天未签约算超时"),
|
||||
class_id: str | None = Query(None, description="按班级筛选"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return ApiResponse(data=employment_dao.get_employment_warnings(db, days, class_id))
|
||||
|
||||
@app.get("/salary-market/{student_id}", response_model=ApiResponse[SalaryMarketComparison], summary="薪资行情对标")
|
||||
def salary_market_compare(student_id: str, db: Session = Depends(get_db)):
|
||||
data = employment_dao.compare_salary_with_market(db, student_id)
|
||||
if not data:
|
||||
raise HTTPException(status_code=404, detail="该学生无有效就业记录")
|
||||
return ApiResponse(data=data)
|
||||
@@ -0,0 +1,106 @@
|
||||
# 导入类型提示 List,用于指定响应模型为列表
|
||||
from typing import List
|
||||
|
||||
# 导入 FastAPI 的 APIRouter(路由)、Depends(依赖注入)、HTTPException(HTTP 异常)
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
# 从 dao.region_sql 导入数据库操作函数,并为添加、修改、删除起了别名,避免与路由函数重名
|
||||
from dao.region_sql import (
|
||||
get_region_part,
|
||||
add_region as dao_add_resp,
|
||||
update_region as dao_update_resp,
|
||||
delete_region as dao_delete_resp,
|
||||
)
|
||||
|
||||
# 导入数据库会话依赖,用于每个请求获取独立的 Session
|
||||
from database import get_db
|
||||
|
||||
# 导入 Pydantic 模型:地区响应体、地区添加请求体、通用操作响应体、地区修改请求体
|
||||
from schema.stu_info_region_schema import (
|
||||
region_resp,
|
||||
region_req,
|
||||
region_add_resp,
|
||||
region_update_req,
|
||||
)
|
||||
|
||||
# 创建地区模块的路由实例
|
||||
region_api = APIRouter()
|
||||
|
||||
|
||||
# -------------------- 查询地区(支持按省份过滤) --------------------
|
||||
@region_api.get('/region', response_model=List[region_resp], summary='查询')
|
||||
def get_regions_part(province: str = None, gs=Depends(get_db)):
|
||||
"""
|
||||
根据省份查询地区信息。
|
||||
- province 为可选参数,不传则返回全部地区。
|
||||
- 返回列表,每个元素符合 region_resp 模型。
|
||||
"""
|
||||
try:
|
||||
# 调用 dao 层查询函数,传入数据库会话和省份过滤条件
|
||||
data = get_region_part(gs, province)
|
||||
return data
|
||||
except Exception as e:
|
||||
# 捕获异常,返回 500 错误,详情为字符串化的异常信息
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# -------------------- 添加地区 --------------------
|
||||
@region_api.post('/region', response_model=region_add_resp, summary='添加')
|
||||
def add_region(rreq: region_req, gs=Depends(get_db)):
|
||||
"""
|
||||
新增一条地区记录。
|
||||
- 请求体必须符合 region_req 模型。
|
||||
- 返回统一的 region_add_resp(code + message)。
|
||||
"""
|
||||
try:
|
||||
# 将 Pydantic 模型转为字典,便于传给 dao 层
|
||||
data = rreq.model_dump()
|
||||
# 调用 dao 层添加函数(别名为 dao_add_resp)
|
||||
dao_add_resp(gs, data)
|
||||
return region_add_resp(code=200, message='数据已添加')
|
||||
except Exception as e:
|
||||
# 注意:此处状态码写成了 200,通常异常应返回 500。
|
||||
# 建议改为 status_code=500。
|
||||
raise HTTPException(status_code=200, detail=str(e))
|
||||
|
||||
|
||||
# -------------------- 修改地区 --------------------
|
||||
@region_api.put('/region', response_model=region_add_resp, summary='修改')
|
||||
def update_region(code: str, rreq: region_update_req, gs=Depends(get_db)):
|
||||
"""
|
||||
根据地区编码更新地区信息。
|
||||
- code 为查询参数,指定要修改的地区编码。
|
||||
- 请求体为 region_update_req,字段均为可选,只更新传入的字段。
|
||||
- 返回统一的 region_add_resp。
|
||||
"""
|
||||
try:
|
||||
# exclude_unset=True:只获取客户端显式传入的字段,未传的字段不参与更新
|
||||
data = rreq.model_dump(exclude_unset=True)
|
||||
# 调用 dao 层更新函数(别名为 dao_update_resp),传入编码和待更新数据
|
||||
res = dao_update_resp(gs, code, data)
|
||||
if not res:
|
||||
# 没有更新任何行,说明编码不存在或数据未变化
|
||||
raise HTTPException(status_code=500, detail='没有更新!')
|
||||
# res 可能是 dao 返回的消息字符串,直接作为 message 返回
|
||||
return region_add_resp(code=200, message=res)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# -------------------- 删除地区 --------------------
|
||||
@region_api.delete('/region', response_model=region_add_resp, summary='删除')
|
||||
def delete_region(code: str, gs=Depends(get_db)):
|
||||
"""
|
||||
根据地区编码删除地区记录。
|
||||
- code 为查询参数,指定要删除的地区编码。
|
||||
- 返回统一的 region_add_resp。
|
||||
"""
|
||||
try:
|
||||
# 调用 dao 层删除函数(别名为 dao_delete_resp),传入编码
|
||||
res = dao_delete_resp(gs, code)
|
||||
if not res:
|
||||
# 没有删除任何行,可能编码不存在
|
||||
raise HTTPException(status_code=500, detail='没有删除')
|
||||
return region_add_resp(code=200, message='删除成功')
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1,101 @@
|
||||
# 导入 FastAPI 的 APIRouter(用于组织路由)、Depends(依赖注入)、HTTPException(抛出 HTTP 异常)
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
# 从 dao 层导入数据库操作函数,并对修改和删除函数起了别名,避免与路由函数重名
|
||||
from dao.student_info_sql import (
|
||||
get_part_student_info,
|
||||
add_student_info,
|
||||
update_student_info as update_info,
|
||||
delete_student_info as delete_info,
|
||||
)
|
||||
|
||||
# 导入数据库会话依赖,用于每个请求获取独立的 Session
|
||||
from database import get_db
|
||||
|
||||
# 导入 Pydantic 模型:请求体、响应体、通用操作响应体
|
||||
from schema.stu_info_region_schema import stu_info_req, stu_info_resp, stu_info_add_resp
|
||||
|
||||
# 创建学生信息模块的路由实例
|
||||
student_info_api = APIRouter()
|
||||
|
||||
|
||||
# -------------------- 查询学生信息(支持条件过滤) --------------------
|
||||
@student_info_api.get('/student', response_model=list[stu_info_resp], summary='查询')
|
||||
def get_student_info_part(
|
||||
student_id: str = None,
|
||||
student_name: str = None,
|
||||
gender: str = None,
|
||||
gs=Depends(get_db)
|
||||
):
|
||||
"""
|
||||
根据学号、姓名、性别查询学生信息。
|
||||
- 所有查询参数均为可选,不传则返回全部未删除记录。
|
||||
- 返回列表,每个元素符合 stu_info_resp 模型。
|
||||
"""
|
||||
try:
|
||||
# 调用 dao 层查询函数,传入数据库会话和过滤条件
|
||||
data = get_part_student_info(gs, student_id, student_name, gender)
|
||||
return data
|
||||
except Exception as err:
|
||||
# 捕获异常,返回 500 错误,详情为原始异常信息
|
||||
raise HTTPException(status_code=500, detail=err)
|
||||
|
||||
|
||||
# -------------------- 添加学生信息 --------------------
|
||||
@student_info_api.post('/student', response_model=stu_info_add_resp, summary='添加')
|
||||
def add_student_infomation(stu_info: stu_info_req, gs=Depends(get_db)):
|
||||
"""
|
||||
新增一条学生记录。
|
||||
- 请求体必须符合 stu_info_req 模型。
|
||||
- 返回统一的 stu_info_add_resp(code + message)。
|
||||
"""
|
||||
try:
|
||||
# 将 Pydantic 模型转为字典,便于传给 dao 层
|
||||
data = stu_info.model_dump()
|
||||
# 调用 dao 层添加函数
|
||||
res = add_student_info(gs, data)
|
||||
if not res:
|
||||
# 如果 dao 返回假值(如 None、空字符串),说明添加失败
|
||||
raise HTTPException(status_code=500, detail='添加失败')
|
||||
return stu_info_add_resp(code=200, message='添加成功')
|
||||
except Exception as e:
|
||||
# 将异常信息转为字符串,避免序列化问题
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# -------------------- 修改学生信息 --------------------
|
||||
@student_info_api.put('/student', response_model=stu_info_add_resp, summary='修改')
|
||||
def update_student_info(stu_info: stu_info_req, gs=Depends(get_db)):
|
||||
"""
|
||||
根据学号更新学生信息。
|
||||
- 请求体必须包含完整的 stu_info_req(包括 student_id)。
|
||||
- 返回统一的 stu_info_add_resp。
|
||||
"""
|
||||
try:
|
||||
data = stu_info.model_dump()
|
||||
# 调用 dao 层更新函数(已别名为 update_info)
|
||||
res = update_info(gs, data)
|
||||
if not res:
|
||||
# 没有更新任何行,说明学号不存在或数据未变化
|
||||
raise HTTPException(status_code=500, detail='没有更新!')
|
||||
return stu_info_add_resp(code=200, message='更新成功')
|
||||
except Exception as err:
|
||||
raise HTTPException(status_code=500, detail=str(err))
|
||||
|
||||
|
||||
# -------------------- 删除学生信息(逻辑删除) --------------------
|
||||
@student_info_api.delete('/student', response_model=stu_info_add_resp, summary='删除')
|
||||
def delete_student_info(sid: str, gs=Depends(get_db)):
|
||||
"""
|
||||
根据学号删除学生记录(通常为逻辑删除)。
|
||||
- sid 为查询参数,返回统一的 stu_info_add_resp。
|
||||
"""
|
||||
try:
|
||||
# 调用 dao 层删除函数(已别名为 delete_info),传入学号
|
||||
res = delete_info(gs, sid)
|
||||
if not res:
|
||||
# 没有删除任何行,可能学号不存在
|
||||
raise HTTPException(status_code=500, detail='没有删除')
|
||||
return stu_info_add_resp(code=200, message='删除成功')
|
||||
except Exception as err:
|
||||
raise HTTPException(status_code=500, detail=str(err))
|
||||
@@ -0,0 +1,172 @@
|
||||
import uuid
|
||||
from datetime import date, timedelta, datetime as dt
|
||||
from typing import Optional
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
TABLE = "employment_info"
|
||||
COLS = ["emp_id", "student_id", "class_id", "education", "code",
|
||||
"job_title", "company_name", "salary", "offer_date",
|
||||
"resume_open_date", "employment_int", "is_deleted",
|
||||
"create_time", "update_time"]
|
||||
SELECT_COLS = ", ".join(COLS)
|
||||
|
||||
|
||||
def _convert(d: dict) -> dict:
|
||||
if d is None:
|
||||
return None
|
||||
out = {}
|
||||
for col, val in d.items():
|
||||
if val is None:
|
||||
out[col] = None
|
||||
elif isinstance(val, dt):
|
||||
out[col] = val.isoformat()
|
||||
elif isinstance(val, date):
|
||||
out[col] = val.isoformat()
|
||||
elif hasattr(val, '__float__') and not isinstance(val, bool):
|
||||
try:
|
||||
out[col] = float(val)
|
||||
except:
|
||||
out[col] = val
|
||||
else:
|
||||
out[col] = val
|
||||
return out
|
||||
|
||||
|
||||
def _row_to_dict(row) -> dict:
|
||||
return _convert(dict(row._mapping)) if row else None
|
||||
|
||||
|
||||
def _rows_to_list(rows) -> list[dict]:
|
||||
return [_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
# ==================== 基础 CRUD ====================
|
||||
|
||||
def get_by_emp_id(db: Session, emp_id: str) -> Optional[dict]:
|
||||
sql = text(f"SELECT {SELECT_COLS} FROM {TABLE} WHERE emp_id = :emp_id AND is_deleted = 0")
|
||||
return _row_to_dict(db.execute(sql, {"emp_id": emp_id}).fetchone())
|
||||
|
||||
def list_by_student(db: Session, student_id: str) -> list[dict]:
|
||||
sql = text(f"SELECT {SELECT_COLS} FROM {TABLE} WHERE student_id = :sid AND is_deleted = 0 ORDER BY offer_date DESC")
|
||||
return _rows_to_list(db.execute(sql, {"sid": student_id}).fetchall())
|
||||
|
||||
def list_by_class(db: Session, class_id: str) -> list[dict]:
|
||||
sql = text(f"SELECT {SELECT_COLS} FROM {TABLE} WHERE class_id = :cid AND is_deleted = 0 ORDER BY offer_date DESC")
|
||||
return _rows_to_list(db.execute(sql, {"cid": class_id}).fetchall())
|
||||
|
||||
def list_employment(db: Session, filters: dict) -> list[dict]:
|
||||
conds = ["is_deleted = 0"]
|
||||
params = {}
|
||||
if filters.get("student_id"):
|
||||
conds.append("student_id = :sid"); params["sid"] = filters["student_id"]
|
||||
if filters.get("company_name"):
|
||||
conds.append("company_name LIKE :cn"); params["cn"] = f"%{filters['company_name']}%"
|
||||
if filters.get("salary_min") is not None:
|
||||
conds.append("salary >= :smin"); params["smin"] = filters["salary_min"]
|
||||
if filters.get("salary_max") is not None:
|
||||
conds.append("salary <= :smax"); params["smax"] = filters["salary_max"]
|
||||
sql = text(f"SELECT {SELECT_COLS} FROM {TABLE} WHERE {' AND '.join(conds)} ORDER BY offer_date DESC")
|
||||
return _rows_to_list(db.execute(sql, params).fetchall())
|
||||
|
||||
def create_by_student(db: Session, student_id: str, data_dict: dict) -> dict:
|
||||
emp_id = f"EMP{student_id}_{uuid.uuid4().hex[:8].upper()}"
|
||||
data_dict.pop("student_id", None)
|
||||
all_cols = ["emp_id", "student_id", "is_deleted"] + list(data_dict.keys())
|
||||
all_vals = [emp_id, student_id, 0] + list(data_dict.values())
|
||||
placeholders = ", ".join(f":{c}" for c in all_cols)
|
||||
col_names = ", ".join(all_cols)
|
||||
sql = text(f"INSERT INTO {TABLE} ({col_names}) VALUES ({placeholders})")
|
||||
db.execute(sql, dict(zip(all_cols, all_vals)))
|
||||
db.commit()
|
||||
return get_by_emp_id(db, emp_id)
|
||||
|
||||
def update_by_emp_id(db: Session, emp_id: str, updates: dict) -> Optional[dict]:
|
||||
sql = text(f"UPDATE {TABLE} SET is_deleted = 0 WHERE emp_id = :emp_id")
|
||||
db.execute(sql, {"emp_id": emp_id})
|
||||
db.commit()
|
||||
if not get_by_emp_id(db, emp_id):
|
||||
return None
|
||||
set_clause = ", ".join(f"{k} = :{k}" for k in updates.keys())
|
||||
sql = text(f"UPDATE {TABLE} SET {set_clause} WHERE emp_id = :emp_id")
|
||||
db.execute(sql, {"emp_id": emp_id, **updates})
|
||||
db.commit()
|
||||
return get_by_emp_id(db, emp_id)
|
||||
|
||||
def delete_by_emp_id(db: Session, emp_id: str) -> bool:
|
||||
sql = text(f"UPDATE {TABLE} SET is_deleted = 0 WHERE emp_id = :emp_id")
|
||||
db.execute(sql, {"emp_id": emp_id})
|
||||
db.commit()
|
||||
if not get_by_emp_id(db, emp_id):
|
||||
return False
|
||||
db.execute(text(f"UPDATE {TABLE} SET is_deleted = 1 WHERE emp_id = :emp_id"), {"emp_id": emp_id})
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
# ==================== 新增功能点 ====================
|
||||
|
||||
def get_latest_by_student(db: Session, student_id: str) -> Optional[dict]:
|
||||
sql = text(f"SELECT {SELECT_COLS} FROM {TABLE} WHERE student_id = :sid AND is_deleted = 0 ORDER BY offer_date DESC LIMIT 1")
|
||||
return _row_to_dict(db.execute(sql, {"sid": student_id}).fetchone())
|
||||
|
||||
def get_class_employment_stats(db: Session, class_id: str) -> dict:
|
||||
base_sql = text(f"SELECT employment_int, COUNT(*) as cnt FROM {TABLE} WHERE class_id = :cid AND is_deleted = 0 GROUP BY employment_int")
|
||||
salary_sql = text(f"SELECT AVG(salary), MAX(salary), MIN(salary), COUNT(*) FROM {TABLE} WHERE class_id = :cid AND is_deleted = 0")
|
||||
job_sql = text(f"SELECT job_title, COUNT(*) FROM {TABLE} WHERE class_id = :cid AND is_deleted = 0 GROUP BY job_title")
|
||||
rows = db.execute(base_sql, {"cid": class_id}).fetchall()
|
||||
salary_row = db.execute(salary_sql, {"cid": class_id}).fetchone()
|
||||
job_rows = db.execute(job_sql, {"cid": class_id}).fetchall()
|
||||
stats = {r[0]: r[1] for r in rows}
|
||||
total = sum(stats.values())
|
||||
return {
|
||||
"class_id": class_id,
|
||||
"total_count": total,
|
||||
"employed_count": stats.get(1, 0),
|
||||
"graduate_count": stats.get(2, 0),
|
||||
"flexible_count": stats.get(3, 0),
|
||||
"employment_rate": round((stats.get(1, 0) + stats.get(2, 0) + stats.get(3, 0)) / total * 100, 2) if total > 0 else 0.0,
|
||||
"avg_salary": salary_row[0], "max_salary": salary_row[1], "min_salary": salary_row[2],
|
||||
"job_title_distribution": {r[0]: r[1] for r in job_rows},
|
||||
}
|
||||
|
||||
def get_employment_warnings(db: Session, days: int = 30, class_id: str | None = None) -> list[dict]:
|
||||
cutoff = date.today() - timedelta(days=days)
|
||||
conds = ["is_deleted = 0", "resume_open_date IS NOT NULL", "resume_open_date < :cutoff", "salary IS NULL"]
|
||||
params = {"cutoff": cutoff}
|
||||
if class_id:
|
||||
conds.append("class_id = :cid"); params["cid"] = class_id
|
||||
sql = text(f"SELECT {SELECT_COLS} FROM {TABLE} WHERE {' AND '.join(conds)}")
|
||||
result = []
|
||||
for row in db.execute(sql, params).fetchall():
|
||||
emp = _row_to_dict(row)
|
||||
overdue = (date.today() - date.fromisoformat(emp["resume_open_date"])).days - days
|
||||
result.append({
|
||||
"emp_id": emp["emp_id"], "student_id": emp["student_id"], "class_id": emp["class_id"],
|
||||
"job_title": emp["job_title"], "company_name": emp["company_name"],
|
||||
"resume_open_date": emp["resume_open_date"],
|
||||
"warning_reason": f"简历开放{days}天内未签约,已超{overdue}天",
|
||||
"days_overdue": overdue,
|
||||
})
|
||||
return result
|
||||
|
||||
def compare_salary_with_market(db: Session, student_id: str) -> Optional[dict]:
|
||||
emp = get_latest_by_student(db, student_id)
|
||||
if not emp or not emp.get("code") or not emp.get("job_title"):
|
||||
return None
|
||||
market_sql = text(f"SELECT COUNT(*), AVG(salary), MAX(salary), MIN(salary) FROM {TABLE} WHERE code = :code AND job_title = :jt AND salary IS NOT NULL AND is_deleted = 0")
|
||||
m = db.execute(market_sql, {"code": emp["code"], "jt": emp["job_title"]}).fetchone()
|
||||
deviation = None; advice = "无"
|
||||
if m[1] and emp.get("salary"):
|
||||
deviation = round((float(emp["salary"]) - float(m[1])) / float(m[1]) * 100, 2)
|
||||
if deviation >= 10: advice = "薪资高于行情10%+,非常优秀"
|
||||
elif deviation >= 0: advice = "薪资符合市场行情"
|
||||
elif deviation >= -10: advice = "薪资略低于行情,可考虑谈薪"
|
||||
else: advice = "薪资明显低于行情,建议与HR沟通"
|
||||
return {
|
||||
"code": emp["code"], "job_title": emp["job_title"],
|
||||
"sample_count": m[0], "avg_salary": m[1], "max_salary": m[2], "min_salary": m[3],
|
||||
"current_student_id": student_id, "current_salary": emp["salary"],
|
||||
"deviation_percent": deviation, "advice": advice,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
# 导入 SQLAlchemy 的 Session 类,用于类型提示
|
||||
from sqlalchemy.orm import Session
|
||||
# 导入地区维度表模型
|
||||
from model.student_info_region_model import Region_dimension
|
||||
|
||||
|
||||
# ==================== 查询地区 ====================
|
||||
def get_region_part(session: Session, province: str):
|
||||
"""
|
||||
根据省份查询地区信息。
|
||||
:param session: 数据库会话
|
||||
:param province: 省份名称,若为空则查询全部
|
||||
:return: Region_dimension 对象列表
|
||||
"""
|
||||
try:
|
||||
# 如果传入了省份(非空),则按省份过滤
|
||||
if province:
|
||||
data = session.query(Region_dimension).filter(
|
||||
Region_dimension.province == province
|
||||
).all()
|
||||
else:
|
||||
# 否则查询所有地区
|
||||
data = session.query(Region_dimension).all()
|
||||
return data
|
||||
except Exception as e:
|
||||
# 出现异常直接向上抛出,由 API 层统一处理
|
||||
raise e
|
||||
|
||||
|
||||
# ==================== 添加地区 ====================
|
||||
def add_region(session: Session, d1: dict):
|
||||
"""
|
||||
新增一条地区记录。
|
||||
:param session: 数据库会话
|
||||
:param d1: 地区信息字典,需包含 code、province、city、district
|
||||
:return: 成功时返回字符串 '添加成功'
|
||||
"""
|
||||
try:
|
||||
# 先检查该地区编码是否已存在
|
||||
data = session.query(Region_dimension).filter(
|
||||
Region_dimension.code == d1["code"]
|
||||
).first()
|
||||
|
||||
# 如果已存在,抛出 ValueError(注意:这里没有带错误信息,调用方需自行处理)
|
||||
if data:
|
||||
raise ValueError
|
||||
|
||||
# 根据字典创建 ORM 对象
|
||||
obj = Region_dimension(**d1)
|
||||
session.add(obj) # 加入会话
|
||||
session.commit() # 提交事务,写入数据库
|
||||
session.refresh(obj) # 刷新对象,获取数据库生成的默认值(如自增 id)
|
||||
return '添加成功'
|
||||
except Exception as e:
|
||||
# 出现异常时回滚事务,避免脏数据
|
||||
session.rollback()
|
||||
raise e
|
||||
|
||||
|
||||
# ==================== 修改地区 ====================
|
||||
def update_region(session: Session, code, d1: dict):
|
||||
"""
|
||||
根据地区编码更新地区信息(部分字段更新)。
|
||||
:param session: 数据库会话
|
||||
:param code: 要修改的地区编码
|
||||
:param d1: 待更新的字段字典
|
||||
:return: 成功返回 '修改成功';未找到记录返回 None
|
||||
"""
|
||||
try:
|
||||
# 使用 query.update() 批量更新,返回受影响行数
|
||||
rows = session.query(Region_dimension).filter(
|
||||
Region_dimension.code == code
|
||||
).update(d1)
|
||||
|
||||
# 如果受影响行数为 0,说明编码不存在,无记录可更新
|
||||
if rows == 0:
|
||||
return None
|
||||
|
||||
session.commit()
|
||||
return '修改成功'
|
||||
except Exception as e:
|
||||
# 异常时回滚
|
||||
session.rollback()
|
||||
raise e
|
||||
|
||||
|
||||
# ==================== 删除地区 ====================
|
||||
def delete_region(session: Session, code):
|
||||
"""
|
||||
根据地区编码删除地区记录(物理删除)。
|
||||
:param session: 数据库会话
|
||||
:param code: 要删除的地区编码
|
||||
:return: 成功返回 '删除成功';未找到记录返回 None
|
||||
"""
|
||||
try:
|
||||
# 使用 query.delete() 物理删除,返回受影响行数
|
||||
rows = session.query(Region_dimension).filter(
|
||||
Region_dimension.code == code
|
||||
).delete()
|
||||
|
||||
# 如果受影响行数为 0,说明编码不存在
|
||||
if rows == 0:
|
||||
return None
|
||||
|
||||
session.commit()
|
||||
return '删除成功'
|
||||
except Exception as e:
|
||||
# 异常时回滚
|
||||
session.rollback()
|
||||
raise e
|
||||
@@ -0,0 +1,153 @@
|
||||
# 导入 SQLAlchemy 的 Session 类,用于类型提示
|
||||
from sqlalchemy.orm import Session
|
||||
# 导入学生信息模型和性别枚举
|
||||
from model.student_info_region_model import Student_info, GenderEnum
|
||||
# 导入完整性错误异常,用于捕获外键/唯一约束等数据库错误
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
|
||||
# ==================== 条件查询学生信息 ====================
|
||||
def get_part_student_info(session: Session, student_id: str, student_name: str, gender: str):
|
||||
"""
|
||||
根据学号、姓名、性别查询学生信息(均为可选条件)。
|
||||
:param session: 数据库会话
|
||||
:param student_id: 学号,精确匹配
|
||||
:param student_name: 姓名,精确匹配
|
||||
:param gender: 性别,前端传“男/女”,内部转为“1/0”
|
||||
:return: 学生对象列表
|
||||
"""
|
||||
# 前端传来的性别为 男/女,与数据库的枚举值('0'/'1')不符,需要转换
|
||||
# 注意:若 gender 为 None(不筛选),此处会被错误地转为 '0',导致只查女生。
|
||||
# 建议调用方在不需要筛选性别时不要传该参数,或修改此段逻辑。
|
||||
if gender == '男':
|
||||
gender = '1'
|
||||
else:
|
||||
gender = '0'
|
||||
|
||||
try:
|
||||
# 创建基础查询
|
||||
query = session.query(Student_info)
|
||||
|
||||
# 过滤逻辑删除:只查询未删除的记录(is_deleted = '0')
|
||||
query = query.filter(Student_info.is_deleted == '0')
|
||||
|
||||
# 如果传入学号,则按学号精确过滤
|
||||
if student_id is not None:
|
||||
query = query.filter(Student_info.student_id == student_id)
|
||||
|
||||
# 如果传入姓名,则按姓名精确过滤
|
||||
# 若需模糊查询,可将 == 改为 like(f"%{student_name}%")
|
||||
if student_name is not None:
|
||||
query = query.filter(Student_info.student_name == student_name)
|
||||
|
||||
# 如果传入性别,则按性别过滤(已转为 '0'/'1',再转为枚举)
|
||||
if gender is not None:
|
||||
# 把字符串转成枚举,SQLAlchemy 才能正确比较
|
||||
query = query.filter(Student_info.gender == GenderEnum(gender))
|
||||
|
||||
# 返回所有匹配记录
|
||||
return query.all()
|
||||
except Exception as e:
|
||||
# 出现异常直接向上抛出,由 API 层统一处理
|
||||
raise e
|
||||
|
||||
|
||||
# ==================== 添加学生信息 ====================
|
||||
def add_student_info(session: Session, data: dict):
|
||||
"""
|
||||
新增一条学生记录。
|
||||
:param session: 数据库会话
|
||||
:param data: 学生信息字典,键需与模型字段对应
|
||||
:return: 成功时返回字符串 '添加成功'
|
||||
"""
|
||||
try:
|
||||
# 先检查学号是否已存在
|
||||
rows = session.query(Student_info).filter(
|
||||
Student_info.student_id == data['student_id']
|
||||
).first()
|
||||
|
||||
# 如果已存在,返回 ValueError 类(注意:这里返回的是类,不是实例,也不是抛出异常)
|
||||
# 调用方如果做 `if not res` 判断,ValueError 类是真值,可能导致逻辑错误。
|
||||
# 建议改为 `raise ValueError("学号已存在")`
|
||||
if rows is not None:
|
||||
return ValueError
|
||||
|
||||
# 根据字典创建 ORM 对象
|
||||
obj = Student_info(**data)
|
||||
session.add(obj) # 加入会话
|
||||
session.commit() # 提交事务,写入数据库
|
||||
return '添加成功'
|
||||
except Exception as e:
|
||||
# 出现异常时回滚事务,避免脏数据
|
||||
session.rollback()
|
||||
raise e
|
||||
|
||||
|
||||
# ==================== 修改学生信息 ====================
|
||||
def update_student_info(session: Session, data: dict):
|
||||
"""
|
||||
根据学号更新学生信息(全量更新)。
|
||||
:param session: 数据库会话
|
||||
:param data: 包含 student_id 及待更新字段的字典
|
||||
:return: 成功返回 '修改成功';未找到记录返回 None
|
||||
"""
|
||||
try:
|
||||
# 使用 query.update() 批量更新,返回受影响行数
|
||||
rows = session.query(Student_info).filter(
|
||||
Student_info.student_id == data['student_id']
|
||||
).update(data)
|
||||
|
||||
# 如果受影响行数为 0,说明学号不存在,无记录可更新
|
||||
if rows == 0:
|
||||
return None
|
||||
|
||||
session.commit()
|
||||
return '修改成功'
|
||||
|
||||
except IntegrityError as e:
|
||||
# 捕获数据库完整性错误(外键、唯一约束等),回滚并翻译为友好提示
|
||||
session.rollback()
|
||||
err_msg = str(e.orig) # 获取底层 MySQL 错误信息
|
||||
|
||||
if 'fk_student_region' in err_msg:
|
||||
# 外键 fk_student_region 失败:region_id 在地区表中不存在
|
||||
raise ValueError(
|
||||
f"region_id '{data.get('region_id')}' 在地区表中不存在,请先维护 region_dimension"
|
||||
)
|
||||
elif 'id_card' in err_msg:
|
||||
# 身份证号唯一约束冲突
|
||||
raise ValueError(f"身份证号 '{data.get('id_card')}' 已被其他学生占用")
|
||||
else:
|
||||
# 其他完整性错误
|
||||
raise ValueError(f"数据完整性错误: {err_msg}")
|
||||
|
||||
except Exception as e:
|
||||
# 其他异常同样回滚并抛出
|
||||
session.rollback()
|
||||
raise e
|
||||
|
||||
|
||||
# ==================== 删除学生信息(逻辑删除) ====================
|
||||
def delete_student_info(session: Session, student_id: str):
|
||||
"""
|
||||
根据学号进行逻辑删除(将 is_deleted 置为 '1')。
|
||||
:param session: 数据库会话
|
||||
:param student_id: 学号
|
||||
:return: 成功返回 '修改成功';未找到记录返回 None
|
||||
"""
|
||||
try:
|
||||
# 使用 update 将 is_deleted 改为 '1',实现逻辑删除
|
||||
rows = session.query(Student_info).filter(
|
||||
Student_info.student_id == student_id
|
||||
).update({'is_deleted': '1'})
|
||||
|
||||
# 没有匹配记录,返回 None
|
||||
if rows == 0:
|
||||
return None
|
||||
|
||||
session.commit()
|
||||
return '修改成功'
|
||||
except Exception as e:
|
||||
# 异常时回滚
|
||||
session.rollback()
|
||||
raise e
|
||||
@@ -0,0 +1,48 @@
|
||||
# 导入 Python 标准库 enum,用于定义枚举类型
|
||||
import enum
|
||||
|
||||
# ==================== 性别枚举 ====================
|
||||
class GenderEnum(str, enum.Enum):
|
||||
"""
|
||||
性别枚举:继承 str 和 enum.Enum,使成员值可直接作为字符串使用。
|
||||
与数据库 enum('0','1') 对应。
|
||||
"""
|
||||
female = "0" # 女
|
||||
male = "1" # 男
|
||||
|
||||
|
||||
# ==================== 学籍状态枚举 ====================
|
||||
class StudentStatusEnum(str, enum.Enum):
|
||||
"""
|
||||
学籍状态枚举,与数据库 enum('0','1','2','3','4') 对应。
|
||||
"""
|
||||
studying = "0" # 在读
|
||||
suspended = "1" # 休学
|
||||
quit = "2" # 退学
|
||||
graduated = "3" # 毕业
|
||||
completed = "4" # 结业
|
||||
|
||||
|
||||
# ==================== 学历层次枚举 ====================
|
||||
class EducationLevelEnum(str, enum.Enum):
|
||||
"""
|
||||
学历层次枚举,与数据库 enum('1','2','3','4') 对应。
|
||||
"""
|
||||
college = "1" # 大专
|
||||
bachelor = "2" # 本科
|
||||
master = "3" # 硕士研究生
|
||||
doctor = "4" # 博士研究生
|
||||
|
||||
|
||||
# ==================== 逻辑删除枚举 ====================
|
||||
class IsDeletedEnum(str, enum.Enum):
|
||||
"""
|
||||
逻辑删除标志枚举。
|
||||
注意:此处成员值为整数 0 和 1,而其他枚举均为字符串。
|
||||
数据库列定义为 enum('0','1')(字符串),因此建议将此处改为:
|
||||
no = "0"
|
||||
yes = "1"
|
||||
以保持类型一致,避免 SQLAlchemy 绑定参数时出现类型不匹配问题。
|
||||
"""
|
||||
no = "0" # 未删除
|
||||
yes = "1" # 已删除
|
||||
@@ -2,7 +2,12 @@ from fastapi import FastAPI
|
||||
from database import engine, Base
|
||||
Base.metadata.create_all(engine)
|
||||
from api.calss import class_api
|
||||
from api.employment_api import app as employment_router
|
||||
from api.student_info_api import student_info_api
|
||||
from api.region_api import region_api
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.add_middleware(
|
||||
@@ -15,7 +20,14 @@ app.add_middleware(
|
||||
|
||||
app.include_router(class_api,tags=['班级接口'],prefix='/stutent')
|
||||
|
||||
app.include_router(student_info_api,tags=['学生信息'])
|
||||
|
||||
app.include_router(region_api,tags=['地区信息'])
|
||||
|
||||
app.include_router(employment_router)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("main:app", host='127.0.0.1', port=12345)
|
||||
uvicorn.run("main:app", host='127.0.0.1', port=12345)
|
||||
+1
-50
@@ -70,53 +70,4 @@ class StudentClassIntermediate(Base):
|
||||
student_id =Column(String(20))
|
||||
is_deleted= Column(Integer)
|
||||
create_time= Column(DateTime)
|
||||
update_time= Column(DateTime)
|
||||
|
||||
|
||||
|
||||
|
||||
class GenderEnum(str, enum.Enum):
|
||||
FEMALE = '0'
|
||||
MALE = '1'
|
||||
|
||||
class StudentStatusEnum(str, enum.Enum):
|
||||
NORMAL = '0' # 正常
|
||||
SUSPENDED = '1' # 休学
|
||||
DROPPED = '2' # 退学
|
||||
GRADUATED = '3' # 毕业
|
||||
OTHER = '4' # 其他
|
||||
|
||||
class EducationLevelEnum(str, enum.Enum):
|
||||
COLLEGE = '1' # 大专
|
||||
UNDERGRADUATE = '2' # 本科
|
||||
MASTER = '3' # 硕士
|
||||
DOCTOR = '4' # 博士
|
||||
|
||||
class IsDeletedEnum(str, enum.Enum):
|
||||
ACTIVE = '0' # 未删除
|
||||
DELETED = '1' # 已删除
|
||||
|
||||
# 3. 定义学生模型类
|
||||
class Student_info(Base):
|
||||
__tablename__ = "student_info"
|
||||
student_id = Column(String(20),primary_key=True, comment='学号')
|
||||
student_name = Column(String(50), nullable=False, comment='学生姓名')
|
||||
gender = Column(Enum(GenderEnum), nullable=False, comment='性别: 0=女, 1=男')
|
||||
id_card = Column(String(18), nullable=False, comment='身份证号码,收紧至18位')
|
||||
birthday = Column(Date, nullable=True, comment='出生日期')
|
||||
ethnicity = Column(String(20), nullable=True, comment='民族')
|
||||
region_id = Column(String(12), nullable=True, comment='籍贯地区编码,外键关联地区维度表(region_dimension)')
|
||||
phone = Column(String(11), nullable=True, comment='手机号码,收紧至11位')
|
||||
graduation_school = Column(String(20), nullable=True, comment='毕业学校')
|
||||
major = Column(String(20), nullable=False, comment='就读专业')
|
||||
class_id = Column(String(20), nullable=False, index=True, comment='所在班级,外键关联班级表(class_info)')
|
||||
enrollment_date = Column(Date, nullable=True, comment='入学日期')
|
||||
graduation_date = Column(Date, nullable=True, comment='毕业日期')
|
||||
student_status = Column(Enum(StudentStatusEnum), nullable=False, default=StudentStatusEnum.NORMAL,
|
||||
comment='学籍状态')
|
||||
education_level = Column(Enum(EducationLevelEnum), nullable=False,
|
||||
comment='学历层次:1=大专, 2=本科, 3=硕士研究生, 4=博士研究生')
|
||||
is_deleted = Column(Enum(IsDeletedEnum), nullable=False, default=IsDeletedEnum.ACTIVE,
|
||||
comment='逻辑删除:0=未删除, 1=已删除')
|
||||
create_time = Column(DateTime, nullable=False, server_default=func.now(), comment='创建时间')
|
||||
update_time = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now(), comment='更新时间')
|
||||
update_time= Column(DateTime)
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""就业信息 ORM 实体"""
|
||||
from sqlalchemy import Column, String, ForeignKey, Numeric, Date, DateTime, Integer
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from database import Base
|
||||
|
||||
|
||||
class Employment(Base):
|
||||
__tablename__ = "employment_info"
|
||||
__table_args__ = {
|
||||
"comment": "学生就业信息管理",
|
||||
"mysql_charset": "utf8mb4",
|
||||
"mysql_collate": "utf8mb4_general_ci",
|
||||
}
|
||||
emp_id = Column(String(32), primary_key=True, comment="就业记录ID")
|
||||
student_id = Column(String(20), ForeignKey("student_info.uk_student_id"),
|
||||
nullable=False, index=True, comment="学号")
|
||||
class_id = Column(String(64), nullable=True, comment="班级ID")
|
||||
code = Column(String(12), ForeignKey("region.code"),
|
||||
nullable=False, index=True, comment="就业城市编码")
|
||||
job_title = Column(String(50), nullable=False, comment="就业岗位名称")
|
||||
company_name = Column(String(128), nullable=False, comment="就业公司名称")
|
||||
salary = Column(Numeric(12, 2), nullable=True, comment="就业月薪(元)")
|
||||
offer_date = Column(Date, nullable=False, comment="offer下发/签约时间")
|
||||
resume_open_date = Column(Date, nullable=True, comment="就业开放时间")
|
||||
employment_int = Column(Integer, nullable=False, default=1,
|
||||
comment="就业状态:1=已就业,2=升学,3=灵活就业")
|
||||
is_deleted = Column(Integer, nullable=False, default=0,
|
||||
comment="逻辑删除:0=未删除,1=已删除")
|
||||
create_time = Column(DateTime, nullable=False, server_default=func.now(),
|
||||
comment="创建时间")
|
||||
update_time = Column(DateTime, nullable=False, server_default=func.now(),
|
||||
onupdate=func.now(), comment="更新时间")
|
||||
@@ -0,0 +1,95 @@
|
||||
# 从 database 模块导入 Base(ORM 基类)和 engine(数据库引擎,别名为 db_engine)
|
||||
from database import Base, engine as db_engine
|
||||
|
||||
# 从 sqlalchemy 导入 Enum,并别名为 SAEnum,用于定义数据库枚举列
|
||||
from sqlalchemy import Enum as SAEnum, Integer
|
||||
|
||||
# 从 sqlalchemy 导入常用组件:inspect(检查表是否存在)、Column(列)、
|
||||
# VARCHAR/BIGINT/Date/DateTime(字段类型)、func(SQL 函数,如 now())
|
||||
from sqlalchemy import inspect, Column, VARCHAR, BIGINT, Date, DateTime, func
|
||||
|
||||
# 从自定义枚举模块导入学生状态、性别、学历层次、逻辑删除枚举
|
||||
from enums import StudentStatusEnum, GenderEnum, EducationLevelEnum, IsDeletedEnum
|
||||
|
||||
|
||||
def _sa_enum(e):
|
||||
"""
|
||||
统一让 SQLAlchemy 存枚举的 value 而不是 name。
|
||||
默认 SQLAlchemy 的 Enum 存的是枚举成员名(如 'male'),
|
||||
这里通过 values_callable 指定存枚举的值(如 '1'、'0')。
|
||||
"""
|
||||
return SAEnum(e, values_callable=lambda x: [m.value for m in x])
|
||||
|
||||
# ======================== 学生 ==========================
|
||||
# 学生信息表模型,继承 Base(declarative_base 生成的基类)
|
||||
class Student_info(Base):
|
||||
__tablename__ = 'student_info' # 数据库表名
|
||||
# 表参数:指定字符集和排序规则为 utf8mb4
|
||||
__table_args__ = {'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_general_ci'}
|
||||
|
||||
# 学号,主键,非空,长度 20
|
||||
student_id = Column(VARCHAR(20), primary_key=True, nullable=False, comment='学号,收紧长度')
|
||||
# 学生姓名,非空,长度 50
|
||||
student_name = Column(VARCHAR(50), nullable=False, comment='学生姓名')
|
||||
# 性别,枚举类型(存 value:'0'=女,'1'=男),非空
|
||||
gender = Column(Integer, nullable=False, comment='性别:0=女,1=男')
|
||||
# 身份证号码,唯一,非空,长度 18
|
||||
id_card = Column(VARCHAR(18), unique=True, nullable=False, comment='身份证号码,收紧至18位')
|
||||
# 出生日期,可空
|
||||
birthday = Column(Date, nullable=True, comment='出生日期')
|
||||
# 民族,可空,长度 20
|
||||
ethnicity = Column(VARCHAR(20), nullable=True, comment='民族')
|
||||
# 籍贯地区编码,可空,带索引(注释说外键关联 region_dimension,但模型里未定义 ForeignKey)
|
||||
region_id = Column(VARCHAR(12), nullable=True, index=True,
|
||||
comment='籍贯地区编码,外键关联地区维度表(region_dimension)')
|
||||
# 手机号码,可空,长度 11
|
||||
phone = Column(VARCHAR(11), nullable=True, comment='手机号码,收紧至11位')
|
||||
# 毕业学校,可空,长度 20
|
||||
graduation_school = Column(VARCHAR(20), nullable=True, comment='毕业学校')
|
||||
# 就读专业,非空,长度 20
|
||||
major = Column(VARCHAR(20), nullable=False, comment='就读专业')
|
||||
# 所在班级,非空,长度 20,带索引(注释说外键关联 class_info,但模型里未定义 ForeignKey)
|
||||
class_id = Column(VARCHAR(20), nullable=False, index=True, comment='所在班级,外键关联班级表(class_info)')
|
||||
# 入学日期,可空
|
||||
enrollment_date = Column(Date, nullable=True, comment='入学日期')
|
||||
# 毕业日期,可空
|
||||
graduation_date = Column(Date, nullable=True, comment='毕业日期')
|
||||
# 学籍状态,枚举,非空:0=在读,1=休学,2=退学,3=毕业,4=结业
|
||||
student_status = Column(Integer, nullable=False,comment='学籍状态:0=在读,1=休学,2=退学,3=毕业,4=结业')
|
||||
# 学历层次,枚举,非空:1=大专,2=本科,3=硕士研究生,4=博士研究生
|
||||
education_level = Column(Integer, nullable=False,comment='学历层次:1=大专,2=本科,3=硕士研究生,4=博士研究生')
|
||||
# 逻辑删除标志,枚举,非空,默认值为 IsDeletedEnum.no('0'=未删除)
|
||||
is_deleted = Column(Integer, default=IsDeletedEnum.no, nullable=False,comment='逻辑删除:0=未删除,1=已删除')
|
||||
# 创建时间,非空,服务器默认值为当前时间(数据库端生成)
|
||||
create_time = Column(DateTime, nullable=False, server_default=func.now(), comment='创建时间')
|
||||
# 更新时间,非空,服务器默认值为当前时间,更新时自动刷新为当前时间
|
||||
update_time = Column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now(),
|
||||
comment='更新时间')
|
||||
|
||||
# ======================== 地区 ==========================
|
||||
# 地区维度表模型
|
||||
class Region_dimension(Base):
|
||||
__tablename__ = 'region_dimension' # 表名
|
||||
# 表参数:utf8mb4 字符集
|
||||
__table_args__ = {'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_general_ci'}
|
||||
|
||||
# 自增主键,BIGINT 类型
|
||||
id = Column(BIGINT, primary_key=True, autoincrement=True, comment='自增主键')
|
||||
# 地区编码,唯一,非空,长度 12
|
||||
code = Column(VARCHAR(12), unique=True, nullable=False, comment='地区编码')
|
||||
# 省 / 直辖市,非空
|
||||
province = Column(VARCHAR, nullable=False, comment='省 / 直辖市')
|
||||
# 市,非空
|
||||
city = Column(VARCHAR, nullable=False, comment='市')
|
||||
# 区 / 县,非空
|
||||
district = Column(VARCHAR, nullable=False, comment='区 / 县')
|
||||
|
||||
|
||||
# 判断表是否存在,不存在则创建表
|
||||
# 检查 student_info 表是否存在
|
||||
if not inspect(db_engine).has_table('student_info'):
|
||||
# 创建所有继承 Base 的表(此处会创建 student_info 和 region_dimension)
|
||||
Base.metadata.create_all(db_engine)
|
||||
# 检查 region_dimension 表是否存在
|
||||
if not inspect(db_engine).has_table('region_dimension'):
|
||||
Base.metadata.create_all(db_engine)
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""统一响应包装体"""
|
||||
from typing import Generic, Optional, TypeVar
|
||||
from pydantic import BaseModel
|
||||
T = TypeVar("T")
|
||||
class ApiResponse(BaseModel, Generic[T]):
|
||||
code: int = 200
|
||||
msg: str = "success"
|
||||
data: Optional[T] = None
|
||||
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class EmploymentBase(BaseModel):
|
||||
student_id: str = Field(..., max_length=20, description="学生ID")
|
||||
class_id: Optional[str] = Field(None, max_length=64, description="班级ID")
|
||||
education: Optional[str] = Field(None, max_length=20, description="学历:专科/本科/硕士/博士")
|
||||
code: str = Field(..., max_length=12, description="就业城市编码")
|
||||
job_title: str = Field(..., max_length=50, description="就业岗位名称")
|
||||
company_name: str = Field(..., max_length=128, description="就业公司名称")
|
||||
salary: Optional[float] = Field(None, description="就业月薪(元)")
|
||||
offer_date: date = Field(..., description="offer下发/签约时间")
|
||||
resume_open_date: Optional[date] = Field(None, description="简历开放时间")
|
||||
employment_int: int = Field(..., description="就业状态:1=已就业,2=升学,3=灵活就业")
|
||||
|
||||
|
||||
class EmploymentUpsert(EmploymentBase):
|
||||
pass
|
||||
|
||||
|
||||
class EmploymentUpdate(BaseModel):
|
||||
class_id: Optional[str] = Field(None, max_length=64, description="班级ID")
|
||||
education: Optional[str] = Field(None, max_length=20, description="学历")
|
||||
code: Optional[str] = Field(None, max_length=12, description="就业城市编码")
|
||||
job_title: Optional[str] = Field(None, max_length=50, description="就业岗位名称")
|
||||
company_name: Optional[str] = Field(None, max_length=128, description="就业公司名称")
|
||||
salary: Optional[float] = Field(None, description="就业月薪(元)")
|
||||
offer_date: Optional[date] = Field(None, description="offer下发/签约时间")
|
||||
resume_open_date: Optional[date] = Field(None, description="简历开放时间")
|
||||
employment_int: Optional[int] = Field(None, description="就业状态:1=已就业,2=升学,3=灵活就业")
|
||||
|
||||
model_config = ConfigDict(
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"class_id": "c01",
|
||||
"education": "本科",
|
||||
"code": "310115",
|
||||
"job_title": "AI开发工程师",
|
||||
"company_name": "上海拼多多",
|
||||
"salary": 25000,
|
||||
"offer_date": "2026-08-01",
|
||||
"resume_open_date": "2026-05-15",
|
||||
"employment_int": 1,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class EmploymentOut(BaseModel):
|
||||
emp_id: str = Field(..., max_length=32, description="就业记录ID")
|
||||
student_id: str = Field(..., max_length=20, description="学生ID")
|
||||
class_id: Optional[str] = Field(None, max_length=64, description="班级ID")
|
||||
education: Optional[str] = Field(None, max_length=20, description="学历")
|
||||
code: str = Field(..., max_length=12, description="就业城市编码")
|
||||
job_title: str = Field(..., max_length=50, description="就业岗位名称")
|
||||
company_name: str = Field(..., max_length=128, description="就业公司名称")
|
||||
salary: Optional[float] = Field(None, description="就业月薪(元)")
|
||||
offer_date: date = Field(..., description="offer下发/签约时间")
|
||||
resume_open_date: Optional[date] = Field(None, description="简历开放时间")
|
||||
employment_int: int = Field(..., description="就业状态")
|
||||
is_deleted: int = Field(..., description="是否删除 0否1是")
|
||||
create_time: datetime = Field(..., description="创建时间")
|
||||
update_time: datetime = Field(..., description="更新时间")
|
||||
|
||||
|
||||
# ==================== 新增功能点 ====================
|
||||
|
||||
class ClassEmploymentStats(BaseModel):
|
||||
class_id: str = Field(..., description="班级ID")
|
||||
total_count: int = Field(..., description="班级总就业记录数")
|
||||
employed_count: int = Field(..., description="已就业")
|
||||
graduate_count: int = Field(..., description="升学")
|
||||
flexible_count: int = Field(..., description="灵活就业")
|
||||
employment_rate: float = Field(..., description="就业率(%)")
|
||||
avg_salary: Optional[float] = Field(None, description="平均薪资")
|
||||
max_salary: Optional[float] = Field(None, description="最高薪资")
|
||||
min_salary: Optional[float] = Field(None, description="最低薪资")
|
||||
job_title_distribution: dict = Field(..., description="各岗位人数分布")
|
||||
|
||||
|
||||
class SalaryMarketComparison(BaseModel):
|
||||
code: str = Field(..., description="城市编码")
|
||||
job_title: str = Field(..., description="岗位名称")
|
||||
sample_count: int = Field(..., description="行情样本数")
|
||||
avg_salary: Optional[float] = Field(None, description="行情平均薪资")
|
||||
max_salary: Optional[float] = Field(None, description="行情最高薪资")
|
||||
min_salary: Optional[float] = Field(None, description="行情最低薪资")
|
||||
current_student_id: str = Field(..., description="学生ID")
|
||||
current_salary: Optional[float] = Field(None, description="当前学生薪资")
|
||||
deviation_percent: Optional[float] = Field(None, description="偏差百分比")
|
||||
advice: str = Field(..., description="薪资建议")
|
||||
@@ -0,0 +1,84 @@
|
||||
# 从 pydantic 导入 BaseModel(模型基类)和 ConfigDict(模型配置)
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
# 导入日期类型,用于 birthday、enrollment_date 等字段
|
||||
from datetime import date
|
||||
# 导入 Optional,用于声明可选字段(可为 None)
|
||||
from typing import Optional
|
||||
# 从自定义枚举模块导入学生状态、性别、学历层次枚举
|
||||
from enums import StudentStatusEnum, GenderEnum, EducationLevelEnum
|
||||
|
||||
# Optional 类型层面的声明,告诉 Pydantic:这个字段可以是 None
|
||||
|
||||
# ======================== 学生 ==========================
|
||||
# 学生信息请求体:用于新增/修改学生时的入参校验
|
||||
class stu_info_req(BaseModel):
|
||||
student_id: str # 学号
|
||||
student_name: str # 学生姓名
|
||||
gender: GenderEnum # 性别(枚举:0=女,1=男)
|
||||
id_card: str # 身份证号码
|
||||
birthday: date # 出生日期
|
||||
ethnicity: str # 民族
|
||||
region_id: str # 籍贯地区编码
|
||||
phone: str # 手机号码
|
||||
graduation_school: str # 毕业学校
|
||||
major: str # 就读专业
|
||||
class_id: str # 所在班级
|
||||
enrollment_date: date # 入学日期
|
||||
graduation_date: date # 毕业日期
|
||||
student_status: StudentStatusEnum # 学籍状态(枚举:0=在读,1=休学,2=退学,3=毕业,4=结业)
|
||||
education_level: EducationLevelEnum # 学历层次(枚举:1=大专,2=本科,3=硕士,4=博士)
|
||||
|
||||
# 学生信息响应体:用于查询学生时的出参序列化
|
||||
class stu_info_resp(BaseModel):
|
||||
student_id: str # 学号
|
||||
student_name: str # 学生姓名
|
||||
gender: GenderEnum # 性别
|
||||
id_card: str # 身份证号码
|
||||
birthday: Optional[date] = None # 出生日期(可空)
|
||||
ethnicity: Optional[str] = None # 民族(可空)
|
||||
region_id: Optional[str] = None # 籍贯地区编码(可空)
|
||||
phone: Optional[str] = None # 手机号码(可空)
|
||||
graduation_school: Optional[str] = None # 毕业学校(可空)
|
||||
major: str # 就读专业
|
||||
class_id: str # 所在班级
|
||||
enrollment_date: Optional[date] = None # 入学日期(可空)
|
||||
graduation_date: Optional[date] = None # 毕业日期(可空)
|
||||
student_status: StudentStatusEnum # 学籍状态
|
||||
education_level: EducationLevelEnum # 学历层次
|
||||
|
||||
# 配置:允许从 ORM 对象属性中读取数据(配合 from_attributes=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# 学生信息增删改操作的统一响应体
|
||||
class stu_info_add_resp(BaseModel):
|
||||
code: int # 状态码,200 表示成功
|
||||
message: Optional[str] = None # 提示信息(可空)
|
||||
|
||||
# ======================== 地区 ==========================
|
||||
# 地区维度表 添加请求体:用于新增地区时的入参校验
|
||||
class region_req(BaseModel):
|
||||
code: str # 地区编码
|
||||
province: str # 省 / 直辖市
|
||||
city: str # 市
|
||||
district: str # 区 / 县
|
||||
|
||||
# 地区维度表 修改请求体:所有字段均可选,code 不在修改范围内
|
||||
class region_update_req(BaseModel):
|
||||
province: Optional[str] = None # 省 / 直辖市(可选)
|
||||
city: Optional[str] = None # 市(可选)
|
||||
district: Optional[str] = None # 区 / 县(可选)
|
||||
|
||||
# 地区维度 响应体:用于查询地区时的出参序列化
|
||||
class region_resp(BaseModel):
|
||||
code: str # 地区编码
|
||||
province: str # 省 / 直辖市
|
||||
city: str # 市
|
||||
district: str # 区 / 县
|
||||
|
||||
# 配置:允许从 ORM 对象属性中读取数据
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# 地区增删改操作的统一响应体
|
||||
class region_add_resp(BaseModel):
|
||||
code: int # 状态码,200 表示成功
|
||||
message: Optional[str] = None # 提示信息(可空)
|
||||
Reference in New Issue
Block a user