Files
offerpai_backend/.kiro/steering/数据清洗方案.md
T
2026-03-26 20:14:45 +08:00

5.9 KiB
Raw Blame History

inclusion
inclusion
manual

数据清洗方案

总体架构

爬虫(公司网络) → app_job_data(原始数据)
                      ↓
              Java定时任务读取(多线程)
                      ↓
            三次AI调用:结构化 → 专业匹配 → 技能提取
                      ↓
         写入业务表(bg_company + bg_job + 关联表 + bg_skill_tag)
                      ↓
            公司信息不完整的 → 调AI补充

所有清洗逻辑放在 manager 模块,通过 @Scheduled 定时任务触发。


一、岗位清洗触发逻辑

1.1 状态管理

app_job_data.clean_status

  • 0=待清洗:新爬到的数据,默认值
  • 1=清洗中:定时任务已锁定,正在处理
  • 2=已入库:清洗成功,已写入 bg_job
  • 3=已丢弃:AI判定为无效数据

1.2 两个定时任务

任务A:岗位清洗(每5分钟)

  1. 事务内 SELECT FOR UPDATE + UPDATE clean_status=1 批量锁定
  2. 丢入线程池,多线程并发处理
  3. 每条独立更新状态

任务B:僵尸恢复(每30分钟)

将超时10分钟仍在清洗中的数据重置为待清洗。


二、岗位清洗逻辑(三次AI调用)

2.1 前置校验

  • description 为空或长度 < 20 → 标记丢弃,跳过

2.2 参考数据缓存(DictCacheService

启动时加载:

  • bg_job_category 叶子节点(level=3),格式 id:name(一级/二级)
  • bg_industry 叶子节点(level=2),格式 id:name(一级)
  • bg_major_category 叶子节点(level=3),格式 id:name(一级/二级)
  • bg_china_regions_code 省市级,供城市名匹配

2.3 第一次AI:提取岗位结构化信息

输入

  • 原始字段:job_title、salary、location、company、experience、education、description
  • 参考列表:岗位分类、行业

返回JSON

{
  "valid": true,
  "title": "Java高级开发工程师",
  "salary": "15-25K",
  "education": 2,
  "minExperience": 3,
  "employmentType": 0,
  "categoryId": 12,
  "requiredIndustryId": 5,
  "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

2.4 第二次AI:专业匹配

岗位入库后,单独调用AI匹配专业要求。

输入

  • 岗位标题、职责、要求
  • 三级专业分类列表(845条,格式 id:name(一级/二级)

返回JSON

{
  "requiredMajorIds": [101, 203],
  "majorSensitivity": 1
}

规则

  • requiredMajorIds:从专业列表中选最相关的,最多3个,无明确要求则空数组
  • majorSensitivity0=不限 1=优先 2=强制
  • majorSensitivity=0 时,requiredMajorIds 应为空数组
  • 结果更新到 bg_job 的 required_major_ids 和 major_sensitivity 字段

2.5 第三次AI:技能提取

自由提取岗位核心技能,不再依赖预定义标签库。

输入

  • 岗位标题、职责、要求

返回JSON

["java", "spring boot", "mysql", "redis"]

prompt 规则

  • 统一小写字母
  • 尽量简短,使用业界通用缩写
  • 提取范围:技术栈、专业领域知识、行业工具、专业资质能力
  • 不提取纯软技能(沟通、协作、学习能力等)
  • 无专业能力要求的岗位(销售、行政等)返回空数组
  • 最多15个

技能入库流程

  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

并发安全

依靠数据库唯一索引保证,不加应用层锁。多线程同时插入相同技能名时,INSERT IGNORE 自动忽略重复。

2.6 容错设计

  • 第二次、第三次AI调用失败不影响岗位入库
  • 每次调用独立 try-catch,仅日志记录

三、公司数据触发逻辑

3.1 状态

bg_company.status:0=待完善 1=已完善 2=禁用 3=补充中 4=补充失败

3.2 定时任务

  • 任务C:公司补充(每小时),SELECT FOR UPDATE 锁定 status=0 的数据
  • 任务D:僵尸恢复(每小时,与C错开),重置超时10分钟的 status=3 数据

四、公司数据补充逻辑(AI补充)

4.1 流程

  1. 拿 short_name 调AI
  2. prompt 附带行业列表,AI直接返回 industryId
  3. valid=false → status=4(补充失败)
  4. regionCodeAI返回城市名,Java侧匹配
  5. 回填 bg_company 各字段,status=1

4.2 AI返回JSON

{
  "valid": true,
  "name": "北京字节跳动科技有限公司",
  "city": "北京",
  "companyType": "独角兽",
  "industryId": 5,
  "tags": ["短视频", "人工智能"],
  "summary": "全球领先的内容平台和科技公司",
  "description": "字节跳动成立于2012年...",
  "foundedYear": "2012",
  "address": "北京市海淀区...",
  "scale": "10000人以上",
  "website": "https://www.bytedance.com",
  "financingStage": "已上市",
  "latestValuation": "2200亿美元",
  "news": ["新闻1", "新闻2", "新闻3"]
}

五、关键设计决策

决策点 结论 原因
技能标签来源 AI自由提取,自动入库 bg_skill_tag 去掉预定义标签限制,覆盖面更广
技能并发去重 INSERT IGNORE + 唯一索引 不加应用层锁,性能好
专业匹配 单独一次AI调用 专业列表845条,和第一次prompt合并会超token
AI调用次数 三次(结构化 + 专业 + 技能) 各维度独立,容错互不影响
公司数据来源 AI补充,不用工商API 公司简称查API不精确,AI覆盖率高