feat: Add job nature field to job and related database migrations
This commit is contained in:
232
docs/app-job-suggest-api.md
Normal file
232
docs/app-job-suggest-api.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# 岗位名称联想接口文档
|
||||
|
||||
## 1. 接口说明
|
||||
|
||||
根据用户输入的岗位关键词,从有效岗位名称预先生成的 IK 分词缓存中查询联想结果,返回匹配的 IK 分词和包含该分词的完整岗位名称,供首页搜索框快速选择。
|
||||
|
||||
接口查询 Redis 缓存,不在用户输入时访问岗位数据库。
|
||||
|
||||
## 2. 接口地址
|
||||
|
||||
**后端接口:** `GET /app/job/suggest`
|
||||
|
||||
**前端代理地址:** `GET /api/app/job/suggest`
|
||||
|
||||
前端页面通过代理地址调用,代理转发到后端 `/app/job/suggest`。
|
||||
|
||||
## 3. 请求参数
|
||||
|
||||
请求方式:`GET`
|
||||
|
||||
请求参数通过 Query String 传递。
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| keyword | String | 否(建议传入) | - | 用户输入的岗位关键词或 IK 分词前缀。未传、为空或仅包含空格时返回空数组 |
|
||||
| limit | Integer | 否 | 10 | 返回结果数量,服务端限制在 1~20 之间,超过 20 按 20 返回,小于 1 按 1 返回 |
|
||||
|
||||
## 4. 请求示例
|
||||
|
||||
```http
|
||||
GET /app/job/suggest?keyword=后端&limit=10
|
||||
```
|
||||
|
||||
前端代理调用:
|
||||
|
||||
```http
|
||||
GET /api/app/job/suggest?keyword=后端&limit=10
|
||||
```
|
||||
|
||||
使用 cURL 调用:
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8080/app/job/suggest?keyword=%E5%90%8E%E7%AB%AF&limit=10"
|
||||
```
|
||||
|
||||
如果通过前端代理访问:
|
||||
|
||||
```bash
|
||||
curl "http://47.111.103.66/shihezi/api/app/job/suggest?keyword=%E5%90%8E%E7%AB%AF&limit=10"
|
||||
```
|
||||
|
||||
> 实际部署时请根据前端代理配置确认 `/shihezi` 前缀和后端服务地址。
|
||||
|
||||
## 5. 响应参数
|
||||
|
||||
| 参数名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| code | Number | 状态码,200 表示成功 |
|
||||
| msg | String | 提示信息 |
|
||||
| data | Array<String> | 联想结果列表,结果内容为 IK 分词或完整岗位名称 |
|
||||
|
||||
## 6. 成功响应示例
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
GET /app/job/suggest?keyword=后端&limit=10
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": [
|
||||
"后端",
|
||||
"后端工程师",
|
||||
"高级Java后端开发工程师"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 空关键词响应
|
||||
|
||||
当 `keyword` 未传、为空或仅包含空格时,接口直接返回空数组。
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
GET /app/job/suggest?keyword=
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 无匹配结果响应
|
||||
|
||||
当输入关键词没有匹配的 IK 分词时,接口返回空数组。
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
GET /app/job/suggest?keyword=不存在的岗位&limit=10
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "操作成功",
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 返回规则
|
||||
|
||||
### 9.1 IK 分词规则
|
||||
|
||||
定时任务从有效岗位名称中使用 IK `smart` 模式分词。例如:
|
||||
|
||||
```text
|
||||
岗位名称:高级Java后端开发工程师
|
||||
IK 分词:高级、java、后端、开发、工程师
|
||||
```
|
||||
|
||||
缓存中的词项只来自 IK 分词结果,不额外生成单字、字符片段或其他自定义前缀。
|
||||
|
||||
用户输入会进行统一标准化处理:
|
||||
|
||||
- 全角字符转换为半角字符;
|
||||
- 英文字母转换为小写;
|
||||
- 去除空白字符。
|
||||
|
||||
标准化后,接口对缓存中的 IK 分词进行前缀匹配。例如输入 `工程` 可以匹配以 `工程` 开头的 IK 词项。
|
||||
|
||||
### 9.2 完整岗位名称规则
|
||||
|
||||
为了提升联想效果,接口不仅返回匹配到的 IK 分词,还会返回包含该分词的完整岗位名称。
|
||||
|
||||
例如 Redis 中存在以下映射:
|
||||
|
||||
```text
|
||||
后端 -> 高级Java后端开发工程师
|
||||
工程师 -> 高级Java后端开发工程师、后端工程师
|
||||
```
|
||||
|
||||
输入 `后端` 时,接口会将 `后端` 和相关完整岗位名称合并返回,并自动去重。
|
||||
|
||||
### 9.3 结果数量
|
||||
|
||||
- 默认最多返回 10 条;
|
||||
- `limit` 最大为 20;
|
||||
- 返回结果包含 IK 分词和完整岗位名称,二者合计不超过 `limit`;
|
||||
- 结果自动去重;
|
||||
- 没有结果时返回空数组,不返回 `null`。
|
||||
|
||||
## 10. 数据缓存和定时重建
|
||||
|
||||
Redis 使用 Hash 保存岗位名称联想数据:
|
||||
|
||||
```text
|
||||
Redis Key:app:job:title:suggest
|
||||
```
|
||||
|
||||
数据结构:
|
||||
|
||||
| Hash 内容 | 说明 |
|
||||
|-----------|------|
|
||||
| field | IK 分词,例如 `后端`、`工程师` |
|
||||
| value | 包含该分词的完整岗位名称列表 |
|
||||
|
||||
定时任务只处理有效岗位,岗位有效条件如下:
|
||||
|
||||
```sql
|
||||
del_flag = '0'
|
||||
AND job_status = '0'
|
||||
AND job_title IS NOT NULL
|
||||
AND trim(job_title) <> ''
|
||||
```
|
||||
|
||||
Quartz 定时任务调用目标:
|
||||
|
||||
```text
|
||||
jobCron.rebuildJobTitleSuggest()
|
||||
```
|
||||
|
||||
应用启动时会初始化一次缓存,之后由 Quartz 定时任务按配置周期重建。重建时先写入临时缓存,完成后再替换正式缓存,避免接口读取到不完整数据。
|
||||
|
||||
## 11. 前端接入建议
|
||||
|
||||
首页搜索框可以按以下方式接入:
|
||||
|
||||
1. 将普通输入框改为支持下拉选项的 `AutoComplete` 或自定义下拉组件。
|
||||
2. 用户输入停止约 250ms 后调用接口,避免每次键盘事件都请求后端。
|
||||
3. 输入为空时清空下拉选项,不调用接口。
|
||||
4. 调用 `/api/app/job/suggest`,将返回的 `data` 数组直接转换为下拉选项。
|
||||
5. 用户点击某个联想项时,将选中的完整文本回填到搜索框,并执行岗位搜索。
|
||||
6. 用户直接回车或点击搜索按钮时,仍使用搜索框当前文本执行搜索。
|
||||
7. 岗位列表页使用与原有搜索接口一致的岗位名称参数,例如 `name`,将完整岗位名称作为搜索条件传递。
|
||||
8. 建议使用请求序号、取消请求或其他方式处理异步请求顺序,避免旧请求结果覆盖用户最新输入的结果。
|
||||
|
||||
前端请求示例:
|
||||
|
||||
```ts
|
||||
const response = await get('/api/app/job/suggest', {
|
||||
params: {
|
||||
keyword: value,
|
||||
limit: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const options = (response.data || []).map((item: string) => ({
|
||||
value: item,
|
||||
label: item,
|
||||
}));
|
||||
```
|
||||
|
||||
选择联想结果后,建议以完整岗位名称进行搜索:
|
||||
|
||||
```ts
|
||||
handleSearch(selectedValue);
|
||||
// selectedValue 例如:高级Java后端开发工程师
|
||||
```
|
||||
@@ -120,6 +120,9 @@ public class ESJobDocument
|
||||
@IndexField(fieldType = FieldType.TEXT, analyzer = Analyzer.IK_SMART, searchAnalyzer = Analyzer.IK_SMART)
|
||||
private String jobCategory;
|
||||
|
||||
@ApiModelProperty("岗位性质,对应业务字典 job_nature")
|
||||
private String jobNature;
|
||||
|
||||
@JsonIgnore
|
||||
@ApiModelProperty("学历要求 对应字典education int类型 es方便查询")
|
||||
private Integer education_int;
|
||||
|
||||
@@ -148,6 +148,10 @@ public class Job extends BaseEntity
|
||||
@ApiModelProperty("岗位分类")
|
||||
private String jobCategory;
|
||||
|
||||
@Excel(name = "岗位性质")
|
||||
@ApiModelProperty("岗位性质,对应业务字典 job_nature")
|
||||
private String jobNature;
|
||||
|
||||
@TableField(exist = false)
|
||||
@ApiModelProperty("公司性质")
|
||||
private String companyNature;
|
||||
|
||||
@@ -242,6 +242,7 @@ public class BussinessDictTypeServiceImpl implements IBussinessDictTypeService
|
||||
filed.put("data_source","数据来源");
|
||||
filed.put("job_url","岗位url连接");
|
||||
filed.put("job_category",jobTitleMapper.selectList(null).stream().map(JobTitle::getJobName).collect(Collectors.toList()));
|
||||
filed.put("job_nature",this.selectDictDataByType("job_nature").stream().map(BussinessDictData::getDictLabel).collect(Collectors.toList()));
|
||||
return filed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,26 +439,13 @@ public class JobServiceImpl extends ServiceImpl<JobMapper,Job> implements IJobSe
|
||||
{
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String formattedDate = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
Job existingJob = jobMapper.selectById(job.getJobId());
|
||||
//修改岗位状态
|
||||
if(job.getIsPublish()!=null) {
|
||||
if(job.getIsPublish()==1){
|
||||
job.setPostingDate(formattedDate);
|
||||
//修改es缓存
|
||||
iesJobSearchService.updateJob(job.getJobId());
|
||||
//添加岗位上新
|
||||
Job parmJob=null;
|
||||
if(job.getCompanyId()==null){
|
||||
//传递job消息不完整
|
||||
parmJob=jobMapper.getJobInfo(job.getJobId());
|
||||
}
|
||||
List<AppUser> users=companyCollectionMapper.selectAppuserList(parmJob.getCompanyId());
|
||||
if(users!=null&&users.size()>0){
|
||||
List<Notice> notices= NoticeUtils.createGwsxNotice(users,parmJob);
|
||||
noticeMapper.batchInsert(notices);
|
||||
}
|
||||
}else {
|
||||
job.setPostingDate(null);
|
||||
iesJobSearchService.deleteJob(job.getJobId());
|
||||
}
|
||||
}
|
||||
int i=jobMapper.updateById(job);
|
||||
@@ -468,6 +455,25 @@ public class JobServiceImpl extends ServiceImpl<JobMapper,Job> implements IJobSe
|
||||
handleJobContact(job);
|
||||
//添加附件
|
||||
handleJobFile(job);
|
||||
|
||||
// 数据库更新成功后再同步 ES,确保岗位性质等最新字段能立即反映到 PC 岗位卡片。
|
||||
if (Integer.valueOf(1).equals(job.getIsPublish())
|
||||
|| (job.getIsPublish() == null && existingJob != null
|
||||
&& Integer.valueOf(1).equals(existingJob.getIsPublish()))) {
|
||||
iesJobSearchService.updateJob(job.getJobId());
|
||||
|
||||
// 添加岗位上新通知,始终使用数据库中的完整岗位信息。
|
||||
Job publishedJob = jobMapper.getJobInfo(job.getJobId());
|
||||
if (publishedJob != null && publishedJob.getCompanyId() != null) {
|
||||
List<AppUser> users = companyCollectionMapper.selectAppuserList(publishedJob.getCompanyId());
|
||||
if(users != null && !users.isEmpty()){
|
||||
List<Notice> notices = NoticeUtils.createGwsxNotice(users, publishedJob);
|
||||
noticeMapper.batchInsert(notices);
|
||||
}
|
||||
}
|
||||
} else if (Integer.valueOf(0).equals(job.getIsPublish())) {
|
||||
iesJobSearchService.deleteJob(job.getJobId());
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<result property="jobUrl" column="job_url" />
|
||||
<result property="rowId" column="row_id" />
|
||||
<result property="jobCategory" column="job_category" />
|
||||
<result property="jobNature" column="job_nature" />
|
||||
<result property="isExplain" column="is_explain" />
|
||||
<result property="explainUrl" column="explain_url" />
|
||||
<result property="cover" column="cover" />
|
||||
@@ -74,6 +75,7 @@
|
||||
<result property="jobUrl" column="job_url" />
|
||||
<result property="rowId" column="row_id" />
|
||||
<result property="jobCategory" column="job_category" />
|
||||
<result property="jobNature" column="job_nature" />
|
||||
<result property="isExplain" column="is_explain" />
|
||||
<result property="explainUrl" column="explain_url" />
|
||||
<result property="cover" column="cover" />
|
||||
@@ -112,7 +114,7 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectJobVo">
|
||||
select job_id, job_title, min_salary, max_salary, education, experience, company_name, job_location, posting_date, vacancies, del_flag, create_by, create_time, update_by, update_time, remark, latitude, longitude, "view", company_id , is_hot ,is_urgent,apply_num,is_publish, description,job_location_area_code,data_source,job_url,job_category,is_explain,explain_url,cover,job_type,job_address, review_status,is_key_populations,key_populations from job
|
||||
select job_id, job_title, min_salary, max_salary, education, experience, company_name, job_location, posting_date, vacancies, del_flag, create_by, create_time, update_by, update_time, remark, latitude, longitude, "view", company_id , is_hot ,is_urgent,apply_num,is_publish, description,job_location_area_code,data_source,job_url,job_category,job_nature,is_explain,explain_url,cover,job_type,job_address, review_status,is_key_populations,key_populations from job
|
||||
</sql>
|
||||
<insert id="insertBatchRowWork">
|
||||
INSERT INTO row_work (
|
||||
@@ -142,7 +144,7 @@
|
||||
job_title, min_salary, max_salary, education, experience, company_name, job_location,
|
||||
job_location_area_code, posting_date, vacancies, latitude, longitude, "view", company_id,
|
||||
is_hot, is_urgent, apply_num, description, is_publish, data_source, job_url, remark, del_flag,
|
||||
create_by, create_time, row_id, job_category,job_type,job_address,job_status,is_key_populations,
|
||||
create_by, create_time, row_id, job_category,job_nature,job_type,job_address,job_status,is_key_populations,
|
||||
key_populations
|
||||
) VALUES
|
||||
<foreach collection="list" item="job" separator=",">
|
||||
@@ -152,7 +154,7 @@
|
||||
#{job.vacancies}, #{job.latitude}, #{job.longitude}, #{job.view}, #{job.companyId},
|
||||
#{job.isHot}, #{job.isUrgent}, #{job.applyNum}, #{job.description}, #{job.isPublish}, #{job.dataSource},
|
||||
#{job.jobUrl}, #{job.remark}, #{job.delFlag}, #{job.createBy}, #{job.createTime},
|
||||
#{job.rowId}, #{job.jobCategory},#{job.jobType},#{job.jobAddress},#{job.jobStatus},#{job.isKeyPopulations},#{job.keyPopulations}
|
||||
#{job.rowId}, #{job.jobCategory},#{job.jobNature},#{job.jobType},#{job.jobAddress},#{job.jobStatus},#{job.isKeyPopulations},#{job.keyPopulations}
|
||||
)
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
<select id="selectJobListByJobFairIdAndCompanyId" resultType="com.ruoyi.cms.domain.Job">
|
||||
select pfj.id as fair_relation_id, j.job_id, j.job_title, j.min_salary, j.max_salary, j.education, j.experience,
|
||||
j.company_name, j.job_location, j.job_location_area_code, j.posting_date, j.vacancies,
|
||||
j.company_id, j.description, j.job_category, j.job_address
|
||||
j.company_id, j.description, j.job_category, j.job_nature, j.job_address
|
||||
from public_job_fair_job pfj
|
||||
inner join job j on pfj.job_id = j.job_id
|
||||
where pfj.job_fair_id = #{jobFairId} and pfj.company_id = #{companyId} and pfj.del_flag = '0' and j.del_flag = '0'
|
||||
|
||||
29
sql/migration_add_job_nature.sql
Normal file
29
sql/migration_add_job_nature.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- 岗位性质:后台可选、PC 岗位卡片返回的业务字典字段。
|
||||
-- 岗位性质非必填;字典项由后台“业务字典”维护。
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE "shz"."job"
|
||||
ADD COLUMN IF NOT EXISTS "job_nature" VARCHAR(100);
|
||||
|
||||
COMMENT ON COLUMN "shz"."job"."job_nature"
|
||||
IS '岗位性质,对应业务字典 job_nature,非必填';
|
||||
|
||||
INSERT INTO "shz"."bussiness_dict_type" (
|
||||
"dict_id", "dict_name", "dict_type", "status", "create_by", "create_time", "remark"
|
||||
)
|
||||
SELECT
|
||||
(SELECT COALESCE(MAX("dict_id"), 0) + 1 FROM "shz"."bussiness_dict_type"),
|
||||
'岗位性质',
|
||||
'job_nature',
|
||||
'0',
|
||||
'system',
|
||||
CURRENT_TIMESTAMP,
|
||||
'岗位管理使用,字典项由业务字典维护'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "shz"."bussiness_dict_type"
|
||||
WHERE "dict_type" = 'job_nature'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
30
sql/migration_add_job_nature_prod.sql
Normal file
30
sql/migration_add_job_nature_prod.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- 正式库:岗位性质字段及业务字典类型迁移。
|
||||
-- 执行目标:正式 HighGo 数据库的 shz schema。
|
||||
-- 岗位性质非必填;具体字典项由后台“业务字典”维护。
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE "shz"."job"
|
||||
ADD COLUMN IF NOT EXISTS "job_nature" VARCHAR(100);
|
||||
|
||||
COMMENT ON COLUMN "shz"."job"."job_nature"
|
||||
IS '岗位性质,对应业务字典 job_nature,非必填';
|
||||
|
||||
INSERT INTO "shz"."bussiness_dict_type" (
|
||||
"dict_id", "dict_name", "dict_type", "status", "create_by", "create_time", "remark"
|
||||
)
|
||||
SELECT
|
||||
(SELECT COALESCE(MAX("dict_id"), 0) + 1 FROM "shz"."bussiness_dict_type"),
|
||||
'岗位性质',
|
||||
'job_nature',
|
||||
'0',
|
||||
'system',
|
||||
CURRENT_TIMESTAMP,
|
||||
'岗位管理使用,字典项由业务字典维护'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "shz"."bussiness_dict_type"
|
||||
WHERE "dict_type" = 'job_nature'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
30
sql/migration_add_job_nature_test.sql
Normal file
30
sql/migration_add_job_nature_test.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- 测试库:岗位性质字段及业务字典类型迁移。
|
||||
-- 执行目标:测试 HighGo 数据库的 shz schema。
|
||||
-- 岗位性质非必填;具体字典项由后台“业务字典”维护。
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE "shz"."job"
|
||||
ADD COLUMN IF NOT EXISTS "job_nature" VARCHAR(100);
|
||||
|
||||
COMMENT ON COLUMN "shz"."job"."job_nature"
|
||||
IS '岗位性质,对应业务字典 job_nature,非必填';
|
||||
|
||||
INSERT INTO "shz"."bussiness_dict_type" (
|
||||
"dict_id", "dict_name", "dict_type", "status", "create_by", "create_time", "remark"
|
||||
)
|
||||
SELECT
|
||||
(SELECT COALESCE(MAX("dict_id"), 0) + 1 FROM "shz"."bussiness_dict_type"),
|
||||
'岗位性质',
|
||||
'job_nature',
|
||||
'0',
|
||||
'system',
|
||||
CURRENT_TIMESTAMP,
|
||||
'岗位管理使用,字典项由业务字典维护'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "shz"."bussiness_dict_type"
|
||||
WHERE "dict_type" = 'job_nature'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user