From 8784a143f1f200bc97f0e4870e230684eb839a98 Mon Sep 17 00:00:00 2001 From: "UNISINSIGHT\\rdpnr_jiangpeng" Date: Tue, 7 Apr 2026 17:44:22 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E9=A1=B9=E7=9B=AE=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=8E=A8=E9=80=81CRM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/project/order/OrderDetail.vue | 4 + .../src/main/resources/application-dev.yml | 5 +- .../src/main/resources/application-prod.yml | 5 +- .../com/ruoyi/sip/domain/ProjectInfo.java | 2 +- .../OpportunityUpdateRequestDto.java | 58 +++++++ .../IOpportunityIntegrationService.java | 9 ++ .../sip/service/IProjectInfoService.java | 2 + .../OpportunityIntegrationServiceImpl.java | 143 ++++++++++++++++++ .../service/impl/ProjectInfoServiceImpl.java | 91 ++++++++++- .../impl/ProjectOrderInfoServiceImpl.java | 23 ++- 10 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 ruoyi-sip/src/main/java/com/ruoyi/sip/dto/integration/OpportunityUpdateRequestDto.java create mode 100644 ruoyi-sip/src/main/java/com/ruoyi/sip/service/IOpportunityIntegrationService.java create mode 100644 ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/OpportunityIntegrationServiceImpl.java diff --git a/oms_web/oms_vue/src/views/project/order/OrderDetail.vue b/oms_web/oms_vue/src/views/project/order/OrderDetail.vue index e2f9b7e8..1fa035da 100644 --- a/oms_web/oms_vue/src/views/project/order/OrderDetail.vue +++ b/oms_web/oms_vue/src/views/project/order/OrderDetail.vue @@ -417,6 +417,10 @@ export default { this.$emit('update:visible', false); }, cancel() { + if (this.$listeners['update:visible']) { + this.handleClose(); + return; + } if (this.projectId && this.orderId === null) { this.$router.go(-1); } else { diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index c7bfbbca..7fa3c976 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -68,4 +68,7 @@ unis: # 执行单截止时间 endHour: 96 mail: - enabled: false \ No newline at end of file + enabled: false +opportunity: + integration: + base-url: http://192.168.2.250:8080 diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index aaf12b95..8bc699e6 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -65,4 +65,7 @@ spring: merge-sql: true wall: config: - multi-statement-allow: true \ No newline at end of file + multi-statement-allow: true +opportunity: + integration: + base-url: https://crm.unissense.top diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectInfo.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectInfo.java index 2e1df01c..50e1442d 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectInfo.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/domain/ProjectInfo.java @@ -56,12 +56,12 @@ public class ProjectInfo extends BaseEntity */ @Excel(name = "项目阶段", dictType = "project_stage") private String projectStage; + /** 建设类型 */ private String constructionType; /** 项目把握度 */ @Excel(name = "项目把握度") private String projectGraspDegree; /** 汇智支撑人员id */ - private String hzSupportUser; @Excel(name = "汇智负责人") private String hzSupportUserName; diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/dto/integration/OpportunityUpdateRequestDto.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/dto/integration/OpportunityUpdateRequestDto.java new file mode 100644 index 00000000..48bfc493 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/dto/integration/OpportunityUpdateRequestDto.java @@ -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; + /** + * 把握度,建议传 A、B、C + */ + private String confidencePct; + /** + * 项目阶段 + */ + private String stage; + /** + * 建设类型 + */ + private String opportunityType; + /** + * 售前 ID + */ + private String preSalesId; + /** + * 售前姓名 + */ + private String preSalesName; + /** + * 竞品名称 + */ + private String competitorName; + /** + * 是否归档 + */ + private Boolean archived; +} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IOpportunityIntegrationService.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IOpportunityIntegrationService.java new file mode 100644 index 00000000..e74d8752 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IOpportunityIntegrationService.java @@ -0,0 +1,9 @@ +package com.ruoyi.sip.service; + +import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto; + +public interface IOpportunityIntegrationService { + + Long updateOpportunity(OpportunityUpdateRequestDto requestDto); + +} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectInfoService.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectInfoService.java index 0997b7b8..505b0eec 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectInfoService.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/IProjectInfoService.java @@ -78,4 +78,6 @@ public interface IProjectInfoService ProjectInfo insertProjectInfoForApi(ApiProjectAddDto dto); + void scheduleOpportunityUpdateByProjectId(Long projectId); + } diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/OpportunityIntegrationServiceImpl.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/OpportunityIntegrationServiceImpl.java new file mode 100644 index 00000000..fcdd9c10 --- /dev/null +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/OpportunityIntegrationServiceImpl.java @@ -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 requestEntity = new HttpEntity<>(payload, headers); + ResponseEntity 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; + } +} diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectInfoServiceImpl.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectInfoServiceImpl.java index 8645ff5f..a4e75220 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectInfoServiceImpl.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectInfoServiceImpl.java @@ -26,6 +26,7 @@ import com.ruoyi.sip.dto.ApiProjectAddDto; import com.ruoyi.sip.dto.HomepageQueryDto; import com.ruoyi.sip.dto.StatisticsDetailDto; import com.ruoyi.sip.dto.StatisticsDto; +import com.ruoyi.sip.dto.integration.OpportunityUpdateRequestDto; import com.ruoyi.sip.mapper.ProjectInfoMapper; import com.ruoyi.sip.service.*; 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.stereotype.Service; 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.File; @@ -53,6 +56,7 @@ import java.time.Period; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -88,6 +92,8 @@ public class ProjectInfoServiceImpl implements IProjectInfoService { private IProjectUserCollectInfoService projectUserCollectInfoService; @Autowired private IProjectOrderInfoService orderInfoService; + @Autowired + private IOpportunityIntegrationService opportunityIntegrationService; private static final String PROJECT_CODE_PREFIX = "V"; private static final Integer PROJECT_CODE_LENGTH = 6; @@ -281,7 +287,7 @@ public class ProjectInfoServiceImpl implements IProjectInfoService { } //变更属地校验 List 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)) { throw new ServiceException("该项目存在订单流转,无法更改代表处"); @@ -313,9 +319,85 @@ public class ProjectInfoServiceImpl implements IProjectInfoService { if(CollUtil.isEmpty(projectInfos)){ quotationService.unBind(oldProjectInfo.getQuotationId()); } + //项目信息异步推送CRM + scheduleOpportunityUpdateByProjectId(projectInfo.getId()); 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) { StringBuilder logContent = new StringBuilder(); //简略信息变更 @@ -453,6 +535,13 @@ public class ProjectInfoServiceImpl implements IProjectInfoService { 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); + } + /** * 比较产品信息列表并生成日志内容 * diff --git a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java index 984a466a..32727881 100644 --- a/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java +++ b/ruoyi-sip/src/main/java/com/ruoyi/sip/service/impl/ProjectOrderInfoServiceImpl.java @@ -147,6 +147,9 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To @Autowired @Lazy private ProjectOrderInfoToolProvider projectOrderInfoToolProvider; + @Autowired + @Lazy + private IProjectInfoService projectInfoService; /** * 查询订单管理 * @@ -324,6 +327,7 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To } //修改项目预计下单时间 projectInfoMapper.updateOrderTimeById(projectOrderInfo.getProjectId()); + projectInfoService.scheduleOpportunityUpdateByProjectId(projectOrderInfo.getProjectId()); return i; } @@ -518,7 +522,24 @@ public class ProjectOrderInfoServiceImpl implements IProjectOrderInfoService, To */ @Override public int deleteProjectOrderInfoByIds(String ids) { - return projectOrderInfoMapper.deleteProjectOrderInfoByIds(Convert.toStrArray(ids)); + String[] idArray = Convert.toStrArray(ids); + Set 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; } /**