Browse Source

外呼工作流新增版本管理,支持一键回退旧版本

zyy 2 weeks ago
parent
commit
c43ef99582

+ 27 - 0
fs-company/src/main/java/com/fs/company/controller/company/CompanyWorkflowController.java

@@ -76,6 +76,7 @@ public class CompanyWorkflowController extends BaseController {
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
         param.setCompanyId(loginUser.getUser().getCompanyId());
         param.setCompanyId(loginUser.getUser().getCompanyId());
         param.setCompanyUserId(loginUser.getUser().getUserId());
         param.setCompanyUserId(loginUser.getUser().getUserId());
+        param.setUserName(loginUser.getUsername());
         Long workflowId = companyWorkflowService.saveCompanyWorkflow(param);
         Long workflowId = companyWorkflowService.saveCompanyWorkflow(param);
         return AjaxResult.success(workflowId);
         return AjaxResult.success(workflowId);
     }
     }
@@ -178,4 +179,30 @@ public class CompanyWorkflowController extends BaseController {
         return R.ok().put("data",optionVOS);
         return R.ok().put("data",optionVOS);
     }
     }
 
 
+    /**
+     * 查询某个工作流的版本列表
+     */
+    @GetMapping("/versionList/{workflowId}")
+    public AjaxResult versionList(@PathVariable Long workflowId) {
+        return AjaxResult.success(companyWorkflowService.selectVersionListByWorkflowId(workflowId));
+    }
+
+    /**
+     * 查询某个版本详情
+     */
+    @GetMapping("/versionDetail/{versionId}")
+    public AjaxResult versionDetail(@PathVariable Long versionId) {
+        return AjaxResult.success(companyWorkflowService.selectVersionDetailByVersionId(versionId));
+    }
+
+    /**
+     * 回退到指定版本
+     */
+    @PostMapping("/versionRollback/{versionId}")
+    public AjaxResult rollbackVersion(@PathVariable Long versionId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Long workflowId = companyWorkflowService.rollbackWorkflowVersion(versionId,loginUser.getUsername());
+        return AjaxResult.success(workflowId);
+    }
+
 }
 }

+ 58 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowEdgeVersion.java

@@ -0,0 +1,58 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI工作流连线对象 fs_ai_workflow_edge版本表
+ */
+@Data
+public class CompanyWorkflowEdgeVersion implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    /**
+     * 版本ID
+     */
+    private Long versionId;
+
+    /**
+     * 工作流ID
+     */
+    private Long workflowId;
+
+    /**
+     * 原连线ID
+     */
+    private Long edgeId;
+
+    private String edgeKey;
+
+    private String edgeLabel;
+
+    private String sourceNodeKey;
+
+    private String targetNodeKey;
+
+    private String sourceAnchor;
+
+    private String targetAnchor;
+
+    private String edgeType;
+
+    private String edgeColor;
+
+    private String conditionExpr;
+
+    private Integer sortOrder;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 67 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowNodeVersion.java

@@ -0,0 +1,67 @@
+package com.fs.company.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * AI工作流节点对象 fs_ai_workflow_node
+ */
+@Data
+public class CompanyWorkflowNodeVersion implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+
+    /**
+     * 版本ID
+     */
+    private Long versionId;
+
+    /**
+     * 工作流ID
+     */
+    private Long workflowId;
+
+    /**
+     * 原节点ID
+     */
+    private Long nodeId;
+
+    private String nodeKey;
+
+    private String nodeName;
+
+    private String nodeType;
+
+    private String nodeIcon;
+
+    private String nodeColor;
+
+    private Integer posX;
+
+    private Integer posY;
+
+    private Integer width;
+
+    private Integer height;
+
+    private String nodeConfig;
+
+    private Integer sortOrder;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+
+    /**
+     * 语音URL
+     */
+    private String voiceUrl;
+}

+ 94 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowVersion.java

