From 387967167df149b820e3af450e762a88f0473824 Mon Sep 17 00:00:00 2001 From: Yangk Date: Thu, 4 Dec 2025 10:12:52 +0800 Subject: [PATCH] =?UTF-8?q?add=E9=A1=B9=E7=9B=AE=E5=94=AE=E5=90=8E?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ErpAfterSalesController.java | 11 ++ .../oa/erp/domain/bo/ErpAfterSalesBo.java | 17 ++ .../oa/erp/service/IErpAfterSalesService.java | 8 + .../impl/ErpAfterSalesServiceImpl.java | 158 ++++++++++++++---- 4 files changed, 159 insertions(+), 35 deletions(-) diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpAfterSalesController.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpAfterSalesController.java index d452016c..0ef6d794 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpAfterSalesController.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/controller/ErpAfterSalesController.java @@ -113,4 +113,15 @@ public class ErpAfterSalesController extends BaseController { return R.ok(list); } + /** + * 提交项目售后信息并提交流程 + */ + @SaCheckPermission("oa/erp:afterSales:add") + @Log(title = "项目售后信息", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/submitAndFlowStart") + public R submitAndFlowStart(@Validated(AddGroup.class) @RequestBody ErpAfterSalesBo bo) { + return R.ok(erpAfterSalesService.submitAndFlowStart(bo)); + } + } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpAfterSalesBo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpAfterSalesBo.java index 3da4e06d..d45df539 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpAfterSalesBo.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/bo/ErpAfterSalesBo.java @@ -13,10 +13,12 @@ import java.math.BigDecimal; import java.util.Date; import com.fasterxml.jackson.annotation.JsonFormat; import java.util.List; // 1. 别忘了导包 List +import java.util.Map; // 2. 导入两个子表的 BO 对象,因为前端传的是子表的业务数据 import org.dromara.oa.erp.domain.bo.ErpAfterSalesLaborCostsBo; import org.dromara.oa.erp.domain.bo.ErpAfterSalesMaterialCostsBo; +import org.dromara.workflow.api.domain.RemoteFlowInstanceBizExt; /** * 项目售后信息业务对象 erp_after_sales @@ -180,5 +182,20 @@ public class ErpAfterSalesBo extends BaseEntity { */ private List materialCostsList; + /** + * 流程定义编码 + */ + private String flowCode; + + /** + * 流程变量 前端会提交一个元素{'entity': {业务详情数据对象}} + */ + private Map variables; + + /** + * 业务扩展参数 + */ + private RemoteFlowInstanceBizExt bizExt; + } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpAfterSalesService.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpAfterSalesService.java index 386b5e46..6c4418f2 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpAfterSalesService.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/IErpAfterSalesService.java @@ -66,4 +66,12 @@ public interface IErpAfterSalesService { * @return 是否删除成功 */ Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 提交售后信息并启动流程 + * @param bo + * @return + */ + ErpAfterSalesVo submitAndFlowStart(ErpAfterSalesBo bo); + } diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpAfterSalesServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpAfterSalesServiceImpl.java index afd8fe6c..0e6e3554 100644 --- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpAfterSalesServiceImpl.java +++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpAfterSalesServiceImpl.java @@ -1,6 +1,12 @@ package org.dromara.oa.erp.service.impl; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import lombok.extern.slf4j.Slf4j; +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.seata.spring.annotation.GlobalTransactional; +import org.dromara.common.core.enums.BusinessStatusEnum; +import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.MapstructUtils; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.mybatis.core.page.TableDataInfo; @@ -10,6 +16,7 @@ import com.github.yulichang.toolkit.JoinWrappers; import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; +import org.dromara.common.tenant.helper.TenantHelper; import org.dromara.oa.erp.domain.ErpAfterSalesLaborCosts; import org.dromara.oa.erp.domain.ErpAfterSalesMaterialCosts; import org.dromara.oa.erp.domain.bo.ErpAfterSalesLaborCostsBo; @@ -18,6 +25,8 @@ import org.dromara.oa.erp.domain.vo.ErpAfterSalesLaborCostsVo; import org.dromara.oa.erp.domain.vo.ErpAfterSalesMaterialCostsVo; import org.dromara.oa.erp.service.IErpAfterSalesLaborCostsService; import org.dromara.oa.erp.service.IErpAfterSalesMaterialCostsService; +import org.dromara.workflow.api.RemoteWorkflowService; +import org.dromara.workflow.api.domain.RemoteStartProcess; import org.springframework.stereotype.Service; import org.dromara.oa.erp.domain.bo.ErpAfterSalesBo; import org.dromara.oa.erp.domain.vo.ErpAfterSalesVo; @@ -26,7 +35,12 @@ import org.dromara.oa.erp.mapper.ErpAfterSalesMapper; import org.dromara.oa.erp.service.IErpAfterSalesService; import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.springframework.context.event.EventListener; +import org.dromara.workflow.api.event.ProcessEvent; + + +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Collection; @@ -39,12 +53,14 @@ import java.util.Collection; */ @RequiredArgsConstructor @Service +@Slf4j public class ErpAfterSalesServiceImpl implements IErpAfterSalesService { private final ErpAfterSalesMapper baseMapper; - // 【修改点 1】注入两个子表的 Service - // 注意:这里必须加 final,配合 @RequiredArgsConstructor 使用 + @DubboReference(timeout = 30000) + private RemoteWorkflowService remoteWorkflowService; + private final IErpAfterSalesLaborCostsService laborCostsService; private final IErpAfterSalesMaterialCostsService materialCostsService; @@ -56,31 +72,21 @@ public class ErpAfterSalesServiceImpl implements IErpAfterSalesService { */ @Override public ErpAfterSalesVo queryById(Long afterSalesId){ - // 1. 查询主表 VO ErpAfterSalesVo vo = baseMapper.selectVoById(afterSalesId); - // 2. 如果主表存在,继续查询子表 if (vo != null) { - // ============ 处理人工费 ============ - // 2.1 使用 list 方法查询出 Entity 列表 List laborEntities = laborCostsService.list( new LambdaQueryWrapper() .eq(ErpAfterSalesLaborCosts::getAfterSalesId, afterSalesId) ); - // 2.2 将 Entity 列表转换为 VO 列表 (核心修复点) List laborVos = MapstructUtils.convert(laborEntities, ErpAfterSalesLaborCostsVo.class); - // 2.3 塞入主表对象 vo.setLaborCostsList(laborVos); - // ============ 处理材料费 ============ - // 3.1 使用 list 方法查询出 Entity 列表 List materialEntities = materialCostsService.list( new LambdaQueryWrapper() .eq(ErpAfterSalesMaterialCosts::getAfterSalesId, afterSalesId) ); - // 3.2 将 Entity 列表转换为 VO 列表 List materialVos = MapstructUtils.convert(materialEntities, ErpAfterSalesMaterialCostsVo.class); - // 3.3 塞入主表对象 vo.setMaterialCostsList(materialVos); } return vo; @@ -155,35 +161,23 @@ public class ErpAfterSalesServiceImpl implements IErpAfterSalesService { @Override @Transactional(rollbackFor = Exception.class) // 【重要】开启事务!如果子表保存失败,主表也要回滚,防止数据不一致 public Boolean insertByBo(ErpAfterSalesBo bo) { - // 1. 将 BO (业务对象) 转换为 Entity (数据库实体) - // MapstructUtils 是 Plus 封装的高性能 Bean 拷贝工具 ErpAfterSales add = MapstructUtils.convert(bo, ErpAfterSales.class); - // 2. 校验数据(这是生成的代码自带的) validEntityBeforeSave(add); - // 3. 保存主表 - // baseMapper.insert(add) 返回受影响行数,大于0表示成功 - // 成功后,MyBatis 会自动把生成的 ID 回填到 add 对象中 boolean flag = baseMapper.insert(add) > 0; + bo.setAfterSalesId(add.getAfterSalesId()); // 将生成的 ID 回填给 BO - // 4. 如果主表保存成功,开始处理子表 if (flag) { - Long afterSalesId = add.getAfterSalesId(); // 拿到主表刚才生成的 ID + Long afterSalesId = add.getAfterSalesId(); - // --- 处理子表 A:人员费用 --- List laborBoList = bo.getLaborCostsList(); - // 使用 CollUtil.isNotEmpty 判断列表不为空 if (CollUtil.isNotEmpty(laborBoList)) { - // 将 BO 列表转为 Entity 列表 List laborList = MapstructUtils.convert(laborBoList, ErpAfterSalesLaborCosts.class); - // 遍历每个子对象,把主表 ID 填进去,建立外键关联 laborList.forEach(item -> item.setAfterSalesId(afterSalesId)); - // 批量保存 laborCostsService.saveBatch(laborList); } - // --- 处理子表 B:材料费用 --- List materialBoList = bo.getMaterialCostsList(); if (CollUtil.isNotEmpty(materialBoList)) { List materialList = MapstructUtils.convert(materialBoList, ErpAfterSalesMaterialCosts.class); @@ -203,17 +197,14 @@ public class ErpAfterSalesServiceImpl implements IErpAfterSalesService { @Override @Transactional(rollbackFor = Exception.class) // 【重要】开启事务 public Boolean updateByBo(ErpAfterSalesBo bo) { - // 1. 更新主表数据 ErpAfterSales update = MapstructUtils.convert(bo, ErpAfterSales.class); validEntityBeforeSave(update); - // updateById 会根据主键 ID 更新其他字段 int row = baseMapper.updateById(update); - // 2. 如果主表更新成功,开始处理子表 if (row > 0) { Long afterSalesId = bo.getAfterSalesId(); - // ================== 1. 处理人员费用 ================== + // ================== 处理人员费用 ================== // 先删除旧的 laborCostsService.remove(new LambdaQueryWrapper() .eq(ErpAfterSalesLaborCosts::getAfterSalesId, afterSalesId)); @@ -223,7 +214,7 @@ public class ErpAfterSalesServiceImpl implements IErpAfterSalesService { if (CollUtil.isNotEmpty(laborBoList)) { List laborList = MapstructUtils.convert(laborBoList, ErpAfterSalesLaborCosts.class); - // 【核心修改点 👇】遍历列表,清空 ID + // 遍历列表,清空 ID laborList.forEach(item -> { item.setLaborCostsId(null); // 关键:清空ID,让MP重新生成雪花ID,避免和逻辑删除的数据冲突 item.setAfterSalesId(afterSalesId); @@ -232,17 +223,14 @@ public class ErpAfterSalesServiceImpl implements IErpAfterSalesService { laborCostsService.saveBatch(laborList); } - // ================== 2. 处理材料费用 ================== - // 先删除旧的 + // ================== 处理材料费用 ================== materialCostsService.remove(new LambdaQueryWrapper() .eq(ErpAfterSalesMaterialCosts::getAfterSalesId, afterSalesId)); - // 后新增新的 List materialBoList = bo.getMaterialCostsList(); if (CollUtil.isNotEmpty(materialBoList)) { List materialList = MapstructUtils.convert(materialBoList, ErpAfterSalesMaterialCosts.class); - // 【核心修改点 👇】遍历列表,清空 ID materialList.forEach(item -> { item.setMaterialCostsId(null); // 关键:清空ID item.setAfterSalesId(afterSalesId); @@ -275,4 +263,104 @@ public class ErpAfterSalesServiceImpl implements IErpAfterSalesService { } return baseMapper.deleteByIds(ids) > 0; } + + /** + * 提交项目售后信息并提交流程 + * + * @param bo + * @return + */ + @Override + @GlobalTransactional(rollbackFor = Exception.class) // 开启全局事务 + public ErpAfterSalesVo submitAndFlowStart(ErpAfterSalesBo bo) { + + ErpAfterSales add = MapstructUtils.convert(bo, ErpAfterSales.class); + validEntityBeforeSave(add); + + if (bo.getAfterSalesId() == null) { + this.insertByBo(bo); + + } else { + this.updateByBo(bo); + } + + // 准备启动流程参数 + // 后端发起需要忽略权限 + if (bo.getVariables() == null) { + bo.setVariables(new HashMap<>()); + } + bo.getVariables().put("ignore", true); + RemoteStartProcess startProcess = new RemoteStartProcess(); + startProcess.setBusinessId(bo.getAfterSalesId().toString()); + startProcess.setFlowCode(bo.getFlowCode()); + startProcess.setVariables(bo.getVariables()); + startProcess.setBizExt(bo.getBizExt()); + + // 确保 BizExt 里也有 BusinessId + if (bo.getBizExt() != null) { + bo.getBizExt().setBusinessId(startProcess.getBusinessId()); + } + + // 调用远程服务启动流程 + // startCompleteTask 表示“启动并自动完成第一个发起节点”,直接流转到下一个审批人 + boolean flagOne = remoteWorkflowService.startCompleteTask(startProcess); + + if (!flagOne) { + throw new ServiceException("流程发起异常"); + } + + return MapstructUtils.convert(add, ErpAfterSalesVo.class); + } + + /** + * 总体流程监听(例如: 草稿,撤销,退回,作废,终止,已完成等) + * + * @param processEvent 参数 + */ + @EventListener(condition = "#processEvent.flowCode == 'OAAS'") + public void processHandler(ProcessEvent processEvent) { + // 多租户上下文切换(防止异步线程找不到租户) + TenantHelper.dynamic(processEvent.getTenantId(), () -> { + log.info("【售后流程监听】开始处理: key={}, status={}", processEvent.getFlowCode(), processEvent.getStatus()); + + // 根据 BusinessId 查询出当前的售后单 + Long afterSalesId = Convert.toLong(processEvent.getBusinessId()); + ErpAfterSales afterSales = baseMapper.selectById(afterSalesId); + + if (afterSales == null) { + log.error("未找到对应的售后单据,id={}", afterSalesId); + return; + } + + // 同步流程状态 (flow_status) + afterSales.setFlowStatus(processEvent.getStatus()); + + // 根据流程状态,更新业务状态 (after_sales_status) + String status = processEvent.getStatus(); + + // A. 审批中 + if (BusinessStatusEnum.WAITING.getStatus().equals(status)) { + afterSales.setAfterSalesStatus("1"); + } + // B. 流程结束/审批通过 (Finish) + else if (BusinessStatusEnum.FINISH.getStatus().equals(status)) { + afterSales.setAfterSalesStatus("2"); + } + // C. 驳回 (Back) 或 撤销 (Cancel) + else if (BusinessStatusEnum.BACK.getStatus().equals(status) + || BusinessStatusEnum.CANCEL.getStatus().equals(status)) { + afterSales.setAfterSalesStatus("0"); + } + // D. 作废 (Invalid) 或 终止 (Termination) + else if (BusinessStatusEnum.INVALID.getStatus().equals(status) + || BusinessStatusEnum.TERMINATION.getStatus().equals(status)) { + afterSales.setAfterSalesStatus("0"); + } + + // 4. 更新数据库 + baseMapper.updateById(afterSales); + }); + } + + }