feat(candidate): 增加候选人LLM筛选及评分功能
- 在候选人数据库表中新增llm_filtered标记、llm_score评分及详情字段及索引 - 在领域模型Candidate中新增LLM筛选状态和评分相关属性 - 更新ORM映射类添加llm_filtered、llm_score、llm_score_details字段映射 - 扩展候选人数据访问层,支持基于LLM筛选状态和评分范围的查询与分页 - 提供批量标记候选人LLM筛选状态的接口支持 - 新增候选人管理API路由,包含查询、筛选、标记和更新评分的接口 - 定义完整的请求和响应Schema,使用统一响应封装结构 - 更新应用启动代码注册候选人管理接口路由,完善模块导入及初始化逻辑
This commit is contained in:
@@ -54,6 +54,9 @@ CREATE TABLE IF NOT EXISTS candidates (
|
||||
salary_min INT, -- 期望薪资下限(K)
|
||||
salary_max INT, -- 期望薪资上限(K)
|
||||
status VARCHAR(32) DEFAULT 'NEW', -- NEW, ANALYZED, PUSHED, CONTACTED, INTERVIEWED, HIRED, REJECTED
|
||||
llm_filtered TINYINT DEFAULT 0, -- 是否通过LLM筛选: 0-否, 1-是
|
||||
llm_score DECIMAL(4,1), -- LLM综合评分 0-100
|
||||
llm_score_details JSON, -- LLM各维度评分详情
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_source_source_id (source, source_id),
|
||||
@@ -61,7 +64,10 @@ CREATE TABLE IF NOT EXISTS candidates (
|
||||
INDEX idx_email (email),
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_llm_filtered (llm_filtered),
|
||||
INDEX idx_llm_score (llm_score),
|
||||
INDEX idx_llm_filtered_score (llm_filtered, llm_score)
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
|
||||
@@ -37,7 +37,7 @@ class RecruiterModel(Base):
|
||||
class CandidateModel(Base):
|
||||
"""候选人主表"""
|
||||
__tablename__ = 'candidates'
|
||||
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
source = Column(String(32), nullable=False)
|
||||
source_id = Column(String(128), nullable=False)
|
||||
@@ -56,6 +56,9 @@ class CandidateModel(Base):
|
||||
salary_min = Column(Integer)
|
||||
salary_max = Column(Integer)
|
||||
status = Column(String(32), default='NEW')
|
||||
llm_filtered = Column(Integer, default=0) # 是否通过LLM筛选: 0-否, 1-是
|
||||
llm_score = Column(DECIMAL(4, 1)) # LLM综合评分 0-100
|
||||
llm_score_details = Column(JSON) # LLM各维度评分详情
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ FastAPI主应用入口
|
||||
- routes/recruiter.py: 招聘者账号管理
|
||||
- routes/scheduler.py: 定时任务管理
|
||||
- routes/system.py: 系统接口
|
||||
- routes/candidate.py: 候选人管理
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from .routes import recruiter_router, scheduler_router
|
||||
from .routes import recruiter_router, scheduler_router, candidate_router
|
||||
from .routes.system import router as system_router
|
||||
|
||||
|
||||
@@ -22,7 +23,7 @@ def create_app() -> FastAPI:
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
|
||||
# CORS配置
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -31,17 +32,20 @@ def create_app() -> FastAPI:
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# 注册路由
|
||||
# 系统路由(根路径、健康检查等)
|
||||
app.include_router(system_router)
|
||||
|
||||
|
||||
# 招聘者账号管理路由
|
||||
app.include_router(recruiter_router)
|
||||
|
||||
|
||||
# 定时任务管理路由
|
||||
app.include_router(scheduler_router)
|
||||
|
||||
|
||||
# 候选人管理路由
|
||||
app.include_router(candidate_router)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ API路由模块
|
||||
- recruiter: 招聘者账号管理
|
||||
- scheduler: 定时任务管理
|
||||
- system: 系统接口
|
||||
- candidate: 候选人管理
|
||||
"""
|
||||
|
||||
from .recruiter import router as recruiter_router
|
||||
@@ -22,8 +23,15 @@ except ImportError:
|
||||
from fastapi import APIRouter
|
||||
system_router = APIRouter()
|
||||
|
||||
try:
|
||||
from .candidate import router as candidate_router
|
||||
except ImportError:
|
||||
from fastapi import APIRouter
|
||||
candidate_router = APIRouter()
|
||||
|
||||
__all__ = [
|
||||
"recruiter_router",
|
||||
"scheduler_router",
|
||||
"system_router"
|
||||
"system_router",
|
||||
"candidate_router"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
候选人管理 API 路由
|
||||
|
||||
提供候选人查询、筛选、标记等功能
|
||||
"""
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from ..schemas import (
|
||||
BaseResponse, PaginationData,
|
||||
CandidateResponse, CandidateListResponse,
|
||||
CandidateFilterRequest, CandidateMarkFilteredRequest,
|
||||
CandidateUpdateScoreRequest
|
||||
)
|
||||
from ...mapper.candidate_mapper import CandidateMapper
|
||||
|
||||
|
||||
router = APIRouter(prefix="/candidates", tags=["候选人管理"])
|
||||
|
||||
|
||||
def _candidate_to_response(candidate) -> CandidateResponse:
|
||||
"""将领域实体转换为响应模型"""
|
||||
from ...domain.enums import Gender
|
||||
|
||||
gender_map = {Gender.MALE: "男", Gender.FEMALE: "女", Gender.UNKNOWN: "未知"}
|
||||
|
||||
return CandidateResponse(
|
||||
id=candidate.id,
|
||||
name=candidate.name,
|
||||
source=candidate.source.value if candidate.source else "",
|
||||
status=candidate.status.value if candidate.status else "",
|
||||
phone=candidate.phone,
|
||||
email=candidate.email,
|
||||
gender=gender_map.get(candidate.gender, "未知"),
|
||||
age=candidate.age,
|
||||
location=candidate.location,
|
||||
current_company=candidate.current_company,
|
||||
current_position=candidate.current_position,
|
||||
education=candidate.education,
|
||||
school=candidate.school,
|
||||
llm_filtered=candidate.llm_filtered,
|
||||
llm_score=float(candidate.llm_score) if candidate.llm_score else None,
|
||||
llm_score_details=candidate.llm_score_details,
|
||||
created_at=candidate.created_at,
|
||||
updated_at=candidate.updated_at
|
||||
)
|
||||
|
||||
|
||||
@router.get("/filtered", response_model=BaseResponse[CandidateListResponse])
|
||||
async def get_filtered_candidates(
|
||||
llm_filtered: bool = Query(True, description="是否通过LLM筛选"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量")
|
||||
):
|
||||
"""
|
||||
获取LLM筛选通过的候选人列表
|
||||
|
||||
Args:
|
||||
llm_filtered: 是否只显示通过LLM筛选的候选人
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
|
||||
Returns:
|
||||
BaseResponse[CandidateListResponse]: 统一响应格式的候选人列表
|
||||
"""
|
||||
try:
|
||||
mapper = CandidateMapper()
|
||||
candidates, total = mapper.find_by_llm_filtered(llm_filtered, page, page_size)
|
||||
|
||||
response = CandidateListResponse(
|
||||
total=total,
|
||||
items=[_candidate_to_response(c) for c in candidates]
|
||||
)
|
||||
|
||||
return BaseResponse.success(data=response, msg="获取候选人列表成功")
|
||||
except Exception as e:
|
||||
return BaseResponse.error(msg=f"获取候选人列表失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/filter", response_model=BaseResponse[CandidateListResponse])
|
||||
async def filter_candidates(request: CandidateFilterRequest):
|
||||
"""
|
||||
筛选查询候选人
|
||||
|
||||
支持多条件组合筛选:
|
||||
- llm_filtered: 是否通过LLM筛选
|
||||
- status: 候选人状态
|
||||
- source: 来源渠道
|
||||
- keyword: 关键词搜索(姓名/公司/职位)
|
||||
|
||||
Returns:
|
||||
BaseResponse[CandidateListResponse]: 统一响应格式的候选人列表
|
||||
"""
|
||||
try:
|
||||
mapper = CandidateMapper()
|
||||
candidates, total = mapper.find_filtered_candidates(
|
||||
llm_filtered=request.llm_filtered,
|
||||
status=request.status,
|
||||
source=request.source,
|
||||
keyword=request.keyword,
|
||||
min_score=request.min_score,
|
||||
max_score=request.max_score,
|
||||
page=request.page,
|
||||
page_size=request.page_size
|
||||
)
|
||||
|
||||
response = CandidateListResponse(
|
||||
total=total,
|
||||
items=[_candidate_to_response(c) for c in candidates]
|
||||
)
|
||||
|
||||
return BaseResponse.success(data=response, msg="筛选候选人成功")
|
||||
except Exception as e:
|
||||
return BaseResponse.error(msg=f"筛选候选人失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/mark-filtered", response_model=BaseResponse[dict])
|
||||
async def mark_candidates_filtered(request: CandidateMarkFilteredRequest):
|
||||
"""
|
||||
批量标记候选人的LLM筛选状态
|
||||
|
||||
Args:
|
||||
request: 包含候选人ID列表和筛选状态
|
||||
|
||||
Returns:
|
||||
BaseResponse[dict]: 统一响应格式,包含更新的记录数
|
||||
"""
|
||||
try:
|
||||
if not request.candidate_ids:
|
||||
return BaseResponse.error(msg="候选人ID列表不能为空")
|
||||
|
||||
mapper = CandidateMapper()
|
||||
updated_count = mapper.mark_llm_filtered(
|
||||
candidate_ids=request.candidate_ids,
|
||||
llm_filtered=request.llm_filtered
|
||||
)
|
||||
|
||||
return BaseResponse.success(
|
||||
data={"updated_count": updated_count},
|
||||
msg=f"成功标记 {updated_count} 个候选人"
|
||||
)
|
||||
except Exception as e:
|
||||
return BaseResponse.error(msg=f"标记候选人失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/{candidate_id}", response_model=BaseResponse[CandidateResponse])
|
||||
async def get_candidate_detail(candidate_id: str):
|
||||
"""
|
||||
获取候选人详情
|
||||
|
||||
Args:
|
||||
candidate_id: 候选人ID
|
||||
|
||||
Returns:
|
||||
BaseResponse[CandidateResponse]: 统一响应格式的候选人详情
|
||||
"""
|
||||
try:
|
||||
mapper = CandidateMapper()
|
||||
candidate = mapper.find_by_id(candidate_id)
|
||||
|
||||
if not candidate:
|
||||
return BaseResponse.error(msg="候选人不存在", code=404)
|
||||
|
||||
return BaseResponse.success(
|
||||
data=_candidate_to_response(candidate),
|
||||
msg="获取候选人详情成功"
|
||||
)
|
||||
except Exception as e:
|
||||
return BaseResponse.error(msg=f"获取候选人详情失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/update-score", response_model=BaseResponse[CandidateResponse])
|
||||
async def update_candidate_score(request: CandidateUpdateScoreRequest):
|
||||
"""
|
||||
更新候选人LLM评分
|
||||
|
||||
Args:
|
||||
request: 包含候选人ID、评分和评分详情
|
||||
|
||||
Returns:
|
||||
BaseResponse[CandidateResponse]: 统一响应格式的更新后候选人详情
|
||||
"""
|
||||
try:
|
||||
mapper = CandidateMapper()
|
||||
candidate = mapper.find_by_id(request.candidate_id)
|
||||
|
||||
if not candidate:
|
||||
return BaseResponse.error(msg="候选人不存在", code=404)
|
||||
|
||||
# 更新评分信息
|
||||
from decimal import Decimal
|
||||
candidate.llm_score = Decimal(str(request.llm_score))
|
||||
candidate.llm_score_details = request.llm_score_details
|
||||
candidate.llm_filtered = True # 有评分即视为已筛选
|
||||
|
||||
# 保存更新
|
||||
mapper.save(candidate)
|
||||
|
||||
return BaseResponse.success(
|
||||
data=_candidate_to_response(candidate),
|
||||
msg="更新候选人评分成功"
|
||||
)
|
||||
except Exception as e:
|
||||
return BaseResponse.error(msg=f"更新候选人评分失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/by-score-range", response_model=BaseResponse[CandidateListResponse])
|
||||
async def get_candidates_by_score_range(
|
||||
min_score: float = Query(..., ge=0, le=100, description="最低LLM评分"),
|
||||
max_score: float = Query(..., ge=0, le=100, description="最高LLM评分"),
|
||||
page: int = Query(1, ge=1, description="页码"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量")
|
||||
):
|
||||
"""
|
||||
根据LLM评分范围查询候选人
|
||||
|
||||
Args:
|
||||
min_score: 最低LLM评分
|
||||
max_score: 最高LLM评分
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
|
||||
Returns:
|
||||
BaseResponse[CandidateListResponse]: 统一响应格式的候选人列表
|
||||
"""
|
||||
try:
|
||||
if min_score > max_score:
|
||||
return BaseResponse.error(msg="最低评分不能大于最高评分")
|
||||
|
||||
mapper = CandidateMapper()
|
||||
candidates, total = mapper.find_filtered_candidates(
|
||||
min_score=min_score,
|
||||
max_score=max_score,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
|
||||
response = CandidateListResponse(
|
||||
total=total,
|
||||
items=[_candidate_to_response(c) for c in candidates]
|
||||
)
|
||||
|
||||
return BaseResponse.success(data=response, msg="获取候选人列表成功")
|
||||
except Exception as e:
|
||||
return BaseResponse.error(msg=f"获取候选人列表失败: {str(e)}")
|
||||
@@ -3,12 +3,49 @@ API共享Schema定义
|
||||
|
||||
集中定义所有API请求和响应的数据模型
|
||||
"""
|
||||
from typing import List, Optional, Any
|
||||
from typing import List, Optional, Any, TypeVar, Generic
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ============== 通用响应 ==============
|
||||
# ============== 统一响应封装 ==============
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class BaseResponse(BaseModel, Generic[T]):
|
||||
"""
|
||||
统一API响应封装
|
||||
|
||||
Attributes:
|
||||
code: 状态码,200为成功,500为失败
|
||||
msg: 响应消息
|
||||
data: 响应数据,泛型类型
|
||||
"""
|
||||
code: int = Field(..., description="状态码: 200成功, 500失败")
|
||||
msg: str = Field(..., description="响应消息")
|
||||
data: Optional[T] = Field(None, description="响应数据")
|
||||
|
||||
@classmethod
|
||||
def success(cls, data: Optional[T] = None, msg: str = "操作成功") -> "BaseResponse[T]":
|
||||
"""创建成功响应"""
|
||||
return cls(code=200, msg=msg, data=data)
|
||||
|
||||
@classmethod
|
||||
def error(cls, msg: str = "操作失败", code: int = 500) -> "BaseResponse[T]":
|
||||
"""创建失败响应"""
|
||||
return cls(code=code, msg=msg, data=None)
|
||||
|
||||
|
||||
class PaginationData(BaseModel, Generic[T]):
|
||||
"""分页数据封装"""
|
||||
total: int = Field(..., description="总记录数")
|
||||
page: int = Field(..., description="当前页码")
|
||||
page_size: int = Field(..., description="每页数量")
|
||||
items: List[T] = Field(..., description="数据列表")
|
||||
|
||||
|
||||
# ============== 通用响应 (兼容旧代码) ==============
|
||||
|
||||
class APIResponse(BaseModel):
|
||||
"""通用API响应"""
|
||||
@@ -136,20 +173,58 @@ class JobConfigUpdate(BaseModel):
|
||||
# ============== 候选人(预留) ==============
|
||||
|
||||
class CandidateResponse(BaseModel):
|
||||
"""候选人响应(预留)"""
|
||||
"""候选人响应"""
|
||||
id: str
|
||||
name: str
|
||||
source: str
|
||||
status: str
|
||||
phone: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
gender: Optional[str] = None
|
||||
age: Optional[int] = None
|
||||
location: Optional[str] = None
|
||||
current_company: Optional[str] = None
|
||||
current_position: Optional[str] = None
|
||||
education: Optional[str] = None
|
||||
school: Optional[str] = None
|
||||
llm_filtered: bool = Field(default=False, description="是否通过LLM筛选")
|
||||
llm_score: Optional[float] = Field(None, description="LLM综合评分 0-100")
|
||||
llm_score_details: Optional[Dict[str, Any]] = Field(None, description="LLM各维度评分详情")
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class CandidateListResponse(BaseModel):
|
||||
"""候选人列表响应(预留)"""
|
||||
"""候选人列表响应"""
|
||||
total: int
|
||||
items: List[CandidateResponse]
|
||||
|
||||
|
||||
class CandidateFilterRequest(BaseModel):
|
||||
"""候选人筛选请求"""
|
||||
llm_filtered: Optional[bool] = Field(None, description="是否通过LLM筛选")
|
||||
status: Optional[str] = Field(None, description="候选人状态")
|
||||
source: Optional[str] = Field(None, description="来源渠道")
|
||||
keyword: Optional[str] = Field(None, description="关键词搜索(姓名/公司/职位)")
|
||||
min_score: Optional[float] = Field(None, ge=0, le=100, description="最低LLM评分")
|
||||
max_score: Optional[float] = Field(None, ge=0, le=100, description="最高LLM评分")
|
||||
page: int = Field(default=1, ge=1, description="页码")
|
||||
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
||||
|
||||
|
||||
class CandidateMarkFilteredRequest(BaseModel):
|
||||
"""标记候选人通过LLM筛选请求"""
|
||||
candidate_ids: List[str] = Field(..., description="候选人ID列表")
|
||||
llm_filtered: bool = Field(default=True, description="是否通过LLM筛选")
|
||||
|
||||
|
||||
class CandidateUpdateScoreRequest(BaseModel):
|
||||
"""更新候选人LLM评分请求"""
|
||||
candidate_id: str = Field(..., description="候选人ID")
|
||||
llm_score: float = Field(..., ge=0, le=100, description="LLM综合评分 0-100")
|
||||
llm_score_details: Optional[Dict[str, Any]] = Field(None, description="LLM各维度评分详情,如:{'专业能力': 85, '经验匹配': 90}")
|
||||
|
||||
|
||||
# ============== 职位(预留) ==============
|
||||
|
||||
class JobPositionResponse(BaseModel):
|
||||
|
||||
@@ -70,6 +70,9 @@ class Candidate:
|
||||
|
||||
# 状态管理
|
||||
status: CandidateStatus = CandidateStatus.NEW
|
||||
llm_filtered: bool = False # 是否通过LLM筛选
|
||||
llm_score: Optional[Decimal] = None # LLM综合评分 0-100
|
||||
llm_score_details: Optional[Dict[str, Any]] = None # LLM各维度评分详情
|
||||
|
||||
# 元数据
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
@@ -23,14 +23,14 @@ class CandidateMapper:
|
||||
"""将模型转换为实体"""
|
||||
from decimal import Decimal
|
||||
from ..domain.candidate import SalaryRange
|
||||
|
||||
|
||||
salary_expectation = None
|
||||
if model.salary_min is not None or model.salary_max is not None:
|
||||
salary_expectation = SalaryRange(
|
||||
min_salary=model.salary_min,
|
||||
max_salary=model.salary_max
|
||||
)
|
||||
|
||||
|
||||
return Candidate(
|
||||
id=model.id,
|
||||
source=CandidateSource(model.source.lower()),
|
||||
@@ -49,6 +49,9 @@ class CandidateMapper:
|
||||
school=model.school,
|
||||
salary_expectation=salary_expectation,
|
||||
status=CandidateStatus(model.status.lower()) if model.status else CandidateStatus.NEW,
|
||||
llm_filtered=bool(model.llm_filtered) if model.llm_filtered is not None else False,
|
||||
llm_score=Decimal(str(model.llm_score)) if model.llm_score else None,
|
||||
llm_score_details=model.llm_score_details,
|
||||
created_at=model.created_at,
|
||||
updated_at=model.updated_at
|
||||
)
|
||||
@@ -73,7 +76,10 @@ class CandidateMapper:
|
||||
school=entity.school,
|
||||
salary_min=entity.salary_expectation.min_salary if entity.salary_expectation else None,
|
||||
salary_max=entity.salary_expectation.max_salary if entity.salary_expectation else None,
|
||||
status=entity.status.value if entity.status else 'new'
|
||||
status=entity.status.value if entity.status else 'new',
|
||||
llm_filtered=1 if entity.llm_filtered else 0,
|
||||
llm_score=entity.llm_score,
|
||||
llm_score_details=entity.llm_score_details
|
||||
)
|
||||
|
||||
def save(self, candidate: Candidate) -> Candidate:
|
||||
@@ -102,6 +108,9 @@ class CandidateMapper:
|
||||
existing.salary_min = candidate.salary_expectation.min_salary if candidate.salary_expectation else None
|
||||
existing.salary_max = candidate.salary_expectation.max_salary if candidate.salary_expectation else None
|
||||
existing.status = candidate.status.value if candidate.status else 'new'
|
||||
existing.llm_filtered = 1 if candidate.llm_filtered else 0
|
||||
existing.llm_score = candidate.llm_score
|
||||
existing.llm_score_details = candidate.llm_score_details
|
||||
existing.updated_at = datetime.now()
|
||||
else:
|
||||
# 插入新记录
|
||||
@@ -159,7 +168,148 @@ class CandidateMapper:
|
||||
results = session.execute(
|
||||
select(CandidateModel).where(CandidateModel.name == name)
|
||||
).scalars().all()
|
||||
|
||||
|
||||
return [self._model_to_entity(r) for r in results]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def find_by_llm_filtered(self, llm_filtered: bool, page: int = 1, page_size: int = 20) -> tuple:
|
||||
"""
|
||||
根据LLM筛选状态查找候选人
|
||||
|
||||
Args:
|
||||
llm_filtered: 是否通过LLM筛选
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
|
||||
Returns:
|
||||
tuple: (候选人列表, 总记录数)
|
||||
"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
from sqlalchemy import func
|
||||
|
||||
# 构建查询条件
|
||||
stmt = select(CandidateModel).where(
|
||||
CandidateModel.llm_filtered == (1 if llm_filtered else 0)
|
||||
)
|
||||
|
||||
# 获取总记录数
|
||||
count_stmt = select(func.count()).select_from(CandidateModel).where(
|
||||
CandidateModel.llm_filtered == (1 if llm_filtered else 0)
|
||||
)
|
||||
total = session.execute(count_stmt).scalar()
|
||||
|
||||
# 分页查询
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
results = session.execute(stmt).scalars().all()
|
||||
|
||||
return [self._model_to_entity(r) for r in results], total
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def find_filtered_candidates(self, llm_filtered: Optional[bool] = None, status: Optional[str] = None,
|
||||
source: Optional[str] = None, keyword: Optional[str] = None,
|
||||
min_score: Optional[float] = None, max_score: Optional[float] = None,
|
||||
page: int = 1, page_size: int = 20) -> tuple:
|
||||
"""
|
||||
筛选查询候选人
|
||||
|
||||
Args:
|
||||
llm_filtered: 是否通过LLM筛选
|
||||
status: 候选人状态
|
||||
source: 来源渠道
|
||||
keyword: 关键词搜索(姓名/公司/职位)
|
||||
min_score: 最低LLM评分
|
||||
max_score: 最高LLM评分
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
|
||||
Returns:
|
||||
tuple: (候选人列表, 总记录数)
|
||||
"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
from sqlalchemy import func, or_, and_
|
||||
|
||||
# 构建查询条件
|
||||
stmt = select(CandidateModel)
|
||||
conditions = []
|
||||
|
||||
if llm_filtered is not None:
|
||||
conditions.append(CandidateModel.llm_filtered == (1 if llm_filtered else 0))
|
||||
if status:
|
||||
conditions.append(CandidateModel.status == status.upper())
|
||||
if source:
|
||||
conditions.append(CandidateModel.source == source.upper())
|
||||
if keyword:
|
||||
conditions.append(or_(
|
||||
CandidateModel.name.contains(keyword),
|
||||
CandidateModel.current_company.contains(keyword),
|
||||
CandidateModel.current_position.contains(keyword)
|
||||
))
|
||||
if min_score is not None:
|
||||
conditions.append(CandidateModel.llm_score >= min_score)
|
||||
if max_score is not None:
|
||||
conditions.append(CandidateModel.llm_score <= max_score)
|
||||
|
||||
if conditions:
|
||||
stmt = stmt.where(*conditions)
|
||||
|
||||
# 获取总记录数
|
||||
count_stmt = select(func.count()).select_from(CandidateModel)
|
||||
if conditions:
|
||||
count_stmt = count_stmt.where(*conditions)
|
||||
total = session.execute(count_stmt).scalar()
|
||||
|
||||
# 分页查询,按LLM评分降序,再按创建时间倒序
|
||||
stmt = stmt.order_by(
|
||||
CandidateModel.llm_score.desc().nullslast(),
|
||||
CandidateModel.created_at.desc()
|
||||
)
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
results = session.execute(stmt).scalars().all()
|
||||
|
||||
return [self._model_to_entity(r) for r in results], total
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def find_by_id(self, candidate_id: str) -> Optional[Candidate]:
|
||||
"""根据ID查找候选人"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
result = session.execute(
|
||||
select(CandidateModel).where(CandidateModel.id == candidate_id)
|
||||
).scalar_one_or_none()
|
||||
|
||||
return self._model_to_entity(result) if result else None
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def mark_llm_filtered(self, candidate_ids: List[str], llm_filtered: bool = True) -> int:
|
||||
"""
|
||||
批量标记候选人的LLM筛选状态
|
||||
|
||||
Args:
|
||||
candidate_ids: 候选人ID列表
|
||||
llm_filtered: 是否通过LLM筛选
|
||||
|
||||
Returns:
|
||||
int: 更新的记录数
|
||||
"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
from sqlalchemy import update
|
||||
|
||||
stmt = update(CandidateModel).where(
|
||||
CandidateModel.id.in_(candidate_ids)
|
||||
).values(
|
||||
llm_filtered=1 if llm_filtered else 0,
|
||||
updated_at=datetime.now()
|
||||
)
|
||||
|
||||
result = session.execute(stmt)
|
||||
session.commit()
|
||||
return result.rowcount
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
Reference in New Issue
Block a user