@@ -0,0 +1,94 @@
+package com.fs.company.domain;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class CompanyWorkflowVersion {
+
+    /**
+     * 版本ID
+     */
+    private Long versionId;
+
+    /**
+     * 工作流ID
+     */
+    private Long workflowId;
+
+    /**
+     * 版本号
+     */
+    private Integer versionNo;
+
+    /**
+     * 工作流名称
+     */
+    private String workflowName;
+
+    /**
+     * 工作流描述
+     */
+    private String workflowDesc;
+
+    /**
+     * 工作流类型
+     */
+    private Integer workflowType;
+
+    /**
+     * 状态 0禁用 1启用
+     */
+    private Integer status;
+
+    /**
+     * 画布JSON
+     */
+    private String canvasData;
+
+    /**
+     * 开始节点key
+     */
+    private String startNodeKey;
+
+    /**
+     * 结束节点key
+     */
+    private String endNodeKey;
+
+    /**
+     * 公司用户ID
+     */
+    private Long companyUserId;
+
+    /**
+     * 公司ID
+     */
+    private Integer companyId;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 快照时间
+     */
+    private Date snapshotTime;
+
+    /**
+     * 快照人
+     */
+    private String snapshotBy;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowEdgeVersionMapper.java

@@ -0,0 +1,15 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyWorkflowEdgeVersion;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyWorkflowEdgeVersionMapper {
+
+    int batchInsert(@Param("list") List<CompanyWorkflowEdgeVersion> list);
+
+    int deleteByVersionIds(@Param("versionIds") List<Long> versionIds);
+
+    List<CompanyWorkflowEdgeVersion> selectByVersionId(@Param("versionId") Long versionId);
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowNodeVersionMapper.java

@@ -0,0 +1,15 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyWorkflowNodeVersion;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyWorkflowNodeVersionMapper {
+
+    int batchInsert(@Param("list") List<CompanyWorkflowNodeVersion> list);
+
+    int deleteByVersionIds(@Param("versionIds") List<Long> versionIds);
+
+    List<CompanyWorkflowNodeVersion> selectByVersionId(@Param("versionId") Long versionId);
+}

+ 25 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowVersionMapper.java

@@ -0,0 +1,25 @@
+package com.fs.company.mapper;
+
+import com.fs.company.domain.CompanyWorkflowVersion;
+import com.fs.company.vo.CompanyWorkflowVersionVo;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyWorkflowVersionMapper {
+
+    int insertCompanyWorkflowVersion(CompanyWorkflowVersion version);
+
+    Integer selectMaxVersionNoByWorkflowId(@Param("workflowId") Long workflowId);
+
+    List<Long> selectVersionIdsByWorkflowId(@Param("workflowId") Long workflowId);
+
+    List<Long> selectExpireVersionIdsByWorkflowId(@Param("workflowId") Long workflowId,
+                                                  @Param("retainCount") Integer retainCount);
+
+    int deleteByVersionIds(@Param("versionIds") List<Long> versionIds);
+
+    List<CompanyWorkflowVersionVo> selectVersionListByWorkflowId(@Param("workflowId") Long workflowId);
+
+    CompanyWorkflowVersion selectCompanyWorkflowVersionByVersionId(@Param("versionId") Long versionId);
+}

+ 3 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWorkflowSaveParam.java

@@ -35,10 +35,13 @@ public class CompanyWorkflowSaveParam implements Serializable {
     private String canvasData;
     private String canvasData;
     private Long companyId;
     private Long companyId;
     private Long companyUserId;
     private Long companyUserId;
+    private String userName;
 
 
     /** 节点列表 */
     /** 节点列表 */
     private List<CompanyWorkflowNode> nodes;
     private List<CompanyWorkflowNode> nodes;
 
 
     /** 连线列表 */
     /** 连线列表 */
     private List<CompanyWorkflowEdge> edges;
     private List<CompanyWorkflowEdge> edges;
+
+    private String remark;
 }
 }

+ 20 - 4
fs-service/src/main/java/com/fs/company/service/ICompanyWorkflowService.java

@@ -7,10 +7,7 @@ import com.fs.company.domain.CompanyWorkflowNode;
 import com.fs.company.domain.CompanyWorkflowNodeType;
 import com.fs.company.domain.CompanyWorkflowNodeType;
 import com.fs.company.param.CompanyWorkflowSaveParam;
 import com.fs.company.param.CompanyWorkflowSaveParam;
 import com.fs.company.param.CompanyWorkflowUpdateBindWCParam;
 import com.fs.company.param.CompanyWorkflowUpdateBindWCParam;
-import com.fs.company.vo.CompanyWorkflowExportVO;
-import com.fs.company.vo.CompanyWorkflowNodeVoiceVo;
-import com.fs.company.vo.CompanyWorkflowVO;
-import com.fs.company.vo.OptionVO;
+import com.fs.company.vo.*;
 
 
 import java.util.List;
 import java.util.List;
 
 
@@ -32,6 +29,25 @@ public interface ICompanyWorkflowService {
      */
      */
     List<CompanyWorkflow> selectCompanyWorkflowList(CompanyWorkflow fsAiWorkflow);
     List<CompanyWorkflow> selectCompanyWorkflowList(CompanyWorkflow fsAiWorkflow);
 
 
+    /**
+     * 查询工作流版本详情(包含节点和连线)
+     */
+    CompanyWorkflowVersionDetailVo selectVersionDetailByVersionId(Long versionId);
+
+    /**
+     * 查询工作流版本列表
+     * @param workflowId
+     * @return
+     */
+    List<CompanyWorkflowVersionVo> selectVersionListByWorkflowId(Long workflowId);
+
+    /**
+     * 回滚工作流版本
+     * @param versionId
+     * @return
+     */
+    Long rollbackWorkflowVersion(Long versionId,String userName);
+
     /**
     /**
      * 保存工作流(新增或更新)
      * 保存工作流(新增或更新)
      */
      */

+ 343 - 27
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowServiceImpl.java

@@ -1,17 +1,17 @@
 package com.fs.company.service.impl;
 package com.fs.company.service.impl;
 
 
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.exception.ServiceException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.common.utils.spring.SpringUtils;
 import com.fs.company.domain.*;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.*;
 import com.fs.company.mapper.*;
 import com.fs.company.param.CompanyWorkflowSaveParam;
 import com.fs.company.param.CompanyWorkflowSaveParam;
 import com.fs.company.param.CompanyWorkflowUpdateBindWCParam;
 import com.fs.company.param.CompanyWorkflowUpdateBindWCParam;
 import com.fs.company.service.ICompanyWorkflowService;
 import com.fs.company.service.ICompanyWorkflowService;
-import com.fs.company.vo.CompanyWorkflowExportVO;
-import com.fs.company.vo.CompanyWorkflowNodeVoiceVo;
-import com.fs.company.vo.CompanyWorkflowVO;
-import com.fs.company.vo.OptionVO;
+import com.fs.company.vo.*;
 import com.fs.enums.NodeTypeEnum;
 import com.fs.enums.NodeTypeEnum;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -38,6 +38,14 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
     private CompanyWorkflowEdgeMapper companyWorkflowEdgeMapper;
     private CompanyWorkflowEdgeMapper companyWorkflowEdgeMapper;
     @Autowired
     @Autowired
     private CompanyWorkflowNodeTypeMapper companyWorkflowNodeTypeMapper;
     private CompanyWorkflowNodeTypeMapper companyWorkflowNodeTypeMapper;
+    @Autowired
+    private CompanyWorkflowVersionMapper companyWorkflowVersionMapper;
+
+    @Autowired
+    private CompanyWorkflowNodeVersionMapper companyWorkflowNodeVersionMapper;
+
+    @Autowired
+    private CompanyWorkflowEdgeVersionMapper companyWorkflowEdgeVersionMapper;
 
 
     private static List<String> aiCallNodeTypes = Arrays.asList("param", "ai_chat","end");
     private static List<String> aiCallNodeTypes = Arrays.asList("param", "ai_chat","end");
     @Override
     @Override
@@ -58,14 +66,103 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
         return companyWorkflowMapper.selectCompanyWorkflowList(fsAiWorkflow);
         return companyWorkflowMapper.selectCompanyWorkflowList(fsAiWorkflow);
     }
     }
 
 
