修改bug 完善文档
This commit is contained in:
@@ -1,163 +0,0 @@
|
||||
# 评分体系改造方案
|
||||
|
||||
## 核心公式
|
||||
|
||||
`MatchScore = Sedu × 30% + Sexp × 40% + Sskill × 30%`
|
||||
|
||||
---
|
||||
|
||||
## 一、表结构改动
|
||||
|
||||
### 1. 新建表:bg_major_category(专业分类,三级树形)
|
||||
|
||||
```sql
|
||||
create table bg_major_category (
|
||||
id bigint not null comment 'ID' primary key,
|
||||
name varchar(50) not null comment '类型名称',
|
||||
root_id bigint not null comment '根节点ID,顶级=自身ID',
|
||||
parent_id bigint not null comment '父级ID,0=顶级',
|
||||
level int not null comment '层级 1=一级 2=二级 3=三级'
|
||||
) comment '专业分类' row_format = DYNAMIC;
|
||||
```
|
||||
|
||||
基于《教育部普通高等学校本科专业目录》建立层级索引,AI 将简历/岗位专业归一化到标准 ID。
|
||||
|
||||
### 2. bg_skill_tag — 改造
|
||||
|
||||
| 操作 | 字段 | 说明 |
|
||||
|------|------|------|
|
||||
| 删除 | `category_id` | 不再挂岗位分类 |
|
||||
| 新增 | `name` 唯一索引 | 去重用 |
|
||||
|
||||
定位从"人工预定义"变为"AI 提取自动入库"。提取技能时先查表去重,不存在则 insert,拿 ID 写关联表。
|
||||
|
||||
### 3. bg_job — 新增字段
|
||||
|
||||
| 字段 | 类型 | 备注 |
|
||||
|------|------|------|
|
||||
| `required_major_ids` | json | 要求专业ID数组,关联 bg_major_category |
|
||||
| `major_sensitivity` | tinyint | 专业敏感度 0=不限 1=优先 2=强制 |
|
||||
|
||||
### 4. bg_user_profile — 新增字段
|
||||
|
||||
| 字段 | 类型 | 备注 |
|
||||
|------|------|------|
|
||||
| `major_ids` | json | 用户专业ID数组,关联 bg_major_category |
|
||||
| `school_rank` | tinyint | 学校等级 1=C9/985/QS前50 2=211/双一流/QS前200 3=普通一本/QS前500 4=其他 |
|
||||
| `company_prestige` | tinyint | 公司背书 1=名企 2=普通实习 3=校内活动 4=无 |
|
||||
| `experience_duration` | tinyint | 经历时长 1=≥3月 2=1-3月 3=≤1月 |
|
||||
| `role_depth` | tinyint | 职责深度 1=主导/创新 2=执行/应用 3=辅助/学习 |
|
||||
| `output_quality` | tinyint | 量化产出 1=有量化结果 2=有具体产出 3=纯描述 |
|
||||
| `honors` | json | 荣誉/竞赛/论文,格式:{"national":[],"provincial":[],"school":[],"paper":[]} |
|
||||
|
||||
以上字段全部不在前端展示,纯用于评分计算,由 AI 分析简历后写入。
|
||||
|
||||
### 5. bg_user_profile — 废弃字段
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `work_years` | 被经历质量维度替代 |
|
||||
| `experience_industry_ids` | 行业前置过滤废除 |
|
||||
|
||||
### 6. 不变的表
|
||||
|
||||
- `bg_job_skill_tag_relation` — 结构不变,数据来源从预定义标签变为 AI 自动入库标签
|
||||
- `bg_user_profile_skill_tag_relation` — 同上
|
||||
|
||||
---
|
||||
|
||||
## 二、评分算法
|
||||
|
||||
### 维度1:教育背景(Sedu)— 权重 30%
|
||||
|
||||
学校等级分(Srank)与专业相关度(Smajor)的权重由专业敏感度决定:
|
||||
|
||||
| 专业敏感度 | 公式 | 特殊规则 |
|
||||
|-----------|------|---------|
|
||||
| 强制(2) | Sedu = Srank × 0.4 + Smajor × 0.6 | 若 Smajor < 40,Sedu = Sedu × 0.5 |
|
||||
| 优先(1) | Sedu = Srank × 0.6 + Smajor × 0.4 | 无 |
|
||||
| 不限(0) | Sedu = Srank × 1.0 | Smajor 默认100,不减分 |
|
||||
|
||||
学校等级映射:1→100, 2→80, 3→60, 4→40
|
||||
|
||||
专业相关度(基于 bg_major_category 树形匹配):
|
||||
- 完全匹配(同三级专业)→ 100
|
||||
- 相关对口(同二级专业类)→ 70
|
||||
- 跨专业相关(同一级学科门类)→ 30
|
||||
- 不相关 → 0
|
||||
|
||||
### 维度2:经历质量(Sexp)— 权重 40%
|
||||
|
||||
`Sexp = 公司背书 × 30% + 时长 × 10% + 经历深度 × 60%`
|
||||
|
||||
公司背书映射:1→100, 2→60, 3→30, 4→0
|
||||
经历时长映射:1→100, 2→60, 3→30
|
||||
|
||||
经历深度(60%)内部拆解:
|
||||
|
||||
| 子项 | 占比 | 说明 |
|
||||
|------|------|------|
|
||||
| 语义相关度 | 40% | AI 判断经历与岗位方向的相关性 |
|
||||
| 职责深度 | 25% | 映射:1→100, 2→80, 3→40 |
|
||||
| 量化产出 | 20% | 映射:1→100, 2→70, 3→40 |
|
||||
| 荣誉加分 | 15% | 封顶累加制,总分不超过100 |
|
||||
|
||||
荣誉分值规则:
|
||||
- 国奖级(national):每项 +15-20
|
||||
- 省奖/大厂赛(provincial):每项 +8-10
|
||||
- 院校级/专业证(school):每项 +3-5
|
||||
- 顶刊顶会论文:每项 +15-20,普通论文:每项 +8-10
|
||||
|
||||
### 维度3:技能匹配(Sskill)— 权重 30%
|
||||
|
||||
废除产品方案中的向量距离、技能近亲表、分层技能池。
|
||||
|
||||
采用简化方案:
|
||||
- AI 自由提取技能 + prompt demo 约束颗粒度
|
||||
- 提取结果入 bg_skill_tag(去重),写关联表
|
||||
- 匹配时通过 skill_tag_id 做集合碰撞
|
||||
- 公式:`Sskill = (匹配数量 / 岗位要求数量) × 100`
|
||||
|
||||
---
|
||||
|
||||
## 三、数据清洗改造
|
||||
|
||||
### 岗位清洗(JobCleanService)
|
||||
|
||||
改动:
|
||||
- 第二次 AI 调用从"预定义标签匹配"改为"自由提取 + prompt demo 约束"
|
||||
- 新增:提取要求专业 → 归一化到 bg_major_category ID
|
||||
- 新增:提取专业敏感度
|
||||
- 技能入库逻辑:查 bg_skill_tag 去重 → insert → 写关联表
|
||||
|
||||
### 用户简历分析(UserSkillTagMatchService)
|
||||
|
||||
改动:
|
||||
- 可能简化为一次 AI 调用,直接从简历全文提取
|
||||
- 新增:识别学校等级、公司背书、经历时长、职责深度、量化产出、荣誉
|
||||
- 新增:提取用户专业 → 归一化到 bg_major_category ID
|
||||
- 技能入库逻辑同上
|
||||
|
||||
### DictCacheService
|
||||
|
||||
- 删掉 skillTagMap、getSkillTagText()、getSkillTagIds()
|
||||
- 新增:加载 bg_major_category 缓存(供专业归一化使用)
|
||||
|
||||
---
|
||||
|
||||
## 四、影响文件汇总
|
||||
|
||||
| 文件 | 改动 |
|
||||
|------|------|
|
||||
| SkillTag.java | 删 categoryId |
|
||||
| Job.java | 加 requiredMajorIds、majorSensitivity |
|
||||
| UserProfile.java | 加 7 个新字段,删 workYears、experienceIndustryIds |
|
||||
| MajorCategory.java | 新建 PO |
|
||||
| MajorCategoryMapper.java | 新建 Mapper |
|
||||
| DictCacheService | 删技能缓存,加专业缓存 |
|
||||
| JobCleanService | 改技能提取 + 新增专业/敏感度提取 |
|
||||
| JobCleanTransactionService | 改技能入库逻辑 |
|
||||
| UserSkillTagMatchService | 大改,新增多维度识别 |
|
||||
| JobMatchService | 重写,三维度算法全换 |
|
||||
| JobMatchScoreDto | 改字段(教育/经历/技能) |
|
||||
| JobDto / JobDetailDto | matchScore 范围可能变化 |
|
||||
+103
-269
@@ -11,106 +11,59 @@ inclusion: manual
|
||||
↓
|
||||
Java定时任务读取(多线程)
|
||||
↓
|
||||
调用AI API清洗/结构化
|
||||
三次AI调用:结构化 → 专业匹配 → 技能提取
|
||||
↓
|
||||
写入业务表(bg_company + bg_job + 关联表)
|
||||
写入业务表(bg_company + bg_job + 关联表 + bg_skill_tag)
|
||||
↓
|
||||
公司信息不完整的 → 调工商API补充
|
||||
公司信息不完整的 → 调AI补充
|
||||
```
|
||||
|
||||
所有清洗逻辑放在 `manager` 模块,通过 `@Scheduled` 定时任务触发。
|
||||
|
||||
## 讨论分区
|
||||
|
||||
整体方案分为四部分逐步讨论:
|
||||
1. ✅ 岗位清洗触发逻辑
|
||||
2. ✅ 岗位清洗逻辑
|
||||
3. ✅ 公司数据触发逻辑
|
||||
4. ✅ 公司数据补充逻辑(AI补充)
|
||||
|
||||
---
|
||||
|
||||
## 一、岗位清洗触发逻辑
|
||||
|
||||
### 1.1 表结构变更
|
||||
### 1.1 状态管理
|
||||
|
||||
`app_job_data` 新增字段:
|
||||
|
||||
```sql
|
||||
ALTER TABLE app_job_data ADD COLUMN clean_status TINYINT(1) DEFAULT 0 NOT NULL COMMENT '清洗状态 0=待清洗 1=清洗中 2=已入库 3=已丢弃';
|
||||
CREATE INDEX idx_clean_status ON app_job_data (clean_status);
|
||||
```
|
||||
|
||||
状态说明:
|
||||
- 0=待清洗:新爬到的数据,默认值,不影响爬虫原有插入逻辑
|
||||
`app_job_data.clean_status`:
|
||||
- 0=待清洗:新爬到的数据,默认值
|
||||
- 1=清洗中:定时任务已锁定,正在处理
|
||||
- 2=已入库:清洗成功,已写入 bg_job
|
||||
- 3=已丢弃:AI判定为无效数据,不入库
|
||||
|
||||
时间记录:不加额外时间字段,利用已有的 `updated_at`(ON UPDATE CURRENT_TIMESTAMP),状态变更时自动更新。
|
||||
- 3=已丢弃:AI判定为无效数据
|
||||
|
||||
### 1.2 两个定时任务
|
||||
|
||||
#### 任务A:岗位清洗任务(高频,每5分钟)
|
||||
#### 任务A:岗位清洗(每5分钟)
|
||||
1. 事务内 `SELECT FOR UPDATE` + `UPDATE clean_status=1` 批量锁定
|
||||
2. 丢入线程池,多线程并发处理
|
||||
3. 每条独立更新状态
|
||||
|
||||
1. 批量锁定(事务内 `SELECT ... FOR UPDATE` + `UPDATE`):先在事务内对 `clean_status=0 AND is_valid=1` 的行加行锁并查出数据,再更新 `clean_status=1`,事务提交后释放锁。行锁保证并发安全,其他线程会阻塞直到事务提交
|
||||
2. 将锁定的数据丢入线程池,多线程并发调用 AI API 清洗
|
||||
3. 每条处理完毕后,单独更新 `clean_status` 为 2(已入库)或 3(已丢弃)
|
||||
4. 单条写入事务:bg_job 入库 + clean_status 更新放在同一个短事务中,保证一致性
|
||||
|
||||
#### 任务B:僵尸恢复任务(低频,每30分钟)
|
||||
|
||||
处理因发布重启导致卡在"清洗中"的僵尸数据:
|
||||
|
||||
```sql
|
||||
UPDATE app_job_data SET clean_status=0 WHERE clean_status=1 AND updated_at < NOW() - INTERVAL 10 MINUTE
|
||||
```
|
||||
|
||||
一条SQL搞定,将超时10分钟仍在"清洗中"的数据重置为待清洗,下次任务A会重新捞取处理。
|
||||
|
||||
### 1.3 去重保障
|
||||
|
||||
即使同一条数据被重复清洗(僵尸恢复后重新处理),写入 `bg_job` 时通过 `source_id` 判断是否已存在,存在则跳过,不会产生重复数据。
|
||||
|
||||
### 1.4 设计决策记录
|
||||
|
||||
| 决策点 | 结论 | 原因 |
|
||||
|--------|------|------|
|
||||
| 清洗状态放哪 | app_job_data 加字段 | 同库,简单直接 |
|
||||
| 是否加"清洗中"状态 | 是 | 多线程并发需要锁定机制 |
|
||||
| 长事务 vs 短事务 | 短事务(单条) | AI调用耗时长,不能hold连接 |
|
||||
| 僵尸恢复方式 | 独立低频定时任务 | 避免每次清洗任务都多一次查询,节省性能 |
|
||||
| 是否加 clean_time 字段 | 否 | updated_at 自动更新,够用 |
|
||||
| 失败重试 | 僵尸恢复任务自动处理 | clean_status=1 超时后重置为0,自动重试 |
|
||||
#### 任务B:僵尸恢复(每30分钟)
|
||||
将超时10分钟仍在清洗中的数据重置为待清洗。
|
||||
|
||||
---
|
||||
|
||||
## 二、岗位清洗逻辑
|
||||
## 二、岗位清洗逻辑(三次AI调用)
|
||||
|
||||
### 2.1 前置校验(Java侧,不调AI)
|
||||
### 2.1 前置校验
|
||||
- `description` 为空或长度 < 20 → 标记丢弃,跳过
|
||||
|
||||
- `description` 为空或长度 < 20 → 直接标记 `clean_status=3`(丢弃),跳过,节省AI调用成本
|
||||
### 2.2 参考数据缓存(DictCacheService)
|
||||
|
||||
### 2.2 参考数据准备
|
||||
启动时加载:
|
||||
- `bg_job_category` 叶子节点(level=3),格式 `id:name(一级/二级)`
|
||||
- `bg_industry` 叶子节点(level=2),格式 `id:name(一级)`
|
||||
- `bg_major_category` 叶子节点(level=3),格式 `id:name(一级/二级)`
|
||||
- `bg_china_regions_code` 省市级,供城市名匹配
|
||||
|
||||
应用启动时加载并缓存(定期刷新):
|
||||
- `bg_job_category` 全量:只取叶子节点(level=3),拼成 `id:name(一级/二级)` 文本列表
|
||||
- `bg_industry` 全量:只取叶子节点(level=2),拼成 `id:name(一级)` 文本列表
|
||||
- `bg_skill_tag` 全量:按 `categoryId` 分组缓存为 `Map<Long, List<SkillTag>>`,供第二次 AI 调用使用
|
||||
|
||||
分类和行业列表作为 prompt 的一部分传给AI,ID由人工维护为短数字,不使用雪花ID。
|
||||
|
||||
地区数据(`bg_china_regions_code`)不传给AI,由Java侧根据AI返回的城市名自行匹配。
|
||||
|
||||
### 2.3 AI 调用(单次调用,返回结构化JSON)
|
||||
### 2.3 第一次AI:提取岗位结构化信息
|
||||
|
||||
#### 输入
|
||||
|
||||
- 原始字段:job_title、salary、location、company、experience、education、description
|
||||
- 参考列表:岗位分类(id:name)、行业(id:name)
|
||||
|
||||
#### AI 返回 JSON 结构
|
||||
- 参考列表:岗位分类、行业
|
||||
|
||||
#### 返回JSON
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
@@ -121,186 +74,101 @@ UPDATE app_job_data SET clean_status=0 WHERE clean_status=1 AND updated_at < NOW
|
||||
"employmentType": 0,
|
||||
"categoryId": 12,
|
||||
"requiredIndustryId": 5,
|
||||
"description": "1. 负责核心业务系统开发...",
|
||||
"requirement": "1. 本科及以上学历...",
|
||||
"bonus": "1. 有分布式系统经验优先...",
|
||||
"tags": ["数据分析", "产品策略", "团队协作"],
|
||||
"skillTags": ["Java", "Spring Boot", "MySQL"],
|
||||
"description": "岗位职责...",
|
||||
"requirement": "任职要求...",
|
||||
"bonus": "加分项...",
|
||||
"tags": ["数据分析", "产品策略"],
|
||||
"skillTags": ["Java", "Spring Boot"],
|
||||
"companyShortName": "字节跳动",
|
||||
"cities": ["北京", "上海"]
|
||||
}
|
||||
```
|
||||
|
||||
#### 各字段清洗规则
|
||||
#### Java处理流程
|
||||
1. valid=false → 丢弃
|
||||
2. source_id 去重检查
|
||||
3. 公司处理(查或创建 bg_company)
|
||||
4. 地区匹配(cities → region_code)
|
||||
5. 写入 bg_job + bg_job_region_relation + 更新 clean_status=2
|
||||
|
||||
| 字段 | 来源 | 规则 |
|
||||
|------|------|------|
|
||||
| valid | AI综合判断 | 数据是否有效,false则丢弃 |
|
||||
| title | job_title | 存在则保留;不存在则AI从description归纳生成 |
|
||||
| salary | salary | 有效则标准化(10-20K / 20K / 面议);无效或空则null |
|
||||
| education | education + description | 映射为 0=不限 1=大专 2=本科 3=硕士 4=博士 |
|
||||
| minExperience | experience + description | 提取最低年限数值,不要求则为0 |
|
||||
| employmentType | description | 判断 0=全职 1=兼职,默认0 |
|
||||
| categoryId | description + job_title | 必选,从分类列表中选最接近的,不允许返回null |
|
||||
| requiredIndustryId | description(任职要求部分) | 仅当明确提到行业经验要求时设置;列表中无完全匹配则选最相似的;未提到则null |
|
||||
| description | description + experience + education | 提取"岗位职责"部分,保持原文风格,格式化展示 |
|
||||
| requirement | description + experience + education | 提取"任职要求"部分,保持原文风格,格式化展示 |
|
||||
| bonus | description + experience + education | 提取"加分项"部分,无则空 |
|
||||
| tags | description + job_title | 核心职能标签(如数据分析、产品策略、团队协作),最多5个 |
|
||||
| skillTags | description | 技能关键词(如Java、Spring Boot、MySQL),最多8个 |
|
||||
| companyShortName | company | 提取简洁的公司简称,去掉地区后缀、招聘后缀、括号内容等,保持"中国平安""字节跳动"风格 |
|
||||
| cities | location | 提取城市名列表,精确到市级 |
|
||||
### 2.4 第二次AI:专业匹配
|
||||
|
||||
### 2.4 AI 返回后的 Java 处理流程
|
||||
岗位入库后,单独调用AI匹配专业要求。
|
||||
|
||||
1. **valid=false** → 更新 `clean_status=3`,结束
|
||||
2. **公司处理**:按AI清洗后的 `companyShortName` 查 `bg_company.short_name`,存在则拿 `company_id`;不存在则创建一条(short_name=companyShortName, status=0待完善),拿新ID
|
||||
3. **地区处理**:`cities` 列表逐个匹配 `bg_china_regions_code`(按name匹配到市级),匹配上的准备写入关联表
|
||||
4. **去重**:用 `source_id`(app_job_data.id)查 `bg_job`,已存在则跳过,更新 `clean_status=2`
|
||||
5. **写入 bg_job**:组装所有字段,`source_id`=app_job_data.id,`source_url`=detail_url,`status=0`(上架)
|
||||
6. **写入 bg_job_region_relation**:岗位ID + 匹配到的region_code,一岗多地区
|
||||
7. **更新 app_job_data.clean_status=2**
|
||||
#### 输入
|
||||
- 岗位标题、职责、要求
|
||||
- 三级专业分类列表(845条,格式 `id:name(一级/二级)`)
|
||||
|
||||
步骤 2-7 放在一个短事务中,保证数据一致性。
|
||||
|
||||
### 2.5 技能标签匹配(第二次 AI 调用)
|
||||
|
||||
`bg_skill_tag` 是预定义的技能标签池,挂在岗位类型下,用于岗位-简历匹配度计算。与 `bg_job.skill_tags`(自由文本,展示用)是两套东西。
|
||||
|
||||
#### 关联表
|
||||
|
||||
```sql
|
||||
CREATE TABLE bg_job_skill_tag_relation (
|
||||
id BIGINT NOT NULL,
|
||||
job_id BIGINT NOT NULL COMMENT '岗位ID',
|
||||
skill_tag_id BIGINT NOT NULL COMMENT '技能标签ID',
|
||||
create_time DATETIME NOT NULL COMMENT '创建时间',
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_job_id (job_id),
|
||||
INDEX idx_skill_tag_id (skill_tag_id),
|
||||
UNIQUE INDEX uk_job_skill (job_id, skill_tag_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='岗位-技能标签关联表';
|
||||
#### 返回JSON
|
||||
```json
|
||||
{
|
||||
"requiredMajorIds": [101, 203],
|
||||
"majorSensitivity": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 流程
|
||||
#### 规则
|
||||
- requiredMajorIds:从专业列表中选最相关的,最多3个,无明确要求则空数组
|
||||
- majorSensitivity:0=不限 1=优先 2=强制
|
||||
- majorSensitivity=0 时,requiredMajorIds 应为空数组
|
||||
- 结果更新到 bg_job 的 required_major_ids 和 major_sensitivity 字段
|
||||
|
||||
1. 第一次 AI 调用完成后,拿到 `categoryId`
|
||||
2. 从缓存取该 `categoryId` 下的 skillTag 列表,为空则跳过
|
||||
3. 发起第二次 AI 调用:传岗位标题+职责+要求 + 该分类下的标签列表(`id:name`),AI 返回匹配的标签 ID 数组
|
||||
4. 校验返回的 ID 确实存在于该 categoryId 的标签池中(防幻觉)
|
||||
5. 写入 `bg_job_skill_tag_relation`(在 saveJobData 事务中一并写入)
|
||||
6. 第二次 AI 调用失败不影响岗位入库,仅日志记录
|
||||
### 2.5 第三次AI:技能提取
|
||||
|
||||
自由提取岗位核心技能,不再依赖预定义标签库。
|
||||
|
||||
#### 输入
|
||||
- 岗位标题、职责、要求
|
||||
|
||||
#### 返回JSON
|
||||
```json
|
||||
["java", "spring boot", "mysql", "redis"]
|
||||
```
|
||||
|
||||
#### prompt 规则
|
||||
- 统一小写字母
|
||||
- 尽量简短,使用业界通用缩写
|
||||
- 提取范围:技术栈、专业领域知识、行业工具、专业资质能力
|
||||
- 不提取纯软技能(沟通、协作、学习能力等)
|
||||
- 无专业能力要求的岗位(销售、行政等)返回空数组
|
||||
- 最多15个
|
||||
|
||||
- 只能从给定列表中选择,不允许自创
|
||||
- 不限制个数,不重复即可
|
||||
- 只返回 ID 数组,如 `[1, 3, 7]`
|
||||
#### 技能入库流程
|
||||
1. 遍历AI返回的技能名,统一转小写
|
||||
2. `INSERT IGNORE INTO bg_skill_tag`(依靠 name 唯一索引去重,ID 由 IdWorker 生成)
|
||||
3. `SELECT id FROM bg_skill_tag WHERE name=?` 拿到ID
|
||||
4. 写入 bg_job_skill_tag_relation
|
||||
|
||||
### 2.6 设计决策记录
|
||||
#### 并发安全
|
||||
依靠数据库唯一索引保证,不加应用层锁。多线程同时插入相同技能名时,INSERT IGNORE 自动忽略重复。
|
||||
|
||||
| 决策点 | 结论 | 原因 |
|
||||
|--------|------|------|
|
||||
| AI调用次数 | 一次调用返回全部字段 | 减少API调用成本和延迟 |
|
||||
| 分类/行业列表怎么给AI | 直接传 id:name 文本 | ID人工维护为短数字,token消耗可控 |
|
||||
| 地区匹配方式 | AI输出城市名,Java侧匹配 | 城市名无歧义,不需要传参考列表 |
|
||||
| categoryId 是否可空 | 不可空,必须选一个 | 岗位分类是核心维度 |
|
||||
| requiredIndustryId 何时设置 | 仅描述中明确提到行业经验要求时 | 行业经验是任职要求,不是所有岗位都有 |
|
||||
| tags 定位 | 核心职能标签,最多5个 | 区别于福利标签,体现岗位核心能力要求 |
|
||||
| skillTags 数量 | 最多8个 | 控制数量,保持精炼 |
|
||||
| source_id 取值 | app_job_data.id | 简单直接,用于去重 |
|
||||
| 公司不存在时 | 自动创建 status=0 待完善 | 后续由公司数据补充逻辑完善 |
|
||||
| skillTag 存储方式 | 独立关联表 bg_job_skill_tag_relation | 百万级数据量,JSON查询性能差 |
|
||||
| skillTag 匹配时机 | 第二次 AI 调用,岗位入库后 | 需要先确定 categoryId 才能缩小标签范围 |
|
||||
| skillTag 与 skill_tags 的关系 | 两套独立数据 | skill_tags 是展示用自由文本,skillTag 是计算用预定义标签 |
|
||||
| 第二次 AI 失败是否影响入库 | 不影响 | 标签匹配是增强功能,不阻断主流程 |
|
||||
### 2.6 容错设计
|
||||
- 第二次、第三次AI调用失败不影响岗位入库
|
||||
- 每次调用独立 try-catch,仅日志记录
|
||||
|
||||
---
|
||||
|
||||
## 三、公司数据触发逻辑
|
||||
|
||||
### 3.1 状态扩展
|
||||
### 3.1 状态
|
||||
`bg_company.status`:0=待完善 1=已完善 2=禁用 3=补充中 4=补充失败
|
||||
|
||||
`bg_company.status` 扩展为5个值:
|
||||
- 0=待完善:岗位清洗时自动创建的公司,只有 short_name
|
||||
- 1=已完善:AI补充完成
|
||||
- 2=禁用:人工标记禁用
|
||||
- 3=补充中:定时任务已锁定,正在调用AI
|
||||
- 4=补充失败:AI明确不认识该公司,不再自动重试
|
||||
|
||||
### 3.2 两个定时任务(与岗位清洗同一套模式)
|
||||
|
||||
#### 任务C:公司数据补充任务(低频,每小时)
|
||||
|
||||
1. 批量锁定(事务内 `SELECT ... FOR UPDATE` + `UPDATE`):
|
||||
```sql
|
||||
-- 事务内执行,行锁保证并发安全
|
||||
SELECT * FROM bg_company WHERE status=0 LIMIT N FOR UPDATE;
|
||||
UPDATE bg_company SET status=3, update_time=NOW() WHERE id IN (...);
|
||||
```
|
||||
⚠️ 锁定时必须同时更新 `update_time`,因为 `bg_company` 的 `update_time` 不像 `app_job_data.updated_at` 那样由数据库自动维护,需要 Java 侧手动设值。如果不更新,后续僵尸恢复任务无法正确判断超时。
|
||||
|
||||
2. 将锁定的数据丢入线程池,多线程并发调用AI补充
|
||||
3. 每条处理完毕后,回填公司信息,更新 `status=1`(已完善)
|
||||
4. AI明确不认识该公司(valid=false)→ 更新 `status=4`(补充失败)
|
||||
5. AI调用异常或解析失败 → 保持 `status=3`,由僵尸恢复任务重置
|
||||
|
||||
#### 任务D:公司僵尸恢复任务(低频,每小时,与任务C错开)
|
||||
|
||||
处理因发布重启导致卡在"补充中"的僵尸数据:
|
||||
|
||||
```sql
|
||||
UPDATE bg_company SET status=0 WHERE status=3 AND update_time < NOW() - INTERVAL 10 MINUTE
|
||||
```
|
||||
|
||||
超时10分钟仍在"补充中"的数据重置为待完善,下次任务C会重新捞取处理。
|
||||
|
||||
### 3.3 与岗位清洗触发逻辑的对比
|
||||
|
||||
| 对比项 | 岗位清洗 | 公司补充 |
|
||||
|--------|----------|----------|
|
||||
| 状态字段 | app_job_data.clean_status | bg_company.status |
|
||||
| 锁定值 | 1=清洗中 | 3=补充中 |
|
||||
| 完成值 | 2=已入库 / 3=已丢弃 | 1=已完善 / 4=补充失败 |
|
||||
| 时间字段 | updated_at(数据库自动) | update_time(Java手动设值) |
|
||||
| 锁定时是否需手动更新时间 | 不需要 | **需要**,否则僵尸恢复无法判断超时 |
|
||||
| 触发频率 | 每5分钟 | 每小时 |
|
||||
| 僵尸恢复频率 | 每30分钟 | 每小时(与任务C错开) |
|
||||
|
||||
### 3.4 设计决策记录
|
||||
|
||||
| 决策点 | 结论 | 原因 |
|
||||
|--------|------|------|
|
||||
| 是否与岗位清洗同步触发 | 否,独立定时任务 | AI调用场景不同,频率不同 |
|
||||
| 触发模式 | 复用岗位清洗的"SELECT FOR UPDATE + 僵尸恢复"模式 | 统一架构,行锁保证并发安全 |
|
||||
| 锁定时是否更新时间 | 是 | bg_company.update_time 非数据库自动维护,不更新则僵尸恢复失效 |
|
||||
| 补充频率 | 每小时 | 公司数据量少,AI调用成本可控 |
|
||||
### 3.2 定时任务
|
||||
- 任务C:公司补充(每小时),SELECT FOR UPDATE 锁定 status=0 的数据
|
||||
- 任务D:僵尸恢复(每小时,与C错开),重置超时10分钟的 status=3 数据
|
||||
|
||||
---
|
||||
|
||||
## 四、公司数据补充逻辑(AI补充)
|
||||
|
||||
### 4.1 方案说明
|
||||
|
||||
原计划使用工商API(天眼查等)查询公司信息,但考虑到:
|
||||
- 公司简称不能很好地查询到精确数据
|
||||
- 用AI生成完整企业名字再查API,准确性也无法保证
|
||||
- 我们的公司数据来源于招聘平台,基本都是有一定规模的企业,AI训练数据覆盖率高
|
||||
|
||||
因此改为直接使用AI补充公司数据,一次调用返回全部字段。
|
||||
|
||||
### 4.2 补充流程
|
||||
|
||||
1. 拿 `short_name`(公司简称),拼 prompt 调 AI
|
||||
2. prompt 中附带行业列表(`DictCacheService.getIndustryText()`),让 AI 直接返回 `industryId`
|
||||
3. AI 返回结构化 JSON,包含 `valid` 字段判断是否认识该公司
|
||||
4. `valid=false` → 更新 `status=4`(补充失败),结束
|
||||
5. `regionCode`:AI 返回城市名,Java 侧 `DictCacheService.matchRegionCode` 匹配
|
||||
6. 解析 JSON,回填 `bg_company` 各字段
|
||||
7. 更新 `status=1`(已完善)
|
||||
|
||||
### 4.3 AI 返回 JSON 结构
|
||||
### 4.1 流程
|
||||
1. 拿 short_name 调AI
|
||||
2. prompt 附带行业列表,AI直接返回 industryId
|
||||
3. valid=false → status=4(补充失败)
|
||||
4. regionCode:AI返回城市名,Java侧匹配
|
||||
5. 回填 bg_company 各字段,status=1
|
||||
|
||||
### 4.2 AI返回JSON
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
@@ -308,61 +176,27 @@ UPDATE bg_company SET status=0 WHERE status=3 AND update_time < NOW() - INTERVAL
|
||||
"city": "北京",
|
||||
"companyType": "独角兽",
|
||||
"industryId": 5,
|
||||
"tags": ["短视频", "人工智能", "社交平台"],
|
||||
"summary": "全球领先的内容平台和科技公司,旗下拥有抖音、今日头条等产品",
|
||||
"description": "字节跳动成立于2012年,是一家以技术驱动的全球化互联网公司...",
|
||||
"tags": ["短视频", "人工智能"],
|
||||
"summary": "全球领先的内容平台和科技公司",
|
||||
"description": "字节跳动成立于2012年...",
|
||||
"foundedYear": "2012",
|
||||
"address": "北京市海淀区北三环西路27号",
|
||||
"address": "北京市海淀区...",
|
||||
"scale": "10000人以上",
|
||||
"website": "https://www.bytedance.com",
|
||||
"financingStage": "已上市",
|
||||
"latestValuation": "2200亿美元",
|
||||
"news": [
|
||||
"字节跳动2024年营收突破1200亿美元创历史新高",
|
||||
"TikTok全球月活用户突破15亿大关",
|
||||
"字节跳动加大AI大模型研发投入布局人工智能赛道"
|
||||
]
|
||||
"news": ["新闻1", "新闻2", "新闻3"]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 各字段补充规则
|
||||
---
|
||||
|
||||
| bg_company 字段 | AI返回字段 | 规则 |
|
||||
|-----------------|-----------|------|
|
||||
| name | name | 公司全称,AI不确定则null |
|
||||
| logoUrl | — | AI无法提供,留空 |
|
||||
| regionCode | city | AI返回城市名,Java侧matchRegionCode匹配 |
|
||||
| companyType | companyType | 上市企业、独角兽、国企、民营企业、外资企业等 |
|
||||
| industryId | industryId | 从行业列表中选,不确定则null |
|
||||
| tags | tags | 公司标签,JSON数组,最多5个 |
|
||||
| summary | summary | 一句话简介,100字以内 |
|
||||
| description | description | 公司详细描述,500字以内 |
|
||||
| foundedYear | foundedYear | 成立年份,如"2012" |
|
||||
| address | address | 注册/总部地址 |
|
||||
| scale | scale | 企业规模,如"1000-5000人"、"10000人以上" |
|
||||
| website | website | 官网地址 |
|
||||
| financingStage | financingStage | 融资状态,如"A轮"、"已上市"、"不需要融资" |
|
||||
| latestValuation | latestValuation | 最新估值,AI知道就给,不知道null |
|
||||
| news | news | 3条相关新闻,每条50字以内,JSON数组 |
|
||||
|
||||
### 4.5 prompt 规则
|
||||
|
||||
- AI 不认识该公司 → `valid=false`,其他字段不需要返回
|
||||
- 不确定的字段返回 null,不要编造
|
||||
- `industryId` 必须从给定行业列表中选择
|
||||
- `news` 最多3条,每条50字以内,基于AI知识库中最新的信息
|
||||
- `tags` 最多5个,体现公司核心业务特征
|
||||
- 字符串值中不允许出现Tab、换行等控制字符
|
||||
- 只返回JSON,不要其他内容
|
||||
|
||||
### 4.6 设计决策记录
|
||||
## 五、关键设计决策
|
||||
|
||||
| 决策点 | 结论 | 原因 |
|
||||
|--------|------|------|
|
||||
| 数据来源 | AI补充,不用工商API | 公司简称查API不精确,AI对招聘平台企业覆盖率高 |
|
||||
| AI不认识的公司 | status=4(补充失败),不再自动重试 | 避免无限重试浪费AI调用 |
|
||||
| logoUrl | 留空 | AI无法提供图片URL |
|
||||
| news 时效性 | 不要求实时,取AI知识库内最新的3条 | 求职场景不需要实时新闻 |
|
||||
| latestValuation | AI知道就给,不知道null | 大部分招聘企业AI有数据 |
|
||||
| regionCode 匹配方式 | AI返回城市名,Java侧匹配 | 复用已有的matchRegionCode逻辑 |
|
||||
| industryId 匹配方式 | prompt带行业列表,AI直接返回ID | 复用已有的行业列表文本 |
|
||||
| 技能标签来源 | AI自由提取,自动入库 bg_skill_tag | 去掉预定义标签限制,覆盖面更广 |
|
||||
| 技能并发去重 | INSERT IGNORE + 唯一索引 | 不加应用层锁,性能好 |
|
||||
| 专业匹配 | 单独一次AI调用 | 专业列表845条,和第一次prompt合并会超token |
|
||||
| AI调用次数 | 三次(结构化 + 专业 + 技能) | 各维度独立,容错互不影响 |
|
||||
| 公司数据来源 | AI补充,不用工商API | 公司简称查API不精确,AI覆盖率高 |
|
||||
|
||||
+40
-64
@@ -86,7 +86,8 @@ offerpie/back-end
|
||||
│ ├─ JobRegionRelationMapper.java # 岗位-地区关联Mapper
|
||||
│ ├─ JobSkillTagRelationMapper.java # 岗位-技能标签关联Mapper
|
||||
│ ├─ IndustryMapper.java # 行业Mapper
|
||||
│ ├─ SkillTagMapper.java # 技能标签Mapper
|
||||
│ ├─ SkillTagMapper.java # 技能标签Mapper(含insertIgnore自定义SQL)
|
||||
│ ├─ MajorCategoryMapper.java # 专业分类Mapper
|
||||
│ ├─ UserJobFavoriteMapper.java # 用户收藏岗位Mapper
|
||||
│ ├─ UserJobApplicationMapper.java # 用户投递记录Mapper
|
||||
│ ├─ UserJobDislikeMapper.java # 用户不感兴趣记录Mapper
|
||||
@@ -116,7 +117,8 @@ offerpie/back-end
|
||||
│ │ ├─ JobRegionRelation.java # 岗位-地区关联表(bg_job_region_relation)
|
||||
│ │ ├─ JobSkillTagRelation.java # 岗位-技能标签关联表(bg_job_skill_tag_relation)
|
||||
│ │ ├─ Industry.java # 行业字典表(bg_industry)
|
||||
│ │ ├─ SkillTag.java # 技能标签表(bg_skill_tag)
|
||||
│ │ ├─ SkillTag.java # 技能标签表(bg_skill_tag),AI自动入库,name唯一索引
|
||||
│ │ ├─ MajorCategory.java # 专业分类表(bg_major_category),三级树形结构
|
||||
│ │ ├─ UserJobFavorite.java # 用户收藏岗位表(bg_user_job_favorite)
|
||||
│ │ ├─ UserJobApplication.java # 用户投递记录表(bg_user_job_application)
|
||||
│ │ ├─ UserJobDislike.java # 用户不感兴趣记录表(bg_user_job_dislike)
|
||||
@@ -129,10 +131,10 @@ offerpie/back-end
|
||||
│ │ ├─ UserProfileCompetition.java # 用户竞赛经历表(bg_user_profile_competition)
|
||||
│ │ ├─ UserProfileSkillTagRelation.java # 用户技能标签关联表(bg_user_profile_skill_tag_relation)
|
||||
│ │ └─ AppJobData.java # 爬虫岗位原始数据表(app_job_data)
|
||||
│ └─ vo/ # ViewObject(OssUrlVo、DescriptionParagraph、JobListItemVo 等)
|
||||
│ └─ vo/ # ViewObject(OssUrlVo、DescriptionParagraph、JobListItemVo、UserHonorsVo 等)
|
||||
├─ resources/mapper/ # MyBatis XML 映射文件
|
||||
│ └─ JobMapper.xml # 岗位自定义SQL(selectJobPage)
|
||||
└─ service/ # 业务 Service(OssService、SmsService、DictCacheService、JobCleanService、JobCleanTransactionService、CompanyCleanService、CompanyCleanTransactionService、UserSkillTagMatchService、JobMatchService 等)
|
||||
└─ service/ # 业务 Service(OssService、SmsService、DictCacheService、JobCleanService、JobCleanTransactionService、CompanyCleanService、CompanyCleanTransactionService、UserProfileAnalyzeService、JobMatchService 等)
|
||||
```
|
||||
> **设计理念** – 业务实体和 Mapper 位于 `manager`,B 端和 C 端共享;C 端特有的注解、切面、权限服务、路由菜单服务位于 `client-api`,避免 B 端误用;`common` 提供统一的技术支撑。
|
||||
|
||||
@@ -160,21 +162,22 @@ offerpie/back-end
|
||||
| `ChinaRegionsCode` | manager | 地区表(bg_china_regions_code),行政区划编码,省/市/区三级,code为主键。 |
|
||||
| `JobCategory` | manager | 岗位类型表(bg_job_category),树形结构,一级/二级分类。 |
|
||||
| `Company` | manager | 公司表(bg_company),公司基本信息、融资、规模、新闻动态等。 |
|
||||
| `Job` | manager | 岗位表(bg_job),岗位信息、薪资、标签、技能标签,关联公司和岗位类型。 |
|
||||
| `Job` | manager | 岗位表(bg_job),岗位信息、薪资、标签、技能标签、要求专业、专业敏感度,关联公司和岗位类型。 |
|
||||
| `JobRegionRelation` | manager | 岗位-地区关联表(bg_job_region_relation),一个岗位可关联多个地区。 |
|
||||
| `UserJobFavorite` | manager | 用户收藏岗位表(bg_user_job_favorite),用户与岗位唯一约束。 |
|
||||
| `UserJobApplication` | manager | 用户投递记录表(bg_user_job_application),记录投递状态流转。 |
|
||||
| `Industry` | manager | 行业字典表(bg_industry),树形结构,一级/二级分类。 |
|
||||
| `SkillTag` | manager | 技能标签表(bg_skill_tag),挂在岗位类型下,不分级,用于匹配度计算。 |
|
||||
| `SkillTag` | manager | 技能标签表(bg_skill_tag),AI自动入库,name唯一索引,用于匹配度计算。 |
|
||||
| `MajorCategory` | manager | 专业分类表(bg_major_category),基于教育部专业目录,三级树形结构,用于专业匹配。 |
|
||||
| `UserJobDislike` | manager | 用户不感兴趣记录表(bg_user_job_dislike),记录用户对岗位的不感兴趣原因,冗余公司ID/地区编码/行业ID方便推荐过滤。 |
|
||||
| `UserJobIntention` | manager | 用户求职意向表(bg_user_job_intention),存储意向岗位类型、城市、行业(JSON数组,节点可能为任意级别)和工作类型。 |
|
||||
| `UserProfile` | manager | 用户个人资料表(bg_user_profile),存储身份证号、所在城市、微信号、作品集链接、工作年限、经验行业IDs、技能标签、证书标签。 |
|
||||
| `UserProfile` | manager | 用户个人资料表(bg_user_profile),存储身份证号、所在城市、微信号、作品集链接、专业IDs、学校等级、公司背书、经历时长、职责深度、量化产出、荣誉、技能标签、证书标签。 |
|
||||
| `UserProfileEducation` | manager | 用户教育经历表(bg_user_profile_education,profile子表),学校、专业、学历、起止年份、描述段落(JSON对象数组)。 |
|
||||
| `UserProfileWork` | manager | 用户工作经历表(bg_user_profile_work,profile子表),公司、职位、起止时间、描述段落(JSON对象数组)。 |
|
||||
| `UserProfileInternship` | manager | 用户实习经历表(bg_user_profile_internship,profile子表),公司、职位、起止时间、描述段落(JSON对象数组)。 |
|
||||
| `UserProfileProject` | manager | 用户项目经历表(bg_user_profile_project,profile子表),公司、项目名、角色、起止时间、描述段落(JSON对象数组)。 |
|
||||
| `UserProfileCompetition` | manager | 用户竞赛经历表(bg_user_profile_competition,profile子表),竞赛名、奖项、获奖时间、描述段落(JSON对象数组)。 |
|
||||
| `JobSkillTagRelation` | manager | 岗位-技能标签关联表(bg_job_skill_tag_relation),预定义技能标签与岗位的关联,用于匹配度计算。 |
|
||||
| `JobSkillTagRelation` | manager | 岗位-技能标签关联表(bg_job_skill_tag_relation),AI提取的技能标签与岗位的关联,用于匹配度计算。 |
|
||||
| `UserProfileSkillTagRelation` | manager | 用户技能标签关联表(bg_user_profile_skill_tag_relation),记录用户匹配的技能标签,由AI自动识别生成。 |
|
||||
| `AppJobData` | manager | 爬虫岗位原始数据表(app_job_data),存储爬虫抓取的原始岗位数据,供清洗服务读取并写入业务表。 |
|
||||
|
||||
@@ -241,79 +244,52 @@ offerpie/back-end
|
||||
### 整体架构
|
||||
- **触发时机**:用户访问岗位列表接口(支持筛选条件:地区/岗位类型/行业/工作类型)
|
||||
- **核心流程**:扩展层级筛选 → 查询不感兴趣记录 → 扩展排除列表 → SQL分页查询 → 查询收藏状态 → 批量计算匹配度 → 组装返回
|
||||
- **匹配度计算**:行业30% + 技能40% + 经验20%,在Java层批量计算(避免SQL性能问题)
|
||||
|
||||
### 数据库表(使用)
|
||||
| 表名 | 用途 |
|
||||
|------|------|
|
||||
| `bg_job` | 岗位主表(查询岗位列表) |
|
||||
| `bg_company` | 公司表(关联查询公司信息) |
|
||||
| `bg_china_regions_code` | 地区表(关联查询地区名称、扩展子级) |
|
||||
| `bg_job_category` | 岗位类型表(关联查询类型名称、扩展子级) |
|
||||
| `bg_industry` | 行业表(扩展子级、判断父级匹配) |
|
||||
| `bg_user_job_dislike` | 不感兴趣记录(排除岗位/公司/地区/行业) |
|
||||
| `bg_user_job_favorite` | 收藏记录(查询收藏状态) |
|
||||
| `bg_user_profile` | 用户简历(匹配度计算) |
|
||||
| `bg_job_skill_tag_relation` | 岗位技能标签(技能匹配) |
|
||||
| `bg_user_profile_skill_tag_relation` | 用户技能标签(技能匹配) |
|
||||
- **匹配度计算**:教育30% + 经历40% + 技能30%,在Java层批量计算
|
||||
|
||||
### 匹配度计算规则
|
||||
#### 行业匹配(30分)
|
||||
- 岗位无要求(required_industry_id=null)→ 30分
|
||||
- 用户无简历(profile不存在或experienceIndustryIds为空)→ 0分
|
||||
- 完全匹配(用户行业包含岗位行业)→ 30分
|
||||
- 父级匹配(用户行业与岗位行业有相同父级)→ 22分
|
||||
- 不匹配 → 0分
|
||||
|
||||
#### 技能匹配(40分)
|
||||
- 岗位无要求(无技能标签关联)→ 40分
|
||||
- 用户无技能(无技能标签关联)→ 0分
|
||||
- 匹配公式:`(匹配数量 / 岗位要求数量) * 40`
|
||||
总分公式:`MatchScore = Sedu × 30% + Sexp × 40% + Sskill × 30%`
|
||||
|
||||
#### 经验匹配(20分)
|
||||
- 岗位无要求(min_experience=0)→ 20分
|
||||
- 用户无简历(profile不存在或workYears=null)→ 0分
|
||||
- 计算公式:`min((workYears - minExp) / minExp * 0.8 + 0.2, 1.0) * 20`
|
||||
#### 教育维度(Sedu)
|
||||
- 学校等级分(Srank):1→100, 2→80, 3→60, 4→40
|
||||
- 专业相关度(Smajor):通过 bg_major_category 树形匹配,完全匹配→100,同二级→70,同一级→30,不相关→0
|
||||
- 根据 majorSensitivity 决定权重:强制(学校0.4+专业0.6,专业<40则总分减半)、优先(学校0.6+专业0.4)、不限(仅学校分)
|
||||
|
||||
### 排除逻辑
|
||||
- **排除岗位**:直接排除 `jobId`
|
||||
- **排除公司**:直接排除 `companyId`
|
||||
- **排除地区**:排除 `regionCode` + 其所有子级地区
|
||||
- **排除行业**:排除 `requiredIndustryId` + 其所有子级行业
|
||||
#### 经历维度(Sexp)
|
||||
- `Sexp = 公司背书×30% + 时长×10% + 经历深度×60%`
|
||||
- 公司背书:1→100, 2→60, 3→30, 4→0
|
||||
- 经历时长:1→100, 2→60, 3→30
|
||||
- 经历深度 = 职责深度×40% + 量化产出×30% + 荣誉×30%
|
||||
- 职责深度:1→100, 2→80, 3→40
|
||||
- 量化产出:1→100, 2→70, 3→40
|
||||
- 荣誉:封顶累加制(national每项20、provincial每项10、school每项5、paper每项15),总分不超过100
|
||||
|
||||
#### 技能维度(Sskill)
|
||||
- 岗位无要求 → 100分
|
||||
- 用户无技能 → 0分
|
||||
- 匹配公式:`(匹配数量 / 岗位要求数量) × 100`
|
||||
|
||||
### 核心服务
|
||||
- **JobService**(client-api):岗位列表查询主流程编排
|
||||
- **JobMatchService**(manager):批量计算匹配度(行业/技能/经验三个维度)
|
||||
- **JobMatchService**(manager):批量计算匹配度(教育/经历/技能三个维度)
|
||||
|
||||
### 接口定义
|
||||
- **路径**:`POST /job/list`
|
||||
- **入参**:`JobQueryParam`(分页 + 地区/岗位类型/行业/工作类型筛选)
|
||||
- **出参**:`PageResult<JobDto>`(岗位列表 + 匹配度 + 收藏状态)
|
||||
|
||||
## 4.5️⃣ 用户技能标签匹配设计
|
||||
## 4.5️⃣ 用户简历分析设计
|
||||
### 整体架构
|
||||
- **触发时机**:用户保存个人资料(主表或任意子表)后异步触发
|
||||
- **匹配流程**:两次 AI 调用 → 第一次识别用户所属的二级岗位分类(1-10个),第二次从候选技能标签中匹配用户技能
|
||||
- **数据存储**:全量替换 `bg_user_profile_skill_tag_relation` 表
|
||||
|
||||
### 数据库表(1张)
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `bg_user_profile_skill_tag_relation` | 用户技能标签关联(user_id + skill_tag_id 唯一约束) |
|
||||
- **分析流程**:三次 AI 调用 → 第一次综合分析(学校等级、公司背书、经历时长、职责深度、量化产出、荣誉),第二次专业归一化(匹配到 bg_major_category ID),第三次技能提取(自由提取,入 bg_skill_tag)
|
||||
- **数据存储**:维度数据更新 bg_user_profile,技能全量替换 bg_user_profile_skill_tag_relation
|
||||
|
||||
### 核心流程
|
||||
1. 查询用户完整个人资料(主表 + 5张子表)
|
||||
2. 数据有效性检查(主表或所有子表为空 → 清空关联表)
|
||||
3. 第一次 AI 调用:识别用户所属的二级岗位分类(1-10个),信息不足返回空数组
|
||||
4. 查询三级分类:根据二级分类 ID 查询所有三级分类(技能标签挂在三级分类下)
|
||||
5. 查询候选技能标签(WHERE category_id IN 三级分类列表)
|
||||
6. 第二次 AI 调用:从候选标签中匹配用户技能标签
|
||||
7. 全量替换关联表(先 DELETE,结果非空则 BATCH INSERT)
|
||||
3. 第一次 AI 调用:综合分析(schoolRank、companyPrestige、experienceDuration、roleDepth、outputQuality、honors)→ 更新 bg_user_profile
|
||||
4. 第二次 AI 调用:专业归一化(传三级专业分类列表)→ 更新 bg_user_profile.major_ids
|
||||
5. 第三次 AI 调用:技能提取(自由提取,prompt与岗位侧一致)→ INSERT IGNORE 入 bg_skill_tag → 全量替换 bg_user_profile_skill_tag_relation
|
||||
|
||||
### 异步执行
|
||||
- 使用 Spring `@Async` 注解,配置独立线程池(核心5线程,最大10线程,队列200)
|
||||
- 异常统一记录日志,不影响主流程
|
||||
- AI 调用失败不修改关联表(保持原状)
|
||||
- 使用 Spring `@Async` 注解,配置独立线程池
|
||||
- 每次 AI 调用独立容错,某一次失败不影响其他维度
|
||||
- 核心服务:**UserProfileAnalyzeService**(manager)
|
||||
|
||||
## 4.6️⃣ 邀请模块设计
|
||||
### 数据库表(1张)
|
||||
|
||||
@@ -5,7 +5,6 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jiayunet.pojo.po.SkillTag;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* 技能标签Mapper
|
||||
@@ -19,6 +18,6 @@ public interface SkillTagMapper extends CommonMapper<SkillTag> {
|
||||
* INSERT IGNORE:依靠 name 唯一索引去重,并发安全
|
||||
* <p>id 由调用方传入(雪花算法生成),重复 name 时忽略插入</p>
|
||||
*/
|
||||
@Insert("INSERT IGNORE INTO bg_skill_tag (id, name, create_time) VALUES (#{id}, #{name}, #{createTime})")
|
||||
int insertIgnore(@Param("id") Long id, @Param("name") String name, @Param("createTime") Instant createTime);
|
||||
@Insert("INSERT IGNORE INTO bg_skill_tag (id, name) VALUES (#{id}, #{name})")
|
||||
int insertIgnore(@Param("id") Long id, @Param("name") String name);
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ public class UserProfileAnalyzeService {
|
||||
* 查找或创建技能标签(依靠数据库唯一索引保证并发安全)
|
||||
*/
|
||||
private Long findOrCreateSkillTag(String name) {
|
||||
skillTagMapper.insertIgnore(IdWorker.getId(), name, Instant.now());
|
||||
skillTagMapper.insertIgnore(IdWorker.getId(), name);
|
||||
SkillTag tag = skillTagMapper.selectOne(new LambdaQueryWrapper<SkillTag>().eq(SkillTag::getName, name).last("LIMIT 1"));
|
||||
return tag != null ? tag.getId() : null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user