feat(job): 增加职位评价方案及管理接口
- 在数据库jobs表新增evaluation_schema_id字段及外键约束 - ORM模型JobModel新增evaluation_schema_id关联字段 - 扩展配置项,增加评分阈值相关参数及简历处理配置 - 新增职位管理路由job_router,支持职位CRUD及评价方案关联功能 - 职位API支持列表查询、筛选、详情查看、创建、更新、删除及评价方案绑定和获取 - recruiter路由统一响应格式为BaseResponse,完善异常处理及提示信息 - scheduler路由任务接口统一响应格式,新增单个任务状态查询接口 - scheduler路由中支持任务立即运行、暂停、恢复和配置更新操作,增加异常捕获和提示 - 其他内部细节优化API异常处理及返回统一格式
This commit is contained in:
@@ -95,6 +95,7 @@ CREATE TABLE IF NOT EXISTS jobs (
|
|||||||
source VARCHAR(32) NOT NULL, -- BOSS, LIEPIN, etc.
|
source VARCHAR(32) NOT NULL, -- BOSS, LIEPIN, etc.
|
||||||
source_id VARCHAR(128) NOT NULL,
|
source_id VARCHAR(128) NOT NULL,
|
||||||
recruiter_id VARCHAR(64), -- 关联的招聘者账号ID
|
recruiter_id VARCHAR(64), -- 关联的招聘者账号ID
|
||||||
|
evaluation_schema_id VARCHAR(64), -- 关联的评价方案ID
|
||||||
title VARCHAR(256) NOT NULL,
|
title VARCHAR(256) NOT NULL,
|
||||||
department VARCHAR(128),
|
department VARCHAR(128),
|
||||||
location VARCHAR(128),
|
location VARCHAR(128),
|
||||||
@@ -114,6 +115,7 @@ CREATE TABLE IF NOT EXISTS jobs (
|
|||||||
UNIQUE KEY uk_source_source_id (source, source_id),
|
UNIQUE KEY uk_source_source_id (source, source_id),
|
||||||
INDEX idx_status (status),
|
INDEX idx_status (status),
|
||||||
INDEX idx_recruiter_id (recruiter_id),
|
INDEX idx_recruiter_id (recruiter_id),
|
||||||
|
INDEX idx_evaluation_schema_id (evaluation_schema_id),
|
||||||
INDEX idx_last_sync_at (last_sync_at),
|
INDEX idx_last_sync_at (last_sync_at),
|
||||||
FOREIGN KEY (recruiter_id) REFERENCES recruiters(id) ON DELETE SET NULL
|
FOREIGN KEY (recruiter_id) REFERENCES recruiters(id) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
@@ -200,6 +202,7 @@ ON DUPLICATE KEY UPDATE
|
|||||||
dimensions = VALUES(dimensions),
|
dimensions = VALUES(dimensions),
|
||||||
weights = VALUES(weights);
|
weights = VALUES(weights);
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO evaluation_schemas (id, name, description, dimensions, weights) VALUES
|
INSERT INTO evaluation_schemas (id, name, description, dimensions, weights) VALUES
|
||||||
('java_backend', 'Java后端工程师评价方案', '针对Java后端开发岗位的综合评价方案',
|
('java_backend', 'Java后端工程师评价方案', '针对Java后端开发岗位的综合评价方案',
|
||||||
'[
|
'[
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class JobModel(Base):
|
|||||||
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)
|
||||||
recruiter_id = Column(String(64), ForeignKey('recruiters.id'))
|
recruiter_id = Column(String(64), ForeignKey('recruiters.id'))
|
||||||
|
evaluation_schema_id = Column(String(64), ForeignKey('evaluation_schemas.id')) # 关联的评价方案ID
|
||||||
title = Column(String(256), nullable=False)
|
title = Column(String(256), nullable=False)
|
||||||
department = Column(String(128))
|
department = Column(String(128))
|
||||||
location = Column(String(128))
|
location = Column(String(128))
|
||||||
|
|||||||
@@ -44,6 +44,20 @@ class Settings(BaseSettings):
|
|||||||
notify_email_from: Optional[str] = Field(default=None, description="发件人地址")
|
notify_email_from: Optional[str] = Field(default=None, description="发件人地址")
|
||||||
notify_email_to: Optional[str] = Field(default=None, description="收件人地址")
|
notify_email_to: Optional[str] = Field(default=None, description="收件人地址")
|
||||||
|
|
||||||
|
# 评分阈值配置 (前缀: SCORE_)
|
||||||
|
score_threshold_greet: float = Field(default=70.0, description="触发打招呼的最低分数")
|
||||||
|
score_threshold_notify: float = Field(default=70.0, description="触发HR通知的最低分数")
|
||||||
|
auto_greet_enabled: bool = Field(default=True, description="是否启用自动打招呼")
|
||||||
|
auto_notify_enabled: bool = Field(default=True, description="是否启用自动通知HR")
|
||||||
|
greet_message_template: str = Field(
|
||||||
|
default="您好,我们对您的简历很感兴趣,期待与您进一步交流!",
|
||||||
|
description="打招呼消息模板"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 简历处理配置 (前缀: RESUME_)
|
||||||
|
resume_process_delay: float = Field(default=1.0, description="处理每个候选人后的延迟秒数,避免请求过快")
|
||||||
|
resume_max_per_job: int = Field(default=20, description="每个职位最多处理的候选人数量")
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
env_file_encoding = "utf-8"
|
env_file_encoding = "utf-8"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ FastAPI主应用入口
|
|||||||
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, candidate_router
|
from .routes import recruiter_router, scheduler_router, candidate_router, job_router
|
||||||
from .routes.system import router as system_router
|
from .routes.system import router as system_router
|
||||||
|
|
||||||
|
|
||||||
@@ -46,6 +46,9 @@ def create_app() -> FastAPI:
|
|||||||
# 候选人管理路由
|
# 候选人管理路由
|
||||||
app.include_router(candidate_router)
|
app.include_router(candidate_router)
|
||||||
|
|
||||||
|
# 职位管理路由
|
||||||
|
app.include_router(job_router)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,16 @@ except ImportError:
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
candidate_router = APIRouter()
|
candidate_router = APIRouter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .job import router as job_router
|
||||||
|
except ImportError:
|
||||||
|
from fastapi import APIRouter
|
||||||
|
job_router = APIRouter()
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"recruiter_router",
|
"recruiter_router",
|
||||||
"scheduler_router",
|
"scheduler_router",
|
||||||
"system_router",
|
"system_router",
|
||||||
"candidate_router"
|
"candidate_router",
|
||||||
|
"job_router"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,409 @@
|
|||||||
|
"""
|
||||||
|
职位管理 API 路由
|
||||||
|
|
||||||
|
提供职位CRUD、关联评价方案等功能
|
||||||
|
"""
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Query
|
||||||
|
|
||||||
|
from ..schemas import (
|
||||||
|
BaseResponse,
|
||||||
|
JobPositionResponse, JobPositionListResponse,
|
||||||
|
JobPositionCreateRequest, JobPositionUpdateRequest,
|
||||||
|
JobPositionFilterRequest, JobBindSchemaRequest,
|
||||||
|
EvaluationSchemaResponse, EvaluationSchemaListResponse
|
||||||
|
)
|
||||||
|
from ...domain.job import Job
|
||||||
|
from ...domain.enums import CandidateSource
|
||||||
|
from ...mapper.job_mapper import JobMapper
|
||||||
|
from ...mapper.evaluation_mapper import EvaluationMapper
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/jobs", tags=["职位管理"])
|
||||||
|
|
||||||
|
|
||||||
|
def _job_to_response(job: Job) -> JobPositionResponse:
|
||||||
|
"""将领域实体转换为响应模型"""
|
||||||
|
return JobPositionResponse(
|
||||||
|
id=job.id,
|
||||||
|
title=job.title,
|
||||||
|
source=job.source.value if job.source else "",
|
||||||
|
status=job.status.value if job.status else "",
|
||||||
|
recruiter_id=job.recruiter_id,
|
||||||
|
evaluation_schema_id=job.evaluation_schema_id,
|
||||||
|
department=job.department,
|
||||||
|
location=job.location,
|
||||||
|
salary_min=job.salary_min,
|
||||||
|
salary_max=job.salary_max,
|
||||||
|
requirements=job.requirements,
|
||||||
|
description=job.description,
|
||||||
|
candidate_count=job.candidate_count,
|
||||||
|
new_candidate_count=job.new_candidate_count,
|
||||||
|
last_sync_at=job.last_sync_at,
|
||||||
|
created_at=job.created_at,
|
||||||
|
updated_at=job.updated_at
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("", response_model=BaseResponse[JobPositionListResponse])
|
||||||
|
async def list_jobs(
|
||||||
|
source: Optional[str] = Query(None, description="平台来源"),
|
||||||
|
recruiter_id: Optional[str] = Query(None, description="招聘者账号ID"),
|
||||||
|
evaluation_schema_id: Optional[str] = Query(None, description="评价方案ID"),
|
||||||
|
status: Optional[str] = Query(None, description="状态: ACTIVE, PAUSED, CLOSED, ARCHIVED"),
|
||||||
|
keyword: Optional[str] = Query(None, description="关键词搜索(标题/部门)"),
|
||||||
|
page: int = Query(1, ge=1, description="页码"),
|
||||||
|
page_size: int = Query(20, ge=1, le=100, description="每页数量")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取职位列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: 平台来源
|
||||||
|
recruiter_id: 招聘者账号ID
|
||||||
|
evaluation_schema_id: 评价方案ID
|
||||||
|
status: 职位状态
|
||||||
|
keyword: 关键词搜索
|
||||||
|
page: 页码
|
||||||
|
page_size: 每页数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[JobPositionListResponse]: 统一响应格式的职位列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
mapper = JobMapper()
|
||||||
|
jobs, total = mapper.find_filtered_jobs(
|
||||||
|
source=source,
|
||||||
|
recruiter_id=recruiter_id,
|
||||||
|
evaluation_schema_id=evaluation_schema_id,
|
||||||
|
status=status,
|
||||||
|
keyword=keyword,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size
|
||||||
|
)
|
||||||
|
|
||||||
|
response = JobPositionListResponse(
|
||||||
|
total=total,
|
||||||
|
items=[_job_to_response(job) for job in jobs]
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse.success(data=response, msg="获取职位列表成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取职位列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/filter", response_model=BaseResponse[JobPositionListResponse])
|
||||||
|
async def filter_jobs(request: JobPositionFilterRequest):
|
||||||
|
"""
|
||||||
|
筛选查询职位
|
||||||
|
|
||||||
|
支持多条件组合筛选
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[JobPositionListResponse]: 统一响应格式的职位列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
mapper = JobMapper()
|
||||||
|
jobs, total = mapper.find_filtered_jobs(
|
||||||
|
source=request.source,
|
||||||
|
recruiter_id=request.recruiter_id,
|
||||||
|
evaluation_schema_id=request.evaluation_schema_id,
|
||||||
|
status=request.status,
|
||||||
|
keyword=request.keyword,
|
||||||
|
page=request.page,
|
||||||
|
page_size=request.page_size
|
||||||
|
)
|
||||||
|
|
||||||
|
response = JobPositionListResponse(
|
||||||
|
total=total,
|
||||||
|
items=[_job_to_response(job) for job in jobs]
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse.success(data=response, msg="筛选职位成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"筛选职位失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{job_id}", response_model=BaseResponse[JobPositionResponse])
|
||||||
|
async def get_job_detail(job_id: str):
|
||||||
|
"""
|
||||||
|
获取职位详情
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: 职位ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[JobPositionResponse]: 统一响应格式的职位详情
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
mapper = JobMapper()
|
||||||
|
job = mapper.find_by_id(job_id)
|
||||||
|
|
||||||
|
if not job:
|
||||||
|
return BaseResponse.error(msg="职位不存在", code=404)
|
||||||
|
|
||||||
|
return BaseResponse.success(
|
||||||
|
data=_job_to_response(job),
|
||||||
|
msg="获取职位详情成功"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取职位详情失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=BaseResponse[JobPositionResponse])
|
||||||
|
async def create_job(request: JobPositionCreateRequest):
|
||||||
|
"""
|
||||||
|
创建职位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: 创建职位请求
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[JobPositionResponse]: 统一响应格式的新建职位详情
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from ...domain.job import JobStatus
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
mapper = JobMapper()
|
||||||
|
|
||||||
|
# 验证source
|
||||||
|
try:
|
||||||
|
source = CandidateSource(request.source.lower())
|
||||||
|
except ValueError:
|
||||||
|
return BaseResponse.error(msg=f"无效的平台来源: {request.source}")
|
||||||
|
|
||||||
|
# 创建职位实体
|
||||||
|
job = Job(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
source=source,
|
||||||
|
source_id=request.source_id,
|
||||||
|
recruiter_id=request.recruiter_id,
|
||||||
|
evaluation_schema_id=request.evaluation_schema_id,
|
||||||
|
title=request.title,
|
||||||
|
department=request.department,
|
||||||
|
location=request.location,
|
||||||
|
salary_min=request.salary_min,
|
||||||
|
salary_max=request.salary_max,
|
||||||
|
requirements=request.requirements,
|
||||||
|
description=request.description,
|
||||||
|
status=JobStatus.ACTIVE
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存到数据库
|
||||||
|
saved_job = mapper.save(job)
|
||||||
|
|
||||||
|
return BaseResponse.success(
|
||||||
|
data=_job_to_response(saved_job),
|
||||||
|
msg="职位创建成功"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"创建职位失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{job_id}", response_model=BaseResponse[JobPositionResponse])
|
||||||
|
async def update_job(job_id: str, request: JobPositionUpdateRequest):
|
||||||
|
"""
|
||||||
|
更新职位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: 职位ID
|
||||||
|
request: 更新职位请求
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[JobPositionResponse]: 统一响应格式的更新后职位详情
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from ...domain.job import JobStatus
|
||||||
|
|
||||||
|
mapper = JobMapper()
|
||||||
|
job = mapper.find_by_id(job_id)
|
||||||
|
|
||||||
|
if not job:
|
||||||
|
return BaseResponse.error(msg="职位不存在", code=404)
|
||||||
|
|
||||||
|
# 更新字段
|
||||||
|
if request.title is not None:
|
||||||
|
job.title = request.title
|
||||||
|
if request.evaluation_schema_id is not None:
|
||||||
|
job.evaluation_schema_id = request.evaluation_schema_id
|
||||||
|
if request.department is not None:
|
||||||
|
job.department = request.department
|
||||||
|
if request.location is not None:
|
||||||
|
job.location = request.location
|
||||||
|
if request.salary_min is not None:
|
||||||
|
job.salary_min = request.salary_min
|
||||||
|
if request.salary_max is not None:
|
||||||
|
job.salary_max = request.salary_max
|
||||||
|
if request.requirements is not None:
|
||||||
|
job.requirements = request.requirements
|
||||||
|
if request.description is not None:
|
||||||
|
job.description = request.description
|
||||||
|
if request.status is not None:
|
||||||
|
try:
|
||||||
|
job.status = JobStatus(request.status.upper())
|
||||||
|
except ValueError:
|
||||||
|
return BaseResponse.error(msg=f"无效的状态: {request.status}")
|
||||||
|
|
||||||
|
# 保存更新
|
||||||
|
updated_job = mapper.save(job)
|
||||||
|
|
||||||
|
return BaseResponse.success(
|
||||||
|
data=_job_to_response(updated_job),
|
||||||
|
msg="职位更新成功"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"更新职位失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{job_id}", response_model=BaseResponse[dict])
|
||||||
|
async def delete_job(job_id: str):
|
||||||
|
"""
|
||||||
|
删除职位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: 职位ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[dict]: 统一响应格式
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
mapper = JobMapper()
|
||||||
|
job = mapper.find_by_id(job_id)
|
||||||
|
|
||||||
|
if not job:
|
||||||
|
return BaseResponse.error(msg="职位不存在", code=404)
|
||||||
|
|
||||||
|
# 软删除:将状态改为ARCHIVED
|
||||||
|
from ...domain.job import JobStatus
|
||||||
|
job.status = JobStatus.ARCHIVED
|
||||||
|
mapper.save(job)
|
||||||
|
|
||||||
|
return BaseResponse.success(
|
||||||
|
data={"deleted_id": job_id},
|
||||||
|
msg="职位删除成功"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"删除职位失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{job_id}/bind-schema", response_model=BaseResponse[JobPositionResponse])
|
||||||
|
async def bind_evaluation_schema(job_id: str, request: JobBindSchemaRequest):
|
||||||
|
"""
|
||||||
|
职位关联评价方案
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: 职位ID
|
||||||
|
request: 关联评价方案请求
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[JobPositionResponse]: 统一响应格式的更新后职位详情
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
job_mapper = JobMapper()
|
||||||
|
schema_mapper = EvaluationMapper()
|
||||||
|
|
||||||
|
# 验证职位存在
|
||||||
|
job = job_mapper.find_by_id(job_id)
|
||||||
|
if not job:
|
||||||
|
return BaseResponse.error(msg="职位不存在", code=404)
|
||||||
|
|
||||||
|
# 验证评价方案存在
|
||||||
|
schema = schema_mapper.find_schema_by_id(request.evaluation_schema_id)
|
||||||
|
if not schema:
|
||||||
|
return BaseResponse.error(msg="评价方案不存在", code=404)
|
||||||
|
|
||||||
|
# 更新关联
|
||||||
|
job.evaluation_schema_id = request.evaluation_schema_id
|
||||||
|
updated_job = job_mapper.save(job)
|
||||||
|
|
||||||
|
return BaseResponse.success(
|
||||||
|
data=_job_to_response(updated_job),
|
||||||
|
msg=f"职位已成功关联评价方案: {schema.name}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"关联评价方案失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{job_id}/schema", response_model=BaseResponse[EvaluationSchemaResponse])
|
||||||
|
async def get_job_evaluation_schema(job_id: str):
|
||||||
|
"""
|
||||||
|
获取职位关联的评价方案
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: 职位ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[EvaluationSchemaResponse]: 统一响应格式的评价方案详情
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
job_mapper = JobMapper()
|
||||||
|
schema_mapper = EvaluationMapper()
|
||||||
|
|
||||||
|
# 验证职位存在
|
||||||
|
job = job_mapper.find_by_id(job_id)
|
||||||
|
if not job:
|
||||||
|
return BaseResponse.error(msg="职位不存在", code=404)
|
||||||
|
|
||||||
|
# 检查是否有关联的评价方案
|
||||||
|
if not job.evaluation_schema_id:
|
||||||
|
return BaseResponse.error(msg="该职位未关联评价方案", code=404)
|
||||||
|
|
||||||
|
# 获取评价方案
|
||||||
|
schema = schema_mapper.find_schema_by_id(job.evaluation_schema_id)
|
||||||
|
if not schema:
|
||||||
|
return BaseResponse.error(msg="关联的评价方案不存在", code=404)
|
||||||
|
|
||||||
|
response = EvaluationSchemaResponse(
|
||||||
|
id=schema.id,
|
||||||
|
name=schema.name,
|
||||||
|
description=schema.description,
|
||||||
|
dimensions=schema.dimensions,
|
||||||
|
weights=schema.weights,
|
||||||
|
is_default=schema.is_default,
|
||||||
|
created_at=schema.created_at,
|
||||||
|
updated_at=schema.updated_at
|
||||||
|
)
|
||||||
|
|
||||||
|
return BaseResponse.success(data=response, msg="获取评价方案成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取评价方案失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/schemas/list", response_model=BaseResponse[EvaluationSchemaListResponse])
|
||||||
|
async def list_evaluation_schemas(
|
||||||
|
page: int = Query(1, ge=1, description="页码"),
|
||||||
|
page_size: int = Query(20, ge=1, le=100, description="每页数量")
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
获取评价方案列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 页码
|
||||||
|
page_size: 每页数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BaseResponse[EvaluationSchemaListResponse]: 统一响应格式的评价方案列表
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
schema_mapper = EvaluationMapper()
|
||||||
|
schemas, total = schema_mapper.find_all_schemas(page=page, page_size=page_size)
|
||||||
|
|
||||||
|
items = [
|
||||||
|
EvaluationSchemaResponse(
|
||||||
|
id=schema.id,
|
||||||
|
name=schema.name,
|
||||||
|
description=schema.description,
|
||||||
|
dimensions=schema.dimensions,
|
||||||
|
weights=schema.weights,
|
||||||
|
is_default=schema.is_default,
|
||||||
|
created_at=schema.created_at,
|
||||||
|
updated_at=schema.updated_at
|
||||||
|
)
|
||||||
|
for schema in schemas
|
||||||
|
]
|
||||||
|
|
||||||
|
response = EvaluationSchemaListResponse(total=total, items=items)
|
||||||
|
return BaseResponse.success(data=response, msg="获取评价方案列表成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取评价方案列表失败: {str(e)}")
|
||||||
@@ -6,13 +6,13 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
from ..schemas import (
|
from ..schemas import (
|
||||||
RecruiterCreate, RecruiterRegister, RecruiterUpdate,
|
RecruiterCreate, RecruiterRegister, RecruiterUpdate,
|
||||||
RecruiterResponse, RecruiterListResponse, RecruiterRegisterResponse,
|
RecruiterResponse, RecruiterListResponse, RecruiterRegisterResponse,
|
||||||
RecruiterPrivilegeInfo, RecruiterSyncInfo, RecruiterSourceInfo,
|
RecruiterPrivilegeInfo, RecruiterSyncInfo, RecruiterSourceInfo,
|
||||||
APIResponse
|
BaseResponse
|
||||||
)
|
)
|
||||||
from ...domain.candidate import CandidateSource
|
from ...domain.candidate import CandidateSource
|
||||||
from ...domain.recruiter import Recruiter, RecruiterStatus
|
from ...domain.recruiter import Recruiter, RecruiterStatus
|
||||||
@@ -68,10 +68,10 @@ def _build_recruiter_response(recruiter: Recruiter) -> RecruiterResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/sources", response_model=List[RecruiterSourceInfo])
|
@router.get("/sources", response_model=BaseResponse[List[RecruiterSourceInfo]])
|
||||||
async def get_recruiter_sources():
|
async def get_recruiter_sources():
|
||||||
"""获取支持的平台来源列表"""
|
"""获取支持的平台来源列表"""
|
||||||
return [
|
sources = [
|
||||||
RecruiterSourceInfo(
|
RecruiterSourceInfo(
|
||||||
value="boss",
|
value="boss",
|
||||||
label="Boss直聘",
|
label="Boss直聘",
|
||||||
@@ -88,9 +88,10 @@ async def get_recruiter_sources():
|
|||||||
description="智联招聘平台(预留)"
|
description="智联招聘平台(预留)"
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
return BaseResponse.success(data=sources, msg="获取平台来源列表成功")
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=RecruiterListResponse)
|
@router.get("", response_model=BaseResponse[RecruiterListResponse])
|
||||||
async def list_recruiters(
|
async def list_recruiters(
|
||||||
source: Optional[str] = None,
|
source: Optional[str] = None,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
@@ -101,54 +102,64 @@ async def list_recruiters(
|
|||||||
Args:
|
Args:
|
||||||
source: 按平台筛选 (boss, liepin, etc.)
|
source: 按平台筛选 (boss, liepin, etc.)
|
||||||
"""
|
"""
|
||||||
if source:
|
try:
|
||||||
try:
|
if source:
|
||||||
candidate_source = CandidateSource(source.lower())
|
try:
|
||||||
recruiters = service.list_recruiters(candidate_source)
|
candidate_source = CandidateSource(source.lower())
|
||||||
except ValueError:
|
recruiters = service.list_recruiters(candidate_source)
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid source: {source}")
|
except ValueError:
|
||||||
else:
|
return BaseResponse.error(msg=f"无效的来源: {source}", code=400)
|
||||||
recruiters = service.list_recruiters()
|
else:
|
||||||
|
recruiters = service.list_recruiters()
|
||||||
items = [_build_recruiter_response(r) for r in recruiters]
|
|
||||||
|
items = [_build_recruiter_response(r) for r in recruiters]
|
||||||
return RecruiterListResponse(total=len(items), items=items)
|
|
||||||
|
response = RecruiterListResponse(total=len(items), items=items)
|
||||||
|
return BaseResponse.success(data=response, msg="获取招聘者列表成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取招聘者列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{recruiter_id}", response_model=RecruiterResponse)
|
@router.get("/{recruiter_id}", response_model=BaseResponse[RecruiterResponse])
|
||||||
async def get_recruiter(
|
async def get_recruiter(
|
||||||
recruiter_id: str,
|
recruiter_id: str,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
):
|
):
|
||||||
"""获取单个招聘者账号详情"""
|
"""获取单个招聘者账号详情"""
|
||||||
recruiter = service.get_recruiter(recruiter_id)
|
try:
|
||||||
if not recruiter:
|
recruiter = service.get_recruiter(recruiter_id)
|
||||||
raise HTTPException(status_code=404, detail="Recruiter not found")
|
if not recruiter:
|
||||||
|
return BaseResponse.error(msg="招聘者不存在", code=404)
|
||||||
return _build_recruiter_response(recruiter)
|
|
||||||
|
return BaseResponse.success(data=_build_recruiter_response(recruiter), msg="获取招聘者详情成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取招聘者详情失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=RecruiterResponse)
|
@router.post("", response_model=BaseResponse[RecruiterResponse])
|
||||||
async def create_recruiter(
|
async def create_recruiter(
|
||||||
data: RecruiterCreate,
|
data: RecruiterCreate,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
):
|
):
|
||||||
"""手动创建招聘者账号"""
|
"""手动创建招聘者账号"""
|
||||||
try:
|
try:
|
||||||
source = CandidateSource(data.source.lower())
|
try:
|
||||||
except ValueError:
|
source = CandidateSource(data.source.lower())
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid source: {data.source}")
|
except ValueError:
|
||||||
|
return BaseResponse.error(msg=f"无效的来源: {data.source}", code=400)
|
||||||
recruiter = service.add_recruiter(
|
|
||||||
name=data.name,
|
recruiter = service.add_recruiter(
|
||||||
source=source,
|
name=data.name,
|
||||||
wt_token=data.wt_token
|
source=source,
|
||||||
)
|
wt_token=data.wt_token
|
||||||
|
)
|
||||||
return _build_recruiter_response(recruiter)
|
|
||||||
|
return BaseResponse.success(data=_build_recruiter_response(recruiter), msg="创建招聘者成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"创建招聘者失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register", response_model=RecruiterRegisterResponse)
|
@router.post("/register", response_model=BaseResponse[RecruiterRegisterResponse])
|
||||||
async def register_recruiter(
|
async def register_recruiter(
|
||||||
data: RecruiterRegister,
|
data: RecruiterRegister,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
@@ -158,20 +169,17 @@ async def register_recruiter(
|
|||||||
|
|
||||||
根据Token自动获取账号信息并注册
|
根据Token自动获取账号信息并注册
|
||||||
"""
|
"""
|
||||||
# 验证平台来源
|
|
||||||
try:
|
|
||||||
source = CandidateSource(data.source.lower())
|
|
||||||
except ValueError:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid source: {data.source}")
|
|
||||||
|
|
||||||
# 目前仅支持Boss平台
|
|
||||||
if source != CandidateSource.BOSS:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Auto register only supports 'boss' source currently"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 验证平台来源
|
||||||
|
try:
|
||||||
|
source = CandidateSource(data.source.lower())
|
||||||
|
except ValueError:
|
||||||
|
return BaseResponse.error(msg=f"无效的来源: {data.source}", code=400)
|
||||||
|
|
||||||
|
# 目前仅支持Boss平台
|
||||||
|
if source != CandidateSource.BOSS:
|
||||||
|
return BaseResponse.error(msg="自动注册仅支持Boss平台", code=400)
|
||||||
|
|
||||||
# 1. 使用Token创建临时爬虫获取账号信息
|
# 1. 使用Token创建临时爬虫获取账号信息
|
||||||
crawler = BossCrawler(wt_token=data.wt_token)
|
crawler = BossCrawler(wt_token=data.wt_token)
|
||||||
|
|
||||||
@@ -179,10 +187,7 @@ async def register_recruiter(
|
|||||||
if hasattr(crawler.client, 'check_login_status'):
|
if hasattr(crawler.client, 'check_login_status'):
|
||||||
is_valid = crawler.client.check_login_status()
|
is_valid = crawler.client.check_login_status()
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
return RecruiterRegisterResponse(
|
return BaseResponse.error(msg="Token无效或账号已过期,请检查Token是否正确")
|
||||||
success=False,
|
|
||||||
message="Token无效或账号已过期,请检查Token是否正确"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. 获取账号信息
|
# 3. 获取账号信息
|
||||||
account_info = None
|
account_info = None
|
||||||
@@ -190,10 +195,7 @@ async def register_recruiter(
|
|||||||
account_info = crawler.client.get_account_info()
|
account_info = crawler.client.get_account_info()
|
||||||
|
|
||||||
if not account_info:
|
if not account_info:
|
||||||
return RecruiterRegisterResponse(
|
return BaseResponse.error(msg="无法获取账号信息,请检查Token是否有效")
|
||||||
success=False,
|
|
||||||
message="无法获取账号信息,请检查Token是否有效"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 4. 提取账号名称
|
# 4. 提取账号名称
|
||||||
account_name = ""
|
account_name = ""
|
||||||
@@ -212,11 +214,12 @@ async def register_recruiter(
|
|||||||
existing_recruiters = service.list_recruiters(source)
|
existing_recruiters = service.list_recruiters(source)
|
||||||
for existing in existing_recruiters:
|
for existing in existing_recruiters:
|
||||||
if existing.wt_token == data.wt_token:
|
if existing.wt_token == data.wt_token:
|
||||||
return RecruiterRegisterResponse(
|
response_data = RecruiterRegisterResponse(
|
||||||
success=False,
|
success=False,
|
||||||
message=f"该Token已注册为账号: {existing.name}",
|
message=f"该Token已注册为账号: {existing.name}",
|
||||||
recruiter=_build_recruiter_response(existing)
|
recruiter=_build_recruiter_response(existing)
|
||||||
)
|
)
|
||||||
|
return BaseResponse.success(data=response_data, msg="账号已存在")
|
||||||
|
|
||||||
# 6. 创建账号
|
# 6. 创建账号
|
||||||
recruiter = service.add_recruiter(
|
recruiter = service.add_recruiter(
|
||||||
@@ -239,94 +242,98 @@ async def register_recruiter(
|
|||||||
# 权益信息获取失败不影响注册
|
# 权益信息获取失败不影响注册
|
||||||
print(f"[Register] 获取权益信息失败: {e}")
|
print(f"[Register] 获取权益信息失败: {e}")
|
||||||
|
|
||||||
return RecruiterRegisterResponse(
|
response_data = RecruiterRegisterResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message=f"账号注册成功: {account_name}",
|
message=f"账号注册成功: {account_name}",
|
||||||
recruiter=_build_recruiter_response(recruiter)
|
recruiter=_build_recruiter_response(recruiter)
|
||||||
)
|
)
|
||||||
|
return BaseResponse.success(data=response_data, msg="账号注册成功")
|
||||||
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
return RecruiterRegisterResponse(
|
return BaseResponse.error(msg=f"SDK未安装或导入失败: {str(e)}")
|
||||||
success=False,
|
|
||||||
message=f"SDK未安装或导入失败: {str(e)}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return RecruiterRegisterResponse(
|
return BaseResponse.error(msg=f"注册失败: {str(e)}")
|
||||||
success=False,
|
|
||||||
message=f"注册失败: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{recruiter_id}", response_model=RecruiterResponse)
|
@router.put("/{recruiter_id}", response_model=BaseResponse[RecruiterResponse])
|
||||||
async def update_recruiter(
|
async def update_recruiter(
|
||||||
recruiter_id: str,
|
recruiter_id: str,
|
||||||
data: RecruiterUpdate,
|
data: RecruiterUpdate,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
):
|
):
|
||||||
"""更新招聘者账号"""
|
"""更新招聘者账号"""
|
||||||
recruiter = service.get_recruiter(recruiter_id)
|
try:
|
||||||
if not recruiter:
|
recruiter = service.get_recruiter(recruiter_id)
|
||||||
raise HTTPException(status_code=404, detail="Recruiter not found")
|
if not recruiter:
|
||||||
|
return BaseResponse.error(msg="招聘者不存在", code=404)
|
||||||
# 更新字段
|
|
||||||
if data.name:
|
# 更新字段
|
||||||
recruiter.name = data.name
|
if data.name:
|
||||||
if data.wt_token:
|
recruiter.name = data.name
|
||||||
recruiter.wt_token = data.wt_token
|
if data.wt_token:
|
||||||
if data.status:
|
recruiter.wt_token = data.wt_token
|
||||||
try:
|
if data.status:
|
||||||
recruiter.status = RecruiterStatus(data.status.lower())
|
try:
|
||||||
except ValueError:
|
recruiter.status = RecruiterStatus(data.status.lower())
|
||||||
raise HTTPException(
|
except ValueError:
|
||||||
status_code=400,
|
return BaseResponse.error(msg=f"无效的状态: {data.status}", code=400)
|
||||||
detail=f"Invalid status: {data.status}"
|
|
||||||
)
|
updated = service.mapper.save(recruiter)
|
||||||
|
|
||||||
updated = service.mapper.save(recruiter)
|
return BaseResponse.success(data=_build_recruiter_response(updated), msg="更新招聘者成功")
|
||||||
|
except Exception as e:
|
||||||
return _build_recruiter_response(updated)
|
return BaseResponse.error(msg=f"更新招聘者失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{recruiter_id}")
|
@router.delete("/{recruiter_id}", response_model=BaseResponse[dict])
|
||||||
async def delete_recruiter(
|
async def delete_recruiter(
|
||||||
recruiter_id: str,
|
recruiter_id: str,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
):
|
):
|
||||||
"""删除招聘者账号"""
|
"""删除招聘者账号"""
|
||||||
success = service.delete_recruiter(recruiter_id)
|
try:
|
||||||
if not success:
|
success = service.delete_recruiter(recruiter_id)
|
||||||
raise HTTPException(status_code=404, detail="Recruiter not found")
|
if not success:
|
||||||
|
return BaseResponse.error(msg="招聘者不存在", code=404)
|
||||||
return APIResponse(success=True, message="Recruiter deleted successfully")
|
|
||||||
|
return BaseResponse.success(data={"deleted_id": recruiter_id}, msg="删除招聘者成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"删除招聘者失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{recruiter_id}/activate")
|
@router.post("/{recruiter_id}/activate", response_model=BaseResponse[dict])
|
||||||
async def activate_recruiter(
|
async def activate_recruiter(
|
||||||
recruiter_id: str,
|
recruiter_id: str,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
):
|
):
|
||||||
"""启用招聘者账号"""
|
"""启用招聘者账号"""
|
||||||
success = service.activate_recruiter(recruiter_id)
|
try:
|
||||||
if not success:
|
success = service.activate_recruiter(recruiter_id)
|
||||||
raise HTTPException(status_code=404, detail="Recruiter not found")
|
if not success:
|
||||||
|
return BaseResponse.error(msg="招聘者不存在", code=404)
|
||||||
return APIResponse(success=True, message="Recruiter activated")
|
|
||||||
|
return BaseResponse.success(data={"recruiter_id": recruiter_id}, msg="启用招聘者成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"启用招聘者失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{recruiter_id}/deactivate")
|
@router.post("/{recruiter_id}/deactivate", response_model=BaseResponse[dict])
|
||||||
async def deactivate_recruiter(
|
async def deactivate_recruiter(
|
||||||
recruiter_id: str,
|
recruiter_id: str,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
):
|
):
|
||||||
"""停用招聘者账号"""
|
"""停用招聘者账号"""
|
||||||
success = service.deactivate_recruiter(recruiter_id)
|
try:
|
||||||
if not success:
|
success = service.deactivate_recruiter(recruiter_id)
|
||||||
raise HTTPException(status_code=404, detail="Recruiter not found")
|
if not success:
|
||||||
|
return BaseResponse.error(msg="招聘者不存在", code=404)
|
||||||
return APIResponse(success=True, message="Recruiter deactivated")
|
|
||||||
|
return BaseResponse.success(data={"recruiter_id": recruiter_id}, msg="停用招聘者成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"停用招聘者失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{recruiter_id}/sync")
|
@router.post("/{recruiter_id}/sync", response_model=BaseResponse[dict])
|
||||||
async def sync_recruiter(
|
async def sync_recruiter(
|
||||||
recruiter_id: str,
|
recruiter_id: str,
|
||||||
service: RecruiterService = Depends(get_recruiter_service)
|
service: RecruiterService = Depends(get_recruiter_service)
|
||||||
@@ -336,14 +343,17 @@ async def sync_recruiter(
|
|||||||
|
|
||||||
触发账号状态、权益信息、职位列表的同步
|
触发账号状态、权益信息、职位列表的同步
|
||||||
"""
|
"""
|
||||||
recruiter = service.get_recruiter(recruiter_id)
|
try:
|
||||||
if not recruiter:
|
recruiter = service.get_recruiter(recruiter_id)
|
||||||
raise HTTPException(status_code=404, detail="Recruiter not found")
|
if not recruiter:
|
||||||
|
return BaseResponse.error(msg="招聘者不存在", code=404)
|
||||||
# TODO: 触发同步任务
|
|
||||||
# 可以调用 AccountSyncJob 来执行同步
|
# TODO: 触发同步任务
|
||||||
|
# 可以调用 AccountSyncJob 来执行同步
|
||||||
return APIResponse(
|
|
||||||
success=True,
|
return BaseResponse.success(
|
||||||
message=f"Sync triggered for recruiter: {recruiter.name}"
|
data={"recruiter_id": recruiter_id, "recruiter_name": recruiter.name},
|
||||||
)
|
msg=f"已触发账号同步: {recruiter.name}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"同步账号失败: {str(e)}")
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
"""
|
"""
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
|
from fastapi import APIRouter, Depends, BackgroundTasks
|
||||||
|
|
||||||
from ..schemas import (
|
from ..schemas import (
|
||||||
JobInfo, JobStatusInfo, JobConfigUpdate,
|
JobInfo, JobStatusInfo, JobConfigUpdate,
|
||||||
APIResponse
|
BaseResponse
|
||||||
)
|
)
|
||||||
from ...service.scheduler import get_scheduler
|
from ...service.scheduler import get_scheduler
|
||||||
|
|
||||||
@@ -22,32 +22,68 @@ def get_scheduler_instance():
|
|||||||
return get_scheduler()
|
return get_scheduler()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/jobs", response_model=List[JobInfo])
|
@router.get("/jobs", response_model=BaseResponse[List[JobInfo]])
|
||||||
async def list_jobs(
|
async def list_jobs(
|
||||||
scheduler=Depends(get_scheduler_instance)
|
scheduler=Depends(get_scheduler_instance)
|
||||||
):
|
):
|
||||||
"""获取所有定时任务列表"""
|
"""获取所有定时任务列表"""
|
||||||
jobs = scheduler.get_jobs()
|
try:
|
||||||
return [
|
jobs = scheduler.get_jobs()
|
||||||
JobInfo(
|
items = [
|
||||||
id=job["id"],
|
JobInfo(
|
||||||
name=job["name"],
|
id=job["id"],
|
||||||
next_run_time=job.get("next_run_time"),
|
name=job["name"],
|
||||||
trigger=job["trigger"],
|
next_run_time=job.get("next_run_time"),
|
||||||
type=job.get("type")
|
trigger=job["trigger"],
|
||||||
)
|
type=job.get("type")
|
||||||
for job in jobs
|
)
|
||||||
]
|
for job in jobs
|
||||||
|
]
|
||||||
|
return BaseResponse.success(data=items, msg="获取任务列表成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取任务列表失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/jobs/status", response_model=List[JobStatusInfo])
|
@router.get("/jobs/status", response_model=BaseResponse[List[JobStatusInfo]])
|
||||||
async def get_jobs_status(
|
async def get_jobs_status(
|
||||||
scheduler=Depends(get_scheduler_instance)
|
scheduler=Depends(get_scheduler_instance)
|
||||||
):
|
):
|
||||||
"""获取所有Job任务状态"""
|
"""获取所有Job任务状态"""
|
||||||
status_list = scheduler.get_job_status()
|
try:
|
||||||
return [
|
status_list = scheduler.get_job_status()
|
||||||
JobStatusInfo(
|
items = [
|
||||||
|
JobStatusInfo(
|
||||||
|
job_id=s["job_id"],
|
||||||
|
name=s["name"],
|
||||||
|
enabled=s["enabled"],
|
||||||
|
is_running=s["is_running"],
|
||||||
|
last_run_time=s.get("last_run_time"),
|
||||||
|
next_run_time=s.get("next_run_time"),
|
||||||
|
run_count=s["run_count"],
|
||||||
|
success_count=s["success_count"],
|
||||||
|
fail_count=s["fail_count"],
|
||||||
|
last_error=s.get("last_error")
|
||||||
|
)
|
||||||
|
for s in status_list
|
||||||
|
]
|
||||||
|
return BaseResponse.success(data=items, msg="获取任务状态成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取任务状态失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/jobs/{job_id}/status", response_model=BaseResponse[JobStatusInfo])
|
||||||
|
async def get_job_status(
|
||||||
|
job_id: str,
|
||||||
|
scheduler=Depends(get_scheduler_instance)
|
||||||
|
):
|
||||||
|
"""获取指定Job任务状态"""
|
||||||
|
try:
|
||||||
|
status_list = scheduler.get_job_status(job_id)
|
||||||
|
if not status_list:
|
||||||
|
return BaseResponse.error(msg="任务不存在", code=404)
|
||||||
|
|
||||||
|
s = status_list[0]
|
||||||
|
data = JobStatusInfo(
|
||||||
job_id=s["job_id"],
|
job_id=s["job_id"],
|
||||||
name=s["name"],
|
name=s["name"],
|
||||||
enabled=s["enabled"],
|
enabled=s["enabled"],
|
||||||
@@ -59,36 +95,12 @@ async def get_jobs_status(
|
|||||||
fail_count=s["fail_count"],
|
fail_count=s["fail_count"],
|
||||||
last_error=s.get("last_error")
|
last_error=s.get("last_error")
|
||||||
)
|
)
|
||||||
for s in status_list
|
return BaseResponse.success(data=data, msg="获取任务状态成功")
|
||||||
]
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取任务状态失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/jobs/{job_id}/status", response_model=JobStatusInfo)
|
@router.post("/jobs/{job_id}/run", response_model=BaseResponse[dict])
|
||||||
async def get_job_status(
|
|
||||||
job_id: str,
|
|
||||||
scheduler=Depends(get_scheduler_instance)
|
|
||||||
):
|
|
||||||
"""获取指定Job任务状态"""
|
|
||||||
status_list = scheduler.get_job_status(job_id)
|
|
||||||
if not status_list:
|
|
||||||
raise HTTPException(status_code=404, detail="Job not found")
|
|
||||||
|
|
||||||
s = status_list[0]
|
|
||||||
return JobStatusInfo(
|
|
||||||
job_id=s["job_id"],
|
|
||||||
name=s["name"],
|
|
||||||
enabled=s["enabled"],
|
|
||||||
is_running=s["is_running"],
|
|
||||||
last_run_time=s.get("last_run_time"),
|
|
||||||
next_run_time=s.get("next_run_time"),
|
|
||||||
run_count=s["run_count"],
|
|
||||||
success_count=s["success_count"],
|
|
||||||
fail_count=s["fail_count"],
|
|
||||||
last_error=s.get("last_error")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/jobs/{job_id}/run")
|
|
||||||
async def run_job_now(
|
async def run_job_now(
|
||||||
job_id: str,
|
job_id: str,
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
@@ -100,21 +112,24 @@ async def run_job_now(
|
|||||||
Args:
|
Args:
|
||||||
job_id: 任务ID (account_sync 或 resume_process)
|
job_id: 任务ID (account_sync 或 resume_process)
|
||||||
"""
|
"""
|
||||||
# 验证任务是否存在
|
try:
|
||||||
valid_jobs = ["account_sync", "resume_process", "crawl_boss", "analyze_pending"]
|
# 验证任务是否存在
|
||||||
if job_id not in valid_jobs:
|
valid_jobs = ["account_sync", "resume_process", "crawl_boss", "analyze_pending"]
|
||||||
raise HTTPException(status_code=400, detail=f"Invalid job_id. Valid: {valid_jobs}")
|
if job_id not in valid_jobs:
|
||||||
|
return BaseResponse.error(msg=f"无效的任务ID,有效值: {valid_jobs}", code=400)
|
||||||
# 在后台执行
|
|
||||||
background_tasks.add_task(scheduler.run_job_now, job_id)
|
# 在后台执行
|
||||||
|
background_tasks.add_task(scheduler.run_job_now, job_id)
|
||||||
return APIResponse(
|
|
||||||
success=True,
|
return BaseResponse.success(
|
||||||
message=f"Job {job_id} is running in background"
|
data={"job_id": job_id},
|
||||||
)
|
msg=f"任务 {job_id} 已在后台开始执行"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"执行任务失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/jobs/{job_id}/pause")
|
@router.post("/jobs/{job_id}/pause", response_model=BaseResponse[dict])
|
||||||
async def pause_job(
|
async def pause_job(
|
||||||
job_id: str,
|
job_id: str,
|
||||||
scheduler=Depends(get_scheduler_instance)
|
scheduler=Depends(get_scheduler_instance)
|
||||||
@@ -122,12 +137,12 @@ async def pause_job(
|
|||||||
"""暂停指定任务"""
|
"""暂停指定任务"""
|
||||||
try:
|
try:
|
||||||
scheduler.pause_job(job_id)
|
scheduler.pause_job(job_id)
|
||||||
return APIResponse(success=True, message=f"Job {job_id} paused")
|
return BaseResponse.success(data={"job_id": job_id}, msg=f"任务 {job_id} 已暂停")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
return BaseResponse.error(msg=f"暂停任务失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/jobs/{job_id}/resume")
|
@router.post("/jobs/{job_id}/resume", response_model=BaseResponse[dict])
|
||||||
async def resume_job(
|
async def resume_job(
|
||||||
job_id: str,
|
job_id: str,
|
||||||
scheduler=Depends(get_scheduler_instance)
|
scheduler=Depends(get_scheduler_instance)
|
||||||
@@ -135,12 +150,12 @@ async def resume_job(
|
|||||||
"""恢复指定任务"""
|
"""恢复指定任务"""
|
||||||
try:
|
try:
|
||||||
scheduler.resume_job(job_id)
|
scheduler.resume_job(job_id)
|
||||||
return APIResponse(success=True, message=f"Job {job_id} resumed")
|
return BaseResponse.success(data={"job_id": job_id}, msg=f"任务 {job_id} 已恢复")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
return BaseResponse.error(msg=f"恢复任务失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.put("/jobs/{job_id}/config")
|
@router.put("/jobs/{job_id}/config", response_model=BaseResponse[dict])
|
||||||
async def update_job_config(
|
async def update_job_config(
|
||||||
job_id: str,
|
job_id: str,
|
||||||
config: JobConfigUpdate,
|
config: JobConfigUpdate,
|
||||||
@@ -153,75 +168,78 @@ async def update_job_config(
|
|||||||
job_id: 任务ID
|
job_id: 任务ID
|
||||||
config: 配置更新
|
config: 配置更新
|
||||||
"""
|
"""
|
||||||
# 验证任务ID
|
|
||||||
valid_job_ids = ["account_sync", "resume_process"]
|
|
||||||
if job_id not in valid_job_ids:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=f"Can only config Job module tasks: {valid_job_ids}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 构建更新参数
|
|
||||||
update_kwargs = {}
|
|
||||||
if config.enabled is not None:
|
|
||||||
update_kwargs["enabled"] = config.enabled
|
|
||||||
if config.interval_minutes is not None:
|
|
||||||
update_kwargs["interval_minutes"] = config.interval_minutes
|
|
||||||
|
|
||||||
if not update_kwargs:
|
|
||||||
raise HTTPException(status_code=400, detail="No config to update")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 验证任务ID
|
||||||
|
valid_job_ids = ["account_sync", "resume_process"]
|
||||||
|
if job_id not in valid_job_ids:
|
||||||
|
return BaseResponse.error(
|
||||||
|
msg=f"只能配置Job模块任务: {valid_job_ids}",
|
||||||
|
code=400
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构建更新参数
|
||||||
|
update_kwargs = {}
|
||||||
|
if config.enabled is not None:
|
||||||
|
update_kwargs["enabled"] = config.enabled
|
||||||
|
if config.interval_minutes is not None:
|
||||||
|
update_kwargs["interval_minutes"] = config.interval_minutes
|
||||||
|
|
||||||
|
if not update_kwargs:
|
||||||
|
return BaseResponse.error(msg="没有要更新的配置", code=400)
|
||||||
|
|
||||||
scheduler.job_scheduler.update_job_config(job_id, **update_kwargs)
|
scheduler.job_scheduler.update_job_config(job_id, **update_kwargs)
|
||||||
return APIResponse(
|
return BaseResponse.success(
|
||||||
success=True,
|
data={"job_id": job_id, "config": update_kwargs},
|
||||||
message=f"Job {job_id} config updated",
|
msg=f"任务 {job_id} 配置已更新"
|
||||||
data=update_kwargs
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
return BaseResponse.error(msg=f"更新任务配置失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status", response_model=BaseResponse[dict])
|
||||||
async def get_scheduler_status(
|
async def get_scheduler_status(
|
||||||
scheduler=Depends(get_scheduler_instance)
|
scheduler=Depends(get_scheduler_instance)
|
||||||
):
|
):
|
||||||
"""获取调度器整体状态"""
|
"""获取调度器整体状态"""
|
||||||
jobs = scheduler.get_jobs()
|
try:
|
||||||
job_status = scheduler.get_job_status()
|
jobs = scheduler.get_jobs()
|
||||||
|
job_status = scheduler.get_job_status()
|
||||||
return {
|
|
||||||
"running": scheduler._running if hasattr(scheduler, '_running') else False,
|
data = {
|
||||||
"total_jobs": len(jobs),
|
"running": scheduler._running if hasattr(scheduler, '_running') else False,
|
||||||
"job_scheduler_jobs": len([j for j in jobs if j.get("type") == "job"]),
|
"total_jobs": len(jobs),
|
||||||
"legacy_jobs": len([j for j in jobs if j.get("type") == "legacy"]),
|
"job_scheduler_jobs": len([j for j in jobs if j.get("type") == "job"]),
|
||||||
"job_status_summary": {
|
"legacy_jobs": len([j for j in jobs if j.get("type") == "legacy"]),
|
||||||
"total": len(job_status),
|
"job_status_summary": {
|
||||||
"running": sum(1 for s in job_status if s["is_running"]),
|
"total": len(job_status),
|
||||||
"enabled": sum(1 for s in job_status if s["enabled"])
|
"running": sum(1 for s in job_status if s["is_running"]),
|
||||||
|
"enabled": sum(1 for s in job_status if s["enabled"])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return BaseResponse.success(data=data, msg="获取调度器状态成功")
|
||||||
|
except Exception as e:
|
||||||
|
return BaseResponse.error(msg=f"获取调度器状态失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start")
|
@router.post("/start", response_model=BaseResponse[dict])
|
||||||
async def start_scheduler(
|
async def start_scheduler(
|
||||||
scheduler=Depends(get_scheduler_instance)
|
scheduler=Depends(get_scheduler_instance)
|
||||||
):
|
):
|
||||||
"""启动调度器"""
|
"""启动调度器"""
|
||||||
try:
|
try:
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
return APIResponse(success=True, message="Scheduler started")
|
return BaseResponse.success(data={}, msg="调度器已启动")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
return BaseResponse.error(msg=f"启动调度器失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/stop")
|
@router.post("/stop", response_model=BaseResponse[dict])
|
||||||
async def stop_scheduler(
|
async def stop_scheduler(
|
||||||
scheduler=Depends(get_scheduler_instance)
|
scheduler=Depends(get_scheduler_instance)
|
||||||
):
|
):
|
||||||
"""停止调度器"""
|
"""停止调度器"""
|
||||||
try:
|
try:
|
||||||
scheduler.stop()
|
scheduler.stop()
|
||||||
return APIResponse(success=True, message="Scheduler stopped")
|
return BaseResponse.success(data={}, msg="调度器已停止")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
return BaseResponse.error(msg=f"停止调度器失败: {str(e)}")
|
||||||
|
|||||||
@@ -5,49 +5,53 @@
|
|||||||
"""
|
"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from ..schemas import APIResponse
|
from ..schemas import BaseResponse
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(tags=["系统"])
|
router = APIRouter(tags=["系统"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/", response_model=BaseResponse[dict])
|
||||||
async def root():
|
async def root():
|
||||||
"""API根路径"""
|
"""API根路径"""
|
||||||
return {
|
data = {
|
||||||
"name": "简历智能体 API",
|
"name": "简历智能体 API",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"recruiters": "/api/recruiters",
|
"recruiters": "/api/recruiters",
|
||||||
|
"jobs": "/api/jobs",
|
||||||
|
"candidates": "/candidates",
|
||||||
"scheduler": "/api/scheduler",
|
"scheduler": "/api/scheduler",
|
||||||
"health": "/health"
|
"health": "/health"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return BaseResponse.success(data=data, msg="简历智能体 API 运行中")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health")
|
@router.get("/health", response_model=BaseResponse[dict])
|
||||||
async def health_check():
|
async def health_check():
|
||||||
"""健康检查"""
|
"""健康检查"""
|
||||||
return {
|
data = {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"service": "hr-agent-api",
|
"service": "hr-agent-api",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
}
|
}
|
||||||
|
return BaseResponse.success(data=data, msg="服务运行正常")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/status")
|
@router.get("/api/status", response_model=BaseResponse[dict])
|
||||||
async def api_status():
|
async def api_status():
|
||||||
"""获取API状态信息"""
|
"""获取API状态信息"""
|
||||||
return APIResponse(
|
data = {
|
||||||
success=True,
|
"version": "0.1.0",
|
||||||
message="API is running",
|
"features": [
|
||||||
data={
|
"recruiter_management",
|
||||||
"version": "0.1.0",
|
"job_management",
|
||||||
"features": [
|
"candidate_management",
|
||||||
"recruiter_management",
|
"scheduler_management",
|
||||||
"scheduler_management",
|
"auto_register",
|
||||||
"auto_register"
|
"llm_evaluation"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
)
|
return BaseResponse.success(data=data, msg="API运行中")
|
||||||
|
|||||||
@@ -228,16 +228,89 @@ class CandidateUpdateScoreRequest(BaseModel):
|
|||||||
# ============== 职位(预留) ==============
|
# ============== 职位(预留) ==============
|
||||||
|
|
||||||
class JobPositionResponse(BaseModel):
|
class JobPositionResponse(BaseModel):
|
||||||
"""职位响应(预留)"""
|
"""职位响应"""
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
source: str
|
source: str
|
||||||
status: str
|
status: str
|
||||||
recruiter_id: Optional[str] = None
|
recruiter_id: Optional[str] = None
|
||||||
|
evaluation_schema_id: Optional[str] = Field(None, description="关联的评价方案ID")
|
||||||
|
department: Optional[str] = None
|
||||||
|
location: Optional[str] = None
|
||||||
|
salary_min: Optional[int] = None
|
||||||
|
salary_max: Optional[int] = None
|
||||||
|
requirements: Optional[Dict[str, Any]] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
candidate_count: int = Field(default=0, description="候选人数量")
|
||||||
|
new_candidate_count: int = Field(default=0, description="新候选人数量")
|
||||||
|
last_sync_at: Optional[datetime] = None
|
||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
class JobPositionListResponse(BaseModel):
|
class JobPositionListResponse(BaseModel):
|
||||||
"""职位列表响应(预留)"""
|
"""职位列表响应"""
|
||||||
total: int
|
total: int
|
||||||
items: List[JobPositionResponse]
|
items: List[JobPositionResponse]
|
||||||
|
|
||||||
|
|
||||||
|
class JobPositionCreateRequest(BaseModel):
|
||||||
|
"""创建职位请求"""
|
||||||
|
title: str = Field(..., description="职位标题")
|
||||||
|
source: str = Field(default="boss", description="平台来源: boss, liepin, etc.")
|
||||||
|
source_id: str = Field(..., description="来源平台ID")
|
||||||
|
recruiter_id: Optional[str] = Field(None, description="招聘者账号ID")
|
||||||
|
evaluation_schema_id: Optional[str] = Field(None, description="关联的评价方案ID")
|
||||||
|
department: Optional[str] = Field(None, description="部门")
|
||||||
|
location: Optional[str] = Field(None, description="地点")
|
||||||
|
salary_min: Optional[int] = Field(None, description="薪资下限(K)")
|
||||||
|
salary_max: Optional[int] = Field(None, description="薪资上限(K)")
|
||||||
|
requirements: Optional[Dict[str, Any]] = Field(None, description="职位要求JSON")
|
||||||
|
description: Optional[str] = Field(None, description="职位描述")
|
||||||
|
|
||||||
|
|
||||||
|
class JobPositionUpdateRequest(BaseModel):
|
||||||
|
"""更新职位请求"""
|
||||||
|
title: Optional[str] = Field(None, description="职位标题")
|
||||||
|
evaluation_schema_id: Optional[str] = Field(None, description="关联的评价方案ID")
|
||||||
|
department: Optional[str] = Field(None, description="部门")
|
||||||
|
location: Optional[str] = Field(None, description="地点")
|
||||||
|
salary_min: Optional[int] = Field(None, description="薪资下限(K)")
|
||||||
|
salary_max: Optional[int] = Field(None, description="薪资上限(K)")
|
||||||
|
requirements: Optional[Dict[str, Any]] = Field(None, description="职位要求JSON")
|
||||||
|
description: Optional[str] = Field(None, description="职位描述")
|
||||||
|
status: Optional[str] = Field(None, description="状态: ACTIVE, PAUSED, CLOSED, ARCHIVED")
|
||||||
|
|
||||||
|
|
||||||
|
class JobPositionFilterRequest(BaseModel):
|
||||||
|
"""职位筛选请求"""
|
||||||
|
source: Optional[str] = Field(None, description="平台来源")
|
||||||
|
recruiter_id: Optional[str] = Field(None, description="招聘者账号ID")
|
||||||
|
evaluation_schema_id: Optional[str] = Field(None, description="评价方案ID")
|
||||||
|
status: Optional[str] = Field(None, description="状态: ACTIVE, PAUSED, CLOSED, ARCHIVED")
|
||||||
|
keyword: Optional[str] = Field(None, description="关键词搜索(标题/部门)")
|
||||||
|
page: int = Field(default=1, ge=1, description="页码")
|
||||||
|
page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
|
||||||
|
|
||||||
|
|
||||||
|
class JobBindSchemaRequest(BaseModel):
|
||||||
|
"""职位关联评价方案请求"""
|
||||||
|
evaluation_schema_id: str = Field(..., description="评价方案ID")
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluationSchemaResponse(BaseModel):
|
||||||
|
"""评价方案响应"""
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
dimensions: Optional[List[Dict[str, Any]]] = None
|
||||||
|
weights: Optional[Dict[str, float]] = None
|
||||||
|
is_default: bool = False
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluationSchemaListResponse(BaseModel):
|
||||||
|
"""评价方案列表响应"""
|
||||||
|
total: int
|
||||||
|
items: List[EvaluationSchemaResponse]
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class Job:
|
|||||||
source: CandidateSource = CandidateSource.BOSS
|
source: CandidateSource = CandidateSource.BOSS
|
||||||
source_id: str = ""
|
source_id: str = ""
|
||||||
recruiter_id: Optional[str] = None # 关联的招聘者账号ID
|
recruiter_id: Optional[str] = None # 关联的招聘者账号ID
|
||||||
|
evaluation_schema_id: Optional[str] = None # 关联的评价方案ID
|
||||||
|
|
||||||
# 职位信息
|
# 职位信息
|
||||||
title: str = ""
|
title: str = ""
|
||||||
|
|||||||
@@ -4,16 +4,18 @@
|
|||||||
负责处理简历信息入库操作:
|
负责处理简历信息入库操作:
|
||||||
1. 遍历所有活跃账号的职位
|
1. 遍历所有活跃账号的职位
|
||||||
2. 获取职位下的候选人列表
|
2. 获取职位下的候选人列表
|
||||||
3. 获取候选人简历详情
|
3. 获取候选人简历详情,同步进行LLM评分
|
||||||
4. 将简历信息入库
|
4. 将简历信息和评分结果入库
|
||||||
|
5. 超过阈值触发打招呼和HR通知
|
||||||
|
|
||||||
入库更新操作:
|
架构特点:
|
||||||
- 插入/更新 candidates 表
|
- 多账号并行处理(使用asyncio.gather)
|
||||||
- 插入/更新 resumes 表
|
- 每个账号内部串行处理候选人(确保同一账号下操作的顺序性,避免Boss平台检测)
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional, Dict, Any
|
from typing import List, Optional, Dict, Any
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from ..domain.candidate import Candidate, CandidateSource, CandidateStatus
|
from ..domain.candidate import Candidate, CandidateSource, CandidateStatus
|
||||||
from ..domain.resume import Resume
|
from ..domain.resume import Resume
|
||||||
@@ -21,7 +23,9 @@ from ..domain.job import Job
|
|||||||
from ..domain.recruiter import Recruiter, RecruiterStatus
|
from ..domain.recruiter import Recruiter, RecruiterStatus
|
||||||
from ..service.recruiter_service import RecruiterService
|
from ..service.recruiter_service import RecruiterService
|
||||||
from ..service.ingestion.unified_ingestion_service import UnifiedIngestionService
|
from ..service.ingestion.unified_ingestion_service import UnifiedIngestionService
|
||||||
|
from ..service.resume_evaluation_service import ResumeEvaluationService, ProcessResult
|
||||||
from ..service.crawler.base_crawler import BaseCrawler
|
from ..service.crawler.base_crawler import BaseCrawler
|
||||||
|
from ..config.settings import Settings, get_settings
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -35,9 +39,13 @@ class ResumeProcessResult:
|
|||||||
candidates_new: int = 0
|
candidates_new: int = 0
|
||||||
candidates_updated: int = 0
|
candidates_updated: int = 0
|
||||||
candidates_failed: int = 0
|
candidates_failed: int = 0
|
||||||
|
candidates_greeted: int = 0 # 打招呼成功数
|
||||||
|
candidates_notified: int = 0 # 通知HR成功数
|
||||||
|
high_score_count: int = 0 # 高分候选人数量
|
||||||
success: bool = True
|
success: bool = True
|
||||||
message: str = ""
|
message: str = ""
|
||||||
timestamp: datetime = None
|
timestamp: datetime = None
|
||||||
|
process_results: List[ProcessResult] = field(default_factory=list) # 详细处理结果
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if self.timestamp is None:
|
if self.timestamp is None:
|
||||||
@@ -48,62 +56,88 @@ class ResumeProcessJob:
|
|||||||
"""
|
"""
|
||||||
简历处理定时任务
|
简历处理定时任务
|
||||||
|
|
||||||
定时执行简历爬取和入库:
|
定时执行简历爬取、LLM评分和入库:
|
||||||
- 获取所有活跃账号
|
- 获取所有活跃账号
|
||||||
- 遍历账号下的职位
|
- 多账号并行处理(asyncio.gather)
|
||||||
- 获取候选人列表和简历详情
|
- 单账号内串行处理候选人(简历获取+LLM分析同步,避免Boss检测)
|
||||||
- 统一入库处理
|
- 超阈值触发打招呼和HR通知
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
recruiter_service: RecruiterService,
|
recruiter_service: RecruiterService,
|
||||||
ingestion_service: UnifiedIngestionService
|
ingestion_service: UnifiedIngestionService,
|
||||||
|
evaluation_service: Optional[ResumeEvaluationService] = None,
|
||||||
|
settings: Optional[Settings] = None
|
||||||
):
|
):
|
||||||
self.recruiter_service = recruiter_service
|
self.recruiter_service = recruiter_service
|
||||||
self.ingestion_service = ingestion_service
|
self.ingestion_service = ingestion_service
|
||||||
# 每个职位最多处理的候选人数量
|
self.evaluation_service = evaluation_service
|
||||||
self.max_candidates_per_job = 20
|
self.settings = settings or get_settings()
|
||||||
|
|
||||||
|
# 从配置中读取参数
|
||||||
|
self.max_candidates_per_job = self.settings.resume_max_per_job
|
||||||
|
self.process_delay = self.settings.resume_process_delay
|
||||||
|
|
||||||
async def execute(self) -> List[ResumeProcessResult]:
|
async def execute(self) -> List[ResumeProcessResult]:
|
||||||
"""
|
"""
|
||||||
执行简历处理任务
|
执行简历处理任务
|
||||||
|
|
||||||
|
架构:
|
||||||
|
- 多个账号并行处理(使用asyncio.gather)
|
||||||
|
- 每个账号内部串行处理候选人(确保同一账号下操作的顺序性)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[ResumeProcessResult]: 各职位的处理结果
|
List[ResumeProcessResult]: 各职位的处理结果
|
||||||
"""
|
"""
|
||||||
print(f"[{datetime.now()}] 开始执行简历处理任务...")
|
print(f"[{datetime.now()}] 开始执行简历处理任务...")
|
||||||
|
print(f"[{datetime.now()}] 配置: 评分阈值={self.settings.score_threshold_greet}, "
|
||||||
results = []
|
f"自动打招呼={'ON' if self.settings.auto_greet_enabled else 'OFF'}, "
|
||||||
|
f"自动通知={'ON' if self.settings.auto_notify_enabled else 'OFF'}")
|
||||||
|
|
||||||
# 获取所有活跃且同步成功的账号
|
# 获取所有活跃且同步成功的账号
|
||||||
recruiters = self._get_eligible_recruiters()
|
recruiters = self._get_eligible_recruiters()
|
||||||
|
|
||||||
if not recruiters:
|
if not recruiters:
|
||||||
print(f"[{datetime.now()}] 没有符合条件的招聘者账号(需要ACTIVE状态且同步成功)")
|
print(f"[{datetime.now()}] 没有符合条件的招聘者账号(需要ACTIVE状态且同步成功)")
|
||||||
return results
|
return []
|
||||||
|
|
||||||
print(f"[{datetime.now()}] 找到 {len(recruiters)} 个符合条件的账号")
|
print(f"[{datetime.now()}] 找到 {len(recruiters)} 个符合条件的账号,开始并行处理...")
|
||||||
|
|
||||||
for recruiter in recruiters:
|
# 多账号并行处理
|
||||||
try:
|
tasks = [
|
||||||
job_results = await self._process_recruiter_jobs(recruiter)
|
self._process_recruiter_jobs_with_lock(recruiter)
|
||||||
results.extend(job_results)
|
for recruiter in recruiters
|
||||||
except Exception as e:
|
]
|
||||||
print(f"[{datetime.now()}] 处理账号 {recruiter.name} 时发生异常: {e}")
|
|
||||||
|
results_list = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
# 汇总结果
|
||||||
|
all_results = []
|
||||||
|
for i, result in enumerate(results_list):
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
print(f"[{datetime.now()}] 账号 {recruiters[i].name} 处理异常: {result}")
|
||||||
|
elif isinstance(result, list):
|
||||||
|
all_results.extend(result)
|
||||||
|
|
||||||
# 统计结果
|
# 统计结果
|
||||||
total_processed = sum(r.candidates_processed for r in results)
|
total_processed = sum(r.candidates_processed for r in all_results)
|
||||||
total_new = sum(r.candidates_new for r in results)
|
total_new = sum(r.candidates_new for r in all_results)
|
||||||
total_failed = sum(r.candidates_failed for r in results)
|
total_failed = sum(r.candidates_failed for r in all_results)
|
||||||
|
total_greeted = sum(r.candidates_greeted for r in all_results)
|
||||||
|
total_notified = sum(r.candidates_notified for r in all_results)
|
||||||
|
total_high_score = sum(r.high_score_count for r in all_results)
|
||||||
|
|
||||||
print(f"[{datetime.now()}] 简历处理任务完成:")
|
print(f"[{datetime.now()}] 简历处理任务完成:")
|
||||||
print(f" - 处理职位数: {len(results)}")
|
print(f" - 处理职位数: {len(all_results)}")
|
||||||
print(f" - 处理候选人数: {total_processed}")
|
print(f" - 处理候选人数: {total_processed}")
|
||||||
print(f" - 新增候选人数: {total_new}")
|
print(f" - 新增候选人数: {total_new}")
|
||||||
|
print(f" - 高分候选人数: {total_high_score}")
|
||||||
|
print(f" - 打招呼成功数: {total_greeted}")
|
||||||
|
print(f" - HR通知成功数: {total_notified}")
|
||||||
print(f" - 失败数: {total_failed}")
|
print(f" - 失败数: {total_failed}")
|
||||||
|
|
||||||
return results
|
return all_results
|
||||||
|
|
||||||
def _get_eligible_recruiters(self) -> List[Recruiter]:
|
def _get_eligible_recruiters(self) -> List[Recruiter]:
|
||||||
"""
|
"""
|
||||||
@@ -136,9 +170,15 @@ class ResumeProcessJob:
|
|||||||
|
|
||||||
return eligible_recruiters
|
return eligible_recruiters
|
||||||
|
|
||||||
async def _process_recruiter_jobs(self, recruiter: Recruiter) -> List[ResumeProcessResult]:
|
async def _process_recruiter_jobs_with_lock(
|
||||||
|
self,
|
||||||
|
recruiter: Recruiter
|
||||||
|
) -> List[ResumeProcessResult]:
|
||||||
"""
|
"""
|
||||||
处理单个账号下的所有职位
|
处理单个账号下的所有职位(内部串行)
|
||||||
|
|
||||||
|
关键:同一账号下的简历获取和LLM分析必须串行执行,
|
||||||
|
避免Boss平台检测到异常操作模式
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
recruiter: 招聘者账号
|
recruiter: 招聘者账号
|
||||||
@@ -165,7 +205,9 @@ class ResumeProcessJob:
|
|||||||
|
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
try:
|
try:
|
||||||
result = await self._process_single_job(recruiter, crawler, job)
|
result = await self._process_single_job_with_evaluation(
|
||||||
|
recruiter, crawler, job
|
||||||
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_result = ResumeProcessResult(
|
error_result = ResumeProcessResult(
|
||||||
@@ -184,6 +226,111 @@ class ResumeProcessJob:
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
async def _process_single_job_with_evaluation(
|
||||||
|
self,
|
||||||
|
recruiter: Recruiter,
|
||||||
|
crawler: BaseCrawler,
|
||||||
|
job: Job
|
||||||
|
) -> ResumeProcessResult:
|
||||||
|
"""
|
||||||
|
处理单个职位的候选人(集成LLM评分)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
recruiter: 招聘者账号
|
||||||
|
crawler: 爬虫实例
|
||||||
|
job: 职位
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ResumeProcessResult: 处理结果
|
||||||
|
"""
|
||||||
|
result = ResumeProcessResult(
|
||||||
|
job_id=job.id or "",
|
||||||
|
job_title=job.title,
|
||||||
|
recruiter_id=recruiter.id or "",
|
||||||
|
recruiter_name=recruiter.name
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[{datetime.now()}] 处理职位: {job.title} (ID: {job.source_id}, "
|
||||||
|
f"评价方案: {job.evaluation_schema_id or 'general'})")
|
||||||
|
|
||||||
|
# 获取候选人列表
|
||||||
|
candidates = crawler.get_candidates(job.source_id, page=1)
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
result.message = "该职位下没有候选人"
|
||||||
|
print(f"[{datetime.now()}] 职位 {job.title} 没有候选人")
|
||||||
|
return result
|
||||||
|
|
||||||
|
print(f"[{datetime.now()}] 职位 {job.title} 找到 {len(candidates)} 个候选人")
|
||||||
|
|
||||||
|
# 限制处理数量
|
||||||
|
candidates_to_process = candidates[:self.max_candidates_per_job]
|
||||||
|
|
||||||
|
# 串行处理每个候选人(关键:不能并行!)
|
||||||
|
for candidate in candidates_to_process:
|
||||||
|
try:
|
||||||
|
# 使用评分服务同步处理(获取简历 -> LLM分析 -> 入库 -> 触发动作)
|
||||||
|
if self.evaluation_service:
|
||||||
|
process_result = await self.evaluation_service.process_candidate_sync(
|
||||||
|
crawler=crawler,
|
||||||
|
candidate=candidate,
|
||||||
|
job=job,
|
||||||
|
schema_id=job.evaluation_schema_id
|
||||||
|
)
|
||||||
|
|
||||||
|
result.candidates_processed += 1
|
||||||
|
result.process_results.append(process_result)
|
||||||
|
|
||||||
|
if process_result.success:
|
||||||
|
result.candidates_new += 1
|
||||||
|
|
||||||
|
# 统计高分候选人
|
||||||
|
if process_result.overall_score >= self.settings.score_threshold_greet:
|
||||||
|
result.high_score_count += 1
|
||||||
|
|
||||||
|
# 统计打招呼和通知
|
||||||
|
if process_result.greeting_sent:
|
||||||
|
result.candidates_greeted += 1
|
||||||
|
if process_result.notification_sent:
|
||||||
|
result.candidates_notified += 1
|
||||||
|
else:
|
||||||
|
result.candidates_failed += 1
|
||||||
|
else:
|
||||||
|
# 回退到旧的处理方式(仅入库,不评分)
|
||||||
|
process_status = await self._process_single_candidate(
|
||||||
|
recruiter, crawler, job, candidate
|
||||||
|
)
|
||||||
|
|
||||||
|
result.candidates_processed += 1
|
||||||
|
|
||||||
|
if process_status == "new":
|
||||||
|
result.candidates_new += 1
|
||||||
|
elif process_status == "updated":
|
||||||
|
result.candidates_updated += 1
|
||||||
|
elif process_status == "failed":
|
||||||
|
result.candidates_failed += 1
|
||||||
|
|
||||||
|
# 添加延迟,避免请求过快被Boss平台拦截
|
||||||
|
await asyncio.sleep(self.process_delay)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result.candidates_processed += 1
|
||||||
|
result.candidates_failed += 1
|
||||||
|
print(f"[{datetime.now()}] 处理候选人 {candidate.name} 失败: {e}")
|
||||||
|
|
||||||
|
result.success = result.candidates_failed == 0
|
||||||
|
result.message = (
|
||||||
|
f"处理完成: 新增{result.candidates_new}, "
|
||||||
|
f"高分{result.high_score_count}, "
|
||||||
|
f"打招呼{result.candidates_greeted}, "
|
||||||
|
f"通知{result.candidates_notified}, "
|
||||||
|
f"失败{result.candidates_failed}"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[{datetime.now()}] 职位 {job.title} 处理完成: {result.message}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
async def _process_single_job(
|
async def _process_single_job(
|
||||||
self,
|
self,
|
||||||
recruiter: Recruiter,
|
recruiter: Recruiter,
|
||||||
@@ -191,7 +338,9 @@ class ResumeProcessJob:
|
|||||||
job: Job
|
job: Job
|
||||||
) -> ResumeProcessResult:
|
) -> ResumeProcessResult:
|
||||||
"""
|
"""
|
||||||
处理单个职位的候选人
|
处理单个职位的候选人(旧版本,仅入库)
|
||||||
|
|
||||||
|
已废弃,请使用 _process_single_job_with_evaluation
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
recruiter: 招聘者账号
|
recruiter: 招聘者账号
|
||||||
|
|||||||
@@ -1 +1,15 @@
|
|||||||
"""Mapper layer - Data access"""
|
"""Mapper layer - Data access"""
|
||||||
|
|
||||||
|
from .candidate_mapper import CandidateMapper
|
||||||
|
from .job_mapper import JobMapper
|
||||||
|
from .recruiter_mapper import RecruiterMapper
|
||||||
|
from .resume_mapper import ResumeMapper
|
||||||
|
from .evaluation_mapper import EvaluationMapper
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'CandidateMapper',
|
||||||
|
'JobMapper',
|
||||||
|
'RecruiterMapper',
|
||||||
|
'ResumeMapper',
|
||||||
|
'EvaluationMapper',
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
"""Evaluation data mapper using SQLAlchemy"""
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
|
||||||
|
from sqlalchemy import select, update, delete
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from ..domain.evaluation import Evaluation, DimensionScore, EvaluationSchema
|
||||||
|
from ..domain.enums import Recommendation
|
||||||
|
from ..config.database import get_db_manager, EvaluationModel, EvaluationSchemaModel
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluationMapper:
|
||||||
|
"""评价记录数据访问 - SQLAlchemy实现"""
|
||||||
|
|
||||||
|
def __init__(self, db_url: Optional[str] = None):
|
||||||
|
self.db_manager = get_db_manager(db_url)
|
||||||
|
# 确保表存在
|
||||||
|
self.db_manager.create_tables()
|
||||||
|
|
||||||
|
def _get_session(self) -> Session:
|
||||||
|
"""获取数据库会话"""
|
||||||
|
return self.db_manager.get_session()
|
||||||
|
|
||||||
|
def _model_to_entity(self, model: EvaluationModel) -> Evaluation:
|
||||||
|
"""将模型转换为实体"""
|
||||||
|
# 解析维度评分
|
||||||
|
dimension_scores = []
|
||||||
|
if model.dimension_scores:
|
||||||
|
ds_list = model.dimension_scores if isinstance(model.dimension_scores, list) else json.loads(model.dimension_scores)
|
||||||
|
for ds_data in ds_list:
|
||||||
|
dimension_scores.append(DimensionScore(
|
||||||
|
dimension_id=ds_data.get('dimension_id', ''),
|
||||||
|
dimension_name=ds_data.get('dimension_name', ''),
|
||||||
|
score=float(ds_data.get('score', 0)),
|
||||||
|
weight=float(ds_data.get('weight', 1.0)),
|
||||||
|
comment=ds_data.get('comment')
|
||||||
|
))
|
||||||
|
|
||||||
|
# 解析推荐意见
|
||||||
|
recommendation = None
|
||||||
|
if model.recommendation:
|
||||||
|
try:
|
||||||
|
recommendation = Recommendation(model.recommendation.lower())
|
||||||
|
except ValueError:
|
||||||
|
recommendation = None
|
||||||
|
|
||||||
|
# 解析tags, strengths, weaknesses
|
||||||
|
tags = model.tags if isinstance(model.tags, list) else (json.loads(model.tags) if model.tags else [])
|
||||||
|
strengths = model.strengths if isinstance(model.strengths, list) else (json.loads(model.strengths) if model.strengths else [])
|
||||||
|
weaknesses = model.weaknesses if isinstance(model.weaknesses, list) else (json.loads(model.weaknesses) if model.weaknesses else [])
|
||||||
|
|
||||||
|
return Evaluation(
|
||||||
|
id=model.id,
|
||||||
|
candidate_id=model.candidate_id,
|
||||||
|
schema_id=model.schema_id,
|
||||||
|
job_id=model.job_id,
|
||||||
|
overall_score=float(model.overall_score) if model.overall_score else 0.0,
|
||||||
|
dimension_scores=dimension_scores,
|
||||||
|
tags=tags,
|
||||||
|
summary=model.summary,
|
||||||
|
strengths=strengths,
|
||||||
|
weaknesses=weaknesses,
|
||||||
|
recommendation=recommendation,
|
||||||
|
raw_response=model.raw_response,
|
||||||
|
created_at=model.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
def _entity_to_model(self, entity: Evaluation) -> EvaluationModel:
|
||||||
|
"""将实体转换为模型"""
|
||||||
|
# 序列化维度评分
|
||||||
|
dimension_scores_json = [
|
||||||
|
{
|
||||||
|
'dimension_id': ds.dimension_id,
|
||||||
|
'dimension_name': ds.dimension_name,
|
||||||
|
'score': ds.score,
|
||||||
|
'weight': ds.weight,
|
||||||
|
'comment': ds.comment
|
||||||
|
}
|
||||||
|
for ds in entity.dimension_scores
|
||||||
|
] if entity.dimension_scores else []
|
||||||
|
|
||||||
|
return EvaluationModel(
|
||||||
|
id=entity.id or str(uuid.uuid4()),
|
||||||
|
candidate_id=entity.candidate_id,
|
||||||
|
schema_id=entity.schema_id,
|
||||||
|
job_id=entity.job_id,
|
||||||
|
overall_score=entity.overall_score,
|
||||||
|
dimension_scores=dimension_scores_json,
|
||||||
|
tags=entity.tags or [],
|
||||||
|
summary=entity.summary,
|
||||||
|
strengths=entity.strengths or [],
|
||||||
|
weaknesses=entity.weaknesses or [],
|
||||||
|
recommendation=entity.recommendation.value if entity.recommendation else None,
|
||||||
|
raw_response=entity.raw_response
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, evaluation: Evaluation) -> Evaluation:
|
||||||
|
"""保存评价记录(插入或更新)"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
if evaluation.id:
|
||||||
|
# 检查是否已存在
|
||||||
|
existing = session.execute(
|
||||||
|
select(EvaluationModel).where(EvaluationModel.id == evaluation.id)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
# 更新现有记录
|
||||||
|
existing.overall_score = evaluation.overall_score
|
||||||
|
existing.dimension_scores = [
|
||||||
|
{
|
||||||
|
'dimension_id': ds.dimension_id,
|
||||||
|
'dimension_name': ds.dimension_name,
|
||||||
|
'score': ds.score,
|
||||||
|
'weight': ds.weight,
|
||||||
|
'comment': ds.comment
|
||||||
|
}
|
||||||
|
for ds in evaluation.dimension_scores
|
||||||
|
] if evaluation.dimension_scores else []
|
||||||
|
existing.tags = evaluation.tags or []
|
||||||
|
existing.summary = evaluation.summary
|
||||||
|
existing.strengths = evaluation.strengths or []
|
||||||
|
existing.weaknesses = evaluation.weaknesses or []
|
||||||
|
existing.recommendation = evaluation.recommendation.value if evaluation.recommendation else None
|
||||||
|
existing.raw_response = evaluation.raw_response
|
||||||
|
session.commit()
|
||||||
|
return evaluation
|
||||||
|
|
||||||
|
# 插入新记录
|
||||||
|
evaluation.id = evaluation.id or str(uuid.uuid4())
|
||||||
|
model = self._entity_to_model(evaluation)
|
||||||
|
session.add(model)
|
||||||
|
session.commit()
|
||||||
|
return evaluation
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_by_id(self, evaluation_id: str) -> Optional[Evaluation]:
|
||||||
|
"""根据ID查询评价记录"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
result = session.execute(
|
||||||
|
select(EvaluationModel).where(EvaluationModel.id == evaluation_id)
|
||||||
|
)
|
||||||
|
model = result.scalar_one_or_none()
|
||||||
|
return self._model_to_entity(model) if model else None
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_by_candidate_id(self, candidate_id: str) -> List[Evaluation]:
|
||||||
|
"""根据候选人ID查询评价记录"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
result = session.execute(
|
||||||
|
select(EvaluationModel)
|
||||||
|
.where(EvaluationModel.candidate_id == candidate_id)
|
||||||
|
.order_by(EvaluationModel.created_at.desc())
|
||||||
|
)
|
||||||
|
models = result.scalars().all()
|
||||||
|
return [self._model_to_entity(m) for m in models]
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_latest_by_candidate_id(self, candidate_id: str) -> Optional[Evaluation]:
|
||||||
|
"""获取候选人最新的评价记录"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
result = session.execute(
|
||||||
|
select(EvaluationModel)
|
||||||
|
.where(EvaluationModel.candidate_id == candidate_id)
|
||||||
|
.order_by(EvaluationModel.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
model = result.scalar_one_or_none()
|
||||||
|
return self._model_to_entity(model) if model else None
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_by_job_id(self, job_id: str) -> List[Evaluation]:
|
||||||
|
"""根据职位ID查询评价记录"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
result = session.execute(
|
||||||
|
select(EvaluationModel)
|
||||||
|
.where(EvaluationModel.job_id == job_id)
|
||||||
|
.order_by(EvaluationModel.created_at.desc())
|
||||||
|
)
|
||||||
|
models = result.scalars().all()
|
||||||
|
return [self._model_to_entity(m) for m in models]
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_by_schema_id(self, schema_id: str) -> List[Evaluation]:
|
||||||
|
"""根据评价方案ID查询评价记录"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
result = session.execute(
|
||||||
|
select(EvaluationModel)
|
||||||
|
.where(EvaluationModel.schema_id == schema_id)
|
||||||
|
.order_by(EvaluationModel.created_at.desc())
|
||||||
|
)
|
||||||
|
models = result.scalars().all()
|
||||||
|
return [self._model_to_entity(m) for m in models]
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_by_candidate_and_job(self, candidate_id: str, job_id: str) -> Optional[Evaluation]:
|
||||||
|
"""根据候选人ID和职位ID查询评价记录"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
result = session.execute(
|
||||||
|
select(EvaluationModel)
|
||||||
|
.where(EvaluationModel.candidate_id == candidate_id)
|
||||||
|
.where(EvaluationModel.job_id == job_id)
|
||||||
|
.order_by(EvaluationModel.created_at.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
model = result.scalar_one_or_none()
|
||||||
|
return self._model_to_entity(model) if model else None
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_high_score_candidates(
|
||||||
|
self,
|
||||||
|
min_score: float,
|
||||||
|
job_id: Optional[str] = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
查询高分候选人
|
||||||
|
|
||||||
|
Args:
|
||||||
|
min_score: 最低评分
|
||||||
|
job_id: 职位ID(可选)
|
||||||
|
page: 页码
|
||||||
|
page_size: 每页数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (评价列表, 总记录数)
|
||||||
|
"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
stmt = select(EvaluationModel).where(EvaluationModel.overall_score >= min_score)
|
||||||
|
count_stmt = select(func.count()).select_from(EvaluationModel).where(
|
||||||
|
EvaluationModel.overall_score >= min_score
|
||||||
|
)
|
||||||
|
|
||||||
|
if job_id:
|
||||||
|
stmt = stmt.where(EvaluationModel.job_id == job_id)
|
||||||
|
count_stmt = count_stmt.where(EvaluationModel.job_id == job_id)
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
total = session.execute(count_stmt).scalar()
|
||||||
|
|
||||||
|
# 分页查询,按分数降序
|
||||||
|
stmt = stmt.order_by(EvaluationModel.overall_score.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 delete(self, evaluation_id: str) -> bool:
|
||||||
|
"""删除评价记录"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
stmt = delete(EvaluationModel).where(EvaluationModel.id == evaluation_id)
|
||||||
|
result = session.execute(stmt)
|
||||||
|
session.commit()
|
||||||
|
return result.rowcount > 0
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def delete_by_candidate_id(self, candidate_id: str) -> int:
|
||||||
|
"""删除候选人的所有评价记录"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
stmt = delete(EvaluationModel).where(EvaluationModel.candidate_id == candidate_id)
|
||||||
|
result = session.execute(stmt)
|
||||||
|
session.commit()
|
||||||
|
return result.rowcount
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
# ============== 评价方案查询方法 ==============
|
||||||
|
|
||||||
|
def _schema_model_to_entity(self, model: EvaluationSchemaModel) -> EvaluationSchema:
|
||||||
|
"""将评价方案模型转换为实体"""
|
||||||
|
dimensions = model.dimensions if isinstance(model.dimensions, list) else (
|
||||||
|
json.loads(model.dimensions) if model.dimensions else []
|
||||||
|
)
|
||||||
|
weights = model.weights if isinstance(model.weights, dict) else (
|
||||||
|
json.loads(model.weights) if model.weights else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
return EvaluationSchema(
|
||||||
|
id=model.id,
|
||||||
|
name=model.name,
|
||||||
|
description=model.description,
|
||||||
|
dimensions=dimensions,
|
||||||
|
weights=weights,
|
||||||
|
prompt_template=model.prompt_template,
|
||||||
|
is_default=model.is_default,
|
||||||
|
created_at=model.created_at,
|
||||||
|
updated_at=model.updated_at
|
||||||
|
)
|
||||||
|
|
||||||
|
def find_schema_by_id(self, schema_id: str) -> Optional[EvaluationSchema]:
|
||||||
|
"""根据ID查询评价方案"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
result = session.execute(
|
||||||
|
select(EvaluationSchemaModel).where(EvaluationSchemaModel.id == schema_id)
|
||||||
|
)
|
||||||
|
model = result.scalar_one_or_none()
|
||||||
|
return self._schema_model_to_entity(model) if model else None
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_default_schema(self) -> Optional[EvaluationSchema]:
|
||||||
|
"""查询默认评价方案"""
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
result = session.execute(
|
||||||
|
select(EvaluationSchemaModel)
|
||||||
|
.where(EvaluationSchemaModel.is_default == True)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
model = result.scalar_one_or_none()
|
||||||
|
return self._schema_model_to_entity(model) if model else None
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
def find_all_schemas(
|
||||||
|
self,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
查询所有评价方案
|
||||||
|
|
||||||
|
Args:
|
||||||
|
page: 页码
|
||||||
|
page_size: 每页数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (评价方案列表, 总记录数)
|
||||||
|
"""
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
stmt = select(EvaluationSchemaModel)
|
||||||
|
count_stmt = select(func.count()).select_from(EvaluationSchemaModel)
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
total = session.execute(count_stmt).scalar()
|
||||||
|
|
||||||
|
# 分页查询,默认方案在前,按创建时间降序
|
||||||
|
stmt = stmt.order_by(
|
||||||
|
EvaluationSchemaModel.is_default.desc(),
|
||||||
|
EvaluationSchemaModel.created_at.desc()
|
||||||
|
)
|
||||||
|
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||||
|
|
||||||
|
results = session.execute(stmt).scalars().all()
|
||||||
|
return [self._schema_model_to_entity(r) for r in results], total
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
@@ -45,6 +45,7 @@ class JobMapper:
|
|||||||
source=CandidateSource(source_value),
|
source=CandidateSource(source_value),
|
||||||
source_id=model.source_id,
|
source_id=model.source_id,
|
||||||
recruiter_id=model.recruiter_id,
|
recruiter_id=model.recruiter_id,
|
||||||
|
evaluation_schema_id=model.evaluation_schema_id,
|
||||||
title=model.title,
|
title=model.title,
|
||||||
department=model.department,
|
department=model.department,
|
||||||
location=model.location,
|
location=model.location,
|
||||||
@@ -79,6 +80,7 @@ class JobMapper:
|
|||||||
source=entity.source.value,
|
source=entity.source.value,
|
||||||
source_id=entity.source_id,
|
source_id=entity.source_id,
|
||||||
recruiter_id=entity.recruiter_id,
|
recruiter_id=entity.recruiter_id,
|
||||||
|
evaluation_schema_id=entity.evaluation_schema_id,
|
||||||
title=entity.title,
|
title=entity.title,
|
||||||
department=entity.department,
|
department=entity.department,
|
||||||
location=entity.location,
|
location=entity.location,
|
||||||
@@ -114,6 +116,7 @@ class JobMapper:
|
|||||||
description=job.description,
|
description=job.description,
|
||||||
status=job.status.value,
|
status=job.status.value,
|
||||||
recruiter_id=job.recruiter_id,
|
recruiter_id=job.recruiter_id,
|
||||||
|
evaluation_schema_id=job.evaluation_schema_id,
|
||||||
candidate_count=job.candidate_count,
|
candidate_count=job.candidate_count,
|
||||||
new_candidate_count=job.new_candidate_count,
|
new_candidate_count=job.new_candidate_count,
|
||||||
last_sync_at=job.last_sync_at or datetime.now(),
|
last_sync_at=job.last_sync_at or datetime.now(),
|
||||||
@@ -269,3 +272,69 @@ class JobMapper:
|
|||||||
return result.rowcount
|
return result.rowcount
|
||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
def find_filtered_jobs(
|
||||||
|
self,
|
||||||
|
source: Optional[str] = None,
|
||||||
|
recruiter_id: Optional[str] = None,
|
||||||
|
evaluation_schema_id: Optional[str] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
keyword: Optional[str] = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
多条件筛选查询职位
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: 平台来源
|
||||||
|
recruiter_id: 招聘者账号ID
|
||||||
|
evaluation_schema_id: 评价方案ID
|
||||||
|
status: 职位状态
|
||||||
|
keyword: 关键词搜索(标题/部门)
|
||||||
|
page: 页码
|
||||||
|
page_size: 每页数量
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (职位列表, 总记录数)
|
||||||
|
"""
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
session = self._get_session()
|
||||||
|
try:
|
||||||
|
stmt = select(JobModel)
|
||||||
|
count_stmt = select(func.count()).select_from(JobModel)
|
||||||
|
|
||||||
|
# 动态添加筛选条件
|
||||||
|
conditions = []
|
||||||
|
if source:
|
||||||
|
conditions.append(JobModel.source == source.lower())
|
||||||
|
if recruiter_id:
|
||||||
|
conditions.append(JobModel.recruiter_id == recruiter_id)
|
||||||
|
if evaluation_schema_id:
|
||||||
|
conditions.append(JobModel.evaluation_schema_id == evaluation_schema_id)
|
||||||
|
if status:
|
||||||
|
conditions.append(JobModel.status == status.lower())
|
||||||
|
if keyword:
|
||||||
|
conditions.append(
|
||||||
|
(JobModel.title.ilike(f"%{keyword}%")) |
|
||||||
|
(JobModel.department.ilike(f"%{keyword}%"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# 应用条件
|
||||||
|
if conditions:
|
||||||
|
for condition in conditions:
|
||||||
|
stmt = stmt.where(condition)
|
||||||
|
count_stmt = count_stmt.where(condition)
|
||||||
|
|
||||||
|
# 获取总数
|
||||||
|
total = session.execute(count_stmt).scalar()
|
||||||
|
|
||||||
|
# 分页查询,按最后同步时间降序
|
||||||
|
stmt = stmt.order_by(JobModel.last_sync_at.desc().nullslast())
|
||||||
|
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()
|
||||||
|
|||||||
@@ -1 +1,10 @@
|
|||||||
"""Service layer - Business logic"""
|
"""Service layer - Business logic"""
|
||||||
|
|
||||||
|
from .resume_evaluation_service import ResumeEvaluationService, ProcessResult
|
||||||
|
from .recruiter_service import RecruiterService
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'ResumeEvaluationService',
|
||||||
|
'ProcessResult',
|
||||||
|
'RecruiterService',
|
||||||
|
]
|
||||||
|
|||||||
@@ -307,3 +307,94 @@ class BossCrawler(BaseCrawler):
|
|||||||
if highest:
|
if highest:
|
||||||
return getattr(highest, 'schoolName', None)
|
return getattr(highest, 'schoolName', None)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def send_greeting(self, candidate: Candidate, message: str) -> bool:
|
||||||
|
"""
|
||||||
|
向候选人发送打招呼消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
candidate: 候选人对象(需包含 raw_data)
|
||||||
|
message: 打招呼消息内容
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否发送成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
geek_data = getattr(candidate, 'raw_data', None)
|
||||||
|
if geek_data is None:
|
||||||
|
print(f"[BossCrawler] 候选人 {candidate.name} 缺少 raw_data,无法发送打招呼")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 从 geek_data 中提取必要的参数
|
||||||
|
geek_card = getattr(geek_data, 'geekCard', None)
|
||||||
|
if not geek_card:
|
||||||
|
print(f"[BossCrawler] 候选人 {candidate.name} 缺少 geekCard,无法发送打招呼")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取发送消息所需的参数
|
||||||
|
encrypt_geek_id = getattr(geek_card, 'encryptGeekId', '') or getattr(geek_data, 'encryptGeekId', '')
|
||||||
|
encrypt_job_id = getattr(geek_card, 'encryptJobId', '')
|
||||||
|
security_id = getattr(geek_card, 'securityId', '')
|
||||||
|
lid = getattr(geek_card, 'lid', '')
|
||||||
|
expect_id = getattr(geek_card, 'expectId', 0)
|
||||||
|
|
||||||
|
if not encrypt_geek_id:
|
||||||
|
print(f"[BossCrawler] 候选人 {candidate.name} 缺少 encryptGeekId,无法发送打招呼")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 调用 SDK 的打招呼接口
|
||||||
|
# 注意:这里假设 SDK 提供了 send_message 或 greet 方法
|
||||||
|
# 实际方法名需要根据 ylhp-boss-hr SDK 的实际接口调整
|
||||||
|
if hasattr(self.client, 'send_message'):
|
||||||
|
result = self.client.send_message(
|
||||||
|
encryptGeekId=encrypt_geek_id,
|
||||||
|
encryptJobId=encrypt_job_id,
|
||||||
|
securityId=security_id,
|
||||||
|
lid=lid,
|
||||||
|
expectId=expect_id,
|
||||||
|
content=message
|
||||||
|
)
|
||||||
|
print(f"[BossCrawler] 向候选人 {candidate.name} 发送打招呼成功")
|
||||||
|
return True
|
||||||
|
elif hasattr(self.client, 'greet'):
|
||||||
|
result = self.client.greet(
|
||||||
|
encryptGeekId=encrypt_geek_id,
|
||||||
|
encryptJobId=encrypt_job_id,
|
||||||
|
securityId=security_id,
|
||||||
|
lid=lid,
|
||||||
|
expectId=expect_id,
|
||||||
|
content=message
|
||||||
|
)
|
||||||
|
print(f"[BossCrawler] 向候选人 {candidate.name} 发送打招呼成功")
|
||||||
|
return True
|
||||||
|
elif hasattr(self.client, 'say_hi'):
|
||||||
|
result = self.client.say_hi(
|
||||||
|
encryptGeekId=encrypt_geek_id,
|
||||||
|
encryptJobId=encrypt_job_id,
|
||||||
|
securityId=security_id,
|
||||||
|
lid=lid,
|
||||||
|
expectId=expect_id,
|
||||||
|
content=message
|
||||||
|
)
|
||||||
|
print(f"[BossCrawler] 向候选人 {candidate.name} 发送打招呼成功")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"[BossCrawler] SDK 不支持打招呼功能,请检查 ylhp-boss-hr SDK 版本")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[BossCrawler] 向候选人 {candidate.name} 发送打招呼失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def can_send_greeting(self) -> bool:
|
||||||
|
"""
|
||||||
|
检查当前 SDK 是否支持打招呼功能
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否支持
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
hasattr(self.client, 'send_message') or
|
||||||
|
hasattr(self.client, 'greet') or
|
||||||
|
hasattr(self.client, 'say_hi')
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
"""
|
||||||
|
简历评分服务
|
||||||
|
|
||||||
|
负责同步执行:获取简历 -> LLM分析 -> 评分入库 -> 触发后续动作
|
||||||
|
确保在同一账号下串行执行,避免Boss平台检测
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .analysis.resume_analyzer import ResumeAnalyzer
|
||||||
|
from .analysis.evaluation_schema import EvaluationSchemaService
|
||||||
|
from .notification.notification_service import NotificationService
|
||||||
|
from .crawler.base_crawler import BaseCrawler
|
||||||
|
from .crawler.boss_crawler import BossCrawler
|
||||||
|
from ..mapper.candidate_mapper import CandidateMapper
|
||||||
|
from ..mapper.evaluation_mapper import EvaluationMapper
|
||||||
|
from ..domain.candidate import Candidate, CandidateStatus
|
||||||
|
from ..domain.evaluation import Evaluation
|
||||||
|
from ..domain.job import Job
|
||||||
|
from ..domain.resume import Resume
|
||||||
|
from ..config.settings import Settings, get_settings
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProcessResult:
|
||||||
|
"""候选人处理结果"""
|
||||||
|
candidate_id: str
|
||||||
|
candidate_name: str
|
||||||
|
job_id: str
|
||||||
|
job_title: str
|
||||||
|
|
||||||
|
# 处理状态
|
||||||
|
success: bool = True
|
||||||
|
status: str = "new" # new, updated, skipped, failed
|
||||||
|
message: str = ""
|
||||||
|
|
||||||
|
# 评分结果
|
||||||
|
evaluation_id: Optional[str] = None
|
||||||
|
overall_score: float = 0.0
|
||||||
|
|
||||||
|
# 触发动作结果
|
||||||
|
greeting_sent: bool = False
|
||||||
|
notification_sent: bool = False
|
||||||
|
|
||||||
|
# 时间戳
|
||||||
|
timestamp: datetime = field(default_factory=datetime.now)
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"candidate_id": self.candidate_id,
|
||||||
|
"candidate_name": self.candidate_name,
|
||||||
|
"job_id": self.job_id,
|
||||||
|
"job_title": self.job_title,
|
||||||
|
"success": self.success,
|
||||||
|
"status": self.status,
|
||||||
|
"message": self.message,
|
||||||
|
"evaluation_id": self.evaluation_id,
|
||||||
|
"overall_score": self.overall_score,
|
||||||
|
"greeting_sent": self.greeting_sent,
|
||||||
|
"notification_sent": self.notification_sent,
|
||||||
|
"timestamp": self.timestamp.isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ResumeEvaluationService:
|
||||||
|
"""
|
||||||
|
简历评分服务
|
||||||
|
|
||||||
|
负责同步执行:获取简历 -> LLM分析 -> 评分入库 -> 触发后续动作
|
||||||
|
确保在同一账号下串行执行,避免Boss平台检测
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
resume_analyzer: ResumeAnalyzer,
|
||||||
|
candidate_mapper: CandidateMapper,
|
||||||
|
evaluation_mapper: EvaluationMapper,
|
||||||
|
notification_service: Optional[NotificationService] = None,
|
||||||
|
schema_service: Optional[EvaluationSchemaService] = None,
|
||||||
|
settings: Optional[Settings] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
初始化简历评分服务
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resume_analyzer: 简历分析器
|
||||||
|
candidate_mapper: 候选人数据访问
|
||||||
|
evaluation_mapper: 评价数据访问
|
||||||
|
notification_service: 通知服务(可选)
|
||||||
|
schema_service: 评价方案服务(可选)
|
||||||
|
settings: 配置(可选)
|
||||||
|
"""
|
||||||
|
self.resume_analyzer = resume_analyzer
|
||||||
|
self.candidate_mapper = candidate_mapper
|
||||||
|
self.evaluation_mapper = evaluation_mapper
|
||||||
|
self.notification_service = notification_service
|
||||||
|
self.schema_service = schema_service or EvaluationSchemaService()
|
||||||
|
self.settings = settings or get_settings()
|
||||||
|
|
||||||
|
async def process_candidate_sync(
|
||||||
|
self,
|
||||||
|
crawler: BaseCrawler,
|
||||||
|
candidate: Candidate,
|
||||||
|
job: Job,
|
||||||
|
schema_id: Optional[str] = None
|
||||||
|
) -> ProcessResult:
|
||||||
|
"""
|
||||||
|
同步处理单个候选人(获取简历+LLM分析+入库+触发动作)
|
||||||
|
|
||||||
|
关键:必须在同一线程/协程中串行完成,不能并行
|
||||||
|
这是为了确保在查看简历页面时不会进行其他操作,避免Boss平台检测
|
||||||
|
|
||||||
|
Args:
|
||||||
|
crawler: 爬虫实例
|
||||||
|
candidate: 候选人对象
|
||||||
|
job: 职位对象
|
||||||
|
schema_id: 评价方案ID(可选,优先使用job关联的方案)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProcessResult: 处理结果
|
||||||
|
"""
|
||||||
|
result = ProcessResult(
|
||||||
|
candidate_id=candidate.id or "",
|
||||||
|
candidate_name=candidate.name,
|
||||||
|
job_id=job.id or "",
|
||||||
|
job_title=job.title
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[ResumeEvaluationService] 开始处理候选人: {candidate.name}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. 获取简历详情 - 同步操作
|
||||||
|
resume = self._get_resume(crawler, candidate)
|
||||||
|
if not resume:
|
||||||
|
result.success = False
|
||||||
|
result.status = "failed"
|
||||||
|
result.message = "无法获取简历详情"
|
||||||
|
print(f"[ResumeEvaluationService] 候选人 {candidate.name} 无法获取简历")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 2. 确定评价方案ID
|
||||||
|
effective_schema_id = schema_id or job.evaluation_schema_id or "general"
|
||||||
|
|
||||||
|
# 3. 调用LLM进行评分分析 - 同步操作(await确保串行)
|
||||||
|
evaluation = await self._analyze_resume(
|
||||||
|
candidate=candidate,
|
||||||
|
resume=resume,
|
||||||
|
job=job,
|
||||||
|
schema_id=effective_schema_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not evaluation or evaluation.overall_score == 0:
|
||||||
|
result.success = False
|
||||||
|
result.status = "failed"
|
||||||
|
result.message = "LLM分析失败"
|
||||||
|
print(f"[ResumeEvaluationService] 候选人 {candidate.name} LLM分析失败")
|
||||||
|
return result
|
||||||
|
|
||||||
|
result.evaluation_id = evaluation.id
|
||||||
|
result.overall_score = evaluation.overall_score
|
||||||
|
|
||||||
|
# 4. 保存评价结果到数据库
|
||||||
|
saved_evaluation = self.evaluation_mapper.save(evaluation)
|
||||||
|
print(f"[ResumeEvaluationService] 候选人 {candidate.name} 评价已保存, 分数: {evaluation.overall_score}")
|
||||||
|
|
||||||
|
# 5. 更新候选人的LLM评分字段
|
||||||
|
self._update_candidate_score(candidate, evaluation)
|
||||||
|
|
||||||
|
# 6. 检查是否超过阈值,触发后续动作
|
||||||
|
await self._trigger_actions_if_qualified(
|
||||||
|
crawler=crawler,
|
||||||
|
candidate=candidate,
|
||||||
|
evaluation=evaluation,
|
||||||
|
job=job,
|
||||||
|
result=result
|
||||||
|
)
|
||||||
|
|
||||||
|
result.success = True
|
||||||
|
result.status = "new"
|
||||||
|
result.message = f"处理成功, 分数: {evaluation.overall_score}"
|
||||||
|
|
||||||
|
print(f"[ResumeEvaluationService] 候选人 {candidate.name} 处理完成: {result.message}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
result.success = False
|
||||||
|
result.status = "failed"
|
||||||
|
result.message = f"处理异常: {str(e)}"
|
||||||
|
print(f"[ResumeEvaluationService] 候选人 {candidate.name} 处理异常: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_resume(self, crawler: BaseCrawler, candidate: Candidate) -> Optional[Resume]:
|
||||||
|
"""
|
||||||
|
获取简历详情
|
||||||
|
|
||||||
|
Args:
|
||||||
|
crawler: 爬虫实例
|
||||||
|
candidate: 候选人对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Resume: 简历对象
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return crawler.get_resume_detail(candidate)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ResumeEvaluationService] 获取简历失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _analyze_resume(
|
||||||
|
self,
|
||||||
|
candidate: Candidate,
|
||||||
|
resume: Resume,
|
||||||
|
job: Job,
|
||||||
|
schema_id: str
|
||||||
|
) -> Optional[Evaluation]:
|
||||||
|
"""
|
||||||
|
调用LLM进行简历分析
|
||||||
|
|
||||||
|
Args:
|
||||||
|
candidate: 候选人
|
||||||
|
resume: 简历
|
||||||
|
job: 职位
|
||||||
|
schema_id: 评价方案ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Evaluation: 评价结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 确保candidate有ID
|
||||||
|
if not candidate.id:
|
||||||
|
import uuid
|
||||||
|
candidate.id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
evaluation = await self.resume_analyzer.analyze(
|
||||||
|
candidate_id=candidate.id,
|
||||||
|
resume=resume,
|
||||||
|
schema_id=schema_id,
|
||||||
|
job_id=job.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return evaluation
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ResumeEvaluationService] LLM分析失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _update_candidate_score(self, candidate: Candidate, evaluation: Evaluation) -> None:
|
||||||
|
"""
|
||||||
|
更新候选人的LLM评分字段
|
||||||
|
|
||||||
|
Args:
|
||||||
|
candidate: 候选人
|
||||||
|
evaluation: 评价结果
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
candidate.llm_filtered = evaluation.overall_score >= self.settings.score_threshold_greet
|
||||||
|
candidate.llm_score = Decimal(str(evaluation.overall_score))
|
||||||
|
candidate.llm_score_details = evaluation.to_dict()
|
||||||
|
candidate.status = CandidateStatus.ANALYZED
|
||||||
|
|
||||||
|
self.candidate_mapper.save(candidate)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ResumeEvaluationService] 更新候选人评分失败: {e}")
|
||||||
|
|
||||||
|
async def _trigger_actions_if_qualified(
|
||||||
|
self,
|
||||||
|
crawler: BaseCrawler,
|
||||||
|
candidate: Candidate,
|
||||||
|
evaluation: Evaluation,
|
||||||
|
job: Job,
|
||||||
|
result: ProcessResult
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
如果评分超过阈值,触发打招呼和通知
|
||||||
|
|
||||||
|
Args:
|
||||||
|
crawler: 爬虫实例
|
||||||
|
candidate: 候选人
|
||||||
|
evaluation: 评价结果
|
||||||
|
job: 职位
|
||||||
|
result: 处理结果(用于记录触发状态)
|
||||||
|
"""
|
||||||
|
score = evaluation.overall_score
|
||||||
|
|
||||||
|
# 6.1 触发打招呼(如果启用且超阈值)
|
||||||
|
if (self.settings.auto_greet_enabled and
|
||||||
|
score >= self.settings.score_threshold_greet):
|
||||||
|
greeting_success = await self._trigger_greeting(
|
||||||
|
crawler=crawler,
|
||||||
|
candidate=candidate,
|
||||||
|
evaluation=evaluation
|
||||||
|
)
|
||||||
|
result.greeting_sent = greeting_success
|
||||||
|
|
||||||
|
# 6.2 触发HR通知(如果启用且超阈值)
|
||||||
|
if (self.settings.auto_notify_enabled and
|
||||||
|
score >= self.settings.score_threshold_notify):
|
||||||
|
notify_success = await self._trigger_notification(
|
||||||
|
candidate=candidate,
|
||||||
|
evaluation=evaluation,
|
||||||
|
job=job
|
||||||
|
)
|
||||||
|
result.notification_sent = notify_success
|
||||||
|
|
||||||
|
async def _trigger_greeting(
|
||||||
|
self,
|
||||||
|
crawler: BaseCrawler,
|
||||||
|
candidate: Candidate,
|
||||||
|
evaluation: Evaluation
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
触发打招呼
|
||||||
|
|
||||||
|
Args:
|
||||||
|
crawler: 爬虫实例
|
||||||
|
candidate: 候选人
|
||||||
|
evaluation: 评价结果
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 检查爬虫是否支持打招呼
|
||||||
|
if isinstance(crawler, BossCrawler) and crawler.can_send_greeting():
|
||||||
|
message = self.settings.greet_message_template
|
||||||
|
success = crawler.send_greeting(candidate, message)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
# 更新候选人状态为已联系
|
||||||
|
candidate.status = CandidateStatus.CONTACTED
|
||||||
|
self.candidate_mapper.save(candidate)
|
||||||
|
print(f"[ResumeEvaluationService] 向 {candidate.name} 发送打招呼成功")
|
||||||
|
|
||||||
|
return success
|
||||||
|
else:
|
||||||
|
print(f"[ResumeEvaluationService] 爬虫不支持打招呼功能")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ResumeEvaluationService] 触发打招呼失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _trigger_notification(
|
||||||
|
self,
|
||||||
|
candidate: Candidate,
|
||||||
|
evaluation: Evaluation,
|
||||||
|
job: Job
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
触发HR通知
|
||||||
|
|
||||||
|
Args:
|
||||||
|
candidate: 候选人
|
||||||
|
evaluation: 评价结果
|
||||||
|
job: 职位
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否成功
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.notification_service:
|
||||||
|
print(f"[ResumeEvaluationService] 通知服务未配置")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 发送通知
|
||||||
|
result = await self.notification_service.notify(
|
||||||
|
candidate=candidate,
|
||||||
|
evaluation=evaluation,
|
||||||
|
title=f"【人才推荐】{candidate.name} - {job.title}",
|
||||||
|
extra_data={
|
||||||
|
"job_id": job.id,
|
||||||
|
"job_title": job.title,
|
||||||
|
"score": evaluation.overall_score
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
# 更新候选人状态为已推送
|
||||||
|
if candidate.status != CandidateStatus.CONTACTED:
|
||||||
|
candidate.status = CandidateStatus.PUSHED
|
||||||
|
self.candidate_mapper.save(candidate)
|
||||||
|
print(f"[ResumeEvaluationService] 候选人 {candidate.name} 已推送给HR")
|
||||||
|
|
||||||
|
return result.success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ResumeEvaluationService] 触发HR通知失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def should_process_candidate(
|
||||||
|
self,
|
||||||
|
candidate: Candidate,
|
||||||
|
job: Job
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
判断是否应该处理该候选人
|
||||||
|
|
||||||
|
用于跳过已处理过的候选人
|
||||||
|
|
||||||
|
Args:
|
||||||
|
candidate: 候选人
|
||||||
|
job: 职位
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否应该处理
|
||||||
|
"""
|
||||||
|
# 如果候选人已有评价记录,检查是否需要重新评价
|
||||||
|
if candidate.id:
|
||||||
|
existing = self.evaluation_mapper.find_by_candidate_and_job(
|
||||||
|
candidate.id, job.id
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
print(f"[ResumeEvaluationService] 候选人 {candidate.name} 已有评价记录,跳过")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_evaluation_summary(self, candidate_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取候选人评价摘要
|
||||||
|
|
||||||
|
Args:
|
||||||
|
candidate_id: 候选人ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
评价摘要字典
|
||||||
|
"""
|
||||||
|
evaluation = self.evaluation_mapper.find_latest_by_candidate_id(candidate_id)
|
||||||
|
if evaluation:
|
||||||
|
return evaluation.to_dict()
|
||||||
|
return None
|
||||||
Reference in New Issue
Block a user