+//    @Override
+//    @Transactional(rollbackFor = Exception.class)
+//    public Long saveCompanyWorkflow(CompanyWorkflowSaveParam param) {
+//        Date now = DateUtils.getNowDate();
+//        Long workflowId = param.getWorkflowId();
+//
+//        if (workflowId == null) {
+//            // 新增
+//            CompanyWorkflow workflow = new CompanyWorkflow();
+//            workflow.setWorkflowName(param.getWorkflowName());
+//            workflow.setWorkflowDesc(param.getWorkflowDesc());
+//            workflow.setWorkflowType(param.getWorkflowType());
+//            workflow.setCanvasData(param.getCanvasData());
+//            workflow.setStatus(1);
+//            workflow.setVersion(1);
+//            workflow.setCreateTime(now);
+//            workflow.setStartNodeKey(param.getStartNodeKey());
+//            workflow.setEndNodeKey(param.getEndNodeKey());
+//            workflow.setCompanyId(param.getCompanyId());
+//            workflow.setCompanyUserId(param.getCompanyUserId());
+//            companyWorkflowMapper.insertCompanyWorkflow(workflow);
+//            workflowId = workflow.getWorkflowId();
+//        } else {
+//            // 更新
+//            CompanyWorkflow workflow = new CompanyWorkflow();
+//            workflow.setWorkflowId(workflowId);
+//            workflow.setWorkflowName(param.getWorkflowName());
+//            workflow.setWorkflowDesc(param.getWorkflowDesc());
+//            workflow.setWorkflowType(param.getWorkflowType());
+//            workflow.setCanvasData(param.getCanvasData());
+//            workflow.setStartNodeKey(param.getStartNodeKey());
+//            workflow.setEndNodeKey(param.getEndNodeKey());
+//            workflow.setUpdateTime(now);
+//            workflow.setCompanyId(param.getCompanyId());
+//            workflow.setCompanyUserId(param.getCompanyUserId());
+//            companyWorkflowMapper.updateCompanyWorkflow(workflow);
+//            // 删除旧的节点和连线
+//            companyWorkflowNodeMapper.deleteCompanyWorkflowNodeByWorkflowId(workflowId);
+//            companyWorkflowEdgeMapper.deleteCompanyWorkflowEdgeByWorkflowId(workflowId);
+//        }
+//
+//        // 保存节点
+//        List<CompanyWorkflowNode> nodes = param.getNodes();
+//        if (nodes != null && !nodes.isEmpty()) {
+//            for (CompanyWorkflowNode node : nodes) {
+//                node.setWorkflowId(workflowId);
+//                node.setCreateTime(now);
+//            }
+//            companyWorkflowNodeMapper.batchInsertCompanyWorkflowNode(nodes);
+//        }
+//
+//        // 保存连线
+//        List<CompanyWorkflowEdge> edges = param.getEdges();
+//        if (edges != null && !edges.isEmpty()) {
+//            for (CompanyWorkflowEdge edge : edges) {
+//                edge.setWorkflowId(workflowId);
+//                edge.setCreateTime(now);
+//            }
+//            companyWorkflowEdgeMapper.batchInsertCompanyWorkflowEdge(edges);
+//        }
+//
+//        //更新工作流后,需要删除或新增录音节点对应的id
+//
+//        List<String> nodeKeyOlds = companyWorkflowMapper.selectCompanyWorkflowNodeByWorkflowId(workflowId);
+//        List<Long> companyUserIds = companyWorkflowMapper.selectWorkflowCompanyUserByWfId(workflowId);
+//
+//        // 提取新节点的nodeKey
+//        List<String> newNodeKeys = nodes.stream()
+//                .filter(node -> aiCallNodeTypes.contains(node.getNodeType()))
+//                .map(CompanyWorkflowNode::getNodeKey)
+//                .collect(Collectors.toList());
+//
+//        // 找出新增的nodeKey(新节点中有,旧节点中没有)
+//        List<String> nodeKeyAdd = newNodeKeys.stream()
+//                .filter(key -> !nodeKeyOlds.contains(key))
+//                .collect(Collectors.toList());
+//
+//        // 找出需要删除的nodeKey(旧节点中有,新节点中没有)
+//        List<String> nodeKeyDel = nodeKeyOlds.stream()
+//                .filter(key -> !newNodeKeys.contains(key))
+//                .collect(Collectors.toList());
+//        if (!nodeKeyDel.isEmpty())companyWorkflowMapper.deleteAiWorkflowCompanyUserVoice(workflowId, nodeKeyDel);
+//        if (!nodeKeyAdd.isEmpty())companyWorkflowMapper.addAiWorkflowCompanyUserVoice(workflowId,companyUserIds, nodeKeyAdd);
+//        return workflowId;
+//    }
+
     @Override
     @Override
     @Transactional(rollbackFor = Exception.class)
     @Transactional(rollbackFor = Exception.class)
     public Long saveCompanyWorkflow(CompanyWorkflowSaveParam param) {
     public Long saveCompanyWorkflow(CompanyWorkflowSaveParam param) {
         Date now = DateUtils.getNowDate();
         Date now = DateUtils.getNowDate();
         Long workflowId = param.getWorkflowId();
         Long workflowId = param.getWorkflowId();
 
 
+        int versionNo;
+
         if (workflowId == null) {
         if (workflowId == null) {
             // 新增
             // 新增
+            versionNo = 1;
             CompanyWorkflow workflow = new CompanyWorkflow();
             CompanyWorkflow workflow = new CompanyWorkflow();
             workflow.setWorkflowName(param.getWorkflowName());
             workflow.setWorkflowName(param.getWorkflowName());
             workflow.setWorkflowDesc(param.getWorkflowDesc());
             workflow.setWorkflowDesc(param.getWorkflowDesc());
@@ -81,7 +178,10 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
             companyWorkflowMapper.insertCompanyWorkflow(workflow);
             companyWorkflowMapper.insertCompanyWorkflow(workflow);
             workflowId = workflow.getWorkflowId();
             workflowId = workflow.getWorkflowId();
         } else {
         } else {
-            // 更新
+            // 更新时先查当前最大版本号,主表 version 跟着递增
+            Integer maxVersionNo = companyWorkflowVersionMapper.selectMaxVersionNoByWorkflowId(workflowId);
+            versionNo = (maxVersionNo == null ? 0 : maxVersionNo) + 1;
+
             CompanyWorkflow workflow = new CompanyWorkflow();
             CompanyWorkflow workflow = new CompanyWorkflow();
             workflow.setWorkflowId(workflowId);
             workflow.setWorkflowId(workflowId);
             workflow.setWorkflowName(param.getWorkflowName());
             workflow.setWorkflowName(param.getWorkflowName());
@@ -93,6 +193,7 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
             workflow.setUpdateTime(now);
             workflow.setUpdateTime(now);
             workflow.setCompanyId(param.getCompanyId());
             workflow.setCompanyId(param.getCompanyId());
             workflow.setCompanyUserId(param.getCompanyUserId());
             workflow.setCompanyUserId(param.getCompanyUserId());
+            workflow.setVersion(versionNo);
             companyWorkflowMapper.updateCompanyWorkflow(workflow);
             companyWorkflowMapper.updateCompanyWorkflow(workflow);
             // 删除旧的节点和连线
             // 删除旧的节点和连线
             companyWorkflowNodeMapper.deleteCompanyWorkflowNodeByWorkflowId(workflowId);
             companyWorkflowNodeMapper.deleteCompanyWorkflowNodeByWorkflowId(workflowId);
@@ -118,29 +219,10 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
             }
             }
             companyWorkflowEdgeMapper.batchInsertCompanyWorkflowEdge(edges);
             companyWorkflowEdgeMapper.batchInsertCompanyWorkflowEdge(edges);
         }
         }
