Files
offerpai_backend/.kiro/steering/数据清洗方案.md
T
2026-03-19 10:55:30 +08:00

314 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
inclusion: manual
---
# 数据清洗方案
## 总体架构
```
爬虫(公司网络) → app_job_data(原始数据)
Java定时任务读取(多线程)
调用AI API清洗/结构化
写入业务表(bg_company + bg_job + 关联表)
公司信息不完整的 → 调工商API补充
```
所有清洗逻辑放在 `manager` 模块,通过 `@Scheduled` 定时任务触发。
## 讨论分区
整体方案分为四部分逐步讨论:
1. ✅ 岗位清洗触发逻辑
2. ✅ 岗位清洗逻辑
3. ✅ 公司数据触发逻辑
4. ⬜ 公司数据补充逻辑(待定API后补充)
---
## 一、岗位清洗触发逻辑
### 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=待清洗:新爬到的数据,默认值,不影响爬虫原有插入逻辑
- 1=清洗中:定时任务已锁定,正在处理
- 2=已入库:清洗成功,已写入 bg_job
- 3=已丢弃:AI判定为无效数据,不入库
时间记录:不加额外时间字段,利用已有的 `updated_at`ON UPDATE CURRENT_TIMESTAMP),状态变更时自动更新。
### 1.2 两个定时任务
#### 任务A:岗位清洗任务(高频,每5分钟)
1. 批量锁定:`UPDATE app_job_data SET clean_status=1 WHERE clean_status=0 AND is_valid=1 LIMIT N`(原子操作,防止多线程重复捞取)
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,自动重试 |
---
## 二、岗位清洗逻辑
### 2.1 前置校验(Java侧,不调AI)
- `description` 为空或长度 < 20 → 直接标记 `clean_status=3`(丢弃),跳过,节省AI调用成本
### 2.2 参考数据准备
应用启动时加载并缓存(定期刷新):
- `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)
#### 输入
- 原始字段:job_title、salary、location、company、experience、education、description
- 参考列表:岗位分类(id:name)、行业(id:name
#### AI 返回 JSON 结构
```json
{
"valid": true,
"title": "Java高级开发工程师",
"salary": "15-25K",
"education": 2,
"minExperience": 3,
"employmentType": 0,
"categoryId": 12,
"industryId": 5,
"description": "1. 负责核心业务系统开发...",
"requirement": "1. 本科及以上学历...",
"bonus": "1. 有分布式系统经验优先...",
"tags": ["数据分析", "产品策略", "团队协作"],
"skillTags": ["Java", "Spring Boot", "MySQL"],
"companyShortName": "字节跳动",
"cities": ["北京", "上海"]
}
```
#### 各字段清洗规则
| 字段 | 来源 | 规则 |
|------|------|------|
| 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 |
| industryId | 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 返回后的 Java 处理流程
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**
步骤 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='岗位-技能标签关联表';
```
#### 流程
1. 第一次 AI 调用完成后,拿到 `categoryId`
2. 从缓存取该 `categoryId` 下的 skillTag 列表,为空则跳过
3. 发起第二次 AI 调用:传岗位标题+职责+要求 + 该分类下的标签列表(`id:name`),AI 返回匹配的标签 ID 数组
4. 校验返回的 ID 确实存在于该 categoryId 的标签池中(防幻觉)
5. 写入 `bg_job_skill_tag_relation`(在 saveJobData 事务中一并写入)
6. 第二次 AI 调用失败不影响岗位入库,仅日志记录
#### prompt 规则
- 只能从给定列表中选择,不允许自创
- 不限制个数,不重复即可
- 只返回 ID 数组,如 `[1, 3, 7]`
### 2.6 设计决策记录
| 决策点 | 结论 | 原因 |
|--------|------|------|
| AI调用次数 | 一次调用返回全部字段 | 减少API调用成本和延迟 |
| 分类/行业列表怎么给AI | 直接传 id:name 文本 | ID人工维护为短数字,token消耗可控 |
| 地区匹配方式 | AI输出城市名,Java侧匹配 | 城市名无歧义,不需要传参考列表 |
| categoryId 是否可空 | 不可空,必须选一个 | 岗位分类是核心维度 |
| industryId 何时设置 | 仅描述中明确提到行业经验时 | 行业经验是任职要求,不是所有岗位都有 |
| 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 失败是否影响入库 | 不影响 | 标签匹配是增强功能,不阻断主流程 |
---
## 三、公司数据触发逻辑
### 3.1 状态扩展
`bg_company.status` 扩展为4个值:
- 0=待完善:岗位清洗时自动创建的公司,只有 short_name
- 1=已完善:工商API补充完成
- 2=禁用:人工标记禁用
- 3=补充中:定时任务已锁定,正在调用工商API
### 3.2 两个定时任务(与岗位清洗同一套模式)
#### 任务C:公司数据补充任务(低频,每小时)
1. 批量锁定(原子操作):
```sql
UPDATE bg_company SET status=3, update_time=NOW() WHERE status=0 LIMIT N
```
⚠️ 锁定时必须同时更新 `update_time`,因为 `bg_company``update_time` 不像 `app_job_data.updated_at` 那样由数据库自动维护,需要 Java 侧手动设值。如果不更新,后续僵尸恢复任务无法正确判断超时。
2. 将锁定的数据丢入线程池,多线程并发调用工商API
3. 每条处理完毕后,回填公司信息,更新 `status=1`(已完善)
4. 工商API查不到或返回异常 → 保持 `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=已完善 |
| 时间字段 | updated_at(数据库自动) | update_timeJava手动设值) |
| 锁定时是否需手动更新时间 | 不需要 | **需要**,否则僵尸恢复无法判断超时 |
| 触发频率 | 每5分钟 | 每小时 |
| 僵尸恢复频率 | 每30分钟 | 每小时(与任务C错开) |
### 3.4 设计决策记录
| 决策点 | 结论 | 原因 |
|--------|------|------|
| 是否与岗位清洗同步触发 | 否,独立定时任务 | 外部API不同,频率不同,失败场景不同 |
| 触发模式 | 复用岗位清洗的"原子锁定+僵尸恢复"模式 | 统一架构,代码可复用 |
| 锁定时是否更新时间 | 是 | bg_company.update_time 非数据库自动维护,不更新则僵尸恢复失效 |
| 补充频率 | 每小时 | 公司数据量少,工商API可能有频率限制 |
---
## 四、公司数据补充逻辑(待定API后补充)
### 4.1 补充流程概要
1.`short_name`(公司简称)调用工商API搜索
2. API返回匹配的企业列表,取最匹配的一条
3. 回填 `bg_company` 各字段,更新 `status=1`
### 4.2 需要回填的字段
| bg_company 字段 | 来源 | 说明 |
|-----------------|------|------|
| name | 工商API | 公司全称 |
| logoUrl | 待定 | 工商API可能不提供,需另外来源 |
| regionCode | 工商API(注册地址) | 匹配 bg_china_regions_code |
| companyType | 工商API | 上市企业、独角兽、国企等 |
| industryId | 工商API(行业分类) | 匹配 bg_industry |
| tags | 工商API / AI | 公司标签,JSON数组 |
| summary | 工商API / AI | 公司简要介绍 |
| description | 工商API | 公司详细描述 |
| foundedYear | 工商API | 成立时间 |
| address | 工商API | 注册地址 |
| scale | 工商API | 企业规模(人数) |
| website | 工商API | 官网地址 |
| financingStage | 工商API / 其他来源 | 融资状态 |
| latestValuation | 工商API / 其他来源 | 最新估值 |
| news | 新闻API(待定) | 新闻动态,JSON数组 |
### 4.3 待定事项
- [ ] 选定工商信息API(天眼查、企查查、爱企查等)
- [ ] 确认API返回字段与 bg_company 的映射关系
- [ ] 新闻动态数据来源(工商API是否包含,还是需要单独的新闻API)
- [ ] logoUrl 来源(工商API是否提供)
- [ ] 匹配到多条结果时的处理策略
- [ ] 查不到结果时的处理策略(保持待完善 or 标记为其他状态)
- [ ] API调用频率限制和成本评估