feat(recruiter): 添加招聘者账号管理模块及相关支持
- 新增招聘者账号数据库表结构及SQL建表脚本 - 实现招聘者实体类及账号状态枚举 - 添加SQLAlchemy数据库模型及管理器支持招聘者数据存储 - 实现招聘者数据访问层(Mapper)进行增删改查操作 - 开发招聘者服务层,支持账号添加、启用、停用、删除、列表及爬虫注册 - 新增命令行工具add_recruiter.py,便于管理招聘者账号 - 修改主应用初始化流程,集成招聘者服务并通过数据库加载活跃账号爬虫 - 主程序示例中新增招聘者账号展示与调用爬取任务示范 - 更新项目依赖,增加SQLAlchemy、PyMySQL及Cryptography库支持 - 修改.gitignore,新增.qoder目录例外规则
This commit is contained in:
209
src/main/python/cn/yinlihupo/ylhp_hr_2_0/config/database.py
Normal file
209
src/main/python/cn/yinlihupo/ylhp_hr_2_0/config/database.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Database configuration using SQLAlchemy"""
|
||||
from sqlalchemy import create_engine, Column, String, DateTime, Text, DECIMAL, Integer, ForeignKey, JSON
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker, Session
|
||||
from sqlalchemy.sql import func
|
||||
from typing import Optional
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class RecruiterModel(Base):
|
||||
"""招聘者账号表"""
|
||||
__tablename__ = 'recruiters'
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
name = Column(String(128), nullable=False)
|
||||
source = Column(String(32), nullable=False)
|
||||
wt_token = Column(String(512), nullable=False)
|
||||
status = Column(String(32), default='active')
|
||||
last_used_at = Column(DateTime)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class CandidateModel(Base):
|
||||
"""候选人主表"""
|
||||
__tablename__ = 'candidates'
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
source = Column(String(32), nullable=False)
|
||||
source_id = Column(String(128), nullable=False)
|
||||
name = Column(String(64), nullable=False)
|
||||
phone = Column(String(32))
|
||||
email = Column(String(128))
|
||||
wechat = Column(String(64))
|
||||
gender = Column(Integer, default=0)
|
||||
age = Column(Integer)
|
||||
location = Column(String(128))
|
||||
current_company = Column(String(256))
|
||||
current_position = Column(String(128))
|
||||
work_years = Column(DECIMAL(4, 1))
|
||||
education = Column(String(64))
|
||||
school = Column(String(256))
|
||||
salary_min = Column(Integer)
|
||||
salary_max = Column(Integer)
|
||||
status = Column(String(32), default='NEW')
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class ResumeModel(Base):
|
||||
"""简历内容表"""
|
||||
__tablename__ = 'resumes'
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
candidate_id = Column(String(64), ForeignKey('candidates.id'), nullable=False)
|
||||
raw_content = Column(Text)
|
||||
parsed_content = Column(JSON)
|
||||
attachment_url = Column(String(512))
|
||||
attachment_type = Column(String(32))
|
||||
version = Column(Integer, default=1)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class JobModel(Base):
|
||||
"""职位信息表"""
|
||||
__tablename__ = 'jobs'
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
source = Column(String(32), nullable=False)
|
||||
source_id = Column(String(128), nullable=False)
|
||||
title = Column(String(256), nullable=False)
|
||||
department = Column(String(128))
|
||||
location = Column(String(128))
|
||||
salary_min = Column(Integer)
|
||||
salary_max = Column(Integer)
|
||||
requirements = Column(Text)
|
||||
description = Column(Text)
|
||||
status = Column(String(32), default='ACTIVE')
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class EvaluationSchemaModel(Base):
|
||||
"""评价方案表"""
|
||||
__tablename__ = 'evaluation_schemas'
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
name = Column(String(128), nullable=False)
|
||||
description = Column(Text)
|
||||
dimensions = Column(JSON, nullable=False)
|
||||
weights = Column(JSON, nullable=False)
|
||||
prompt_template = Column(Text)
|
||||
is_default = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class EvaluationModel(Base):
|
||||
"""评价记录表"""
|
||||
__tablename__ = 'evaluations'
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
candidate_id = Column(String(64), ForeignKey('candidates.id'), nullable=False)
|
||||
schema_id = Column(String(64), ForeignKey('evaluation_schemas.id'), nullable=False)
|
||||
job_id = Column(String(64), ForeignKey('jobs.id'))
|
||||
overall_score = Column(DECIMAL(4, 1))
|
||||
dimension_scores = Column(JSON)
|
||||
tags = Column(JSON)
|
||||
summary = Column(Text)
|
||||
strengths = Column(JSON)
|
||||
weaknesses = Column(JSON)
|
||||
recommendation = Column(String(32))
|
||||
raw_response = Column(Text)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
class NotificationModel(Base):
|
||||
"""通知记录表"""
|
||||
__tablename__ = 'notifications'
|
||||
|
||||
id = Column(String(64), primary_key=True)
|
||||
candidate_id = Column(String(64), ForeignKey('candidates.id'), nullable=False)
|
||||
evaluation_id = Column(String(64), ForeignKey('evaluations.id'))
|
||||
channel = Column(String(32), nullable=False)
|
||||
content = Column(Text)
|
||||
status = Column(String(32), default='PENDING')
|
||||
error_message = Column(Text)
|
||||
sent_at = Column(DateTime)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
"""数据库管理器"""
|
||||
|
||||
def __init__(self, db_url: str):
|
||||
self.db_url = db_url
|
||||
self.engine = create_engine(db_url, echo=False, pool_pre_ping=True)
|
||||
self.SessionLocal = sessionmaker(bind=self.engine)
|
||||
|
||||
def create_tables(self):
|
||||
"""创建所有表"""
|
||||
Base.metadata.create_all(self.engine)
|
||||
print("Database tables created")
|
||||
|
||||
def get_session(self) -> Session:
|
||||
"""获取数据库会话"""
|
||||
return self.SessionLocal()
|
||||
|
||||
def init_default_data(self):
|
||||
"""初始化默认数据"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
# 检查是否已有默认评价方案
|
||||
from sqlalchemy import select
|
||||
result = session.execute(select(EvaluationSchemaModel).where(EvaluationSchemaModel.id == 'general'))
|
||||
if result.scalar() is None:
|
||||
# 插入通用评价方案
|
||||
general = EvaluationSchemaModel(
|
||||
id='general',
|
||||
name='通用评价方案',
|
||||
description='适用于各类岗位的通用评价方案',
|
||||
dimensions=[
|
||||
{"id": "professional", "name": "专业能力", "description": "岗位相关专业技能水平"},
|
||||
{"id": "experience", "name": "工作经验", "description": "相关工作经验丰富度"},
|
||||
{"id": "education", "name": "教育背景", "description": "学历和专业匹配度"},
|
||||
{"id": "potential", "name": "发展潜力", "description": "未来成长空间"},
|
||||
{"id": "culture_fit", "name": "文化匹配", "description": "与企业文化的匹配度"}
|
||||
],
|
||||
weights={"professional": 0.30, "experience": 0.25, "education": 0.15, "potential": 0.15, "culture_fit": 0.15},
|
||||
is_default=1
|
||||
)
|
||||
session.add(general)
|
||||
|
||||
# 插入Java后端评价方案
|
||||
java = EvaluationSchemaModel(
|
||||
id='java_backend',
|
||||
name='Java后端工程师评价方案',
|
||||
description='针对Java后端开发岗位的综合评价方案',
|
||||
dimensions=[
|
||||
{"id": "tech_capability", "name": "技术能力", "description": "Java技术栈掌握程度", "criteria": ["Java基础扎实程度", "Spring生态熟悉度", "数据库设计与优化", "分布式系统经验"]},
|
||||
{"id": "project_exp", "name": "项目经验", "description": "项目经历的丰富度和质量", "criteria": ["项目复杂度", "承担角色重要性", "技术挑战解决能力"]},
|
||||
{"id": "learning_ability", "name": "学习能力", "description": "学习新技术和适应新环境的能力", "criteria": ["技术广度", "新技术掌握速度", "自我驱动学习"]},
|
||||
{"id": "communication", "name": "沟通协作", "description": "团队协作和沟通能力", "criteria": ["跨团队协作经验", "技术文档能力", "问题表达能力"]},
|
||||
{"id": "stability", "name": "稳定性", "description": "职业稳定性和忠诚度", "criteria": ["平均在职时长", "跳槽频率", "职业发展规划清晰度"]}
|
||||
],
|
||||
weights={"tech_capability": 0.35, "project_exp": 0.25, "learning_ability": 0.15, "communication": 0.15, "stability": 0.10}
|
||||
)
|
||||
session.add(java)
|
||||
|
||||
session.commit()
|
||||
print("Default evaluation schemas inserted")
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
# 全局数据库管理器实例
|
||||
_db_manager: Optional[DatabaseManager] = None
|
||||
|
||||
|
||||
def get_db_manager(db_url: Optional[str] = None) -> DatabaseManager:
|
||||
"""获取数据库管理器实例(单例)"""
|
||||
global _db_manager
|
||||
if _db_manager is None:
|
||||
if db_url is None:
|
||||
from .settings import get_settings
|
||||
db_url = get_settings().db_url
|
||||
_db_manager = DatabaseManager(db_url)
|
||||
return _db_manager
|
||||
@@ -4,6 +4,7 @@ from .candidate import Candidate, CandidateSource, CandidateStatus
|
||||
from .resume import Resume, ResumeParsed
|
||||
from .job import Job, JobStatus
|
||||
from .evaluation import Evaluation, EvaluationSchema, Dimension, DimensionScore
|
||||
from .recruiter import Recruiter, RecruiterStatus
|
||||
from .enums import Gender, Education, Recommendation
|
||||
|
||||
__all__ = [
|
||||
@@ -18,6 +19,8 @@ __all__ = [
|
||||
"EvaluationSchema",
|
||||
"Dimension",
|
||||
"DimensionScore",
|
||||
"Recruiter",
|
||||
"RecruiterStatus",
|
||||
"Gender",
|
||||
"Education",
|
||||
"Recommendation",
|
||||
|
||||
41
src/main/python/cn/yinlihupo/ylhp_hr_2_0/domain/recruiter.py
Normal file
41
src/main/python/cn/yinlihupo/ylhp_hr_2_0/domain/recruiter.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Recruiter entity definitions"""
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
from .candidate import CandidateSource
|
||||
|
||||
|
||||
class RecruiterStatus(Enum):
|
||||
"""招聘者账号状态"""
|
||||
ACTIVE = "active" # 活跃
|
||||
INACTIVE = "inactive" # 停用
|
||||
EXPIRED = "expired" # Token过期
|
||||
|
||||
|
||||
@dataclass
|
||||
class Recruiter:
|
||||
"""招聘者账号实体"""
|
||||
id: Optional[str] = None
|
||||
name: str = "" # 招聘者名称/标识
|
||||
source: CandidateSource = CandidateSource.BOSS
|
||||
wt_token: str = "" # WT Token
|
||||
status: RecruiterStatus = RecruiterStatus.ACTIVE
|
||||
last_used_at: Optional[datetime] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.now()
|
||||
if self.updated_at is None:
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""检查账号是否活跃"""
|
||||
return self.status == RecruiterStatus.ACTIVE
|
||||
|
||||
def mark_used(self):
|
||||
"""标记为已使用"""
|
||||
self.last_used_at = datetime.now()
|
||||
@@ -67,6 +67,7 @@ class HRAgentApplication:
|
||||
self.ingestion_service: Optional[UnifiedIngestionService] = None
|
||||
self.analyzer: Optional[ResumeAnalyzer] = None
|
||||
self.notification_service: Optional[NotificationService] = None
|
||||
self.recruiter_service: Optional[RecruiterService] = None
|
||||
|
||||
self._initialized = False
|
||||
|
||||
@@ -75,7 +76,10 @@ class HRAgentApplication:
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
# 1. 初始化爬虫
|
||||
# 0. 初始化招聘者服务
|
||||
self._init_recruiter_service()
|
||||
|
||||
# 1. 初始化爬虫(从数据库加载)
|
||||
self._init_crawlers()
|
||||
|
||||
# 2. 初始化入库服务
|
||||
@@ -90,13 +94,31 @@ class HRAgentApplication:
|
||||
self._initialized = True
|
||||
print(f"HR Agent {self.settings.app_version} initialized successfully")
|
||||
|
||||
def _init_recruiter_service(self):
|
||||
"""初始化招聘者服务"""
|
||||
from cn.yinlihupo.ylhp_hr_2_0.service.recruiter_service import RecruiterService
|
||||
from cn.yinlihupo.ylhp_hr_2_0.mapper.recruiter_mapper import RecruiterMapper
|
||||
|
||||
mapper = RecruiterMapper(db_url=self.settings.db_url)
|
||||
self.recruiter_service = RecruiterService(mapper=mapper)
|
||||
|
||||
def _init_crawlers(self):
|
||||
"""初始化爬虫"""
|
||||
# Boss 爬虫
|
||||
if self.settings.crawler_boss_wt_token:
|
||||
boss_crawler = BossCrawler(wt_token=self.settings.crawler_boss_wt_token)
|
||||
self.crawler_factory.register(CandidateSource.BOSS, boss_crawler)
|
||||
print("Boss crawler registered")
|
||||
"""初始化爬虫 - 从数据库加载招聘者账号"""
|
||||
# 从数据库加载 Boss 爬虫
|
||||
if self.recruiter_service:
|
||||
count = self.recruiter_service.register_crawlers_to_factory(
|
||||
self.crawler_factory,
|
||||
CandidateSource.BOSS
|
||||
)
|
||||
if count > 0:
|
||||
print(f"Registered {count} Boss crawlers from database")
|
||||
|
||||
# 兼容:从环境变量加载(如果数据库中没有)
|
||||
if not self.crawler_factory.has_crawler(CandidateSource.BOSS):
|
||||
if self.settings.crawler_boss_wt_token:
|
||||
boss_crawler = BossCrawler(wt_token=self.settings.crawler_boss_wt_token)
|
||||
self.crawler_factory.register(CandidateSource.BOSS, boss_crawler)
|
||||
print("Boss crawler registered from environment")
|
||||
|
||||
def _init_ingestion_service(self):
|
||||
"""初始化入库服务"""
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Recruiter data mapper using SQLAlchemy"""
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..domain.recruiter import Recruiter, RecruiterStatus
|
||||
from ..domain.candidate import CandidateSource
|
||||
from ..config.database import get_db_manager, RecruiterModel
|
||||
|
||||
|
||||
class RecruiterMapper:
|
||||
"""招聘者账号数据访问 - 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: RecruiterModel) -> Recruiter:
|
||||
"""将模型转换为实体"""
|
||||
# 处理 source 的大小写(数据库可能是 BOSS,但枚举是 boss)
|
||||
source_value = model.source.lower() if model.source else "boss"
|
||||
# 处理 status 的大小写
|
||||
status_value = model.status.lower() if model.status else "active"
|
||||
|
||||
return Recruiter(
|
||||
id=model.id,
|
||||
name=model.name,
|
||||
source=CandidateSource(source_value),
|
||||
wt_token=model.wt_token,
|
||||
status=RecruiterStatus(status_value),
|
||||
last_used_at=model.last_used_at,
|
||||
created_at=model.created_at,
|
||||
updated_at=model.updated_at
|
||||
)
|
||||
|
||||
def _entity_to_model(self, entity: Recruiter) -> RecruiterModel:
|
||||
"""将实体转换为模型"""
|
||||
return RecruiterModel(
|
||||
id=entity.id or str(uuid.uuid4()),
|
||||
name=entity.name,
|
||||
source=entity.source.value,
|
||||
wt_token=entity.wt_token,
|
||||
status=entity.status.value,
|
||||
last_used_at=entity.last_used_at
|
||||
)
|
||||
|
||||
def save(self, recruiter: Recruiter) -> Recruiter:
|
||||
"""保存招聘者账号"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
if recruiter.id:
|
||||
# 更新
|
||||
stmt = (
|
||||
update(RecruiterModel)
|
||||
.where(RecruiterModel.id == recruiter.id)
|
||||
.values(
|
||||
name=recruiter.name,
|
||||
source=recruiter.source.value,
|
||||
wt_token=recruiter.wt_token,
|
||||
status=recruiter.status.value,
|
||||
last_used_at=recruiter.last_used_at
|
||||
)
|
||||
)
|
||||
session.execute(stmt)
|
||||
else:
|
||||
# 插入
|
||||
recruiter.id = str(uuid.uuid4())
|
||||
model = self._entity_to_model(recruiter)
|
||||
session.add(model)
|
||||
|
||||
session.commit()
|
||||
return recruiter
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def find_by_id(self, recruiter_id: str) -> Optional[Recruiter]:
|
||||
"""根据ID查询"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
result = session.execute(
|
||||
select(RecruiterModel).where(RecruiterModel.id == recruiter_id)
|
||||
)
|
||||
model = result.scalar_one_or_none()
|
||||
return self._model_to_entity(model) if model else None
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def find_by_source(self, source: CandidateSource) -> List[Recruiter]:
|
||||
"""根据平台查询"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
result = session.execute(
|
||||
select(RecruiterModel)
|
||||
.where(RecruiterModel.source == source.value)
|
||||
.order_by(RecruiterModel.created_at.desc())
|
||||
)
|
||||
models = result.scalars().all()
|
||||
return [self._model_to_entity(m) for m in models]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def find_active_by_source(self, source: CandidateSource) -> List[Recruiter]:
|
||||
"""查询指定平台的活跃账号"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
result = session.execute(
|
||||
select(RecruiterModel)
|
||||
.where(RecruiterModel.source == source.value)
|
||||
.where(RecruiterModel.status == 'active')
|
||||
.order_by(RecruiterModel.last_used_at)
|
||||
)
|
||||
models = result.scalars().all()
|
||||
return [self._model_to_entity(m) for m in models]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def find_all(self) -> List[Recruiter]:
|
||||
"""查询所有账号"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
result = session.execute(
|
||||
select(RecruiterModel).order_by(RecruiterModel.created_at.desc())
|
||||
)
|
||||
models = result.scalars().all()
|
||||
return [self._model_to_entity(m) for m in models]
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update_status(self, recruiter_id: str, status: RecruiterStatus) -> bool:
|
||||
"""更新账号状态"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
stmt = (
|
||||
update(RecruiterModel)
|
||||
.where(RecruiterModel.id == recruiter_id)
|
||||
.values(status=status.value)
|
||||
)
|
||||
result = session.execute(stmt)
|
||||
session.commit()
|
||||
return result.rowcount > 0
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update_last_used(self, recruiter_id: str) -> bool:
|
||||
"""更新最后使用时间"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
stmt = (
|
||||
update(RecruiterModel)
|
||||
.where(RecruiterModel.id == recruiter_id)
|
||||
.values(last_used_at=datetime.now())
|
||||
)
|
||||
result = session.execute(stmt)
|
||||
session.commit()
|
||||
return result.rowcount > 0
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def delete(self, recruiter_id: str) -> bool:
|
||||
"""删除账号"""
|
||||
session = self._get_session()
|
||||
try:
|
||||
stmt = delete(RecruiterModel).where(RecruiterModel.id == recruiter_id)
|
||||
result = session.execute(stmt)
|
||||
session.commit()
|
||||
return result.rowcount > 0
|
||||
finally:
|
||||
session.close()
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Recruiter service - Manage recruiter accounts"""
|
||||
from typing import List, Optional
|
||||
|
||||
from ..domain.recruiter import Recruiter, RecruiterStatus
|
||||
from ..domain.candidate import CandidateSource
|
||||
from ..mapper.recruiter_mapper import RecruiterMapper
|
||||
from ..service.crawler import BossCrawler, CrawlerFactory
|
||||
|
||||
|
||||
class RecruiterService:
|
||||
"""
|
||||
招聘者账号服务
|
||||
|
||||
管理招聘者账号的增删改查,以及基于账号的爬虫初始化
|
||||
"""
|
||||
|
||||
def __init__(self, mapper: Optional[RecruiterMapper] = None):
|
||||
self.mapper = mapper or RecruiterMapper()
|
||||
|
||||
def add_recruiter(self, name: str, source: CandidateSource, wt_token: str) -> Recruiter:
|
||||
"""
|
||||
添加招聘者账号
|
||||
|
||||
Args:
|
||||
name: 账号名称/标识
|
||||
source: 平台来源
|
||||
wt_token: WT Token
|
||||
|
||||
Returns:
|
||||
创建的招聘者账号
|
||||
"""
|
||||
recruiter = Recruiter(
|
||||
name=name,
|
||||
source=source,
|
||||
wt_token=wt_token,
|
||||
status=RecruiterStatus.ACTIVE
|
||||
)
|
||||
return self.mapper.save(recruiter)
|
||||
|
||||
def get_recruiter(self, recruiter_id: str) -> Optional[Recruiter]:
|
||||
"""获取指定账号"""
|
||||
return self.mapper.find_by_id(recruiter_id)
|
||||
|
||||
def list_recruiters(self, source: Optional[CandidateSource] = None) -> List[Recruiter]:
|
||||
"""
|
||||
列出招聘者账号
|
||||
|
||||
Args:
|
||||
source: 按平台筛选,不传则返回所有
|
||||
|
||||
Returns:
|
||||
账号列表
|
||||
"""
|
||||
if source:
|
||||
return self.mapper.find_by_source(source)
|
||||
return self.mapper.find_all()
|
||||
|
||||
def list_active_recruiters(self, source: CandidateSource) -> List[Recruiter]:
|
||||
"""获取指定平台的活跃账号"""
|
||||
return self.mapper.find_active_by_source(source)
|
||||
|
||||
def deactivate_recruiter(self, recruiter_id: str) -> bool:
|
||||
"""停用账号"""
|
||||
return self.mapper.update_status(recruiter_id, RecruiterStatus.INACTIVE)
|
||||
|
||||
def activate_recruiter(self, recruiter_id: str) -> bool:
|
||||
"""启用账号"""
|
||||
return self.mapper.update_status(recruiter_id, RecruiterStatus.ACTIVE)
|
||||
|
||||
def mark_recruiter_used(self, recruiter_id: str) -> bool:
|
||||
"""标记账号已使用"""
|
||||
return self.mapper.update_last_used(recruiter_id)
|
||||
|
||||
def delete_recruiter(self, recruiter_id: str) -> bool:
|
||||
"""删除账号"""
|
||||
return self.mapper.delete(recruiter_id)
|
||||
|
||||
def create_crawler_for_recruiter(self, recruiter: Recruiter) -> Optional[BossCrawler]:
|
||||
"""
|
||||
为招聘者创建爬虫实例
|
||||
|
||||
Args:
|
||||
recruiter: 招聘者账号
|
||||
|
||||
Returns:
|
||||
爬虫实例,如果平台不支持返回 None
|
||||
"""
|
||||
if not recruiter.is_active():
|
||||
print(f"Recruiter {recruiter.name} is not active")
|
||||
return None
|
||||
|
||||
if recruiter.source == CandidateSource.BOSS:
|
||||
return BossCrawler(wt_token=recruiter.wt_token)
|
||||
|
||||
# TODO: 支持其他平台
|
||||
print(f"Source {recruiter.source.value} not supported yet")
|
||||
return None
|
||||
|
||||
def register_crawlers_to_factory(self, factory: CrawlerFactory, source: CandidateSource) -> int:
|
||||
"""
|
||||
将指定平台的所有活跃账号注册到爬虫工厂
|
||||
|
||||
Args:
|
||||
factory: 爬虫工厂
|
||||
source: 平台来源
|
||||
|
||||
Returns:
|
||||
注册的账号数量
|
||||
"""
|
||||
recruiters = self.list_active_recruiters(source)
|
||||
count = 0
|
||||
|
||||
for recruiter in recruiters:
|
||||
crawler = self.create_crawler_for_recruiter(recruiter)
|
||||
if crawler:
|
||||
factory.register(source, crawler)
|
||||
count += 1
|
||||
print(f"Registered crawler for recruiter: {recruiter.name}")
|
||||
|
||||
return count
|
||||
Reference in New Issue
Block a user