+        // 保存版本快照
+        saveWorkflowSnapshot(workflowId, versionNo, now, param.getUserName());
+        clearOldVersions(workflowId,3);
 
 
-        //更新工作流后,需要删除或新增录音节点对应的id
-
-        List<String> nodeKeyOlds = companyWorkflowMapper.selectCompanyWorkflowNodeByWorkflowId(workflowId);
-        List<Long> companyUserIds = companyWorkflowMapper.selectWorkflowCompanyUserByWfId(workflowId);
-
-        // 提取新节点的nodeKey
-        List<String> newNodeKeys = nodes.stream()
-                .filter(node -> aiCallNodeTypes.contains(node.getNodeType()))
-                .map(CompanyWorkflowNode::getNodeKey)
-                .collect(Collectors.toList());
-
-        // 找出新增的nodeKey(新节点中有,旧节点中没有)
-        List<String> nodeKeyAdd = newNodeKeys.stream()
-                .filter(key -> !nodeKeyOlds.contains(key))
-                .collect(Collectors.toList());
-
-        // 找出需要删除的nodeKey(旧节点中有,新节点中没有)
-        List<String> nodeKeyDel = nodeKeyOlds.stream()
-                .filter(key -> !newNodeKeys.contains(key))
-                .collect(Collectors.toList());
-        if (!nodeKeyDel.isEmpty())companyWorkflowMapper.deleteAiWorkflowCompanyUserVoice(workflowId, nodeKeyDel);
-        if (!nodeKeyAdd.isEmpty())companyWorkflowMapper.addAiWorkflowCompanyUserVoice(workflowId,companyUserIds, nodeKeyAdd);
         return workflowId;
         return workflowId;
     }
     }
 
 
