feat: 订单退回调用crm接口

dev_1.0.3
chenhao 2026-09-16 12:10:58 +08:00
parent 467e7f74d5
commit d4e33c4fd3
3 changed files with 152 additions and 14 deletions

View File

@ -1,9 +1,25 @@
package com.ruoyi.sip.service; package com.ruoyi.sip.service;
import com.ruoyi.sip.dto.integration.OpportunityOrderReturnRequestDto;
import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto; import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto;
public interface IOpportunityIntegrationService { public interface IOpportunityIntegrationService {
Long updateOpportunity(OpportunityUpdateRequestDto requestDto); Long updateOpportunity(OpportunityUpdateRequestDto requestDto);
/**
* CRM退
*
* @param requestDto 退
*/
void notifyOrderReturn(OpportunityOrderReturnRequestDto requestDto);
/**
* CRM退CRM退
*
* @param requestDto 退
* @param projectId
*/
void scheduleOrderReturnNotify(OpportunityOrderReturnRequestDto requestDto, Long projectId);
} }

View File

@ -11,6 +11,7 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.sip.domain.*; import com.ruoyi.sip.domain.*;
import com.ruoyi.sip.dto.inventory.ProductDetail; import com.ruoyi.sip.dto.inventory.ProductDetail;
import com.ruoyi.sip.dto.inventory.ProductWarehouseInfo; import com.ruoyi.sip.dto.inventory.ProductWarehouseInfo;
import com.ruoyi.sip.dto.integration.OpportunityOrderReturnRequestDto;
import com.ruoyi.sip.flowable.domain.Todo; import com.ruoyi.sip.flowable.domain.Todo;
import com.ruoyi.sip.flowable.mapper.TodoMapper; import com.ruoyi.sip.flowable.mapper.TodoMapper;
import com.ruoyi.sip.flowable.service.TodoCommonTemplate; import com.ruoyi.sip.flowable.service.TodoCommonTemplate;
@ -85,6 +86,8 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
private IInventoryDeliveryService inventoryDeliveryService; private IInventoryDeliveryService inventoryDeliveryService;
@Autowired @Autowired
private ProcessConfig processConfig; private ProcessConfig processConfig;
@Autowired
private IOpportunityIntegrationService opportunityIntegrationService;
@Override @Override
public ExecutionOrderVo selectInfo(Long id) { public ExecutionOrderVo selectInfo(Long id) {
ExecutionOrderVo vo = new ExecutionOrderVo(); ExecutionOrderVo vo = new ExecutionOrderVo();
@ -540,6 +543,8 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
projectOrderInfoMapper.updateProjectOrderInfo(updateOrder); projectOrderInfoMapper.updateProjectOrderInfo(updateOrder);
// 撤单审批通过不重置订单审批状态orderStatus保持原状态 // 撤单审批通过不重置订单审批状态orderStatus保持原状态
recall(orderId, updateBy, false); recall(orderId, updateBy, false);
// 退单审批整体完成后回调CRM商机阶段回退为S4并清空签约信息
notifyCrmOrderReturn(orderId);
} }
} }
return TodoCommonTemplate.super.multiInstanceApproveCallback(activityName, processInstance); return TodoCommonTemplate.super.multiInstanceApproveCallback(activityName, processInstance);
@ -569,4 +574,20 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
result.put("projectOrderInfo", projectOrderInfo); result.put("projectOrderInfo", projectOrderInfo);
return result; return result;
} }
/**
* 退CRM project_code
*
* @param orderId
*/
private void notifyCrmOrderReturn(Long orderId) {
ProjectOrderInfo order = projectOrderInfoService.selectProjectOrderInfoById(orderId);
if (order == null) {
return;
}
OpportunityOrderReturnRequestDto requestDto = new OpportunityOrderReturnRequestDto();
requestDto.setOpportunityCode(order.getProjectCode());
requestDto.setOrderNo(order.getOrderCode());
opportunityIntegrationService.scheduleOrderReturnNotify(requestDto, order.getProjectId());
}
} }

