feat: 订单退回调用crm接口
parent
467e7f74d5
commit
d4e33c4fd3
|
|
@ -1,9 +1,25 @@
|
|||
package com.ruoyi.sip.service;
|
||||
|
||||
import com.ruoyi.sip.dto.integration.OpportunityOrderReturnRequestDto;
|
||||
import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto;
|
||||
|
||||
public interface IOpportunityIntegrationService {
|
||||
|
||||
Long updateOpportunity(OpportunityUpdateRequestDto requestDto);
|
||||
|
||||
/**
|
||||
* 同步调用CRM退单回调接口,失败抛出异常
|
||||
*
|
||||
* @param requestDto 退单回调参数
|
||||
*/
|
||||
void notifyOrderReturn(OpportunityOrderReturnRequestDto requestDto);
|
||||
|
||||
/**
|
||||
* 事务提交后异步通知CRM退单,失败仅记录推送CRM错误日志,不影响退单业务
|
||||
*
|
||||
* @param requestDto 退单回调参数
|
||||
* @param projectId 项目主键,仅用于失败日志定位
|
||||
*/
|
||||
void scheduleOrderReturnNotify(OpportunityOrderReturnRequestDto requestDto, Long projectId);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.ruoyi.common.utils.StringUtils;
|
|||
import com.ruoyi.sip.domain.*;
|
||||
import com.ruoyi.sip.dto.inventory.ProductDetail;
|
||||
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.mapper.TodoMapper;
|
||||
import com.ruoyi.sip.flowable.service.TodoCommonTemplate;
|
||||
|
|
@ -85,6 +86,8 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
|
|||
private IInventoryDeliveryService inventoryDeliveryService;
|
||||
@Autowired
|
||||
private ProcessConfig processConfig;
|
||||
@Autowired
|
||||
private IOpportunityIntegrationService opportunityIntegrationService;
|
||||
@Override
|
||||
public ExecutionOrderVo selectInfo(Long id) {
|
||||
ExecutionOrderVo vo = new ExecutionOrderVo();
|
||||
|
|
@ -540,6 +543,8 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
|
|||
projectOrderInfoMapper.updateProjectOrderInfo(updateOrder);
|
||||
// 撤单审批通过不重置订单审批状态(orderStatus),保持原状态
|
||||
recall(orderId, updateBy, false);
|
||||
// 退单审批整体完成后回调CRM:商机阶段回退为S4并清空签约信息
|
||||
notifyCrmOrderReturn(orderId);
|
||||
}
|
||||
}
|
||||
return TodoCommonTemplate.super.multiInstanceApproveCallback(activityName, processInstance);
|
||||
|
|
@ -569,4 +574,20 @@ public class ExecutionTrackServiceImpl implements IExecutionTrackService, TodoCo
|
|||
result.put("projectOrderInfo", projectOrderInfo);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ 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.domain.OmsSendCrmErrorLog;
|
||||
import com.ruoyi.sip.dto.integration.OpportunityOrderReturnRequestDto;
|
||||
import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto;
|
||||
import com.ruoyi.sip.mapper.OmsSendCrmErrorLogMapper;
|
||||
import com.ruoyi.sip.service.IOpportunityIntegrationService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
|
@ -13,26 +17,40 @@ import org.springframework.http.HttpMethod;
|
|||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
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}")
|
||||
private String baseUrl;
|
||||
|
||||
@Value("${opportunity.integration.update-path:/api/opportunities/integration/update}")
|
||||
private String updatePath;
|
||||
|
||||
@Value("${opportunity.integration.order-return-path:/api/oms/callback/order-return}")
|
||||
private String orderReturnPath;
|
||||
|
||||
@Value("${opportunity.integration.secret:f0eb247f84db4e328fb27ce8ff6e7be96e73a53a7e9c4793395ad10d999e0d77}")
|
||||
private String secret;
|
||||
|
||||
@Autowired
|
||||
private OmsSendCrmErrorLogMapper sendCrmErrorLogMapper;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
@Override
|
||||
|
|
@ -40,18 +58,7 @@ public class OpportunityIntegrationServiceImpl implements IOpportunityIntegratio
|
|||
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());
|
||||
}
|
||||
ResponseEntity<Map> responseEntity = exchange(buildUrl(updatePath), HttpMethod.PUT, payload, "调用商机更新接口");
|
||||
Map responseBody = responseEntity.getBody();
|
||||
if (responseBody == null) {
|
||||
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 trimmedPath = updatePath.startsWith("/") ? updatePath : "/" + updatePath;
|
||||
String trimmedPath = path.startsWith("/") ? path : "/" + path;
|
||||
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) {
|
||||
for (Field field : OpportunityUpdateRequestDto.class.getDeclaredFields()) {
|
||||
if ("opportunityCode".equals(field.getName())) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue