From 148f2cc4f64012c69e3267f576fbf79a24c355cb Mon Sep 17 00:00:00 2001 From: JiaoTianBo Date: Wed, 25 Mar 2026 11:14:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(notification-channels):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E9=80=9A=E7=9F=A5=E6=B8=A0=E9=81=93=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端API新增评价方案列表接口及通知渠道相关接口 - 所有候选人相关API路径添加/api前缀 - 系统首页接口更新候选人路径为/api/candidates - CandidateMapper和JobMapper排序逻辑调整以兼容MySQL null值排序 - 前端candidateApi接口路径添加/api前缀 - 新增notificationChannelApi管理通知渠道,包括增删改查、启用停用及招聘者绑定管理 - 路由新增通知渠道管理页面入口 - 实现NotificationChannels.vue通知渠道的增删改查、搜索筛选、分页、启用停用及招聘者绑定管理功能 - Recruiters.vue中新增通知渠道绑定对话框及绑定相关逻辑,支持招聘者绑定通知渠道管理 - controller/schemas.py新增分页参数PaginationParams及重建模型修正前向引用 - UI组件调整及新增对应表格列、表单校验规则和界面交互逻辑 --- .../controller/routes/candidate.py | 2 +- .../ylhp_hr_2_0/controller/routes/job.py | 78 +- .../ylhp_hr_2_0/controller/routes/system.py | 2 +- .../ylhp_hr_2_0/controller/schemas.py | 21 +- .../ylhp_hr_2_0/mapper/candidate_mapper.py | 6 +- .../ylhp_hr_2_0/mapper/job_mapper.py | 8 +- .../ylhp_hr_2_0_fronted/src/api/api.js | 65 +- .../ylhp_hr_2_0_fronted/src/router/index.js | 6 + .../src/views/NotificationChannels.vue | 679 ++++++++++++++++++ .../src/views/Recruiters.vue | 245 ++++++- 10 files changed, 1042 insertions(+), 70 deletions(-) create mode 100644 src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/NotificationChannels.vue diff --git a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/candidate.py b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/candidate.py index 6ead235..f4d71f7 100644 --- a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/candidate.py +++ b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/candidate.py @@ -15,7 +15,7 @@ from ..schemas import ( from ...mapper.candidate_mapper import CandidateMapper -router = APIRouter(prefix="/candidates", tags=["候选人管理"]) +router = APIRouter(prefix="/api/candidates", tags=["候选人管理"]) def _candidate_to_response(candidate) -> CandidateResponse: diff --git a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/job.py b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/job.py index 701f1fa..8f16b3c 100644 --- a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/job.py +++ b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/job.py @@ -45,6 +45,45 @@ def _job_to_response(job: Job) -> JobPositionResponse: ) +@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)}") + + @router.get("", response_model=BaseResponse[JobPositionListResponse]) async def list_jobs( source: Optional[str] = Query(None, description="平台来源"), @@ -368,42 +407,3 @@ async def get_job_evaluation_schema(job_id: str): 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)}") diff --git a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/system.py b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/system.py index f7c4ff2..b7ce8b1 100644 --- a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/system.py +++ b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/routes/system.py @@ -21,7 +21,7 @@ async def root(): "endpoints": { "recruiters": "/api/recruiters", "jobs": "/api/jobs", - "candidates": "/candidates", + "candidates": "/api/candidates", "scheduler": "/api/scheduler", "health": "/health" } diff --git a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/schemas.py b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/schemas.py index a2056b1..7ed57e1 100644 --- a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/schemas.py +++ b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/controller/schemas.py @@ -3,7 +3,7 @@ API共享Schema定义 集中定义所有API请求和响应的数据模型 """ -from typing import List, Optional, Any, TypeVar, Generic +from typing import List, Optional, Any, TypeVar, Generic, Dict from datetime import datetime from pydantic import BaseModel, Field @@ -46,6 +46,8 @@ class PaginationData(BaseModel, Generic[T]): # ============== 分页参数 ============== + +class PaginationParams(BaseModel): """分页参数""" page: int = Field(default=1, ge=1, description="页码") page_size: int = Field(default=20, ge=1, le=100, description="每页数量") @@ -424,3 +426,20 @@ class RecruiterWithChannelsResponse(BaseModel): recruiter_id: str recruiter_name: str channels: List[RecruiterChannelBindingResponse] + + +# 重建所有模型以解决前向引用问题 +BaseResponse.model_rebuild() +PaginationData.model_rebuild() +CandidateResponse.model_rebuild() +CandidateListResponse.model_rebuild() +JobPositionResponse.model_rebuild() +JobPositionListResponse.model_rebuild() +RecruiterResponse.model_rebuild() +RecruiterListResponse.model_rebuild() +EvaluationSchemaResponse.model_rebuild() +EvaluationSchemaListResponse.model_rebuild() +NotificationChannelResponse.model_rebuild() +NotificationChannelListResponse.model_rebuild() +RecruiterChannelBindingResponse.model_rebuild() +RecruiterChannelBindingListResponse.model_rebuild() diff --git a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/mapper/candidate_mapper.py b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/mapper/candidate_mapper.py index 3f6a2a6..a4281c6 100644 --- a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/mapper/candidate_mapper.py +++ b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/mapper/candidate_mapper.py @@ -262,9 +262,11 @@ class CandidateMapper: count_stmt = count_stmt.where(*conditions) total = session.execute(count_stmt).scalar() - # 分页查询,按LLM评分降序,再按创建时间倒序 + # 分页查询,按LLM评分降序(NULL值在后),再按创建时间倒序 + # 使用MySQL兼容的语法:先按是否为NULL排序,再按值排序 stmt = stmt.order_by( - CandidateModel.llm_score.desc().nullslast(), + CandidateModel.llm_score.is_(None).asc(), # NULL值在后 + CandidateModel.llm_score.desc(), CandidateModel.created_at.desc() ) stmt = stmt.offset((page - 1) * page_size).limit(page_size) diff --git a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/mapper/job_mapper.py b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/mapper/job_mapper.py index d1e767f..a044f56 100644 --- a/src/main/python/cn/yinlihupo/ylhp_hr_2_0/mapper/job_mapper.py +++ b/src/main/python/cn/yinlihupo/ylhp_hr_2_0/mapper/job_mapper.py @@ -330,8 +330,12 @@ class JobMapper: # 获取总数 total = session.execute(count_stmt).scalar() - # 分页查询,按最后同步时间降序 - stmt = stmt.order_by(JobModel.last_sync_at.desc().nullslast()) + # 分页查询,按最后同步时间降序(NULL值在后) + # 使用MySQL兼容的语法:先按是否为NULL排序,再按值排序 + stmt = stmt.order_by( + JobModel.last_sync_at.is_(None).asc(), # NULL值在后 + JobModel.last_sync_at.desc() + ) stmt = stmt.offset((page - 1) * page_size).limit(page_size) results = session.execute(stmt).scalars().all() diff --git a/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/api/api.js b/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/api/api.js index 1eae067..370ddbf 100644 --- a/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/api/api.js +++ b/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/api/api.js @@ -88,22 +88,22 @@ export const jobApi = { // 候选人管理 API export const candidateApi = { // 获取筛选通过的候选人 - getFiltered: (params = {}) => api.get('/candidates/filtered', { params }), - + getFiltered: (params = {}) => api.get('/api/candidates/filtered', { params }), + // 筛选候选人 - filter: (data) => api.post('/candidates/filter', data), - + filter: (data) => api.post('/api/candidates/filter', data), + // 获取候选人详情 - getDetail: (id) => api.get(`/candidates/${id}`), - + getDetail: (id) => api.get(`/api/candidates/${id}`), + // 标记候选人筛选状态 - markFiltered: (data) => api.post('/candidates/mark-filtered', data), - + markFiltered: (data) => api.post('/api/candidates/mark-filtered', data), + // 更新候选人评分 - updateScore: (data) => api.post('/candidates/update-score', data), - + updateScore: (data) => api.post('/api/candidates/update-score', data), + // 根据评分范围查询 - getByScoreRange: (params) => api.get('/candidates/by-score-range', { params }) + getByScoreRange: (params) => api.get('/api/candidates/by-score-range', { params }) } // 定时任务管理 API @@ -139,14 +139,53 @@ export const schedulerApi = { stop: () => api.post('/api/scheduler/stop') } +// 通知渠道管理 API +export const notificationChannelApi = { + // 获取渠道类型列表 + getTypes: () => api.get('/api/notification-channels/types'), + + // 获取通知渠道列表 + getList: (params = {}) => api.get('/api/notification-channels', { params }), + + // 获取通知渠道详情 + getDetail: (id) => api.get(`/api/notification-channels/${id}`), + + // 创建通知渠道 + create: (data) => api.post('/api/notification-channels', data), + + // 更新通知渠道 + update: (id, data) => api.put(`/api/notification-channels/${id}`, data), + + // 删除通知渠道 + delete: (id) => api.delete(`/api/notification-channels/${id}`), + + // 启用通知渠道 + activate: (id) => api.post(`/api/notification-channels/${id}/activate`), + + // 停用通知渠道 + deactivate: (id) => api.post(`/api/notification-channels/${id}/deactivate`), + + // 获取渠道绑定的招聘者列表 + getChannelRecruiters: (id) => api.get(`/api/notification-channels/${id}/recruiters`), + + // 绑定招聘者到渠道 + bindRecruiter: (channelId, recruiterId, data) => api.post(`/api/notification-channels/${channelId}/recruiters/${recruiterId}`, data), + + // 更新绑定配置 + updateBinding: (channelId, recruiterId, data) => api.put(`/api/notification-channels/${channelId}/recruiters/${recruiterId}`, data), + + // 解绑招聘者 + unbindRecruiter: (channelId, recruiterId) => api.delete(`/api/notification-channels/${channelId}/recruiters/${recruiterId}`) +} + // 系统 API export const systemApi = { // 获取首页信息 getHome: () => api.get('/'), - + // 健康检查 health: () => api.get('/health'), - + // 获取API状态 getStatus: () => api.get('/api/status') } diff --git a/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/router/index.js b/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/router/index.js index c049da7..03daa2a 100644 --- a/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/router/index.js +++ b/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/router/index.js @@ -36,6 +36,12 @@ const routes = [ name: 'Scheduler', component: () => import('@/views/Scheduler.vue'), meta: { title: '定时任务', icon: 'time' } + }, + { + path: 'notification-channels', + name: 'NotificationChannels', + component: () => import('@/views/NotificationChannels.vue'), + meta: { title: '通知渠道', icon: 'notification' } } ] } diff --git a/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/NotificationChannels.vue b/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/NotificationChannels.vue new file mode 100644 index 0000000..4aa849f --- /dev/null +++ b/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/NotificationChannels.vue @@ -0,0 +1,679 @@ + + + + + diff --git a/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/Recruiters.vue b/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/Recruiters.vue index 45c9c8c..0789bd3 100644 --- a/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/Recruiters.vue +++ b/src/main/web/cn.yinlihupo/ylhp_hr_2_0_fronted/src/views/Recruiters.vue @@ -98,21 +98,75 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ channelBindForm.high_score_threshold }} 分
+
+ + 添加绑定 + +
+
+
+