fix:项目信息推送CRM

dev_1.0.2
UNISINSIGHT\rdpnr_jiangpeng 2026-04-07 17:44:22 +08:00
parent 9439b82081
commit 8784a143f1
10 changed files with 337 additions and 5 deletions

View File

@ -417,6 +417,10 @@ export default {
this.$emit('update:visible', false); this.$emit('update:visible', false);
}, },
cancel() { cancel() {
if (this.$listeners['update:visible']) {
this.handleClose();
return;
}
if (this.projectId && this.orderId === null) { if (this.projectId && this.orderId === null) {
this.$router.go(-1); this.$router.go(-1);
} else { } else {

View File

@ -68,4 +68,7 @@ unis:
# 执行单截止时间 # 执行单截止时间
endHour: 96 endHour: 96
mail: mail:
enabled: false enabled: false
opportunity:
integration:
base-url: http://192.168.2.250:8080

View File

@ -65,4 +65,7 @@ spring:
merge-sql: true merge-sql: true
wall: wall:
config: config:
multi-statement-allow: true multi-statement-allow: true
opportunity:
integration:
base-url: https://crm.unissense.top

View File

@ -56,12 +56,12 @@ public class ProjectInfo extends BaseEntity
*/ */
@Excel(name = "项目阶段", dictType = "project_stage") @Excel(name = "项目阶段", dictType = "project_stage")
private String projectStage; private String projectStage;
/** 建设类型 */
private String constructionType; private String constructionType;
/** 项目把握度 */ /** 项目把握度 */
@Excel(name = "项目把握度") @Excel(name = "项目把握度")
private String projectGraspDegree; private String projectGraspDegree;
/** 汇智支撑人员id */ /** 汇智支撑人员id */
private String hzSupportUser; private String hzSupportUser;
@Excel(name = "汇智负责人") @Excel(name = "汇智负责人")
private String hzSupportUserName; private String hzSupportUserName;

View File

@ -0,0 +1,58 @@
package com.ruoyi.sip.dto.integration;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class OpportunityUpdateRequestDto {
/**
*
*/
private String opportunityCode;
/**
*
*/
private String opportunityName;
/**
*
*/
private String operatorName;
/**
*
*/
private BigDecimal amount;
/**
* YYYY-MM-DD
*/
private String expectedCloseDate;
/**
* ABC
*/
private String confidencePct;
/**
*
*/
private String stage;
/**
*
*/
private String opportunityType;
/**
* ID
*/
private String preSalesId;
/**
*
*/
private String preSalesName;
/**
*
*/
private String competitorName;
/**
*
*/
private Boolean archived;
}

View File

@ -0,0 +1,9 @@
package com.ruoyi.sip.service;
import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto;
public interface IOpportunityIntegrationService {
Long updateOpportunity(OpportunityUpdateRequestDto requestDto);
}

View File

@ -78,4 +78,6 @@ public interface IProjectInfoService
ProjectInfo insertProjectInfoForApi(ApiProjectAddDto dto); ProjectInfo insertProjectInfoForApi(ApiProjectAddDto dto);
void scheduleOpportunityUpdateByProjectId(Long projectId);
} }

View File

@ -0,0 +1,143 @@
package com.ruoyi.sip.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto;
import com.ruoyi.sip.service.IOpportunityIntegrationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.lang.reflect.Field;
import java.util.Map;
@Slf4j
@Service
public class OpportunityIntegrationServiceImpl implements IOpportunityIntegrationService {
@Value("${opportunity.integration.base-url:http://localhost:8080}")
private String baseUrl;
@Value("${opportunity.integration.update-path:/api/opportunities/integration/update}")
private String updatePath;
@Value("${opportunity.integration.secret:f0eb247f84db4e328fb27ce8ff6e7be96e73a53a7e9c4793395ad10d999e0d77}")
private String secret;
private final RestTemplate restTemplate = new RestTemplate();
@Override
public Long updateOpportunity(OpportunityUpdateRequestDto requestDto) {
validateRequest(requestDto);
JSONObject payload = buildPayload(requestDto);
log.info("调用商机更新接口请求体, opportunityCode:{}, payload:{}", requestDto.getOpportunityCode(), payload.toJSONString());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("X-Internal-Secret", secret);
HttpEntity<JSONObject> requestEntity = new HttpEntity<>(payload, headers);
ResponseEntity<Map> responseEntity;
try {
responseEntity = restTemplate.exchange(buildUrl(), HttpMethod.PUT, requestEntity, Map.class);
} catch (HttpStatusCodeException e) {
throw new ServiceException("调用商机更新接口失败: HTTP " + e.getRawStatusCode() + " " + e.getStatusText() + ", 响应: " + e.getResponseBodyAsString());
} catch (RestClientException e) {
throw new ServiceException("调用商机更新接口失败: " + e.getMessage());
}
Map responseBody = responseEntity.getBody();
if (responseBody == null) {
throw new ServiceException("调用商机更新接口失败: 响应为空");
}
String code = String.valueOf(responseBody.get("code"));
if (!"0".equals(code)) {
throw new ServiceException(String.valueOf(responseBody.get("msg")));
}
Object data = responseBody.get("data");
log.info("商机更新接口调用成功, opportunityCode:{}, data:{}", requestDto.getOpportunityCode(), data);
if (data == null) {
return null;
}
if (data instanceof Number) {
return ((Number) data).longValue();
}
try {
return Long.valueOf(String.valueOf(data));
} catch (NumberFormatException e) {
throw new ServiceException("调用商机更新接口失败: 返回 data 不是数字");
}
}
private String buildUrl() {
String trimmedBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
String trimmedPath = updatePath.startsWith("/") ? updatePath : "/" + updatePath;
return trimmedBaseUrl + trimmedPath;
}
private void validateRequest(OpportunityUpdateRequestDto requestDto) {
if (requestDto == null) {
throw new ServiceException("请求参数不能为空");
}
if (StringUtils.isEmpty(requestDto.getOpportunityCode())) {
throw new ServiceException("opportunityCode 不能为空");
}
if (StringUtils.isEmpty(secret)) {
throw new ServiceException("opportunity.integration.secret 未配置,无法调用商机更新接口");
}
if (!hasAtLeastOneUpdateField(requestDto)) {
throw new ServiceException("至少传入一个需要更新的字段");
}
}
private boolean hasAtLeastOneUpdateField(OpportunityUpdateRequestDto requestDto) {
for (Field field : OpportunityUpdateRequestDto.class.getDeclaredFields()) {
if ("opportunityCode".equals(field.getName())) {
continue;
}
field.setAccessible(true);
Object value;
try {
value = field.get(requestDto);
} catch (IllegalAccessException e) {
throw new ServiceException("参数读取失败: " + field.getName());
}
if (value instanceof String) {
if (StringUtils.isNotEmpty((String) value)) {
return true;
}
continue;
}
if (value != null) {
return true;
}
}
return false;
}
private JSONObject buildPayload(OpportunityUpdateRequestDto requestDto) {
JSONObject payload = new JSONObject();
for (Field field : OpportunityUpdateRequestDto.class.getDeclaredFields()) {
field.setAccessible(true);
Object value;
try {
value = field.get(requestDto);
} catch (IllegalAccessException e) {
throw new ServiceException("参数读取失败: " + field.getName());
}
if (value instanceof String && StringUtils.isEmpty((String) value)) {
continue;
}
if (value != null) {
payload.put(field.getName(), value);
}
}
return payload;
}
}

View File

@ -26,6 +26,7 @@ import com.ruoyi.sip.dto.ApiProjectAddDto;
import com.ruoyi.sip.dto.HomepageQueryDto; import com.ruoyi.sip.dto.HomepageQueryDto;
import com.ruoyi.sip.dto.StatisticsDetailDto; import com.ruoyi.sip.dto.StatisticsDetailDto;
import com.ruoyi.sip.dto.StatisticsDto; import com.ruoyi.sip.dto.StatisticsDto;
import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto;
import com.ruoyi.sip.mapper.ProjectInfoMapper; import com.ruoyi.sip.mapper.ProjectInfoMapper;
import com.ruoyi.sip.service.*; import com.ruoyi.sip.service.*;
import liquibase.hub.model.Project; import liquibase.hub.model.Project;
@ -39,6 +40,8 @@ import org.hibernate.validator.internal.constraintvalidators.bv.time.future.Futu
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@ -53,6 +56,7 @@ import java.time.Period;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -88,6 +92,8 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
private IProjectUserCollectInfoService projectUserCollectInfoService; private IProjectUserCollectInfoService projectUserCollectInfoService;
@Autowired @Autowired
private IProjectOrderInfoService orderInfoService; private IProjectOrderInfoService orderInfoService;
@Autowired
private IOpportunityIntegrationService opportunityIntegrationService;
private static final String PROJECT_CODE_PREFIX = "V"; private static final String PROJECT_CODE_PREFIX = "V";
private static final Integer PROJECT_CODE_LENGTH = 6; private static final Integer PROJECT_CODE_LENGTH = 6;
@ -281,7 +287,7 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
} }
//变更属地校验 //变更属地校验
List<ProjectOrderInfo> projectOrderInfos = orderInfoService.selectProjectOrderInfoByProjectId(Collections.singletonList(projectInfo.getId())); List<ProjectOrderInfo> projectOrderInfos = orderInfoService.selectProjectOrderInfoByProjectId(Collections.singletonList(projectInfo.getId()));
if (!isApi && !oldProjectInfo.getAgentCode().equals(projectInfo.getAgentCode())) { if (!isApi && StringUtils.isNotEmpty(oldProjectInfo.getAgentCode()) && !oldProjectInfo.getAgentCode().equals(projectInfo.getAgentCode())) {
//查询订单信息 如果有抛出异常 //查询订单信息 如果有抛出异常
if (CollUtil.isNotEmpty(projectOrderInfos)) { if (CollUtil.isNotEmpty(projectOrderInfos)) {
throw new ServiceException("该项目存在订单流转,无法更改代表处"); throw new ServiceException("该项目存在订单流转,无法更改代表处");
@ -313,9 +319,85 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
if(CollUtil.isEmpty(projectInfos)){ if(CollUtil.isEmpty(projectInfos)){
quotationService.unBind(oldProjectInfo.getQuotationId()); quotationService.unBind(oldProjectInfo.getQuotationId());
} }
//项目信息异步推送CRM
scheduleOpportunityUpdateByProjectId(projectInfo.getId());
return result; return result;
} }
@Override
public void scheduleOpportunityUpdateByProjectId(Long projectId) {
if (projectId == null) {
return;
}
scheduleOpportunityUpdate(this.selectProjectInfoById(projectId));
}
/**
* CRM
* @param projectInfo
*/
private void scheduleOpportunityUpdate(ProjectInfo projectInfo) {
OpportunityUpdateRequestDto requestDto = buildOpportunityUpdateRequest(projectInfo);
if (requestDto == null) {
return;
}
Runnable task = () -> {
try {
opportunityIntegrationService.updateOpportunity(requestDto);
} catch (Exception e) {
log.error("项目更新后同步商机失败, projectId:{}, opportunityCode:{}", projectInfo.getId(), requestDto.getOpportunityCode(), e);
}
};
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
CompletableFuture.runAsync(task);
}
});
return;
}
CompletableFuture.runAsync(task);
}
private OpportunityUpdateRequestDto buildOpportunityUpdateRequest(ProjectInfo projectInfo) {
if (projectInfo == null || StringUtils.isEmpty(projectInfo.getProjectCode())) {
return null;
}
OpportunityUpdateRequestDto requestDto = new OpportunityUpdateRequestDto();
requestDto.setOpportunityCode(projectInfo.getProjectCode());
requestDto.setOpportunityName(projectInfo.getProjectName());
requestDto.setOperatorName(projectInfo.getOperateInstitution());
requestDto.setAmount(projectInfo.getEstimatedAmount());
requestDto.setExpectedCloseDate(formatterDate(projectInfo.getEstimatedOrderTime(), DateUtils.YYYY_MM_DD));
requestDto.setConfidencePct(projectInfo.getProjectGraspDegree());
requestDto.setStage(projectInfo.getProjectStage());
requestDto.setOpportunityType(projectInfo.getConstructionType());
requestDto.setPreSalesId(projectInfo.getHzSupportUser());
requestDto.setPreSalesName(projectInfo.getHzSupportUserName());
requestDto.setCompetitorName(projectInfo.getCompetitor());
Boolean canGenerate = projectInfo.getCanGenerate();
if (canGenerate == null && projectInfo.getId() != null) {
canGenerate = CollUtil.isNotEmpty(orderInfoService.selectProjectOrderInfoByProjectId(Collections.singletonList(projectInfo.getId())));
}
requestDto.setArchived(canGenerate);
return hasOpportunityUpdateField(requestDto) ? requestDto : null;
}
private boolean hasOpportunityUpdateField(OpportunityUpdateRequestDto requestDto) {
return StringUtils.isNotEmpty(requestDto.getOpportunityName())
|| StringUtils.isNotEmpty(requestDto.getOperatorName())
|| requestDto.getAmount() != null
|| StringUtils.isNotEmpty(requestDto.getExpectedCloseDate())
|| StringUtils.isNotEmpty(requestDto.getConfidencePct())
|| StringUtils.isNotEmpty(requestDto.getStage())
|| StringUtils.isNotEmpty(requestDto.getOpportunityType())
|| requestDto.getPreSalesId() != null
|| StringUtils.isNotEmpty(requestDto.getPreSalesName())
|| StringUtils.isNotEmpty(requestDto.getCompetitorName())
|| requestDto.getArchived() != null;
}
private void recordOperationLogs(ProjectInfo projectInfo, ProjectInfo oldProjectInfo) { private void recordOperationLogs(ProjectInfo projectInfo, ProjectInfo oldProjectInfo) {
StringBuilder logContent = new StringBuilder(); StringBuilder logContent = new StringBuilder();
//简略信息变更 //简略信息变更
@ -453,6 +535,13 @@ public class ProjectInfoServiceImpl implements IProjectInfoService {
return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, date); return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, date);
} }
private String formatterDate(Date date, String pattern) {
if (date == null) {
return null;
}
return DateUtils.parseDateToStr(pattern, date);
}
/** /**
* *
* *

View File

@ -147,6 +147,9 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
@Autowired @Autowired
@Lazy @Lazy
private ProjectOrderInfoToolProvider projectOrderInfoToolProvider; private ProjectOrderInfoToolProvider projectOrderInfoToolProvider;
@Autowired
@Lazy
private IProjectInfoService projectInfoService;
/** /**
* *
* *
@ -324,6 +327,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
} }
//修改项目预计下单时间 //修改项目预计下单时间
projectInfoMapper.updateOrderTimeById(projectOrderInfo.getProjectId()); projectInfoMapper.updateOrderTimeById(projectOrderInfo.getProjectId());
projectInfoService.scheduleOpportunityUpdateByProjectId(projectOrderInfo.getProjectId());
return i; return i;
} }
@ -518,7 +522,24 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To
*/ */
@Override @Override
public int deleteProjectOrderInfoByIds(String ids) { public int deleteProjectOrderInfoByIds(String ids) {
return projectOrderInfoMapper.deleteProjectOrderInfoByIds(Convert.toStrArray(ids)); String[] idArray = Convert.toStrArray(ids);
Set<Long> projectIds = new HashSet<>();
for (String idStr : idArray) {
if (StringUtils.isEmpty(idStr)) {
continue;
}
ProjectOrderInfo orderInfo = projectOrderInfoMapper.selectProjectOrderInfoById(Long.valueOf(idStr));
if (orderInfo != null && orderInfo.getProjectId() != null) {
projectIds.add(orderInfo.getProjectId());
}
}
int rows = projectOrderInfoMapper.deleteProjectOrderInfoByIds(idArray);
if (rows > 0) {
for (Long projectId : projectIds) {
projectInfoService.scheduleOpportunityUpdateByProjectId(projectId);
}
}
return rows;
} }
/** /**