|
|
|
|
@ -37,17 +37,18 @@ import org.springframework.context.event.EventListener;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.Collection;
|
|
|
|
|
import java.util.Collections;
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Objects;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 服务器部署信息Service业务层处理
|
|
|
|
|
* <p>
|
|
|
|
|
* 核心职责:
|
|
|
|
|
* 1. 管理服务器部署信息的增删改查,支持多版本快照管理;
|
|
|
|
|
* 2. 对接 Warm-Flow 工作流引擎,实现草稿→提交审批→审批通过/退回/作废的全生命周期;
|
|
|
|
|
* 3. 版本控制:审批通过的记录不可直接修改,变更时自动创建新版本;
|
|
|
|
|
* 4. 变更日志:新版本创建时自动对比旧版本字段差异,生成变更日志;
|
|
|
|
|
* 5. 安全校验:部署路径防注入校验、端口格式校验、服务器状态校验等。
|
|
|
|
|
*
|
|
|
|
|
* @author zch
|
|
|
|
|
* @date 2026-06-03
|
|
|
|
|
@ -57,7 +58,7 @@ import java.util.regex.Pattern;
|
|
|
|
|
@Service
|
|
|
|
|
public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
|
|
|
|
|
/** 服务器部署操作流程编码 */
|
|
|
|
|
/** 服务器部署操作流程编码(对应 Warm-Flow 中的流程定义编码) */
|
|
|
|
|
public static final String FLOW_CODE_DEPLOY = "OASD";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -65,35 +66,57 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
*/
|
|
|
|
|
private static final String DEPLOY_CODE_RULE = "1036";
|
|
|
|
|
|
|
|
|
|
private static final String YES = "1";
|
|
|
|
|
private static final String NO = "0";
|
|
|
|
|
private static final String YES = "1"; // 是
|
|
|
|
|
private static final String NO = "0"; // 否
|
|
|
|
|
/** 端口格式正则:支持单端口(8080)、逗号分隔(8080,8081)、端口范围(8080-8090) */
|
|
|
|
|
private static final Pattern PORT_PATTERN = Pattern.compile("^\\d{1,5}(-\\d{1,5})?(,\\d{1,5}(-\\d{1,5})?)*$");
|
|
|
|
|
/** 危险路径字符正则:防止命令注入,禁止分号、管道符、反引号、美元符号、重定向符、换行等 */
|
|
|
|
|
private static final Pattern DANGEROUS_PATH_PATTERN = Pattern.compile("[;&|`$<>\\r\\n]");
|
|
|
|
|
|
|
|
|
|
private final ErpServerDeployMapper baseMapper;
|
|
|
|
|
private final ErpServerDeployLogMapper deployLogMapper;
|
|
|
|
|
private final ErpServerInfoMapper serverInfoMapper;
|
|
|
|
|
private final ErpServerDeployMapper baseMapper; // 部署信息 Mapper
|
|
|
|
|
private final ErpServerDeployLogMapper deployLogMapper; // 部署变更日志 Mapper
|
|
|
|
|
private final ErpServerInfoMapper serverInfoMapper; // 服务器基础信息 Mapper(用于校验服务器状态)
|
|
|
|
|
|
|
|
|
|
/** 远程工作流服务(Dubbo 调用,用于发起流程、删除流程实例) */
|
|
|
|
|
@DubboReference(timeout = 30000)
|
|
|
|
|
private RemoteWorkflowService remoteWorkflowService;
|
|
|
|
|
|
|
|
|
|
/** 远程编码规则服务(Dubbo 调用,用于自动生成部署编号) */
|
|
|
|
|
@DubboReference(timeout = 30000)
|
|
|
|
|
private RemoteCodeRuleService remoteCodeRuleService;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 根据部署ID查询详情
|
|
|
|
|
* <p>
|
|
|
|
|
* 返回数据包含:部署基本信息、变更日志列表、同编码的历史版本列表。
|
|
|
|
|
*
|
|
|
|
|
* @param deployId 部署主键ID
|
|
|
|
|
* @return 部署详情VO(含变更日志和版本列表),不存在则返回null
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public ErpServerDeployVo queryById(Long deployId) {
|
|
|
|
|
// 查询部署基本信息
|
|
|
|
|
ErpServerDeployVo vo = queryOneById(deployId);
|
|
|
|
|
if (vo == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
// 补充变更日志列表
|
|
|
|
|
vo.setServerDeployLogList(queryLogs(deployId));
|
|
|
|
|
// 补充同编码的历史版本列表
|
|
|
|
|
if (StringUtils.isNotBlank(vo.getDeployCode())) {
|
|
|
|
|
vo.setVersionList(queryVersions(vo.getDeployCode()));
|
|
|
|
|
}
|
|
|
|
|
return vo;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 根据部署ID查询单条记录(不含关联数据)
|
|
|
|
|
*
|
|
|
|
|
* @param deployId 部署主键ID
|
|
|
|
|
* @return 部署VO,不存在则返回null
|
|
|
|
|
*/
|
|
|
|
|
private ErpServerDeployVo queryOneById(Long deployId) {
|
|
|
|
|
// 使用 MPJ 构造器查询,过滤已删除记录
|
|
|
|
|
MPJLambdaWrapper<ErpServerDeploy> lqw = JoinWrappers.lambda(ErpServerDeploy.class)
|
|
|
|
|
.selectAll(ErpServerDeploy.class)
|
|
|
|
|
.eq(ErpServerDeploy::getDelFlag, NO)
|
|
|
|
|
@ -102,14 +125,28 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
return list.isEmpty() ? null : list.get(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 分页查询服务器部署信息列表
|
|
|
|
|
*
|
|
|
|
|
* @param bo 查询条件
|
|
|
|
|
* @param pageQuery 分页参数
|
|
|
|
|
* @return 分页结果
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public TableDataInfo<ErpServerDeployVo> queryPageList(ErpServerDeployBo bo, PageQuery pageQuery) {
|
|
|
|
|
// 构建查询条件并按创建时间倒序
|
|
|
|
|
MPJLambdaWrapper<ErpServerDeploy> lqw = buildQueryWrapper(bo);
|
|
|
|
|
lqw.orderByDesc(ErpServerDeploy::getCreateTime);
|
|
|
|
|
Page<ErpServerDeployVo> result = baseMapper.selectCustomErpServerDeployVoList(pageQuery.build(), lqw);
|
|
|
|
|
return TableDataInfo.build(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 不分页查询服务器部署信息列表
|
|
|
|
|
*
|
|
|
|
|
* @param bo 查询条件
|
|
|
|
|
* @return 部署信息列表
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<ErpServerDeployVo> queryList(ErpServerDeployBo bo) {
|
|
|
|
|
MPJLambdaWrapper<ErpServerDeploy> lqw = buildQueryWrapper(bo);
|
|
|
|
|
@ -117,6 +154,14 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
return baseMapper.selectCustomErpServerDeployVoList(lqw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询同一部署编码的所有历史版本
|
|
|
|
|
* <p>
|
|
|
|
|
* 按版本号倒序排列,用于前端版本切换下拉。
|
|
|
|
|
*
|
|
|
|
|
* @param deployCode 部署业务编码
|
|
|
|
|
* @return 版本列表,编码为空时返回空列表
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<ErpServerDeployVo> queryVersions(String deployCode) {
|
|
|
|
|
if (StringUtils.isBlank(deployCode)) {
|
|
|
|
|
@ -130,6 +175,12 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
return baseMapper.selectCustomErpServerDeployVoList(lqw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询指定部署的变更日志列表
|
|
|
|
|
*
|
|
|
|
|
* @param deployId 部署主键ID
|
|
|
|
|
* @return 变更日志列表,按创建时间倒序
|
|
|
|
|
*/
|
|
|
|
|
private List<ErpServerDeployLogVo> queryLogs(Long deployId) {
|
|
|
|
|
return deployLogMapper.selectVoList(new LambdaQueryWrapper<ErpServerDeployLog>()
|
|
|
|
|
.eq(ErpServerDeployLog::getDeployId, deployId)
|
|
|
|
|
@ -137,6 +188,14 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
.orderByDesc(ErpServerDeployLog::getCreateTime));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建查询条件包装器
|
|
|
|
|
* <p>
|
|
|
|
|
* 根据查询BO中的非空字段动态拼接 like/eq 条件,始终过滤已删除记录。
|
|
|
|
|
*
|
|
|
|
|
* @param bo 查询条件BO
|
|
|
|
|
* @return MPJ 查询条件包装器
|
|
|
|
|
*/
|
|
|
|
|
private MPJLambdaWrapper<ErpServerDeploy> buildQueryWrapper(ErpServerDeployBo bo) {
|
|
|
|
|
return JoinWrappers.lambda(ErpServerDeploy.class)
|
|
|
|
|
.selectAll(ErpServerDeploy.class)
|
|
|
|
|
@ -151,38 +210,71 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
.eq(StringUtils.isNotBlank(bo.getIsCurrent()), ErpServerDeploy::getIsCurrent, bo.getIsCurrent());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存草稿
|
|
|
|
|
* <p>
|
|
|
|
|
* 不会发起工作流,仅保存数据。状态设为草稿,不设为当前版本。
|
|
|
|
|
* 若已有审批通过的版本,会自动创建新版本;否则直接更新现有记录。
|
|
|
|
|
*
|
|
|
|
|
* @param bo 部署信息BO
|
|
|
|
|
* @return 保存后的部署详情VO
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
public ErpServerDeployVo draft(ErpServerDeployBo bo) {
|
|
|
|
|
// 准备数据:校验服务器状态、生成编号、计算版本号
|
|
|
|
|
prepareForSave(bo, false);
|
|
|
|
|
// 行级锁锁定同编码的所有版本记录,防止并发版本号冲突
|
|
|
|
|
lockVersionRows(bo.getDeployCode());
|
|
|
|
|
// 设置草稿状态
|
|
|
|
|
bo.setDeployStatus(OAStatusEnum.DRAFT.getStatus());
|
|
|
|
|
bo.setFlowStatus(BusinessStatusEnum.DRAFT.getStatus());
|
|
|
|
|
bo.setIsCurrent(NO);
|
|
|
|
|
// 保存(新建版本或更新现有记录)
|
|
|
|
|
saveDraftOrVersion(bo);
|
|
|
|
|
return queryById(bo.getDeployId());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 提交并发起工作流
|
|
|
|
|
* <p>
|
|
|
|
|
* 保存数据后调用远程工作流服务发起审批流程。
|
|
|
|
|
* 包含安全校验:服务器状态检查、旧流程实例清理、同编码审批中版本冲突检测。
|
|
|
|
|
* 使用 @GlobalTransactional 保证跨服务(OA模块 + 工作流模块)的数据一致性。
|
|
|
|
|
*
|
|
|
|
|
* @param bo 部署信息BO
|
|
|
|
|
* @return 提交后的部署详情VO
|
|
|
|
|
* @throws ServiceException 服务器不可用/编号生成失败/流程发起失败等异常
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
@GlobalTransactional(rollbackFor = Exception.class)
|
|
|
|
|
public ErpServerDeployVo submitAndStartFlow(ErpServerDeployBo bo) {
|
|
|
|
|
// 准备数据并设置流程编码
|
|
|
|
|
prepareForSave(bo, true);
|
|
|
|
|
// 行级锁防止并发版本冲突
|
|
|
|
|
lockVersionRows(bo.getDeployCode());
|
|
|
|
|
// 清理被退回或撤销的旧流程实例,避免流程引擎中残留数据
|
|
|
|
|
deleteReturnedOrCanceledInstance(bo.getDeployId());
|
|
|
|
|
// 校验同一部署编码下没有其他审批中的版本
|
|
|
|
|
checkNoApprovingVersion(bo.getDeployCode(), bo.getDeployId());
|
|
|
|
|
// 设置审批中状态
|
|
|
|
|
bo.setDeployStatus(OAStatusEnum.APPROVING.getStatus());
|
|
|
|
|
bo.setFlowStatus(BusinessStatusEnum.WAITING.getStatus());
|
|
|
|
|
bo.setIsCurrent(NO);
|
|
|
|
|
// 保存数据(新建版本或更新现有记录)
|
|
|
|
|
saveDraftOrVersion(bo);
|
|
|
|
|
// 填充工作流变量和业务扩展信息
|
|
|
|
|
fillWorkflowParams(bo);
|
|
|
|
|
|
|
|
|
|
// 构建远程流程启动请求
|
|
|
|
|
RemoteStartProcess startProcess = new RemoteStartProcess();
|
|
|
|
|
startProcess.setBusinessId(String.valueOf(bo.getDeployId()));
|
|
|
|
|
startProcess.setFlowCode(bo.getFlowCode());
|
|
|
|
|
startProcess.setVariables(bo.getVariables());
|
|
|
|
|
startProcess.setBizExt(bo.getBizExt());
|
|
|
|
|
bo.getBizExt().setBusinessId(startProcess.getBusinessId());
|
|
|
|
|
// 调用远程工作流服务发起流程
|
|
|
|
|
boolean started = remoteWorkflowService.startCompleteTask(startProcess);
|
|
|
|
|
if (!started) {
|
|
|
|
|
throw new ServiceException("服务器部署流程发起异常");
|
|
|
|
|
@ -190,37 +282,68 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
return queryById(bo.getDeployId());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 根据BO直接更新部署信息(不走版本控制和工作流)
|
|
|
|
|
* <p>
|
|
|
|
|
* 适用于管理后台直接修改场景,会校验服务器状态和字段合法性。
|
|
|
|
|
*
|
|
|
|
|
* @param bo 部署信息BO
|
|
|
|
|
* @return 更新是否成功
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
public Boolean updateByBo(ErpServerDeployBo bo) {
|
|
|
|
|
if (bo.getDeployId() == null) {
|
|
|
|
|
throw new ServiceException("部署ID不能为空");
|
|
|
|
|
}
|
|
|
|
|
// 校验关联服务器是否已审批通过
|
|
|
|
|
checkServerActive(bo.getServerCode());
|
|
|
|
|
// BO转Entity
|
|
|
|
|
ErpServerDeploy update = MapstructUtils.convert(bo, ErpServerDeploy.class);
|
|
|
|
|
// 保存前字段校验
|
|
|
|
|
validEntityBeforeSave(update);
|
|
|
|
|
return baseMapper.updateById(update) > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存草稿或创建新版本的核心逻辑
|
|
|
|
|
* <p>
|
|
|
|
|
* 判断逻辑:
|
|
|
|
|
* - 如果记录不存在或已生效/已审批通过/当前版本,则创建新版本;
|
|
|
|
|
* - 否则直接更新现有记录。
|
|
|
|
|
* 创建新版本时会自动生成变更日志(与旧当前版本对比)。
|
|
|
|
|
*
|
|
|
|
|
* @param bo 部署信息BO
|
|
|
|
|
*/
|
|
|
|
|
private void saveDraftOrVersion(ErpServerDeployBo bo) {
|
|
|
|
|
// 查询现有记录
|
|
|
|
|
ErpServerDeploy existing = bo.getDeployId() == null ? null : baseMapper.selectById(bo.getDeployId());
|
|
|
|
|
// 判断是否需要创建新版本
|
|
|
|
|
boolean createNewVersion = existing == null || shouldCreateNewVersion(existing);
|
|
|
|
|
ErpServerDeploy oldCurrent = null;
|
|
|
|
|
// 如果创建新版本且有部署编码,先获取旧当前版本(用于变更日志对比)
|
|
|
|
|
if (createNewVersion && StringUtils.isNotBlank(bo.getDeployCode())) {
|
|
|
|
|
oldCurrent = getCurrentVersion(bo.getDeployCode());
|
|
|
|
|
}
|
|
|
|
|
if (createNewVersion) {
|
|
|
|
|
// 清空ID,让数据库生成新记录
|
|
|
|
|
bo.setDeployId(null);
|
|
|
|
|
// 计算下一个版本号
|
|
|
|
|
bo.setVersion(calcNextVersion(bo.getDeployCode()));
|
|
|
|
|
// BO转Entity
|
|
|
|
|
ErpServerDeploy add = MapstructUtils.convert(bo, ErpServerDeploy.class);
|
|
|
|
|
// 保存前校验
|
|
|
|
|
validEntityBeforeSave(add);
|
|
|
|
|
if (baseMapper.insert(add) <= 0) {
|
|
|
|
|
throw new ServiceException("保存部署信息失败");
|
|
|
|
|
}
|
|
|
|
|
// 回写生成的ID到BO
|
|
|
|
|
bo.setDeployId(add.getDeployId());
|
|
|
|
|
// 如果有旧当前版本,生成变更日志
|
|
|
|
|
saveChangeLogIfNeeded(add, oldCurrent);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 不需要新版本:直接更新现有记录
|
|
|
|
|
if (bo.getVersion() == null) {
|
|
|
|
|
bo.setVersion(existing.getVersion());
|
|
|
|
|
}
|
|
|
|
|
@ -229,20 +352,37 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
if (baseMapper.updateById(update) <= 0) {
|
|
|
|
|
throw new ServiceException("更新部署信息失败");
|
|
|
|
|
}
|
|
|
|
|
// 替换变更日志(先删后建)
|
|
|
|
|
replaceChangeLogIfNeeded(update, getCurrentVersion(update.getDeployCode()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 判断是否需要创建新版本
|
|
|
|
|
* <p>
|
|
|
|
|
* 已生效(COMPLETED)、流程已完成(FINISH)、当前版本(isCurrent=1)的记录不可直接修改,必须创建新版本。
|
|
|
|
|
*
|
|
|
|
|
* @param existing 现有记录
|
|
|
|
|
* @return true=需要创建新版本,false=可直接更新
|
|
|
|
|
*/
|
|
|
|
|
private boolean shouldCreateNewVersion(ErpServerDeploy existing) {
|
|
|
|
|
return OAStatusEnum.COMPLETED.getStatus().equals(existing.getDeployStatus())
|
|
|
|
|
|| BusinessStatusEnum.FINISH.getStatus().equals(existing.getFlowStatus())
|
|
|
|
|
|| YES.equals(existing.getIsCurrent());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存前的数据准备:校验服务器状态、生成部署编号、计算版本号、设置流程编码
|
|
|
|
|
*
|
|
|
|
|
* @param bo 部署信息BO
|
|
|
|
|
* @param submit 是否为提交审批(true=设置流程编码,false=仅草稿)
|
|
|
|
|
*/
|
|
|
|
|
private void prepareForSave(ErpServerDeployBo bo, boolean submit) {
|
|
|
|
|
if (bo == null) {
|
|
|
|
|
throw new ServiceException("部署信息不能为空");
|
|
|
|
|
}
|
|
|
|
|
// 校验关联服务器是否存在且已审批通过
|
|
|
|
|
checkServerActive(bo.getServerCode());
|
|
|
|
|
// 如果没有部署编码,调用远程编码规则服务自动生成
|
|
|
|
|
if (StringUtils.isBlank(bo.getDeployCode())) {
|
|
|
|
|
String code = remoteCodeRuleService.selectCodeRuleCode(DEPLOY_CODE_RULE);
|
|
|
|
|
if (StringUtils.isBlank(code)) {
|
|
|
|
|
@ -250,14 +390,24 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
}
|
|
|
|
|
bo.setDeployCode(code);
|
|
|
|
|
}
|
|
|
|
|
// 新记录且未指定版本号时,自动计算下一个版本号
|
|
|
|
|
if (bo.getVersion() == null && bo.getDeployId() == null) {
|
|
|
|
|
bo.setVersion(calcNextVersion(bo.getDeployCode()));
|
|
|
|
|
}
|
|
|
|
|
// 提交审批时设置流程编码
|
|
|
|
|
if (submit) {
|
|
|
|
|
bo.setFlowCode(FLOW_CODE_DEPLOY);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 计算下一个版本号
|
|
|
|
|
* <p>
|
|
|
|
|
* 查询同一部署编码下所有未删除记录的最大版本号,+1 返回。
|
|
|
|
|
*
|
|
|
|
|
* @param deployCode 部署业务编码
|
|
|
|
|
* @return 下一个版本号(从1开始)
|
|
|
|
|
*/
|
|
|
|
|
private Integer calcNextVersion(String deployCode) {
|
|
|
|
|
if (StringUtils.isBlank(deployCode)) {
|
|
|
|
|
return 1;
|
|
|
|
|
@ -265,6 +415,7 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
List<ErpServerDeploy> list = baseMapper.selectList(new LambdaQueryWrapper<ErpServerDeploy>()
|
|
|
|
|
.eq(ErpServerDeploy::getDeployCode, deployCode)
|
|
|
|
|
.eq(ErpServerDeploy::getDelFlag, NO));
|
|
|
|
|
// 取最大版本号 +1,如果没有记录则返回1
|
|
|
|
|
return list.stream()
|
|
|
|
|
.map(ErpServerDeploy::getVersion)
|
|
|
|
|
.filter(Objects::nonNull)
|
|
|
|
|
@ -272,6 +423,13 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
.orElse(0) + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 行级锁锁定同一部署编码的所有版本记录
|
|
|
|
|
* <p>
|
|
|
|
|
* 使用 SELECT ... FOR UPDATE 防止并发场景下版本号重复或状态冲突。
|
|
|
|
|
*
|
|
|
|
|
* @param deployCode 部署业务编码
|
|
|
|
|
*/
|
|
|
|
|
private void lockVersionRows(String deployCode) {
|
|
|
|
|
if (StringUtils.isBlank(deployCode)) {
|
|
|
|
|
return;
|
|
|
|
|
@ -283,6 +441,14 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
.last("for update"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除被退回或撤销的旧流程实例
|
|
|
|
|
* <p>
|
|
|
|
|
* 如果该部署记录的流程状态为已退回(BACK)或已撤销(CANCEL),
|
|
|
|
|
* 调用远程工作流服务删除残留的流程实例,避免重新提交时流程引擎报错。
|
|
|
|
|
*
|
|
|
|
|
* @param deployId 部署主键ID
|
|
|
|
|
*/
|
|
|
|
|
private void deleteReturnedOrCanceledInstance(Long deployId) {
|
|
|
|
|
if (deployId == null) {
|
|
|
|
|
return;
|
|
|
|
|
@ -291,6 +457,7 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
if (existing == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 仅在退回或撤销状态下才需要清理
|
|
|
|
|
if (BusinessStatusEnum.BACK.getStatus().equals(existing.getFlowStatus())
|
|
|
|
|
|| BusinessStatusEnum.CANCEL.getStatus().equals(existing.getFlowStatus())) {
|
|
|
|
|
boolean deleted = remoteWorkflowService.deleteInstance(Collections.singletonList(deployId));
|
|
|
|
|
@ -300,6 +467,15 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 校验同一部署编码下没有其他审批中的版本
|
|
|
|
|
* <p>
|
|
|
|
|
* 防止同一部署项存在多个审批中的版本,避免审批冲突。
|
|
|
|
|
*
|
|
|
|
|
* @param deployCode 部署业务编码
|
|
|
|
|
* @param currentDeployId 当前部署ID(排除自身)
|
|
|
|
|
* @throws ServiceException 已有审批中版本时抛出异常
|
|
|
|
|
*/
|
|
|
|
|
private void checkNoApprovingVersion(String deployCode, Long currentDeployId) {
|
|
|
|
|
Long count = baseMapper.selectCount(new LambdaQueryWrapper<ErpServerDeploy>()
|
|
|
|
|
.eq(ErpServerDeploy::getDeployCode, deployCode)
|
|
|
|
|
@ -311,10 +487,19 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 校验关联服务器是否有效且已审批通过
|
|
|
|
|
* <p>
|
|
|
|
|
* 部署操作要求关联的服务器必须存在、为当前生效版本且业务状态为已生效(COMPLETED)。
|
|
|
|
|
*
|
|
|
|
|
* @param serverCode 服务器业务编码
|
|
|
|
|
* @throws ServiceException 服务器不存在/未生效/未审批通过时抛出异常
|
|
|
|
|
*/
|
|
|
|
|
private void checkServerActive(String serverCode) {
|
|
|
|
|
if (StringUtils.isBlank(serverCode)) {
|
|
|
|
|
throw new ServiceException("请选择关联服务器");
|
|
|
|
|
}
|
|
|
|
|
// 查询当前生效版本的服务器
|
|
|
|
|
ErpServerInfo server = serverInfoMapper.selectOne(new LambdaQueryWrapper<ErpServerInfo>()
|
|
|
|
|
.eq(ErpServerInfo::getServerCode, serverCode)
|
|
|
|
|
.eq(ErpServerInfo::getIsCurrent, YES)
|
|
|
|
|
@ -323,11 +508,20 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
if (server == null) {
|
|
|
|
|
throw new ServiceException("关联服务器不存在或尚未生效");
|
|
|
|
|
}
|
|
|
|
|
// 服务器必须已审批通过才能发起部署
|
|
|
|
|
if (!OAStatusEnum.COMPLETED.getStatus().equals(server.getServerStatus())) {
|
|
|
|
|
throw new ServiceException("关联服务器尚未审批通过,无法发起部署操作");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 保存前的实体字段校验
|
|
|
|
|
* <p>
|
|
|
|
|
* 校验必填字段(服务器编码、部署位置、端口、用途)和格式合法性(路径防注入、端口格式)。
|
|
|
|
|
*
|
|
|
|
|
* @param entity 部署信息实体
|
|
|
|
|
* @throws ServiceException 任一校验不通过时抛出
|
|
|
|
|
*/
|
|
|
|
|
private void validEntityBeforeSave(ErpServerDeploy entity) {
|
|
|
|
|
if (entity == null) {
|
|
|
|
|
throw new ServiceException("部署信息不能为空");
|
|
|
|
|
@ -344,24 +538,44 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
if (StringUtils.isBlank(entity.getDeployPurpose())) {
|
|
|
|
|
throw new ServiceException("部署用途不能为空");
|
|
|
|
|
}
|
|
|
|
|
// 部署路径安全校验(防命令注入)
|
|
|
|
|
validDeployPath(entity.getDeployPath());
|
|
|
|
|
// 部署端口格式校验
|
|
|
|
|
validDeployPort(entity.getDeployPort());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 校验部署路径安全性
|
|
|
|
|
* <p>
|
|
|
|
|
* 限制长度不超过500字符,禁止包含命令连接符、管道符、反引号等危险字符(防止命令注入攻击)。
|
|
|
|
|
*
|
|
|
|
|
* @param deployPath 部署路径
|
|
|
|
|
*/
|
|
|
|
|
private void validDeployPath(String deployPath) {
|
|
|
|
|
if (deployPath.length() > 500) {
|
|
|
|
|
throw new ServiceException("部署位置长度不能超过500个字符");
|
|
|
|
|
}
|
|
|
|
|
// 检测危险字符,防止命令注入
|
|
|
|
|
if (DANGEROUS_PATH_PATTERN.matcher(deployPath).find()) {
|
|
|
|
|
throw new ServiceException("部署位置不能包含命令连接符或换行等危险字符");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 校验部署端口格式
|
|
|
|
|
* <p>
|
|
|
|
|
* 支持格式:单端口(8080)、逗号分隔(8080,8081)、端口范围(8080-8090)。
|
|
|
|
|
* 端口值必须在1-65535之间,范围起始值不能大于结束值。
|
|
|
|
|
*
|
|
|
|
|
* @param deployPort 部署端口字符串
|
|
|
|
|
*/
|
|
|
|
|
private void validDeployPort(String deployPort) {
|
|
|
|
|
// 去除空格后进行格式匹配
|
|
|
|
|
String normalized = deployPort.replace(" ", "");
|
|
|
|
|
if (!PORT_PATTERN.matcher(normalized).matches()) {
|
|
|
|
|
throw new ServiceException("部署端口格式不正确,支持单端口、逗号分隔端口或端口范围");
|
|
|
|
|
}
|
|
|
|
|
// 逐个校验端口范围的有效性
|
|
|
|
|
for (String item : normalized.split(",")) {
|
|
|
|
|
String[] range = item.split("-");
|
|
|
|
|
int start = parsePort(range[0]);
|
|
|
|
|
@ -372,6 +586,13 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析单个端口值并校验范围
|
|
|
|
|
*
|
|
|
|
|
* @param port 端口字符串
|
|
|
|
|
* @return 端口整数值
|
|
|
|
|
* @throws ServiceException 非数字或超出1-65535范围时抛出
|
|
|
|
|
*/
|
|
|
|
|
private int parsePort(String port) {
|
|
|
|
|
int value;
|
|
|
|
|
try {
|
|
|
|
|
@ -385,6 +606,12 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取指定部署编码的当前生效版本
|
|
|
|
|
*
|
|
|
|
|
* @param deployCode 部署业务编码
|
|
|
|
|
* @return 当前生效版本实体,不存在则返回null
|
|
|
|
|
*/
|
|
|
|
|
private ErpServerDeploy getCurrentVersion(String deployCode) {
|
|
|
|
|
return baseMapper.selectOne(new LambdaQueryWrapper<ErpServerDeploy>()
|
|
|
|
|
.eq(ErpServerDeploy::getDeployCode, deployCode)
|
|
|
|
|
@ -393,14 +620,25 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
.last("limit 1"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 如果需要,保存变更日志
|
|
|
|
|
* <p>
|
|
|
|
|
* 对比新旧版本字段差异,有差异时生成变更日志记录。
|
|
|
|
|
* 跳过条件:没有旧版本、版本号≤1(首次创建无需对比)。
|
|
|
|
|
*
|
|
|
|
|
* @param newVersion 新版本实体
|
|
|
|
|
* @param oldVersion 旧当前版本实体(可能为null)
|
|
|
|
|
*/
|
|
|
|
|
private void saveChangeLogIfNeeded(ErpServerDeploy newVersion, ErpServerDeploy oldVersion) {
|
|
|
|
|
if (oldVersion == null || newVersion.getVersion() == null || newVersion.getVersion() <= 1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 对比字段差异
|
|
|
|
|
List<Map<String, Object>> changes = buildChanges(oldVersion, newVersion);
|
|
|
|
|
if (changes.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 构建变更日志并保存
|
|
|
|
|
ErpServerDeployLog log = new ErpServerDeployLog();
|
|
|
|
|
log.setDeployId(newVersion.getDeployId());
|
|
|
|
|
log.setChangeContent(JSONUtil.toJsonStr(changes));
|
|
|
|
|
@ -408,12 +646,31 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
deployLogMapper.insert(log);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 替换变更日志(先删除旧日志再重新生成)
|
|
|
|
|
* <p>
|
|
|
|
|
* 适用于直接更新现有记录的场景,确保日志与最新数据一致。
|
|
|
|
|
*
|
|
|
|
|
* @param newVersion 新版本实体
|
|
|
|
|
* @param oldVersion 旧当前版本实体
|
|
|
|
|
*/
|
|
|
|
|
private void replaceChangeLogIfNeeded(ErpServerDeploy newVersion, ErpServerDeploy oldVersion) {
|
|
|
|
|
// 先删除该部署的所有旧变更日志
|
|
|
|
|
deployLogMapper.delete(new LambdaQueryWrapper<ErpServerDeployLog>()
|
|
|
|
|
.eq(ErpServerDeployLog::getDeployId, newVersion.getDeployId()));
|
|
|
|
|
// 重新生成变更日志
|
|
|
|
|
saveChangeLogIfNeeded(newVersion, oldVersion);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 对比新旧版本字段差异,构建变更记录列表
|
|
|
|
|
* <p>
|
|
|
|
|
* 对比字段包括:关联服务器、部署位置、端口、端口暴露、用途、负责人、附件。
|
|
|
|
|
*
|
|
|
|
|
* @param oldVersion 旧版本实体
|
|
|
|
|
* @param newVersion 新版本实体
|
|
|
|
|
* @return 变更记录列表,每个元素包含 field/label/oldValue/newValue
|
|
|
|
|
*/
|
|
|
|
|
private List<Map<String, Object>> buildChanges(ErpServerDeploy oldVersion, ErpServerDeploy newVersion) {
|
|
|
|
|
List<Map<String, Object>> changes = new ArrayList<>();
|
|
|
|
|
addChange(changes, "server_code", "关联服务器", oldVersion.getServerCode(), newVersion.getServerCode());
|
|
|
|
|
@ -426,6 +683,17 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
return changes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 添加单条变更记录到变更列表
|
|
|
|
|
* <p>
|
|
|
|
|
* 仅当新旧值不相同时才添加。
|
|
|
|
|
*
|
|
|
|
|
* @param changes 变更记录列表
|
|
|
|
|
* @param field 字段名(英文)
|
|
|
|
|
* @param label 字段标签(中文,用于前端展示)
|
|
|
|
|
* @param oldValue 旧值
|
|
|
|
|
* @param newValue 新值
|
|
|
|
|
*/
|
|
|
|
|
private void addChange(List<Map<String, Object>> changes, String field, String label, Object oldValue, Object newValue) {
|
|
|
|
|
if (Objects.equals(oldValue, newValue)) {
|
|
|
|
|
return;
|
|
|
|
|
@ -438,8 +706,16 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
changes.add(change);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 填充工作流参数和业务扩展信息
|
|
|
|
|
* <p>
|
|
|
|
|
* 设置流程变量(部署编码、服务器编码、负责人ID等)和业务扩展信息(标题、编码)。
|
|
|
|
|
*
|
|
|
|
|
* @param bo 部署信息BO
|
|
|
|
|
*/
|
|
|
|
|
private void fillWorkflowParams(ErpServerDeployBo bo) {
|
|
|
|
|
bo.setFlowCode(FLOW_CODE_DEPLOY);
|
|
|
|
|
// 设置流程变量(putIfAbsent 避免覆盖前端传入的自定义变量)
|
|
|
|
|
Map<String, Object> vars = bo.getVariables();
|
|
|
|
|
vars.putIfAbsent("deployCode", bo.getDeployCode());
|
|
|
|
|
vars.putIfAbsent("serverCode", bo.getServerCode());
|
|
|
|
|
@ -447,6 +723,7 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
vars.put("ignore", true);
|
|
|
|
|
bo.setVariables(vars);
|
|
|
|
|
|
|
|
|
|
// 设置业务扩展信息(用于工作流待办列表展示)
|
|
|
|
|
RemoteFlowInstanceBizExt bizExt = bo.getBizExt();
|
|
|
|
|
if (StringUtils.isBlank(bizExt.getBusinessTitle())) {
|
|
|
|
|
bizExt.setBusinessTitle(bo.getDeployCode() + "服务器部署审批");
|
|
|
|
|
@ -457,10 +734,21 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
bo.setBizExt(bizExt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量删除服务器部署信息(带校验)
|
|
|
|
|
* <p>
|
|
|
|
|
* 仅允许删除草稿状态(业务状态和流程状态均为DRAFT)的记录。
|
|
|
|
|
* 删除部署记录时同步删除关联的变更日志。
|
|
|
|
|
*
|
|
|
|
|
* @param ids 部署ID集合
|
|
|
|
|
* @param isValid 是否执行状态校验
|
|
|
|
|
* @return 删除是否成功
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
|
|
|
|
if (isValid) {
|
|
|
|
|
// 逐条校验:仅草稿状态可删除
|
|
|
|
|
for (Long id : ids) {
|
|
|
|
|
ErpServerDeploy deploy = baseMapper.selectById(id);
|
|
|
|
|
if (deploy == null) {
|
|
|
|
|
@ -472,46 +760,84 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 先删除关联的变更日志
|
|
|
|
|
deployLogMapper.delete(new LambdaQueryWrapper<ErpServerDeployLog>().in(ErpServerDeployLog::getDeployId, ids));
|
|
|
|
|
// 再删除部署记录(逻辑删除)
|
|
|
|
|
return baseMapper.deleteByIds(ids) > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 工作流事件监听器(流程编码 OASD)
|
|
|
|
|
* <p>
|
|
|
|
|
* 监听 Warm-Flow 工作流引擎发布的 ProcessEvent 事件,
|
|
|
|
|
* 在正确的租户上下文中处理流程状态变更。
|
|
|
|
|
*
|
|
|
|
|
* @param processEvent 工作流事件
|
|
|
|
|
*/
|
|
|
|
|
@EventListener(condition = "#processEvent.flowCode == 'OASD'")
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
public void processHandler(ProcessEvent processEvent) {
|
|
|
|
|
// 在事件对应的租户上下文中执行,确保多租户数据隔离
|
|
|
|
|
TenantHelper.dynamic(processEvent.getTenantId(), () -> handleProcessEvent(processEvent));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理工作流事件的核心逻辑
|
|
|
|
|
* <p>
|
|
|
|
|
* 根据流程状态更新部署记录的业务状态和当前版本标识:
|
|
|
|
|
* - WAITING(待审批)→ APPROVING(审批中)
|
|
|
|
|
* - FINISH(已完成)→ COMPLETED(已生效),同时激活当前版本
|
|
|
|
|
* - INVALID/TERMINATION(失效/终止)→ INVALID(已作废),取消当前版本
|
|
|
|
|
* - BACK/CANCEL(退回/撤销)→ DRAFT(草稿),取消当前版本
|
|
|
|
|
*
|
|
|
|
|
* @param processEvent 工作流事件
|
|
|
|
|
*/
|
|
|
|
|
public void handleProcessEvent(ProcessEvent processEvent) {
|
|
|
|
|
log.info("服务器部署流程事件: {}", processEvent);
|
|
|
|
|
ErpServerDeploy deploy = baseMapper.selectById(Long.valueOf(processEvent.getBusinessId()));
|
|
|
|
|
if (deploy == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 更新流程状态
|
|
|
|
|
deploy.setFlowStatus(processEvent.getStatus());
|
|
|
|
|
// 根据流程状态联动更新业务状态
|
|
|
|
|
if (Objects.equals(processEvent.getStatus(), BusinessStatusEnum.WAITING.getStatus())) {
|
|
|
|
|
// 审批中
|
|
|
|
|
deploy.setDeployStatus(OAStatusEnum.APPROVING.getStatus());
|
|
|
|
|
} else if (Objects.equals(processEvent.getStatus(), BusinessStatusEnum.FINISH.getStatus())) {
|
|
|
|
|
// 审批通过:设为已生效并激活当前版本
|
|
|
|
|
deploy.setDeployStatus(OAStatusEnum.COMPLETED.getStatus());
|
|
|
|
|
activateCurrentVersion(deploy);
|
|
|
|
|
} else if (Objects.equals(processEvent.getStatus(), BusinessStatusEnum.INVALID.getStatus())
|
|
|
|
|
|| Objects.equals(processEvent.getStatus(), BusinessStatusEnum.TERMINATION.getStatus())) {
|
|
|
|
|
// 失效或终止:设为已作废
|
|
|
|
|
deploy.setDeployStatus(OAStatusEnum.INVALID.getStatus());
|
|
|
|
|
deploy.setIsCurrent(NO);
|
|
|
|
|
} else if (Objects.equals(processEvent.getStatus(), BusinessStatusEnum.BACK.getStatus())
|
|
|
|
|
|| Objects.equals(processEvent.getStatus(), BusinessStatusEnum.CANCEL.getStatus())) {
|
|
|
|
|
// 退回或撤销:回退为草稿状态
|
|
|
|
|
deploy.setDeployStatus(OAStatusEnum.DRAFT.getStatus());
|
|
|
|
|
deploy.setIsCurrent(NO);
|
|
|
|
|
}
|
|
|
|
|
baseMapper.updateById(deploy);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 激活当前版本
|
|
|
|
|
* <p>
|
|
|
|
|
* 将同一部署编码下其他版本的 isCurrent 置为0,将当前版本置为1。
|
|
|
|
|
* 确保同一部署项只有一个生效版本。
|
|
|
|
|
*
|
|
|
|
|
* @param deploy 审批通过的部署记录
|
|
|
|
|
*/
|
|
|
|
|
private void activateCurrentVersion(ErpServerDeploy deploy) {
|
|
|
|
|
// 将同编码的其他版本全部置为非当前版本
|
|
|
|
|
baseMapper.update(null, new LambdaUpdateWrapper<ErpServerDeploy>()
|
|
|
|
|
.set(ErpServerDeploy::getIsCurrent, NO)
|
|
|
|
|
.eq(ErpServerDeploy::getDeployCode, deploy.getDeployCode())
|
|
|
|
|
.eq(ErpServerDeploy::getDelFlag, NO)
|
|
|
|
|
.ne(ErpServerDeploy::getDeployId, deploy.getDeployId()));
|
|
|
|
|
// 当前版本置为生效
|
|
|
|
|
deploy.setIsCurrent(YES);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|