View File

@ -3,9 +3,13 @@ package com.ruoyi.sip.service.impl;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.sip.domain.OmsSendCrmErrorLog;
import com.ruoyi.sip.dto.integration.OpportunityOrderReturnRequestDto;
import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto; import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto;
import com.ruoyi.sip.mapper.OmsSendCrmErrorLogMapper;
import com.ruoyi.sip.service.IOpportunityIntegrationService; import com.ruoyi.sip.service.IOpportunityIntegrationService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -13,26 +17,40 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Slf4j @Slf4j
@Service @Service
public class OpportunityIntegrationServiceImpl implements IOpportunityIntegrationService { public class OpportunityIntegrationServiceImpl implements IOpportunityIntegrationService {
/**
* CRM CRM unisbase.internal-auth.header-name
*/
private static final String SECRET_HEADER = "X-Internal-Secret";
@Value("${opportunity.integration.base-url:http://localhost:8080}") @Value("${opportunity.integration.base-url:http://localhost:8080}")
private String baseUrl; private String baseUrl;
@Value("${opportunity.integration.update-path:/api/opportunities/integration/update}") @Value("${opportunity.integration.update-path:/api/opportunities/integration/update}")
private String updatePath; private String updatePath;
@Value("${opportunity.integration.order-return-path:/api/oms/callback/order-return}")
private String orderReturnPath;
@Value("${opportunity.integration.secret:f0eb247f84db4e328fb27ce8ff6e7be96e73a53a7e9c4793395ad10d999e0d77}") @Value("${opportunity.integration.secret:f0eb247f84db4e328fb27ce8ff6e7be96e73a53a7e9c4793395ad10d999e0d77}")
private String secret; private String secret;
@Autowired
private OmsSendCrmErrorLogMapper sendCrmErrorLogMapper;
private final RestTemplate restTemplate = new RestTemplate(); private final RestTemplate restTemplate = new RestTemplate();
@Override @Override
@ -40,18 +58,7 @@ public class OpportunityIntegrationServiceImpl implements IOpportunityIntegratio
validateRequest(requestDto); validateRequest(requestDto);
JSONObject payload = buildPayload(requestDto); JSONObject payload = buildPayload(requestDto);
log.info("调用商机更新接口请求体, opportunityCode:{}, payload:{}", requestDto.getOpportunityCode(), payload.toJSONString()); log.info("调用商机更新接口请求体, opportunityCode:{}, payload:{}", requestDto.getOpportunityCode(), payload.toJSONString());
HttpHeaders headers = new HttpHeaders(); ResponseEntity<Map> responseEntity = exchange(buildUrl(updatePath), HttpMethod.PUT, payload, "调用商机更新接口");
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(); Map responseBody = responseEntity.getBody();
if (responseBody == null) { if (responseBody == null) {
throw new ServiceException("调用商机更新接口失败: 响应为空"); throw new ServiceException("调用商机更新接口失败: 响应为空");
@ -75,9 +82,91 @@ public class OpportunityIntegrationServiceImpl implements IOpportunityIntegratio
} }
} }
private String buildUrl() { @Override
public void notifyOrderReturn(OpportunityOrderReturnRequestDto requestDto) {
validateOrderReturnRequest(requestDto);
JSONObject payload = new JSONObject();
payload.put("opportunityCode", requestDto.getOpportunityCode());
if (StringUtils.isNotEmpty(requestDto.getOrderNo())) {
payload.put("orderNo", requestDto.getOrderNo());
}
log.info("调用CRM退单回调接口请求体, opportunityCode:{}, payload:{}",
requestDto.getOpportunityCode(), payload.toJSONString());
ResponseEntity<Map> responseEntity = exchange(buildUrl(orderReturnPath), HttpMethod.POST, payload, "调用CRM退单回调接口");
Map responseBody = responseEntity.getBody();
if (responseBody == null) {
throw new ServiceException("调用CRM退单回调接口失败: 响应为空");
}
if (!"0".equals(String.valueOf(responseBody.get("code")))) {
Object message = responseBody.get("message") != null ? responseBody.get("message") : responseBody.get("msg");
throw new ServiceException("调用CRM退单回调接口失败: " + message);
}
log.info("CRM退单回调接口调用成功, opportunityCode:{}, data:{}",
requestDto.getOpportunityCode(), responseBody.get("data"));
}
@Override
public void scheduleOrderReturnNotify(OpportunityOrderReturnRequestDto requestDto, Long projectId) {
if (requestDto == null || StringUtils.isEmpty(requestDto.getOpportunityCode())) {
log.warn("商机编号为空跳过CRM退单回调, projectId:{}", projectId);
return;
}
Runnable task = () -> {
try {
notifyOrderReturn(requestDto);
} catch (Exception e) {
log.error("退单后同步CRM失败, projectId:{}, opportunityCode:{}",
projectId, requestDto.getOpportunityCode(), e);
saveOrderReturnErrorLog(projectId, requestDto.getOpportunityCode(), e);
}
};
// 退单业务与CRM推送解耦事务提交后再异步通知CRM异常不回滚退单失败落错误日志便于重试
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
CompletableFuture.runAsync(task);
}
});
return;
}
CompletableFuture.runAsync(task);
}
private void saveOrderReturnErrorLog(Long projectId, String opportunityCode, Exception cause) {
try {
OmsSendCrmErrorLog errorLog = new OmsSendCrmErrorLog();
errorLog.setProjectId(projectId);
errorLog.setProjectCode(opportunityCode);
String errMsg = "退单回调失败: " + cause.getMessage();
errorLog.setMsg(errMsg.length() > 10240 ? errMsg.substring(0, 10240) : errMsg);
sendCrmErrorLogMapper.insert(errorLog);
} catch (Exception e) {
log.error("记录CRM退单回调错误日志失败, projectId:{}", projectId, e);
}
}
/**
* JSON
*/
private ResponseEntity<Map> exchange(String url, HttpMethod method, JSONObject payload, String operation) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(SECRET_HEADER, secret);
HttpEntity<JSONObject> requestEntity = new HttpEntity<>(payload, headers);
try {
return restTemplate.exchange(url, method, requestEntity, Map.class);
} catch (HttpStatusCodeException e) {
throw new ServiceException(operation + "失败: HTTP " + e.getRawStatusCode() + " " + e.getStatusText()
+ ", 响应: " + e.getResponseBodyAsString());
} catch (RestClientException e) {
throw new ServiceException(operation + "失败: " + e.getMessage());
}
}
private String buildUrl(String path) {
String trimmedBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; String trimmedBaseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
String trimmedPath = updatePath.startsWith("/") ? updatePath : "/" + updatePath; String trimmedPath = path.startsWith("/") ? path : "/" + path;
return trimmedBaseUrl + trimmedPath; return trimmedBaseUrl + trimmedPath;
} }
@ -96,6 +185,18 @@ public class OpportunityIntegrationServiceImpl implements IOpportunityIntegratio
} }
} }
private void validateOrderReturnRequest(OpportunityOrderReturnRequestDto 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 未配置无法调用CRM退单回调接口");
}
}
private boolean hasAtLeastOneUpdateField(OpportunityUpdateRequestDto requestDto) { private boolean hasAtLeastOneUpdateField(OpportunityUpdateRequestDto requestDto) {
for (Field field : OpportunityUpdateRequestDto.class.getDeclaredFields()) { for (Field field : OpportunityUpdateRequestDto.class.getDeclaredFields()) {
if ("opportunityCode".equals(field.getName())) { if ("opportunityCode".equals(field.getName())) {