@@ -378,4 +460,238 @@ public class CompanyWorkflowServiceImpl implements ICompanyWorkflowService {
     public List<OptionVO> optionList(Long companyId) {
     public List<OptionVO> optionList(Long companyId) {
         return companyWorkflowMapper.optionList(companyId);
         return companyWorkflowMapper.optionList(companyId);
     }
     }
+
+
+    @Override
+    public List<CompanyWorkflowVersionVo> selectVersionListByWorkflowId(Long workflowId) {
+        return companyWorkflowVersionMapper.selectVersionListByWorkflowId(workflowId);
+    }
+
+    @Override
+    public CompanyWorkflowVersionDetailVo selectVersionDetailByVersionId(Long versionId) {
+        CompanyWorkflowVersion workflowVersion = companyWorkflowVersionMapper.selectCompanyWorkflowVersionByVersionId(versionId);
+        if (workflowVersion == null) {
+            throw new ServiceException("版本记录不存在,versionId=" + versionId);
+        }
+
+        List<CompanyWorkflowNodeVersion> nodes = companyWorkflowNodeVersionMapper.selectByVersionId(versionId);
+        List<CompanyWorkflowEdgeVersion> edges = companyWorkflowEdgeVersionMapper.selectByVersionId(versionId);
+
+        CompanyWorkflowVersionDetailVo vo = new CompanyWorkflowVersionDetailVo();
+        vo.setWorkflowVersion(workflowVersion);
+        vo.setNodes(nodes);
+        vo.setEdges(edges);
+        return vo;
+    }
+
+
+    /**
+     * 将主表当前状态保存到版本表
+     */
+    /**
+     * 将主表当前状态保存到版本表
+     */
+    private void saveWorkflowSnapshot(Long workflowId, Integer versionNo, Date now, String userName) {
+        CompanyWorkflow workflow = companyWorkflowMapper.selectCompanyWorkflowById(workflowId);
+        if (workflow == null) {
+            throw new ServiceException("工作流不存在,无法生成快照,workflowId=" + workflowId);
+        }
+
+        CompanyWorkflowVersion version = new CompanyWorkflowVersion();
+        version.setWorkflowId(workflowId);
+        version.setVersionNo(versionNo);
+        version.setWorkflowName(workflow.getWorkflowName());
+        version.setWorkflowDesc(workflow.getWorkflowDesc());
+        version.setWorkflowType(workflow.getWorkflowType());
+        version.setStatus(workflow.getStatus());
+        version.setCanvasData(workflow.getCanvasData());
+        version.setStartNodeKey(workflow.getStartNodeKey());
+        version.setEndNodeKey(workflow.getEndNodeKey());
+        version.setCompanyUserId(workflow.getCompanyUserId());
+        version.setCompanyId(Math.toIntExact(workflow.getCompanyId()));
+        version.setRemark(workflow.getRemark());
+        version.setSnapshotTime(now);
+        version.setSnapshotBy(null);
+        version.setCreateTime(workflow.getCreateTime());
+        version.setUpdateTime(workflow.getUpdateTime());
+        version.setSnapshotBy(userName);
+
+        companyWorkflowVersionMapper.insertCompanyWorkflowVersion(version);
+        Long versionId = version.getVersionId();
+
+        // 保存节点快照
+        List<CompanyWorkflowNode> nodes = companyWorkflowNodeMapper.selectCompanyWorkflowNodeByWorkflowId(workflowId);
+        if (nodes != null && !nodes.isEmpty()) {
+            List<CompanyWorkflowNodeVersion> nodeVersionList = new ArrayList<>(nodes.size());
+            for (CompanyWorkflowNode node : nodes) {
+                CompanyWorkflowNodeVersion nv = new CompanyWorkflowNodeVersion();
+                nv.setVersionId(versionId);
+                nv.setWorkflowId(workflowId);
+                nv.setNodeId(node.getNodeId());
+                nv.setNodeKey(node.getNodeKey());
+                nv.setNodeName(node.getNodeName());
+                nv.setNodeType(node.getNodeType());
+                nv.setNodeIcon(node.getNodeIcon());
+                nv.setNodeColor(node.getNodeColor());
+                nv.setPosX(node.getPosX());
+                nv.setPosY(node.getPosY());
+                nv.setWidth(node.getWidth());
+                nv.setHeight(node.getHeight());
+                nv.setNodeConfig(node.getNodeConfig());
+                nv.setSortOrder(node.getSortOrder());
+                nv.setVoiceUrl(node.getVoiceUrl());
+                nv.setCreateTime(node.getCreateTime());
+                nv.setUpdateTime(node.getUpdateTime());
+                nodeVersionList.add(nv);
+            }
+            companyWorkflowNodeVersionMapper.batchInsert(nodeVersionList);
+        }
+
+        // 保存连线快照
+        List<CompanyWorkflowEdge> edges = companyWorkflowEdgeMapper.selectCompanyWorkflowEdgeByWorkflowId(workflowId);
+        if (edges != null && !edges.isEmpty()) {
+            List<CompanyWorkflowEdgeVersion> edgeVersionList = new ArrayList<>(edges.size());
+            for (CompanyWorkflowEdge edge : edges) {
+                CompanyWorkflowEdgeVersion ev = new CompanyWorkflowEdgeVersion();
+                ev.setVersionId(versionId);
+                ev.setWorkflowId(workflowId);
+                ev.setEdgeId(edge.getEdgeId());
+                ev.setEdgeKey(edge.getEdgeKey());
+                ev.setEdgeLabel(edge.getEdgeLabel());
+                ev.setSourceNodeKey(edge.getSourceNodeKey());
+                ev.setTargetNodeKey(edge.getTargetNodeKey());
+                ev.setSourceAnchor(edge.getSourceAnchor());
+                ev.setTargetAnchor(edge.getTargetAnchor());
+                ev.setEdgeType(edge.getEdgeType());
+                ev.setEdgeColor(edge.getEdgeColor());
+                ev.setConditionExpr(edge.getConditionExpr());
+                ev.setSortOrder(edge.getSortOrder());
+                ev.setCreateTime(edge.getCreateTime());
+                ev.setUpdateTime(edge.getUpdateTime());
+                edgeVersionList.add(ev);
+            }
+            companyWorkflowEdgeVersionMapper.batchInsert(edgeVersionList);
+        }
+    }
+
+    /**
+     * 只保留最近 retainCount 个版本
+     */
+    private void clearOldVersions(Long workflowId, int retainCount) {
+        List<Long> expireVersionIds = companyWorkflowVersionMapper.selectExpireVersionIdsByWorkflowId(workflowId, retainCount);
+        if (expireVersionIds == null || expireVersionIds.isEmpty()) {
+            return;
+        }
+        companyWorkflowNodeVersionMapper.deleteByVersionIds(expireVersionIds);
+        companyWorkflowEdgeVersionMapper.deleteByVersionIds(expireVersionIds);
+        companyWorkflowVersionMapper.deleteByVersionIds(expireVersionIds);
+    }
+
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long rollbackWorkflowVersion(Long versionId, String userName) {
+        Date now = DateUtils.getNowDate();
+
+        // 1. 查版本主表
+        CompanyWorkflowVersion version = companyWorkflowVersionMapper.selectCompanyWorkflowVersionByVersionId(versionId);
+        if (version == null) {
+            throw new ServiceException("版本记录不存在,versionId=" + versionId);
+        }
+
+        Long workflowId = version.getWorkflowId();
+
+        // 当前主表版本
+        CompanyWorkflow currentWorkflow = companyWorkflowMapper.selectCompanyWorkflowById(workflowId);
+        if (currentWorkflow != null && currentWorkflow.getVersion() != null
+                && currentWorkflow.getVersion().equals(version.getVersionNo())) {
+            throw new ServiceException("当前版本不允许回退");
+        }
+
+        // 2. 查版本节点、连线
+        List<CompanyWorkflowNodeVersion> nodeVersions = companyWorkflowNodeVersionMapper.selectByVersionId(versionId);
+        List<CompanyWorkflowEdgeVersion> edgeVersions = companyWorkflowEdgeVersionMapper.selectByVersionId(versionId);
+
+        // 3. 计算新的版本号(回退本身也算一次新保存)
+        Integer maxVersionNo = companyWorkflowVersionMapper.selectMaxVersionNoByWorkflowId(workflowId);
+        int newVersionNo = (maxVersionNo == null ? 0 : maxVersionNo) + 1;
+
+        // 4. 更新主表
+        CompanyWorkflow workflow = new CompanyWorkflow();
+        workflow.setWorkflowId(workflowId);
+        workflow.setWorkflowName(version.getWorkflowName());
+        workflow.setWorkflowDesc(version.getWorkflowDesc());
+        workflow.setWorkflowType(version.getWorkflowType());
+        workflow.setCanvasData(version.getCanvasData());
+        workflow.setStatus(version.getStatus());
+        workflow.setStartNodeKey(version.getStartNodeKey());
+        workflow.setEndNodeKey(version.getEndNodeKey());
+        workflow.setCompanyId(version.getCompanyId().longValue());
+        workflow.setCompanyUserId(version.getCompanyUserId());
+        workflow.setRemark(version.getRemark());
+        workflow.setVersion(newVersionNo);
+        workflow.setUpdateTime(now);
+
+        companyWorkflowMapper.updateCompanyWorkflow(workflow);
+
+        // 5. 删除当前节点和连线
+        companyWorkflowNodeMapper.deleteCompanyWorkflowNodeByWorkflowId(workflowId);
+        companyWorkflowEdgeMapper.deleteCompanyWorkflowEdgeByWorkflowId(workflowId);
+
+        // 6. 回写节点
+        if (nodeVersions != null && !nodeVersions.isEmpty()) {
+            List<CompanyWorkflowNode> nodes = new ArrayList<>(nodeVersions.size());
+            for (CompanyWorkflowNodeVersion item : nodeVersions) {
+                CompanyWorkflowNode node = new CompanyWorkflowNode();
+                node.setWorkflowId(workflowId);
+                node.setNodeKey(item.getNodeKey());
+                node.setNodeName(item.getNodeName());
+                node.setNodeType(item.getNodeType());
+                node.setNodeIcon(item.getNodeIcon());
+                node.setNodeColor(item.getNodeColor());
+                node.setPosX(item.getPosX());
+                node.setPosY(item.getPosY());
+                node.setWidth(item.getWidth());
+                node.setHeight(item.getHeight());
+                node.setNodeConfig(item.getNodeConfig());
+                node.setSortOrder(item.getSortOrder());
+                node.setVoiceUrl(item.getVoiceUrl());
+                node.setCreateTime(now);
+                node.setUpdateTime(now);
+                nodes.add(node);
+            }
+            companyWorkflowNodeMapper.batchInsertCompanyWorkflowNode(nodes);
+        }
+
+        // 7. 回写连线
+        if (edgeVersions != null && !edgeVersions.isEmpty()) {
+            List<CompanyWorkflowEdge> edges = new ArrayList<>(edgeVersions.size());
+            for (CompanyWorkflowEdgeVersion item : edgeVersions) {
+                CompanyWorkflowEdge edge = new CompanyWorkflowEdge();
+                edge.setWorkflowId(workflowId);
+                edge.setEdgeKey(item.getEdgeKey());
+                edge.setEdgeLabel(item.getEdgeLabel());
+                edge.setSourceNodeKey(item.getSourceNodeKey());
+                edge.setTargetNodeKey(item.getTargetNodeKey());
+                edge.setSourceAnchor(item.getSourceAnchor());
+                edge.setTargetAnchor(item.getTargetAnchor());
+                edge.setEdgeType(item.getEdgeType());
+                edge.setEdgeColor(item.getEdgeColor());
+                edge.setConditionExpr(item.getConditionExpr());
+                edge.setSortOrder(item.getSortOrder());
+                edge.setCreateTime(now);
+                edge.setUpdateTime(now);
+                edges.add(edge);
+            }
+            companyWorkflowEdgeMapper.batchInsertCompanyWorkflowEdge(edges);
+        }
+
+        // 8. 再存一个新的快照
+        saveWorkflowSnapshot(workflowId, newVersionNo, now, userName);
+
+        // 9. 只保留最近3个版本
+        clearOldVersions(workflowId, 3);
+
+        return workflowId;
+    }
 }
 }

