--- 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 ```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 ```json { "requiredMajorIds": [101, 203], "majorSensitivity": 1 } ``` #### 规则 - requiredMajorIds:从专业列表中选最相关的,最多3个,无明确要求则空数组 - majorSensitivity:0=不限 1=优先 2=强制 - majorSensitivity=0 时,requiredMajorIds 应为空数组 - 结果更新到 bg_job 的 required_major_ids 和 major_sensitivity 字段 ### 2.5 第三次AI:技能提取 自由提取岗位核心技能,不再依赖预定义标签库。 #### 输入 - 岗位标题、职责、要求 #### 返回JSON ```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. regionCode:AI返回城市名,Java侧匹配 5. 回填 bg_company 各字段,status=1 ### 4.2 AI返回JSON ```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覆盖率高 |