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:
2026-03-24 18:34:37 +08:00
parent 7e60476175
commit 1343561979
8 changed files with 511 additions and 17 deletions

View File

@@ -54,6 +54,9 @@ CREATE TABLE IF NOT EXISTS candidates (
salary_min INT, -- 期望薪资下限(K) salary_min INT, -- 期望薪资下限(K)
salary_max INT, -- 期望薪资上限(K) salary_max INT, -- 期望薪资上限(K)
status VARCHAR(32) DEFAULT 'NEW', -- NEW, ANALYZED, PUSHED, CONTACTED, INTERVIEWED, HIRED, REJECTED 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, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_source_source_id (source, source_id), 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_email (email),
INDEX idx_name (name), INDEX idx_name (name),
INDEX idx_status (status), 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)
); );
-- ============================================ -- ============================================

View File

@@ -37,7 +37,7 @@ class RecruiterModel(Base):
class CandidateModel(Base): class CandidateModel(Base):
"""候选人主表""" """候选人主表"""
__tablename__ = 'candidates' __tablename__ = 'candidates'
id = Column(String(64), primary_key=True) id = Column(String(64), primary_key=True)
source = Column(String(32), nullable=False) source = Column(String(32), nullable=False)
source_id = Column(String(128), nullable=False) source_id = Column(String(128), nullable=False)
@@ -56,6 +56,9 @@ class CandidateModel(Base):
salary_min = Column(Integer) salary_min = Column(Integer)
salary_max = Column(Integer) salary_max = Column(Integer)
status = Column(String(32), default='NEW') 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()) created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())

View File

@@ -5,11 +5,12 @@ FastAPI主应用入口
- routes/recruiter.py: 招聘者账号管理 - routes/recruiter.py: 招聘者账号管理
- routes/scheduler.py: 定时任务管理 - routes/scheduler.py: 定时任务管理
- routes/system.py: 系统接口 - routes/system.py: 系统接口
- routes/candidate.py: 候选人管理
""" """
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware 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 from .routes.system import router as system_router
@@ -22,7 +23,7 @@ def create_app() -> FastAPI:
docs_url="/docs", docs_url="/docs",
redoc_url="/redoc" redoc_url="/redoc"
) )
# CORS配置 # CORS配置
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@@ -31,17 +32,20 @@ def create_app() -> FastAPI:
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
) )
# 注册路由 # 注册路由
# 系统路由(根路径、健康检查等) # 系统路由(根路径、健康检查等)
app.include_router(system_router) app.include_router(system_router)
# 招聘者账号管理路由 # 招聘者账号管理路由
app.include_router(recruiter_router) app.include_router(recruiter_router)
# 定时任务管理路由 # 定时任务管理路由
app.include_router(scheduler_router) app.include_router(scheduler_router)
# 候选人管理路由
app.include_router(candidate_router)
return app return app

View File

@@ -5,6 +5,7 @@ API路由模块
- recruiter: 招聘者账号管理 - recruiter: 招聘者账号管理
- scheduler: 定时任务管理 - scheduler: 定时任务管理
- system: 系统接口 - system: 系统接口
- candidate: 候选人管理
""" """
from .recruiter import router as recruiter_router from .recruiter import router as recruiter_router
@@ -22,8 +23,15 @@ except ImportError:
from fastapi import APIRouter from fastapi import APIRouter
system_router = APIRouter() system_router = APIRouter()
try:
from .candidate import router as candidate_router
except ImportError:
from fastapi import APIRouter
candidate_router = APIRouter()
__all__ = [ __all__ = [
"recruiter_router", "recruiter_router",
"scheduler_router", "scheduler_router",
"system_router" "system_router",
"candidate_router"
] ]

View File

@@ -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)}")

View File

@@ -3,12 +3,49 @@ API共享Schema定义
集中定义所有API请求和响应的数据模型 集中定义所有API请求和响应的数据模型
""" """
from typing import List, Optional, Any from typing import List, Optional, Any, TypeVar, Generic
from datetime import datetime from datetime import datetime
from pydantic import BaseModel, Field 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): class APIResponse(BaseModel):
"""通用API响应""" """通用API响应"""
@@ -136,20 +173,58 @@ class JobConfigUpdate(BaseModel):
# ============== 候选人(预留) ============== # ============== 候选人(预留) ==============
class CandidateResponse(BaseModel): class CandidateResponse(BaseModel):
"""候选人响应(预留)""" """候选人响应"""
id: str id: str
name: str name: str
source: str source: str
status: 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 created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class CandidateListResponse(BaseModel): class CandidateListResponse(BaseModel):
"""候选人列表响应(预留)""" """候选人列表响应"""
total: int total: int
items: List[CandidateResponse] 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): class JobPositionResponse(BaseModel):

View File

@@ -70,6 +70,9 @@ class Candidate:
# 状态管理 # 状态管理
status: CandidateStatus = CandidateStatus.NEW 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 created_at: Optional[datetime] = None

View File

@@ -23,14 +23,14 @@ class CandidateMapper:
"""将模型转换为实体""" """将模型转换为实体"""
from decimal import Decimal from decimal import Decimal
from ..domain.candidate import SalaryRange from ..domain.candidate import SalaryRange
salary_expectation = None salary_expectation = None
if model.salary_min is not None or model.salary_max is not None: if model.salary_min is not None or model.salary_max is not None:
salary_expectation = SalaryRange( salary_expectation = SalaryRange(
min_salary=model.salary_min, min_salary=model.salary_min,
max_salary=model.salary_max max_salary=model.salary_max
) )
return Candidate( return Candidate(
id=model.id, id=model.id,
source=CandidateSource(model.source.lower()), source=CandidateSource(model.source.lower()),
@@ -49,6 +49,9 @@ class CandidateMapper:
school=model.school, school=model.school,
salary_expectation=salary_expectation, salary_expectation=salary_expectation,
status=CandidateStatus(model.status.lower()) if model.status else CandidateStatus.NEW, 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, created_at=model.created_at,
updated_at=model.updated_at updated_at=model.updated_at
) )
@@ -73,7 +76,10 @@ class CandidateMapper:
school=entity.school, school=entity.school,
salary_min=entity.salary_expectation.min_salary if entity.salary_expectation else None, 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, 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: 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_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.salary_max = candidate.salary_expectation.max_salary if candidate.salary_expectation else None
existing.status = candidate.status.value if candidate.status else 'new' 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() existing.updated_at = datetime.now()
else: else:
# 插入新记录 # 插入新记录
@@ -159,7 +168,148 @@ class CandidateMapper:
results = session.execute( results = session.execute(
select(CandidateModel).where(CandidateModel.name == name) select(CandidateModel).where(CandidateModel.name == name)
).scalars().all() ).scalars().all()
return [self._model_to_entity(r) for r in results] return [self._model_to_entity(r) for r in results]
finally: finally:
session.close() 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()