+ 18 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyWorkflowVersionDetailVo.java

@@ -0,0 +1,18 @@
+package com.fs.company.vo;
+
+import com.fs.company.domain.CompanyWorkflowEdgeVersion;
+import com.fs.company.domain.CompanyWorkflowNodeVersion;
+import com.fs.company.domain.CompanyWorkflowVersion;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CompanyWorkflowVersionDetailVo {
+
+    private CompanyWorkflowVersion workflowVersion;
+
+    private List<CompanyWorkflowNodeVersion> nodes;
+
+    private List<CompanyWorkflowEdgeVersion> edges;
+}

+ 23 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyWorkflowVersionVo.java

@@ -0,0 +1,23 @@
+package com.fs.company.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class CompanyWorkflowVersionVo {
+
+    private Long versionId;
+
+    private Long workflowId;
+
+    private Integer versionNo;
+
+    private String workflowName;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date snapshotTime;
+
+    private String snapshotBy;
+}

+ 82 - 0
fs-service/src/main/resources/mapper/company/CompanyWorkflowEdgeVersionMapper.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyWorkflowEdgeVersionMapper">
+
+    <resultMap id="CompanyWorkflowEdgeVersionResult" type="com.fs.company.domain.CompanyWorkflowEdgeVersion">
+        <id property="id" column="id"/>
+        <result property="versionId" column="version_id"/>
+        <result property="workflowId" column="workflow_id"/>
+        <result property="edgeId" column="edge_id"/>
+        <result property="edgeKey" column="edge_key"/>
+        <result property="edgeLabel" column="edge_label"/>
+        <result property="sourceNodeKey" column="source_node_key"/>
+        <result property="targetNodeKey" column="target_node_key"/>
+        <result property="sourceAnchor" column="source_anchor"/>
+        <result property="targetAnchor" column="target_anchor"/>
+        <result property="edgeType" column="edge_type"/>
+        <result property="edgeColor" column="edge_color"/>
+        <result property="conditionExpr" column="condition_expr"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <insert id="batchInsert">
+        insert into company_ai_workflow_edge_version
+        (
+        version_id,
+        workflow_id,
+        edge_id,
+        edge_key,
+        edge_label,
+        source_node_key,
+        target_node_key,
+        source_anchor,
+        target_anchor,
+        edge_type,
+        edge_color,
+        condition_expr,
+        sort_order,
+        create_time,
+        update_time
+        )
+        values
+        <foreach collection="list" item="item" separator=",">
+            (
+            #{item.versionId},
+            #{item.workflowId},
+            #{item.edgeId},
+            #{item.edgeKey},
+            #{item.edgeLabel},
+            #{item.sourceNodeKey},
+            #{item.targetNodeKey},
+            #{item.sourceAnchor},
+            #{item.targetAnchor},
+            #{item.edgeType},
+            #{item.edgeColor},
+            #{item.conditionExpr},
+            #{item.sortOrder},
+            #{item.createTime},
+            #{item.updateTime}
+            )
+        </foreach>
+    </insert>
+
+    <delete id="deleteByVersionIds">
+        delete from company_ai_workflow_edge_version
+        where version_id in
+        <foreach collection="versionIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectByVersionId" resultMap="CompanyWorkflowEdgeVersionResult">
+        select *
+        from company_ai_workflow_edge_version
+        where version_id = #{versionId}
+        order by sort_order asc, id asc
+    </select>
+
+</mapper>

