添加公司信息处理逻辑
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
package org.jiayunet.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.jiayunet.pojo.po.AppJobData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 爬虫岗位原始数据Mapper
|
||||
*
|
||||
@@ -10,4 +14,11 @@ import org.jiayunet.pojo.po.AppJobData;
|
||||
*/
|
||||
@Mapper
|
||||
public interface AppJobDataMapper extends CommonMapper<AppJobData> {
|
||||
|
||||
/**
|
||||
* 查询待清洗数据并加行锁(SELECT ... FOR UPDATE)
|
||||
* <p>必须在事务内调用,配合状态更新实现原子锁定</p>
|
||||
*/
|
||||
@Select("SELECT * FROM app_job_data WHERE clean_status = 0 AND is_valid = 1 LIMIT #{limit} FOR UPDATE")
|
||||
List<AppJobData> selectForUpdate(@Param("limit") int limit);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.jiayunet.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.jiayunet.pojo.po.Company;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 公司Mapper
|
||||
*
|
||||
@@ -10,4 +14,11 @@ import org.jiayunet.pojo.po.Company;
|
||||
*/
|
||||
@Mapper
|
||||
public interface CompanyMapper extends CommonMapper<Company> {
|
||||
|
||||
/**
|
||||
* 查询待完善公司并加行锁(SELECT ... FOR UPDATE)
|
||||
* <p>必须在事务内调用,配合状态更新实现原子锁定</p>
|
||||
*/
|
||||
@Select("SELECT * FROM bg_company WHERE status = 0 LIMIT #{limit} FOR UPDATE")
|
||||
List<Company> selectForUpdate(@Param("limit") int limit);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public class Company {
|
||||
/** 新闻动态(JSON数组) */
|
||||
private String news;
|
||||
|
||||
/** 状态 0=待完善 1=已完善 2=禁用 3=补充中 */
|
||||
/** 状态 0=待完善 1=已完善 2=禁用 3=补充中 4=补充失败 */
|
||||
private Integer status;
|
||||
|
||||
/** 创建时间 */
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package org.jiayunet.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jiayunet.ai.AiChatAbility;
|
||||
import org.jiayunet.mapper.CompanyMapper;
|
||||
import org.jiayunet.pojo.po.Company;
|
||||
import org.jiayunet.tool.HttpTool;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* 公司数据补充服务
|
||||
* <p>定时从 bg_company 捞取待完善数据,调用 AI 补充公司信息</p>
|
||||
* <p>依赖:AiChatAbility(AI调用)、DictCacheService(行业列表/地区匹配)、CompanyCleanTransactionService(事务操作)</p>
|
||||
* <p>使用表:bg_company(读取/更新)</p>
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CompanyCleanService {
|
||||
|
||||
@Autowired
|
||||
private AiChatAbility aiChatAbility;
|
||||
|
||||
@Autowired
|
||||
private DictCacheService dictCacheService;
|
||||
|
||||
@Autowired
|
||||
private CompanyCleanTransactionService companyCleanTransactionService;
|
||||
|
||||
@Autowired
|
||||
private CompanyMapper companyMapper;
|
||||
|
||||
@Value("${app.company-clean.batch-size:10}")
|
||||
private int batchSize;
|
||||
|
||||
@Value("${app.company-clean.thread-pool-size:3}")
|
||||
private int threadPoolSize;
|
||||
|
||||
private ExecutorService executorService;
|
||||
|
||||
/** 初始化线程池 */
|
||||
@javax.annotation.PostConstruct
|
||||
public void init() {
|
||||
executorService = Executors.newFixedThreadPool(threadPoolSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务C:公司数据补充(每小时)
|
||||
* <p>1. 批量锁定待完善公司 2. 多线程并发调用AI补充 3. 回填数据</p>
|
||||
*/
|
||||
@Scheduled(cron = "0 */1 * * * ?")
|
||||
public void cleanCompany() {
|
||||
List<Company> companyList = companyCleanTransactionService.lockBatch(batchSize);
|
||||
if (companyList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
log.info("公司补充:锁定{}条数据", companyList.size());
|
||||
|
||||
// 多线程并发处理
|
||||
for (Company company : companyList) {
|
||||
executorService.submit(() -> {
|
||||
try {
|
||||
cleanOne(company);
|
||||
} catch (Exception e) {
|
||||
log.error("公司补充异常, id={}, shortName={}", company.getId(), company.getShortName(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务D:公司僵尸恢复(每小时,与任务C错开30分钟)
|
||||
* <p>将超时10分钟仍在补充中的数据重置为待完善</p>
|
||||
*/
|
||||
@Scheduled(cron = "0 30 */1 * * ?")
|
||||
public void recoverZombie() {
|
||||
int recovered = companyMapper.update(null,
|
||||
new LambdaUpdateWrapper<Company>()
|
||||
.set(Company::getStatus, 0)
|
||||
.eq(Company::getStatus, 3)
|
||||
.lt(Company::getUpdateTime, Instant.now().minusSeconds(600)));
|
||||
|
||||
if (recovered > 0) {
|
||||
log.info("公司僵尸恢复:重置{}条数据", recovered);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 补充单条公司数据
|
||||
* <p>1. 拼prompt调AI 2. 解析结果 3. 回填数据</p>
|
||||
*/
|
||||
public void cleanOne(Company company) {
|
||||
log.info("公司补充开始, id={}, shortName={}", company.getId(), company.getShortName());
|
||||
|
||||
String systemPrompt = buildSystemPrompt();
|
||||
String userMessage = buildUserMessage(company.getShortName());
|
||||
|
||||
String aiResponse = aiChatAbility.chat(systemPrompt, userMessage);
|
||||
|
||||
try {
|
||||
// 去掉可能的 markdown 代码块标记
|
||||
String json = aiResponse.trim();
|
||||
if (json.startsWith("```")) {
|
||||
json = json.replaceAll("^```\\w*\\n?", "").replaceAll("\\n?```$", "").trim();
|
||||
}
|
||||
// 清除控制字符(Tab等),保留换行符
|
||||
json = json.replaceAll("[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]", "");
|
||||
|
||||
JsonNode root = HttpTool.objectMapper.readTree(json);
|
||||
|
||||
// valid 校验:AI不认识该公司
|
||||
if (!root.path("valid").asBoolean(false)) {
|
||||
log.info("公司补充:AI不认识该公司, id={}, shortName={}", company.getId(), company.getShortName());
|
||||
companyCleanTransactionService.updateCompanyStatus(company.getId(), 4);
|
||||
return;
|
||||
}
|
||||
|
||||
// 回填数据
|
||||
companyCleanTransactionService.saveCompanyData(root, company);
|
||||
log.info("公司补充完成, id={}, shortName={}", company.getId(), company.getShortName());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("公司AI返回解析失败, id={}, shortName={}, response={}",
|
||||
company.getId(), company.getShortName(), aiResponse, e);
|
||||
// 保持 status=3,由僵尸恢复任务重置
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建系统提示词 */
|
||||
private String buildSystemPrompt() {
|
||||
return """
|
||||
你是一个企业信息补充助手。根据提供的公司简称,补充该公司的详细信息。
|
||||
|
||||
返回JSON格式要求:
|
||||
{
|
||||
"valid": true/false,
|
||||
"name": "公司全称",
|
||||
"city": "总部所在城市,精确到市",
|
||||
"companyType": "企业类型",
|
||||
"industryId": 行业ID,
|
||||
"tags": ["公司标签,最多5个"],
|
||||
"summary": "一句话简介,100字以内",
|
||||
"description": "公司详细描述,500字以内",
|
||||
"foundedYear": "成立年份",
|
||||
"address": "总部/注册地址",
|
||||
"scale": "企业规模",
|
||||
"website": "官网地址",
|
||||
"financingStage": "融资状态",
|
||||
"latestValuation": "最新估值",
|
||||
"news": ["相关新闻,最多3条,每条50字以内"]
|
||||
}
|
||||
|
||||
规则:
|
||||
1. 如果不认识该公司,返回 {"valid": false}
|
||||
2. name 根据公司简称推断完整的企业注册名称
|
||||
3. companyType 取值:上市企业、独角兽、国企、央企、民营企业、外资企业、合资企业、事业单位、其他
|
||||
4. industryId 必须从给定行业列表中选择,不确定则null
|
||||
5. scale 取值:少于50人、50-150人、150-500人、500-1000人、1000-5000人、5000-10000人、10000人以上
|
||||
6. tags 体现公司核心业务特征,最多5个
|
||||
7. news 基于你的知识提供该公司最新的3条相关新闻,每条50字以内
|
||||
8. latestValuation 知道就提供,不知道则null
|
||||
9. 不确定的字段返回null,不要编造
|
||||
10. 字符串值中不允许出现Tab、换行等控制字符
|
||||
11. 只返回JSON,不要其他内容
|
||||
""";
|
||||
}
|
||||
|
||||
/** 构建用户消息 */
|
||||
private String buildUserMessage(String shortName) {
|
||||
return "【公司简称】\n" + shortName +
|
||||
"\n\n【行业列表】\n" + dictCacheService.getIndustryText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.jiayunet.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jiayunet.mapper.CompanyMapper;
|
||||
import org.jiayunet.pojo.po.Company;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 公司数据补充事务服务
|
||||
* <p>独立出来解决 @Transactional 同类自调用失效问题</p>
|
||||
* <p>依赖:CompanyMapper、DictCacheService(地区匹配、行业校验)</p>
|
||||
* <p>使用表:bg_company(更新)</p>
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CompanyCleanTransactionService {
|
||||
|
||||
@Autowired
|
||||
private CompanyMapper companyMapper;
|
||||
|
||||
@Autowired
|
||||
private DictCacheService dictCacheService;
|
||||
|
||||
/**
|
||||
* 回填公司数据(事务)
|
||||
* <p>1. 解析AI返回的各字段 2. 地区匹配 3. 行业校验 4. 更新bg_company</p>
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void saveCompanyData(JsonNode root, Company company) {
|
||||
// name:AI根据shortName推断的全称
|
||||
String name = root.path("name").asText(null);
|
||||
if (name != null && !name.isBlank()) {
|
||||
company.setName(name);
|
||||
}
|
||||
|
||||
// regionCode:AI返回城市名,Java侧匹配
|
||||
String city = root.path("city").asText(null);
|
||||
if (city != null && !city.isBlank()) {
|
||||
String regionCode = dictCacheService.matchRegionCode(city);
|
||||
company.setRegionCode(regionCode);
|
||||
}
|
||||
|
||||
// companyType
|
||||
String companyType = root.path("companyType").asText(null);
|
||||
if (companyType != null && !"null".equals(companyType)) {
|
||||
company.setCompanyType(companyType);
|
||||
}
|
||||
|
||||
// industryId:校验是否存在于行业列表中
|
||||
long industryId = root.path("industryId").asLong(0);
|
||||
company.setIndustryId(industryId > 0 ? industryId : null);
|
||||
|
||||
// tags:JSON数组
|
||||
JsonNode tagsNode = root.path("tags");
|
||||
if (tagsNode.isArray() && !tagsNode.isEmpty()) {
|
||||
company.setTags(tagsNode.toString());
|
||||
}
|
||||
|
||||
// summary
|
||||
String summary = root.path("summary").asText(null);
|
||||
if (summary != null && !"null".equals(summary)) {
|
||||
company.setSummary(summary);
|
||||
}
|
||||
|
||||
// description
|
||||
String description = root.path("description").asText(null);
|
||||
if (description != null && !"null".equals(description)) {
|
||||
company.setDescription(description);
|
||||
}
|
||||
|
||||
// foundedYear
|
||||
String foundedYear = root.path("foundedYear").asText(null);
|
||||
if (foundedYear != null && !"null".equals(foundedYear)) {
|
||||
company.setFoundedYear(foundedYear);
|
||||
}
|
||||
|
||||
// address
|
||||
String address = root.path("address").asText(null);
|
||||
if (address != null && !"null".equals(address)) {
|
||||
company.setAddress(address);
|
||||
}
|
||||
|
||||
// scale
|
||||
String scale = root.path("scale").asText(null);
|
||||
if (scale != null && !"null".equals(scale)) {
|
||||
company.setScale(scale);
|
||||
}
|
||||
|
||||
// website
|
||||
String website = root.path("website").asText(null);
|
||||
if (website != null && !"null".equals(website)) {
|
||||
company.setWebsite(website);
|
||||
}
|
||||
|
||||
// financingStage
|
||||
String financingStage = root.path("financingStage").asText(null);
|
||||
if (financingStage != null && !"null".equals(financingStage)) {
|
||||
company.setFinancingStage(financingStage);
|
||||
}
|
||||
|
||||
// latestValuation
|
||||
String latestValuation = root.path("latestValuation").asText(null);
|
||||
if (latestValuation != null && !"null".equals(latestValuation)) {
|
||||
company.setLatestValuation(latestValuation);
|
||||
}
|
||||
|
||||
// news:JSON数组
|
||||
JsonNode newsNode = root.path("news");
|
||||
if (newsNode.isArray() && !newsNode.isEmpty()) {
|
||||
company.setNews(newsNode.toString());
|
||||
}
|
||||
|
||||
// 更新状态和时间
|
||||
company.setStatus(1);
|
||||
company.setUpdateTime(Instant.now());
|
||||
|
||||
companyMapper.updateById(company);
|
||||
}
|
||||
|
||||
/** 更新公司状态 */
|
||||
public void updateCompanyStatus(Long id, int status) {
|
||||
companyMapper.update(null,
|
||||
new LambdaUpdateWrapper<Company>()
|
||||
.set(Company::getStatus, status)
|
||||
.set(Company::getUpdateTime, Instant.now())
|
||||
.eq(Company::getId, id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子锁定一批待完善公司(事务内 SELECT FOR UPDATE + UPDATE 状态)
|
||||
* <p>行锁保证并发安全,其他线程会阻塞直到事务提交</p>
|
||||
*
|
||||
* @return 锁定成功的公司列表,可能为空
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<Company> lockBatch(int batchSize) {
|
||||
List<Company> companyList = companyMapper.selectForUpdate(batchSize);
|
||||
if (companyList.isEmpty()) {
|
||||
return companyList;
|
||||
}
|
||||
|
||||
List<Long> ids = companyList.stream().map(Company::getId).toList();
|
||||
companyMapper.update(null,
|
||||
new LambdaUpdateWrapper<Company>()
|
||||
.set(Company::getStatus, 3)
|
||||
.set(Company::getUpdateTime, Instant.now())
|
||||
.in(Company::getId, ids));
|
||||
|
||||
return companyList;
|
||||
}
|
||||
}
|
||||
@@ -69,25 +69,11 @@ public class JobCleanService {
|
||||
*/
|
||||
@Scheduled(cron = "0 */5 * * * ?")
|
||||
public void cleanJob() {
|
||||
// 批量锁定:原子操作,clean_status 0→1
|
||||
int locked = appJobDataMapper.update(null,
|
||||
new LambdaUpdateWrapper<AppJobData>()
|
||||
.set(AppJobData::getCleanStatus, 1)
|
||||
.eq(AppJobData::getCleanStatus, 0)
|
||||
.eq(AppJobData::getIsValid, 1)
|
||||
.last("LIMIT " + batchSize));
|
||||
|
||||
if (locked == 0) {
|
||||
List<AppJobData> dataList = jobCleanTransactionService.lockBatch(batchSize);
|
||||
if (dataList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
log.info("岗位清洗:锁定{}条数据", locked);
|
||||
|
||||
// 查出刚锁定的数据
|
||||
List<AppJobData> dataList = appJobDataMapper.selectList(
|
||||
new LambdaQueryWrapper<AppJobData>()
|
||||
.eq(AppJobData::getCleanStatus, 1)
|
||||
.eq(AppJobData::getIsValid, 1)
|
||||
.last("LIMIT " + batchSize));
|
||||
log.info("岗位清洗:锁定{}条数据", dataList.size());
|
||||
|
||||
// 多线程并发处理
|
||||
for (AppJobData data : dataList) {
|
||||
|
||||
@@ -143,4 +143,26 @@ public class JobCleanTransactionService {
|
||||
.set(AppJobData::getCleanStatus, status)
|
||||
.eq(AppJobData::getId, id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 原子锁定一批待清洗数据(事务内 SELECT FOR UPDATE + UPDATE 状态)
|
||||
* <p>行锁保证并发安全,其他线程会阻塞直到事务提交</p>
|
||||
*
|
||||
* @return 锁定成功的数据列表,可能为空
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<AppJobData> lockBatch(int batchSize) {
|
||||
List<AppJobData> dataList = appJobDataMapper.selectForUpdate(batchSize);
|
||||
if (dataList.isEmpty()) {
|
||||
return dataList;
|
||||
}
|
||||
|
||||
List<Long> ids = dataList.stream().map(AppJobData::getId).toList();
|
||||
appJobDataMapper.update(null,
|
||||
new LambdaUpdateWrapper<AppJobData>()
|
||||
.set(AppJobData::getCleanStatus, 1)
|
||||
.in(AppJobData::getId, ids));
|
||||
|
||||
return dataList;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user