diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerDeploy.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerDeploy.java
index 0380bada..d507bba8 100644
--- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerDeploy.java
+++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerDeploy.java
@@ -1,10 +1,6 @@
package org.dromara.oa.erp.domain;
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableLogic;
-import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
@@ -13,6 +9,10 @@ import java.io.Serial;
/**
* 服务器部署信息对象 erp_server_deploy
+ *
+ * 记录文件/应用在指定服务器上的部署详情,支持多版本管理。
+ * 每次审批通过后会生成新版本,旧版本保留为历史快照,便于追溯和回滚。
+ * 通过 deployCode 关联同一部署项的不同版本,isCurrent 标识当前生效版本。
*
* @author zch
* @date 2026-06-03
@@ -26,68 +26,75 @@ public class ErpServerDeploy extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
- * 部署主键ID
+ * 部署主键ID(雪花算法生成)
*/
@TableId(value = "deploy_id", type = IdType.ASSIGN_ID)
private Long deployId;
/**
- * 部署业务编码
+ * 部署业务编码(通过编码规则自动生成,同一部署项的所有版本共享此编码)
*/
private String deployCode;
/**
- * 关联服务器业务编码
+ * 关联服务器业务编码(指向 erp_server_info 中的 serverCode,须为已审批通过的服务器)
*/
private String serverCode;
/**
- * 版本号
+ * 版本号(同一 deployCode 下递增,从1开始,每次审批通过后变更时自动递增)
*/
private Integer version;
/**
* 是否当前生效版本(1是 0否)
+ *
+ * 审批通过后自动置为1,同时将同 deployCode 的其他版本置为0;
+ * 草稿、审批中、作废等状态下为0。
*/
private String isCurrent;
/**
- * 文件/应用部署位置
+ * 文件/应用部署位置(服务器上的绝对路径或相对路径,最长500字符,禁止包含命令连接符等危险字符)
*/
private String deployPath;
/**
- * 部署端口
+ * 部署端口(支持单端口如8080、逗号分隔如8080,8081、端口范围如8080-8090,端口值范围1-65535)
*/
private String deployPort;
/**
- * 端口是否暴露公网(1是 0否)
+ * 端口是否暴露公网(1是 0否,用于安全审计,标识该部署端口是否可从外网直接访问)
*/
private String portExposed;
/**
- * 文件/应用用途说明
+ * 文件/应用用途说明(描述本次部署的业务目的,如"部署CRM后端服务"等)
*/
private String deployPurpose;
/**
- * 负责人ID
+ * 负责人ID(关联 sys_user 表的用户ID,标识该部署项的运维负责人)
*/
private Long personId;
/**
- * 附件OSS ID
+ * 附件OSS ID(逗号分隔的多个文件ID,存储部署相关文档如配置文件、部署脚本等)
*/
private String ossId;
/**
- * 业务状态(字典 deploy_status)
+ * 业务状态(字典 deploy_status:草稿/审批中/已生效/已作废)
+ *
+ * 与 flowStatus 联动:草稿对应DRAFT,审批中对应APPROVING,审批通过对应COMPLETED,作废对应INVALID。
*/
private String deployStatus;
/**
- * 流程状态(字典 wf_business_status)
+ * 流程状态(字典 wf_business_status:草稿/待审批/已完成/已退回/已撤销/已失效/已终止)
+ *
+ * 由 Warm-Flow 工作流引擎回调驱动更新,业务代码不应直接修改此字段。
*/
private String flowStatus;
@@ -97,13 +104,13 @@ public class ErpServerDeploy extends TenantEntity {
private String remark;
/**
- * 删除标志(0代表存在 1代表删除)
+ * 删除标志(0代表存在 1代表删除,MyBatis-Plus 逻辑删除字段,查询时自动过滤已删除记录)
*/
@TableLogic
private String delFlag;
/**
- * 负责人名称
+ * 负责人名称(非数据库字段,通过 MPJ 连表查询 sys_user 表获取,用于前端展示)
*/
@TableField(exist = false)
private String personName;
diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerDeployLog.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerDeployLog.java
index 3f88ba07..0c02287e 100644
--- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerDeployLog.java
+++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerDeployLog.java
@@ -12,6 +12,9 @@ import java.io.Serial;
/**
* 服务器部署操作日志对象 erp_server_deploy_log
+ *
+ * 记录服务器部署信息每次版本变更时的字段差异,以JSON数组形式存储变更内容。
+ * 用于审计追踪,帮助运维人员了解每次部署变更的具体修改项。
*
* @author zch
* @date 2026-06-03
@@ -25,28 +28,28 @@ public class ErpServerDeployLog extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
- * 日志主键ID
+ * 日志主键ID(数据库自增)
*/
@TableId(value = "log_id", type = IdType.AUTO)
private Long logId;
/**
- * 关联部署快照ID
+ * 关联部署快照ID(指向 erp_server_deploy 表的 deploy_id,标识该变更日志所属的部署版本)
*/
private Long deployId;
/**
- * 变更内容(JSON数组)
+ * 变更内容(JSON数组格式,每个元素包含 field/label/oldValue/newValue 四个字段,记录具体字段的变更详情)
*/
private String changeContent;
/**
- * 变更摘要
+ * 变更摘要(人类可读的变更描述,如"部署项 SD20260603001 第 2 版变更")
*/
private String changeSummary;
/**
- * 删除标志(0代表存在 1代表删除)
+ * 删除标志(0代表存在 1代表删除,MyBatis-Plus 逻辑删除字段,查询时自动过滤已删除记录)
*/
@TableLogic
private String delFlag;
diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerInfo.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerInfo.java
index 7d9b5241..ca541c42 100644
--- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerInfo.java
+++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/domain/ErpServerInfo.java
@@ -14,6 +14,10 @@ import java.util.Date;
/**
* 服务器基础信息对象 erp_server_info
+ *
+ * 记录企业服务器的登记信息,包括物理/虚拟服务器的基础属性、获取方式、租赁信息等。
+ * 支持多版本管理:每次审批通过后变更会生成新版本,旧版本保留为历史快照。
+ * 通过 serverCode 关联同一服务器的不同版本,isCurrent 标识当前生效版本。
*
* @author zch
* @date 2026-06-03
@@ -27,123 +31,132 @@ public class ErpServerInfo extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
- * 服务器主键ID
+ * 服务器主键ID(雪花算法生成)
*/
@TableId(value = "server_id", type = IdType.ASSIGN_ID)
private Long serverId;
/**
- * 服务器业务编码
+ * 服务器业务编码(通过编码规则自动生成,同一服务器的所有版本共享此编码)
*/
private String serverCode;
/**
- * 服务器名称
+ * 服务器名称(用户可读的服务器标识,如"CRM生产服务器"、"测试环境服务器"等)
*/
private String serverName;
/**
- * 版本号
+ * 版本号(同一 serverCode 下递增,从1开始,每次审批通过后变更时自动递增)
*/
private Integer version;
/**
* 是否当前生效版本(1是 0否)
+ *
+ * 审批通过后自动置为1,同时将同 serverCode 的其他版本置为0;
+ * 草稿、审批中、作废等状态下为0。
*/
private String isCurrent;
/**
- * 服务器类型(字典 server_type)
+ * 服务器类型(字典 server_type:local=本地服务器,cloud=云服务器)
*/
private String serverType;
/**
- * 获取方式(字典 acquisition_type)
+ * 获取方式(字典 acquisition_type:purchase=购买,lease=租赁)
*/
private String acquisitionType;
/**
- * 操作系统类型(字典 os_type)
+ * 操作系统类型(字典 os_type:如 Linux/Windows 等)
*/
private String osType;
/**
- * 是否有公网IP(1是 0否)
+ * 是否有公网IP(1是 0否,用于安全分类,标识该服务器是否可直接从外网访问)
*/
private String hasPublicIp;
/**
- * IP地址
+ * IP地址(支持多个IP,逗号或分号分隔;支持IPv4和IPv6格式)
*/
private String ipAddress;
/**
- * 服务器物理位置
+ * 服务器物理位置(如机房名称、机柜编号等,云服务器可填区域信息)
*/
private String location;
/**
- * 购买金额(元)
+ * 购买金额(元,不能为负数,获取方式为购买时填写)
*/
private BigDecimal purchaseAmount;
/**
- * 品牌(本地服务器专用)
+ * 品牌(本地服务器专用,如 Dell/HP/Lenovo 等,serverType 为 local 时必填)
*/
private String brand;
/**
- * 型号(本地服务器专用)
+ * 型号(本地服务器专用,如 PowerEdge R740 等,serverType 为 local 时必填)
*/
private String model;
/**
- * 硬件参数(本地服务器专用)
+ * 硬件参数(本地服务器专用,描述CPU/内存/磁盘等配置信息)
*/
private String hardwareSpec;
/**
- * 租赁起始日期
+ * 租赁起始日期(获取方式为租赁时必填,不能晚于结束日期)
*/
private Date leaseStartDate;
/**
- * 租赁结束日期
+ * 租赁结束日期(获取方式为租赁时必填,不能早于起始日期)
*/
private Date leaseEndDate;
/**
- * 套餐年数
+ * 套餐年数(租赁套餐的年限,用于计算租赁总费用和到期提醒)
*/
private Integer leasePackageYears;
/**
- * 租赁金额(元)
+ * 租赁金额(元,不能为负数,获取方式为租赁时填写)
*/
private BigDecimal leaseAmount;
/**
- * 业务状态(字典 server_status)
+ * 业务状态(字典 server_status:草稿/审批中/已生效/已作废)
+ *
+ * 与 flowStatus 联动:草稿对应DRAFT,审批中对应APPROVING,审批通过对应COMPLETED,作废对应INVALID。
*/
private String serverStatus;
/**
- * 流程状态(字典 wf_business_status)
+ * 流程状态(字典 wf_business_status:草稿/待审批/已完成/已退回/已撤销/已失效/已终止)
+ *
+ * 由 Warm-Flow 工作流引擎回调驱动更新,业务代码不应直接修改此字段。
*/
private String flowStatus;
/**
- * 扩展属性(JSON)
+ * 扩展属性(JSON格式,仅允许保存白名单字段如cpu/memory/disk/bandwidth/region等运维描述信息)
+ *
+ * 安全策略:禁止保存密码、密钥、Token等敏感凭据,保存前会进行敏感词检测和白名单校验。
*/
private String extAttr;
/**
- * 备注
+ * 备注(补充说明信息)
*/
private String remark;
/**
- * 删除标志(0代表存在 1代表删除)
+ * 删除标志(0代表存在 1代表删除,MyBatis-Plus 逻辑删除字段,查询时自动过滤已删除记录)
*/
@TableLogic
private String delFlag;
diff --git a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpServerDeployServiceImpl.java b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpServerDeployServiceImpl.java
index d165d1bc..08623468 100644
--- a/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpServerDeployServiceImpl.java
+++ b/ruoyi-modules/ruoyi-oa/src/main/java/org/dromara/oa/erp/service/impl/ErpServerDeployServiceImpl.java
@@ -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业务层处理
+ *
+ * 核心职责:
+ * 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查询详情
+ *
+ * 返回数据包含:部署基本信息、变更日志列表、同编码的历史版本列表。
+ *
+ * @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 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 queryPageList(ErpServerDeployBo bo, PageQuery pageQuery) {
+ // 构建查询条件并按创建时间倒序
MPJLambdaWrapper lqw = buildQueryWrapper(bo);
lqw.orderByDesc(ErpServerDeploy::getCreateTime);
Page result = baseMapper.selectCustomErpServerDeployVoList(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
+ /**
+ * 不分页查询服务器部署信息列表
+ *
+ * @param bo 查询条件
+ * @return 部署信息列表
+ */
@Override
public List queryList(ErpServerDeployBo bo) {
MPJLambdaWrapper lqw = buildQueryWrapper(bo);
@@ -117,6 +154,14 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
return baseMapper.selectCustomErpServerDeployVoList(lqw);
}
+ /**
+ * 查询同一部署编码的所有历史版本
+ *
+ * 按版本号倒序排列,用于前端版本切换下拉。
+ *
+ * @param deployCode 部署业务编码
+ * @return 版本列表,编码为空时返回空列表
+ */
@Override
public List 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 queryLogs(Long deployId) {
return deployLogMapper.selectVoList(new LambdaQueryWrapper()
.eq(ErpServerDeployLog::getDeployId, deployId)
@@ -137,6 +188,14 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
.orderByDesc(ErpServerDeployLog::getCreateTime));
}
+ /**
+ * 构建查询条件包装器
+ *
+ * 根据查询BO中的非空字段动态拼接 like/eq 条件,始终过滤已删除记录。
+ *
+ * @param bo 查询条件BO
+ * @return MPJ 查询条件包装器
+ */
private MPJLambdaWrapper 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());
}
+ /**
+ * 保存草稿
+ *
+ * 不会发起工作流,仅保存数据。状态设为草稿,不设为当前版本。
+ * 若已有审批通过的版本,会自动创建新版本;否则直接更新现有记录。
+ *
+ * @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());
}
+ /**
+ * 提交并发起工作流
+ *
+ * 保存数据后调用远程工作流服务发起审批流程。
+ * 包含安全校验:服务器状态检查、旧流程实例清理、同编码审批中版本冲突检测。
+ * 使用 @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直接更新部署信息(不走版本控制和工作流)
+ *
+ * 适用于管理后台直接修改场景,会校验服务器状态和字段合法性。
+ *
+ * @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;
}
+ /**
+ * 保存草稿或创建新版本的核心逻辑
+ *
+ * 判断逻辑:
+ * - 如果记录不存在或已生效/已审批通过/当前版本,则创建新版本;
+ * - 否则直接更新现有记录。
+ * 创建新版本时会自动生成变更日志(与旧当前版本对比)。
+ *
+ * @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()));
}
+ /**
+ * 判断是否需要创建新版本
+ *
+ * 已生效(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);
}
}
+ /**
+ * 计算下一个版本号
+ *
+ * 查询同一部署编码下所有未删除记录的最大版本号,+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 list = baseMapper.selectList(new LambdaQueryWrapper()
.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;
}
+ /**
+ * 行级锁锁定同一部署编码的所有版本记录
+ *
+ * 使用 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"));
}
+ /**
+ * 删除被退回或撤销的旧流程实例
+ *
+ * 如果该部署记录的流程状态为已退回(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 {
}
}
+ /**
+ * 校验同一部署编码下没有其他审批中的版本
+ *
+ * 防止同一部署项存在多个审批中的版本,避免审批冲突。
+ *
+ * @param deployCode 部署业务编码
+ * @param currentDeployId 当前部署ID(排除自身)
+ * @throws ServiceException 已有审批中版本时抛出异常
+ */
private void checkNoApprovingVersion(String deployCode, Long currentDeployId) {
Long count = baseMapper.selectCount(new LambdaQueryWrapper()
.eq(ErpServerDeploy::getDeployCode, deployCode)
@@ -311,10 +487,19 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
}
}
+ /**
+ * 校验关联服务器是否有效且已审批通过
+ *
+ * 部署操作要求关联的服务器必须存在、为当前生效版本且业务状态为已生效(COMPLETED)。
+ *
+ * @param serverCode 服务器业务编码
+ * @throws ServiceException 服务器不存在/未生效/未审批通过时抛出异常
+ */
private void checkServerActive(String serverCode) {
if (StringUtils.isBlank(serverCode)) {
throw new ServiceException("请选择关联服务器");
}
+ // 查询当前生效版本的服务器
ErpServerInfo server = serverInfoMapper.selectOne(new LambdaQueryWrapper()
.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("关联服务器尚未审批通过,无法发起部署操作");
}
}
+ /**
+ * 保存前的实体字段校验
+ *
+ * 校验必填字段(服务器编码、部署位置、端口、用途)和格式合法性(路径防注入、端口格式)。
+ *
+ * @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());
}
+ /**
+ * 校验部署路径安全性
+ *
+ * 限制长度不超过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("部署位置不能包含命令连接符或换行等危险字符");
}
}
+ /**
+ * 校验部署端口格式
+ *
+ * 支持格式:单端口(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()
.eq(ErpServerDeploy::getDeployCode, deployCode)
@@ -393,14 +620,25 @@ public class ErpServerDeployServiceImpl implements IErpServerDeployService {
.last("limit 1"));
}
+ /**
+ * 如果需要,保存变更日志
+ *
+ * 对比新旧版本字段差异,有差异时生成变更日志记录。
+ * 跳过条件:没有旧版本、版本号≤1(首次创建无需对比)。
+ *
+ * @param newVersion 新版本实体
+ * @param oldVersion 旧当前版本实体(可能为null)
+ */
private void saveChangeLogIfNeeded(ErpServerDeploy newVersion, ErpServerDeploy oldVersion) {
if (oldVersion == null || newVersion.getVersion() == null || newVersion.getVersion() <= 1) {
return;
}
+ // 对比字段差异
List