+ 88 - 0
fs-service/src/main/resources/mapper/company/CompanyWorkflowNodeVersionMapper.xml

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyWorkflowNodeVersionMapper">
+
+    <resultMap id="CompanyWorkflowNodeVersionResult" type="com.fs.company.domain.CompanyWorkflowNodeVersion">
+        <id property="id" column="id"/>
+        <result property="versionId" column="version_id"/>
+        <result property="workflowId" column="workflow_id"/>
+        <result property="nodeId" column="node_id"/>
+        <result property="nodeKey" column="node_key"/>
+        <result property="nodeName" column="node_name"/>
+        <result property="nodeType" column="node_type"/>
+        <result property="nodeIcon" column="node_icon"/>
+        <result property="nodeColor" column="node_color"/>
+        <result property="posX" column="pos_x"/>
+        <result property="posY" column="pos_y"/>
+        <result property="width" column="width"/>
+        <result property="height" column="height"/>
+        <result property="nodeConfig" column="node_config"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="voiceUrl" column="voice_url"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <insert id="batchInsert">
+        insert into company_ai_workflow_node_version
+        (
+        version_id,
+        workflow_id,
+        node_id,
+        node_key,
+        node_name,
+        node_type,
+        node_icon,
+        node_color,
+        pos_x,
+        pos_y,
+        width,
+        height,
+        node_config,
+        sort_order,
+        voice_url,
+        create_time,
+        update_time
+        )
+        values
+        <foreach collection="list" item="item" separator=",">
+            (
+            #{item.versionId},
+            #{item.workflowId},
+            #{item.nodeId},
+            #{item.nodeKey},
+            #{item.nodeName},
+            #{item.nodeType},
+            #{item.nodeIcon},
+            #{item.nodeColor},
+            #{item.posX},
+            #{item.posY},
+            #{item.width},
+            #{item.height},
+            #{item.nodeConfig},
+            #{item.sortOrder},
+            #{item.voiceUrl},
+            #{item.createTime},
+            #{item.updateTime}
+            )
+        </foreach>
+    </insert>
+
+    <delete id="deleteByVersionIds">
+        delete from company_ai_workflow_node_version
+        where version_id in
+        <foreach collection="versionIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectByVersionId" resultMap="CompanyWorkflowNodeVersionResult">
+        select *
+        from company_ai_workflow_node_version
+        where version_id = #{versionId}
+        order by sort_order asc, id asc
+    </select>
+
+</mapper>

+ 106 - 0
fs-service/src/main/resources/mapper/company/CompanyWorkflowVersionMapper.xml

@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyWorkflowVersionMapper">
+
+    <resultMap id="CompanyWorkflowVersionResult" type="com.fs.company.domain.CompanyWorkflowVersion">
+        <id property="versionId" column="version_id"/>
+        <result property="workflowId" column="workflow_id"/>
+        <result property="versionNo" column="version_no"/>
+        <result property="workflowName" column="workflow_name"/>
+        <result property="workflowDesc" column="workflow_desc"/>
+        <result property="workflowType" column="workflow_type"/>
+        <result property="status" column="status"/>
+        <result property="canvasData" column="canvas_data"/>
+        <result property="startNodeKey" column="start_node_key"/>
+        <result property="endNodeKey" column="end_node_key"/>
+        <result property="companyUserId" column="company_user_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="remark" column="remark"/>
+        <result property="snapshotTime" column="snapshot_time"/>
+        <result property="snapshotBy" column="snapshot_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <insert id="insertCompanyWorkflowVersion" useGeneratedKeys="true" keyProperty="versionId">
+        insert into company_ai_workflow_version
+        (
+            workflow_id,
+            version_no,
+            workflow_name,
+            workflow_desc,
+            workflow_type,
+            status,
+            canvas_data,
+            start_node_key,
+            end_node_key,
+            company_user_id,
+            company_id,
+            remark,
+            snapshot_time,
+            snapshot_by
+        )
+        values
+            (
+                #{workflowId},
+                #{versionNo},
+                #{workflowName},
+                #{workflowDesc},
+                #{workflowType},
+                #{status},
+                #{canvasData},
+                #{startNodeKey},
+                #{endNodeKey},
+                #{companyUserId},
+                #{companyId},
+                #{remark},
+                #{snapshotTime},
+                #{snapshotBy}
+            )
+    </insert>
+
+    <select id="selectMaxVersionNoByWorkflowId" resultType="java.lang.Integer">
+        select ifnull(max(version_no), 0)
+        from company_ai_workflow_version
+        where workflow_id = #{workflowId}
+    </select>
+
+    <select id="selectExpireVersionIdsByWorkflowId" resultType="java.lang.Long">
+        select version_id
+        from company_ai_workflow_version
+        where workflow_id = #{workflowId}
+        order by version_no desc
+            limit 999999 offset #{retainCount}
+    </select>
+
+    <delete id="deleteByVersionIds">
+        delete from company_ai_workflow_version
+        where version_id in
+        <foreach collection="versionIds" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectVersionListByWorkflowId" resultType="com.fs.company.vo.CompanyWorkflowVersionVo">
+        select
+            version_id as versionId,
+            workflow_id as workflowId,
+            version_no as versionNo,
+            workflow_name as workflowName,
+            snapshot_time as snapshotTime,
+            snapshot_by as snapshotBy
+        from company_ai_workflow_version
+        where workflow_id = #{workflowId}
+        order by version_no desc
+    </select>
+
+    <select id="selectCompanyWorkflowVersionByVersionId" resultMap="CompanyWorkflowVersionResult">
+        select *
+        from company_ai_workflow_version
+        where version_id = #{versionId}
+            limit 1
+    </select>
+
+</mapper>