!135 test(improvementproposal): 新增评分/奖级/流水号纯函数与流程单测

* test(improvementproposal): 新增评分/奖级/流水号纯函数与流程单测
* refactor(improvementproposal): 提炼评分/奖级/流水号纯函数 ImprovementProposalSuppo…
* test(tasklist): 新增看板 Service 单测并消除第 11 处重复(normalizePid 复用 support)
* test(tasklist): 新增任务详情 Service/纯函数单测(权限与父子计数规则)
* refactor(tasklist): 提炼任务详情纯函数 TaskListDetailSupport,消除内联重复
* test(supervision): 补齐中小头 Service/枚举/监听器单测
* refactor(xispeak): AfterSDW 监听器复用 FlowVariableSupport,消除第 10 处重复
* test(bgpartymatter): 新增党委会 Service 单测
* test(supervision): 新增反馈流程/反馈Service/问题清单三层校验单测
* refactor(xispeak): 反馈流程经办人分割复用 FlowVariableSupport,消除第 9 处重复
* test(xispeak): 补齐 P1 Service 与监听器单测并消除第 8 处部门ID清洗重复
* test(supervision): 删除已上提的旧 helper 测试文件
* test(supervision): 新增 xispeak 流程/监听器测试并将 helper 测试上提到 common.flow
* refactor(supervision): 流程通用逻辑上提到 common.flow 并消除 fixcontact/xispeak 重复
* test(supervision): 补齐 dqinspecttask Service/监听器单测(validateProblemId 规则锁定)
This commit is contained in:
wsm
2026-08-19 08:55:37 +00:00
parent 41546cfc17
commit 4d006e16d1
50 changed files with 4073 additions and 211 deletions
@@ -1,20 +1,21 @@
package org.jeecg.modules.supervision.fixcontact.listener; package org.jeecg.modules.supervision.common.flow;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
/** /**
* 定点联系流程审批辅助逻辑纯函数无外部依赖可独立测试 * Flowable 流程通用支撑执行实例层级解析无外部依赖可独立测试
* *
* 规则部门领导审批后领导变量写入"上两级"执行实例 * 规则提炼自 fixcontact/xispeak 多处重复内联逻辑
* 部门领导/经办人审批后将审批人写入"上两级"执行实例
* 存在第二层父级存第二层大会签容器/外层作用域否则存第一层 * 存在第二层父级存第二层大会签容器/外层作用域否则存第一层
*/ */
public final class FixContactApprovalHelper { public final class FlowExecutionSupport {
private FixContactApprovalHelper() { private FlowExecutionSupport() {
} }
/** /**
* 解析领导变量应写入的目标执行实例 ID * 解析审批人应写入的目标执行实例 ID
* *
* @param parentId 第一层父级执行实例 ID * @param parentId 第一层父级执行实例 ID
* @param grandParentId 第二层父级执行实例 ID可能为空 * @param grandParentId 第二层父级执行实例 ID可能为空
@@ -1,4 +1,4 @@
package org.jeecg.modules.supervision.fixcontact.flow; package org.jeecg.modules.supervision.common.flow;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
@@ -8,16 +8,16 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* 定点联系流程表达式纯逻辑支撑无外部依赖可独立测试 * Flowable 流程通用支撑流程变量字符串处理无外部依赖可独立测试
* *
* 规则提炼自 FixContactFlow 内联重复逻辑 * 规则提炼自 fixcontact/xispeak 多处重复内联逻辑
* - splitToTrimmedList逗号分割 trim 过滤空白元素 * - splitToTrimmedList逗号分割 trim 过滤空白元素
* - countTokens分割后有效元素个数 * - countTokens分割后有效元素个数
* - cleanDeptId流程变量部门 ID 清洗 [] 与首尾空白 * - cleanDeptId流程变量部门 ID 清洗 [] 与首尾空白
*/ */
public final class FixContactFlowSupport { public final class FlowVariableSupport {
private FixContactFlowSupport() { private FlowVariableSupport() {
} }
public static List<String> splitToTrimmedList(String raw) { public static List<String> splitToTrimmedList(String raw) {
@@ -8,6 +8,7 @@ import org.apache.commons.lang.StringUtils;
import org.flowable.engine.RuntimeService; import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.supervision.common.flow.FlowVariableSupport;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730; import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730; import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730;
@@ -258,7 +259,7 @@ public class FixContactFlow {
execution.getProcessInstanceId()); execution.getProcessInstanceId());
return null; return null;
} }
return FixContactFlowSupport.cleanDeptId(deptVar); return FlowVariableSupport.cleanDeptId(deptVar);
} }
// ${fixContactFlow.getCurrentDeptLeaderList(execution, deptId)} // ${fixContactFlow.getCurrentDeptLeaderList(execution, deptId)}
@@ -357,7 +358,7 @@ public class FixContactFlow {
if (value == null) { if (value == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
return FixContactFlowSupport.splitToTrimmedList(value.toString()); return FlowVariableSupport.splitToTrimmedList(value.toString());
} }
// ${fixContactFlow.getVarAsListCount(execution, varName)} // ${fixContactFlow.getVarAsListCount(execution, varName)}
@@ -367,7 +368,7 @@ public class FixContactFlow {
// ${fixContactFlow.getListSize(data)} // ${fixContactFlow.getListSize(data)}
public int getListSize(String data) { public int getListSize(String data) {
return FixContactFlowSupport.countTokens(data); return FlowVariableSupport.countTokens(data);
} }
// ${fixContactFlow.getIsNeedLeaderApprove(execution)} // ${fixContactFlow.getIsNeedLeaderApprove(execution)}
@@ -606,7 +607,7 @@ public class FixContactFlow {
if (StringUtils.isBlank(rawValue)) { if (StringUtils.isBlank(rawValue)) {
return Collections.emptyList(); return Collections.emptyList();
} }
List<String> result = FixContactFlowSupport.splitToTrimmedList(rawValue); List<String> result = FlowVariableSupport.splitToTrimmedList(rawValue);
log.info("【定点联系-流程表达式】{} 解析结果: {}", logLabel, result); log.info("【定点联系-流程表达式】{} 解析结果: {}", logLabel, result);
return result; return result;
} catch (Exception e) { } catch (Exception e) {
@@ -7,6 +7,7 @@ import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.TaskListener; import org.flowable.engine.delegate.TaskListener;
import org.flowable.engine.runtime.Execution; import org.flowable.engine.runtime.Execution;
import org.flowable.task.service.delegate.DelegateTask; import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.common.flow.FlowExecutionSupport;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -62,8 +63,8 @@ public class FixContactDeptLeaderApproveListener implements TaskListener {
if (parentExecution == null) { if (parentExecution == null) {
return; return;
} }
// 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层(规则见 FixContactApprovalHelper // 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层(规则见 FlowExecutionSupport
String targetId = FixContactApprovalHelper.resolveTargetExecutionId(parentId, parentExecution.getParentId()); String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, parentExecution.getParentId());
try { try {
runtimeService.setVariableLocal(targetId, VARIABLE_NAME, assignee); runtimeService.setVariableLocal(targetId, VARIABLE_NAME, assignee);
log.info("【定点联系-部门领导审批监听器】已存入上两级局部变量, targetExecutionId: {}, variableName: {}, assignee: {}", log.info("【定点联系-部门领导审批监听器】已存入上两级局部变量, targetExecutionId: {}, variableName: {}, assignee: {}",
@@ -8,7 +8,7 @@ import org.flowable.engine.delegate.TaskListener;
import org.flowable.task.service.delegate.DelegateTask; import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant; import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730; import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.flow.FixContactFlowSupport; import org.jeecg.modules.supervision.common.flow.FlowVariableSupport;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service; import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
@@ -75,7 +75,7 @@ public class FixContactWorkerApproveListener implements TaskListener {
log.warn("【定点联系-经办人会签监听器】未获取到子流程当前部门ID,跳过记录经办人, taskId: {}", delegateTask.getId()); log.warn("【定点联系-经办人会签监听器】未获取到子流程当前部门ID,跳过记录经办人, taskId: {}", delegateTask.getId());
return; return;
} }
String deptId = FixContactFlowSupport.cleanDeptId(rawDeptVar); String deptId = FlowVariableSupport.cleanDeptId(rawDeptVar);
FixedContact20260730 fixedContact = fixedContact20260730Service.getByIdForUpdate(rawBusinessKey.toString()); FixedContact20260730 fixedContact = fixedContact20260730Service.getByIdForUpdate(rawBusinessKey.toString());
if (fixedContact == null) { if (fixedContact == null) {
@@ -161,7 +161,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
throw new IllegalStateException("生成提案编号失败,请稍后重试"); throw new IllegalStateException("生成提案编号失败,请稍后重试");
} }
improvementProposal.setProposalNo(formatProposalNo(improvementProposal.getProposalDeptName(), proposalYear, sequence)); improvementProposal.setProposalNo(ImprovementProposalSupport.formatProposalNo(improvementProposal.getProposalDeptName(), proposalYear, sequence));
saveMain(improvementProposal, null, null); saveMain(improvementProposal, null, null);
} }
@@ -287,9 +287,6 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
return calendar.get(Calendar.YEAR); return calendar.get(Calendar.YEAR);
} }
private String formatProposalNo(String proposalDeptName, int proposalYear, int sequence) {
return String.format("%s-%d年-%0" + PROPOSAL_NO_SEQUENCE_LENGTH + "d", proposalDeptName.trim(), proposalYear, sequence);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@@ -382,7 +379,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
public void saveInitialReview(ImprovementProposalBpmActionDTO action, LoginUser loginUser) { public void saveInitialReview(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
validateAward(action.getInitialAward()); validateAward(action.getInitialAward());
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_INITIAL_REVIEW); ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_INITIAL_REVIEW);
String needDeptScore = needsDeptScore(action.getInitialAward()) ? "1" : "0"; String needDeptScore = ImprovementProposalSupport.needsDeptScore(action.getInitialAward()) ? "1" : "0";
ImprovementProposal update = new ImprovementProposal(); ImprovementProposal update = new ImprovementProposal();
update.setId(proposal.getId()); update.setId(proposal.getId());
update.setInitialAward(action.getInitialAward()); update.setInitialAward(action.getInitialAward());
@@ -416,8 +413,8 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
} }
// 改善效果由四个子项在服务端计算,避免前端篡改汇总得分或加权总分。 // 改善效果由四个子项在服务端计算,避免前端篡改汇总得分或加权总分。
BigDecimal effectScore = calculateEffectScore(scoreAction); BigDecimal effectScore = ImprovementProposalSupport.calculateEffectScore(scoreAction);
BigDecimal totalScore = calculateTotalScore(scoreAction, effectScore); BigDecimal totalScore = ImprovementProposalSupport.calculateTotalScore(scoreAction, effectScore);
if (existingScore == null) { if (existingScore == null) {
ImprovementDeptScore newScore = new ImprovementDeptScore(); ImprovementDeptScore newScore = new ImprovementDeptScore();
newScore.setMainId(lockedProposal.getId()); newScore.setMainId(lockedProposal.getId());
@@ -487,7 +484,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
validateAward(action.getLeaderFinalAward()); validateAward(action.getLeaderFinalAward());
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_LEADER_AWARD); ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_LEADER_AWARD);
String awardBase = resolveLeaderAwardBase(proposal); String awardBase = resolveLeaderAwardBase(proposal);
if (awardRank(action.getLeaderFinalAward()) < awardRank(awardBase)) { if (ImprovementProposalSupport.awardRank(action.getLeaderFinalAward()) < ImprovementProposalSupport.awardRank(awardBase)) {
throw new IllegalStateException("负责人确认奖项只能选择当前档位或降档奖项"); throw new IllegalStateException("负责人确认奖项只能选择当前档位或降档奖项");
} }
String needInstituteVote = AWARD_GOLD.equals(action.getLeaderFinalAward()) ? "1" : "0"; String needInstituteVote = AWARD_GOLD.equals(action.getLeaderFinalAward()) ? "1" : "0";
@@ -756,21 +753,8 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
} }
} }
private BigDecimal calculateEffectScore(ImprovementDeptScoreBpmDTO scoreAction) {
return calculateEffectScore(scoreAction.getSafetyScore(), scoreAction.getQualityScore(), scoreAction.getEfficiencyScore(), scoreAction.getCostScore());
}
private BigDecimal calculateEffectScore(BigDecimal safetyScore, BigDecimal qualityScore, BigDecimal efficiencyScore, BigDecimal costScore) {
return safetyScore.add(qualityScore).add(efficiencyScore).add(costScore).divide(BigDecimal.valueOf(4), 2, RoundingMode.HALF_UP);
}
private BigDecimal calculateTotalScore(ImprovementDeptScoreBpmDTO scoreAction, BigDecimal effectScore) {
return effectScore.multiply(new BigDecimal("0.50"))
.add(scoreAction.getPromotionScore().multiply(new BigDecimal("0.20")))
.add(scoreAction.getOriginalityScore().multiply(new BigDecimal("0.20")))
.add(scoreAction.getEffortScore().multiply(new BigDecimal("0.10")))
.setScale(2, RoundingMode.HALF_UP);
}
private void fillDeptScoreDraft(ImprovementDeptScore target, ImprovementDeptScoreBpmDTO source, BigDecimal effectScore, BigDecimal totalScore) { private void fillDeptScoreDraft(ImprovementDeptScore target, ImprovementDeptScoreBpmDTO source, BigDecimal effectScore, BigDecimal totalScore) {
target.setSafetyScore(source.getSafetyScore()); target.setSafetyScore(source.getSafetyScore());
@@ -795,7 +779,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
validateScoreValue(score.getPromotionScore(), "可推广度"); validateScoreValue(score.getPromotionScore(), "可推广度");
validateScoreValue(score.getOriginalityScore(), "独创性"); validateScoreValue(score.getOriginalityScore(), "独创性");
validateScoreValue(score.getEffortScore(), "努力度"); validateScoreValue(score.getEffortScore(), "努力度");
BigDecimal calculatedEffectScore = calculateEffectScore(score.getSafetyScore(), score.getQualityScore(), score.getEfficiencyScore(), score.getCostScore()); BigDecimal calculatedEffectScore = ImprovementProposalSupport.calculateEffectScore(score.getSafetyScore(), score.getQualityScore(), score.getEfficiencyScore(), score.getCostScore());
if (score.getEffectScore() == null || score.getEffectScore().compareTo(calculatedEffectScore) != 0 || score.getTotalScore() == null) { if (score.getEffectScore() == null || score.getEffectScore().compareTo(calculatedEffectScore) != 0 || score.getTotalScore() == null) {
throw new IllegalStateException("请先保存完整个人评分"); throw new IllegalStateException("请先保存完整个人评分");
} }
@@ -820,22 +804,10 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
ImprovementProposal summary = new ImprovementProposal(); ImprovementProposal summary = new ImprovementProposal();
summary.setId(proposal.getId()); summary.setId(proposal.getId());
summary.setDeptAvgScore(averageScore); summary.setDeptAvgScore(averageScore);
summary.setSuggestedAward(resolveSuggestedAward(averageScore)); summary.setSuggestedAward(ImprovementProposalSupport.resolveSuggestedAward(averageScore));
improvementProposalMapper.updateById(summary); improvementProposalMapper.updateById(summary);
} }
private String resolveSuggestedAward(BigDecimal averageScore) {
if (averageScore.compareTo(BigDecimal.valueOf(80)) >= 0) {
return AWARD_GOLD;
}
if (averageScore.compareTo(BigDecimal.valueOf(60)) >= 0) {
return AWARD_SILVER;
}
if (averageScore.compareTo(BigDecimal.valueOf(40)) >= 0) {
return AWARD_BRONZE;
}
return AWARD_PARTICIPATION;
}
private ImprovementProposal validateRunningTask(ImprovementProposalBpmActionDTO action, LoginUser loginUser, String expectedTaskDefKey) { private ImprovementProposal validateRunningTask(ImprovementProposalBpmActionDTO action, LoginUser loginUser, String expectedTaskDefKey) {
if (action == null || oConvertUtils.isEmpty(action.getMainId()) || oConvertUtils.isEmpty(action.getProcessInstanceId()) if (action == null || oConvertUtils.isEmpty(action.getMainId()) || oConvertUtils.isEmpty(action.getProcessInstanceId())
@@ -858,33 +830,24 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
} }
private void validateAward(String award) { private void validateAward(String award) {
if (!isValidAward(award)) { if (!ImprovementProposalSupport.isValidAward(award)) {
throw new IllegalStateException("请选择有效的奖项"); throw new IllegalStateException("请选择有效的奖项");
} }
} }
private boolean isValidAward(String award) {
return AWARD_GOLD.equals(award) || AWARD_SILVER.equals(award) || AWARD_BRONZE.equals(award) || AWARD_PARTICIPATION.equals(award);
}
/** /**
* 负责人只能在当前评定档位内确认或降档:经过评分时以系统建议奖项为准,未评分时以初评奖项为准。 * 负责人只能在当前评定档位内确认或降档:经过评分时以系统建议奖项为准,未评分时以初评奖项为准。
*/ */
private String resolveLeaderAwardBase(ImprovementProposal proposal) { private String resolveLeaderAwardBase(ImprovementProposal proposal) {
String awardBase = "1".equals(proposal.getNeedDeptScore()) ? proposal.getSuggestedAward() : proposal.getInitialAward(); String awardBase = "1".equals(proposal.getNeedDeptScore()) ? proposal.getSuggestedAward() : proposal.getInitialAward();
if (!isValidAward(awardBase)) { if (!ImprovementProposalSupport.isValidAward(awardBase)) {
throw new IllegalStateException("部门评议建议奖项尚未生成,不能确认负责人奖项"); throw new IllegalStateException("部门评议建议奖项尚未生成,不能确认负责人奖项");
} }
return awardBase; return awardBase;
} }
private int awardRank(String award) {
return Integer.parseInt(award);
}
private boolean needsDeptScore(String award) {
return AWARD_GOLD.equals(award) || AWARD_SILVER.equals(award);
}
/** /**
* 任何会改变主表生命周期的操作都必须先锁定并确认仍是未发起草稿, * 任何会改变主表生命周期的操作都必须先锁定并确认仍是未发起草稿,
@@ -0,0 +1,86 @@
package org.jeecg.modules.supervision.improvementproposal.service.impl;
import org.jeecg.modules.supervision.improvementproposal.dto.ImprovementDeptScoreBpmDTO;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 改善提案纯逻辑支撑(无外部依赖,可独立测试)。
*
* 规则(提炼自 ImprovementProposalServiceImpl 内联逻辑):
* - formatProposalNo:流水号格式化(部门-年份-4 位序号)
* - calculateEffectScore:四维效果分平均(HALF_UP 2 位)
* - calculateTotalScore:加权总分(效果 0.5 / 推广 0.2 / 独创 0.2 / 努力 0.1
* - resolveSuggestedAward:按平均分建议奖级(≥80 金 / ≥60 银 / ≥40 铜 / 参与)
* - isValidAward / awardRank / needsDeptScore:奖级合法性/排名/是否需要部门评分
*/
public final class ImprovementProposalSupport {
public static final String AWARD_GOLD = "0";
public static final String AWARD_SILVER = "1";
public static final String AWARD_BRONZE = "2";
public static final String AWARD_PARTICIPATION = "3";
private static final int PROPOSAL_NO_SEQUENCE_LENGTH = 4;
private ImprovementProposalSupport() {
}
public static String formatProposalNo(String proposalDeptName, int proposalYear, int sequence) {
return String.format("%s-%d年-%0" + PROPOSAL_NO_SEQUENCE_LENGTH + "d", proposalDeptName.trim(), proposalYear, sequence);
}
public static BigDecimal calculateEffectScore(BigDecimal safetyScore, BigDecimal qualityScore,
BigDecimal efficiencyScore, BigDecimal costScore) {
return safetyScore.add(qualityScore).add(efficiencyScore).add(costScore)
.divide(BigDecimal.valueOf(4), 2, RoundingMode.HALF_UP);
}
/** DTO 重载(兼容原调用点) */
public static BigDecimal calculateEffectScore(ImprovementDeptScoreBpmDTO scoreAction) {
return calculateEffectScore(scoreAction.getSafetyScore(), scoreAction.getQualityScore(),
scoreAction.getEfficiencyScore(), scoreAction.getCostScore());
}
public static BigDecimal calculateTotalScore(BigDecimal effectScore, BigDecimal promotionScore,
BigDecimal originalityScore, BigDecimal effortScore) {
return effectScore.multiply(new BigDecimal("0.50"))
.add(promotionScore.multiply(new BigDecimal("0.20")))
.add(originalityScore.multiply(new BigDecimal("0.20")))
.add(effortScore.multiply(new BigDecimal("0.10")))
.setScale(2, RoundingMode.HALF_UP);
}
/** DTO 重载(兼容原调用点) */
public static BigDecimal calculateTotalScore(ImprovementDeptScoreBpmDTO scoreAction, BigDecimal effectScore) {
return calculateTotalScore(effectScore, scoreAction.getPromotionScore(),
scoreAction.getOriginalityScore(), scoreAction.getEffortScore());
}
public static String resolveSuggestedAward(BigDecimal averageScore) {
if (averageScore.compareTo(BigDecimal.valueOf(80)) >= 0) {
return AWARD_GOLD;
}
if (averageScore.compareTo(BigDecimal.valueOf(60)) >= 0) {
return AWARD_SILVER;
}
if (averageScore.compareTo(BigDecimal.valueOf(40)) >= 0) {
return AWARD_BRONZE;
}
return AWARD_PARTICIPATION;
}
public static boolean isValidAward(String award) {
return AWARD_GOLD.equals(award) || AWARD_SILVER.equals(award)
|| AWARD_BRONZE.equals(award) || AWARD_PARTICIPATION.equals(award);
}
public static int awardRank(String award) {
return Integer.parseInt(award);
}
public static boolean needsDeptScore(String award) {
return AWARD_GOLD.equals(award) || AWARD_SILVER.equals(award);
}
}
@@ -0,0 +1,65 @@
package org.jeecg.modules.supervision.tasklist.service.impl;
import org.jeecg.common.util.oConvertUtils;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* 任务清单详情纯逻辑支撑(无外部依赖,可独立测试)。
*
* 规则(提炼自 TaskListDetialServiceImpl 内联逻辑):
* - normalizePid:空 pid 归一为 null
* - collectUserIds:逗号分隔用户ID收集到集合(trim + 去空)
* - buildNamesFromMapID 列表 → 姓名列表(按映射,保序拼接)
* - mergeIds:两组用户ID合并去重(LinkedHashSet 保序)
*/
public final class TaskListDetailSupport {
private TaskListDetailSupport() {
}
public static String normalizePid(String pid) {
return oConvertUtils.isNotEmpty(pid) ? pid : null;
}
public static void collectUserIds(String idsStr, Set<String> idSet) {
if (oConvertUtils.isEmpty(idsStr)) {
return;
}
for (String id : idsStr.split(",")) {
String trimmed = id.trim();
if (oConvertUtils.isNotEmpty(trimmed)) {
idSet.add(trimmed);
}
}
}
public static String buildNamesFromMap(String idsStr, Map<String, String> idToRealname) {
if (oConvertUtils.isEmpty(idsStr) || idToRealname.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (String id : idsStr.split(",")) {
String trimmed = id.trim();
if (oConvertUtils.isNotEmpty(trimmed)) {
String name = idToRealname.get(trimmed);
if (name != null) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(name);
}
}
}
return sb.length() > 0 ? sb.toString() : null;
}
public static String mergeIds(String existingIds, String newIds) {
Set<String> idSet = new LinkedHashSet<>();
collectUserIds(existingIds, idSet);
collectUserIds(newIds, idSet);
return idSet.isEmpty() ? null : String.join(",", idSet);
}
}
@@ -75,7 +75,7 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
entity.setTaskDesc(req.getTaskDesc()); entity.setTaskDesc(req.getTaskDesc());
entity.setPriority(req.getPriority()); entity.setPriority(req.getPriority());
entity.setType(req.getType() != null ? req.getType() : "1"); entity.setType(req.getType() != null ? req.getType() : "1");
entity.setPid(normalizePid(req.getPid())); entity.setPid(TaskListDetailSupport.normalizePid(req.getPid()));
entity.setTaskStatus(0); entity.setTaskStatus(0);
entity.setSubTaskCount(0); entity.setSubTaskCount(0);
entity.setCompletedSubTaskCount(0); entity.setCompletedSubTaskCount(0);
@@ -98,10 +98,10 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
} }
if (req.getSortOrder() != null) { if (req.getSortOrder() != null) {
taskListDetialMapper.shiftSortOrderUp(req.getMainId(), normalizePid(req.getPid()), req.getSortOrder()); taskListDetialMapper.shiftSortOrderUp(req.getMainId(), TaskListDetailSupport.normalizePid(req.getPid()), req.getSortOrder());
entity.setSortOrder(req.getSortOrder()); entity.setSortOrder(req.getSortOrder());
} else { } else {
Integer maxSort = taskListDetialMapper.getMaxSortOrder(req.getMainId(), normalizePid(req.getPid())); Integer maxSort = taskListDetialMapper.getMaxSortOrder(req.getMainId(), TaskListDetailSupport.normalizePid(req.getPid()));
entity.setSortOrder(maxSort + 1); entity.setSortOrder(maxSort + 1);
} }
@@ -366,8 +366,8 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
throw new RuntimeException("可阅读者无法移动任务"); throw new RuntimeException("可阅读者无法移动任务");
} }
String oldPid = normalizePid(task.getPid()); String oldPid = TaskListDetailSupport.normalizePid(task.getPid());
String newPid = normalizePid(req.getTargetPid()); String newPid = TaskListDetailSupport.normalizePid(req.getTargetPid());
boolean pidChanged = (oldPid == null && newPid != null) || (oldPid != null && !oldPid.equals(newPid)); boolean pidChanged = (oldPid == null && newPid != null) || (oldPid != null && !oldPid.equals(newPid));
@@ -552,9 +552,6 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
return perm != null ? perm.getPermission() : null; return perm != null ? perm.getPermission() : null;
} }
private String normalizePid(String pid) {
return oConvertUtils.isNotEmpty(pid) ? pid : null;
}
private void fillCreateByName(List<TaskListDetial> list) { private void fillCreateByName(List<TaskListDetial> list) {
if (list == null || list.isEmpty()) { if (list == null || list.isEmpty()) {
@@ -568,9 +565,9 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
if (oConvertUtils.isNotEmpty(task.getCreateBy())) { if (oConvertUtils.isNotEmpty(task.getCreateBy())) {
allUsernames.add(task.getCreateBy()); allUsernames.add(task.getCreateBy());
} }
collectUserIds(task.getAssigneeId(), allUserIds); TaskListDetailSupport.collectUserIds(task.getAssigneeId(), allUserIds);
collectUserIds(task.getParticipantId(), allUserIds); TaskListDetailSupport.collectUserIds(task.getParticipantId(), allUserIds);
collectUserIds(task.getFollowersId(), allUserIds); TaskListDetailSupport.collectUserIds(task.getFollowersId(), allUserIds);
} }
// 2. 批量查询,构建映射表 // 2. 批量查询,构建映射表
@@ -603,48 +600,18 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
} }
} }
if (oConvertUtils.isNotEmpty(task.getAssigneeId()) && oConvertUtils.isEmpty(task.getAssigneeName())) { if (oConvertUtils.isNotEmpty(task.getAssigneeId()) && oConvertUtils.isEmpty(task.getAssigneeName())) {
task.setAssigneeName(buildNamesFromMap(task.getAssigneeId(), idToRealname)); task.setAssigneeName(TaskListDetailSupport.buildNamesFromMap(task.getAssigneeId(), idToRealname));
} }
if (oConvertUtils.isNotEmpty(task.getParticipantId()) && oConvertUtils.isEmpty(task.getParticipantName())) { if (oConvertUtils.isNotEmpty(task.getParticipantId()) && oConvertUtils.isEmpty(task.getParticipantName())) {
task.setParticipantName(buildNamesFromMap(task.getParticipantId(), idToRealname)); task.setParticipantName(TaskListDetailSupport.buildNamesFromMap(task.getParticipantId(), idToRealname));
} }
if (oConvertUtils.isNotEmpty(task.getFollowersId()) && oConvertUtils.isEmpty(task.getFollowersName())) { if (oConvertUtils.isNotEmpty(task.getFollowersId()) && oConvertUtils.isEmpty(task.getFollowersName())) {
task.setFollowersName(buildNamesFromMap(task.getFollowersId(), idToRealname)); task.setFollowersName(TaskListDetailSupport.buildNamesFromMap(task.getFollowersId(), idToRealname));
} }
} }
} }
private void collectUserIds(String idsStr, Set<String> idSet) {
if (oConvertUtils.isEmpty(idsStr)) {
return;
}
for (String id : idsStr.split(",")) {
String trimmed = id.trim();
if (oConvertUtils.isNotEmpty(trimmed)) {
idSet.add(trimmed);
}
}
}
private String buildNamesFromMap(String idsStr, Map<String, String> idToRealname) {
if (oConvertUtils.isEmpty(idsStr) || idToRealname.isEmpty()) {
return null;
}
StringBuilder sb = new StringBuilder();
for (String id : idsStr.split(",")) {
String trimmed = id.trim();
if (oConvertUtils.isNotEmpty(trimmed)) {
String name = idToRealname.get(trimmed);
if (name != null) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(name);
}
}
}
return sb.length() > 0 ? sb.toString() : null;
}
private String translateUserIdsToNames(String ids) { private String translateUserIdsToNames(String ids) {
if (oConvertUtils.isEmpty(ids)) { if (oConvertUtils.isEmpty(ids)) {
@@ -664,26 +631,6 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
return names.toString(); return names.toString();
} }
private String mergeIds(String existingIds, String newIds) {
Set<String> idSet = new LinkedHashSet<>();
if (oConvertUtils.isNotEmpty(existingIds)) {
for (String id : existingIds.split(",")) {
String trimmed = id.trim();
if (oConvertUtils.isNotEmpty(trimmed)) {
idSet.add(trimmed);
}
}
}
if (oConvertUtils.isNotEmpty(newIds)) {
for (String id : newIds.split(",")) {
String trimmed = id.trim();
if (oConvertUtils.isNotEmpty(trimmed)) {
idSet.add(trimmed);
}
}
}
return idSet.isEmpty() ? null : String.join(",", idSet);
}
private String filterUsersBySecLevel(String userIds, Integer listSecLevel) { private String filterUsersBySecLevel(String userIds, Integer listSecLevel) {
if (oConvertUtils.isEmpty(userIds)) return userIds; if (oConvertUtils.isEmpty(userIds)) return userIds;
@@ -233,7 +233,7 @@ public class TaskListServiceImpl extends ServiceImpl<TaskListMapper, TaskList> i
throw new RuntimeException("分组不支持移动操作"); throw new RuntimeException("分组不支持移动操作");
} }
String oldPid = normalizePid(favorite.getPid()); String oldPid = TaskListDetailSupport.normalizePid(favorite.getPid());
String newPid = null; String newPid = null;
if (req.getTargetGroupId() != null) { if (req.getTargetGroupId() != null) {
@@ -242,7 +242,7 @@ public class TaskListServiceImpl extends ServiceImpl<TaskListMapper, TaskList> i
if (targetGroup == null || !"0".equals(targetGroup.getType()) || !userId.equals(targetGroup.getUserId())) { if (targetGroup == null || !"0".equals(targetGroup.getType()) || !userId.equals(targetGroup.getUserId())) {
throw new RuntimeException("目标分组不存在或无权限"); throw new RuntimeException("目标分组不存在或无权限");
} }
newPid = normalizePid(req.getTargetGroupId()); newPid = TaskListDetailSupport.normalizePid(req.getTargetGroupId());
} else { } else {
newPid = ""; newPid = "";
} }
@@ -700,9 +700,6 @@ public class TaskListServiceImpl extends ServiceImpl<TaskListMapper, TaskList> i
return taskListMapper.selectCount(query).intValue(); return taskListMapper.selectCount(query).intValue();
} }
private String normalizePid(String pid) {
return oConvertUtils.isNotEmpty(pid) ? pid : null;
}
private List<TaskList> enrichListSummaries(List<TaskList> lists, String currentUserId) { private List<TaskList> enrichListSummaries(List<TaskList> lists, String currentUserId) {
if (lists == null || lists.isEmpty()) { if (lists == null || lists.isEmpty()) {
@@ -10,6 +10,7 @@ import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.jeecg.modules.supervision.common.OrgConfig; import org.jeecg.modules.supervision.common.OrgConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakFeedbackConfig; import org.jeecg.modules.supervision.xispeak.config.XiSpeakFeedbackConfig;
import org.jeecg.modules.supervision.common.flow.FlowVariableSupport;
import org.jeecg.common.constant.SymbolConstant; import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
@@ -198,10 +199,7 @@ public class XiSpeakFeedbackFlow {
} }
// 将 "张三, 李四 " 转换为 ["张三", "李四"] // 将 "张三, 李四 " 转换为 ["张三", "李四"]
return Arrays.stream(implDeptWorker.split(",")) return FlowVariableSupport.splitToTrimmedList(implDeptWorker);
.map(String::trim) // 去掉前后空格
.filter(StringUtils::isNotBlank) // 过滤掉空字符串(防止出现 "张三,,李四"
.collect(Collectors.toList());
} }
/** /**
@@ -15,6 +15,7 @@ import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak; import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail; import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService; import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.supervision.common.flow.FlowVariableSupport;
import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression; import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -125,16 +126,7 @@ public class XiSpeakFlow {
String rawValue = data.getString(codeName); String rawValue = data.getString(codeName);
// 4. 转换逗号分隔的字符串为 List // 4. 转换逗号分隔的字符串为 List
List<String> resultList; List<String> resultList = FlowVariableSupport.splitToTrimmedList(rawValue);
if (org.apache.commons.lang.StringUtils.isBlank(rawValue)) {
resultList = Collections.emptyList();
} else {
// 使用逗号分割,并过滤掉每个元素前后的空格
resultList = Arrays.stream(rawValue.split(","))
.map(String::trim)
.filter(org.apache.commons.lang.StringUtils::isNotBlank) // 过滤掉因连续逗号产生的空元素
.collect(Collectors.toList());
}
log.info("{} 业务解析结果: {}", logLabel, resultList); log.info("{} 业务解析结果: {}", logLabel, resultList);
return resultList; return resultList;
@@ -285,7 +277,7 @@ public class XiSpeakFlow {
if (org.apache.commons.lang.StringUtils.isBlank(businessKey) || rawDeptVar == null) { if (org.apache.commons.lang.StringUtils.isBlank(businessKey) || rawDeptVar == null) {
return null; return null;
} }
String deptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim(); String deptId = FlowVariableSupport.cleanDeptId(rawDeptVar);
BgXiSpeak xiSpeak = bgXiSpeakService.getById(businessKey); BgXiSpeak xiSpeak = bgXiSpeakService.getById(businessKey);
if (xiSpeak == null || xiSpeak.getApproveInfo() == null) { if (xiSpeak == null || xiSpeak.getApproveInfo() == null) {
return null; return null;
@@ -340,13 +332,7 @@ public class XiSpeakFlow {
} }
public int getListSize(String jsonData){ public int getListSize(String jsonData){
if (org.apache.commons.lang3.StringUtils.isEmpty(jsonData)) { return FlowVariableSupport.countTokens(jsonData);
return 0;
}
return Math.toIntExact(Arrays.stream(jsonData.split(","))
.map(String::trim)
.filter(org.apache.commons.lang3.StringUtils::isNotEmpty)
.count());
} }
// List<String> getImplDeptWorker(String JG_LOCAL_PROCESS_ID,String deptId){ // List<String> getImplDeptWorker(String JG_LOCAL_PROCESS_ID,String deptId){
@@ -421,10 +407,7 @@ public class XiSpeakFlow {
value = execution.getVariable(variableName); value = execution.getVariable(variableName);
} }
if (value != null) { if (value != null) {
return Arrays.stream(String.valueOf(value).split(SymbolConstant.COMMA)) return FlowVariableSupport.splitToTrimmedList(String.valueOf(value));
.map(String::trim)
.filter(org.apache.commons.lang.StringUtils::isNotBlank)
.collect(Collectors.toList());
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("getDeptWorkerList from process variable failed", e); log.warn("getDeptWorkerList from process variable failed", e);
@@ -19,6 +19,7 @@ import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals;
import org.jeecg.modules.tasktask.entity.TaskTask; import org.jeecg.modules.tasktask.entity.TaskTask;
import org.jeecg.modules.tasktask.service.ITaskTaskService; import org.jeecg.modules.tasktask.service.ITaskTaskService;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.common.flow.FlowExecutionSupport;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -75,9 +76,8 @@ public class AfterBgLeaderApproveListener implements TaskListener {
// 4. 获取第二层父级 ID (大会签容器/外层作用域) // 4. 获取第二层父级 ID (大会签容器/外层作用域)
String grandParentId = parentExecution.getParentId(); String grandParentId = parentExecution.getParentId();
// 5. 确定最终存储目标 // 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层)
// 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层 String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId);
String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId;
try { try {
// 6. 执行存储 // 6. 执行存储
@@ -20,6 +20,7 @@ import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals;
import org.jeecg.modules.tasktask.entity.TaskTask; import org.jeecg.modules.tasktask.entity.TaskTask;
import org.jeecg.modules.tasktask.service.ITaskTaskService; import org.jeecg.modules.tasktask.service.ITaskTaskService;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.common.flow.FlowExecutionSupport;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -35,11 +36,7 @@ public class AfterImplDeptLeaderApproveListener implements TaskListener {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final String VARIABLE_NAME = "tem_impl_leader"; private static final String VARIABLE_NAME = "tem_impl_leader";
private static RuntimeService runtimeService; private final RuntimeService runtimeService;
static {
runtimeService = SpringContextUtils.getBean(RuntimeService.class);
}
@Override @Override
public void notify(DelegateTask delegateTask) { public void notify(DelegateTask delegateTask) {
@@ -75,9 +72,8 @@ public class AfterImplDeptLeaderApproveListener implements TaskListener {
// 4. 获取第二层父级 ID (大会签容器/外层作用域) // 4. 获取第二层父级 ID (大会签容器/外层作用域)
String grandParentId = parentExecution.getParentId(); String grandParentId = parentExecution.getParentId();
// 5. 确定最终存储目标 // 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层)
// 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层 String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId);
String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId;
try { try {
// 6. 执行存储 // 6. 执行存储
@@ -20,6 +20,7 @@ import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals;
import org.jeecg.modules.tasktask.entity.TaskTask; import org.jeecg.modules.tasktask.entity.TaskTask;
import org.jeecg.modules.tasktask.service.ITaskTaskService; import org.jeecg.modules.tasktask.service.ITaskTaskService;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.common.flow.FlowVariableSupport;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -74,7 +75,7 @@ public class AfterImplWorkerApproveListener implements TaskListener {
return; return;
} }
String cleanDeptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim(); String cleanDeptId = FlowVariableSupport.cleanDeptId(rawDeptVar);
DeptApproveDetailMap approveMap = xiSpeak.getApproveInfo(); DeptApproveDetailMap approveMap = xiSpeak.getApproveInfo();
if (approveMap == null) { if (approveMap == null) {
@@ -20,6 +20,7 @@ import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals;
import org.jeecg.modules.tasktask.entity.TaskTask; import org.jeecg.modules.tasktask.entity.TaskTask;
import org.jeecg.modules.tasktask.service.ITaskTaskService; import org.jeecg.modules.tasktask.service.ITaskTaskService;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.common.flow.FlowExecutionSupport;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -35,11 +36,7 @@ public class AfterImplWorkerTemStoreListener implements TaskListener {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final String VARIABLE_NAME = "tem_impl_worker"; private static final String VARIABLE_NAME = "tem_impl_worker";
private static RuntimeService runtimeService; private final RuntimeService runtimeService;
static {
runtimeService = SpringContextUtils.getBean(RuntimeService.class);
}
@Override @Override
public void notify(DelegateTask delegateTask) { public void notify(DelegateTask delegateTask) {
@@ -75,9 +72,8 @@ public class AfterImplWorkerTemStoreListener implements TaskListener {
// 4. 获取第二层父级 ID (大会签容器/外层作用域) // 4. 获取第二层父级 ID (大会签容器/外层作用域)
String grandParentId = parentExecution.getParentId(); String grandParentId = parentExecution.getParentId();
// 5. 确定最终存储目标 // 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层)
// 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层 String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId);
String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId;
try { try {
// 6. 执行存储 // 6. 执行存储
@@ -9,6 +9,7 @@ import org.flowable.engine.delegate.ExecutionListener;
import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals; import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig; import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.common.flow.FlowVariableSupport;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -65,10 +66,7 @@ public class AfterSDWApproveHqListener implements ExecutionListener {
// 3. 只有当字段存在且内容不为空时,才进行解析 // 3. 只有当字段存在且内容不为空时,才进行解析
if (oConvertUtils.isNotEmpty(jsonStr)) { if (oConvertUtils.isNotEmpty(jsonStr)) {
List<String> deptIds = Arrays.stream(jsonStr.split(",")) List<String> deptIds = FlowVariableSupport.splitToTrimmedList(jsonStr);
.map(String::trim) // 去掉可能存在的空格
.filter(s -> !s.isEmpty()) // 过滤掉空字符串
.collect(Collectors.toList());
if (deptIds != null && !deptIds.isEmpty()) { if (deptIds != null && !deptIds.isEmpty()) {
// 4. 将提取出的 List 存入子流程变量(供多实例 Collection 使用) // 4. 将提取出的 List 存入子流程变量(供多实例 Collection 使用)
@@ -0,0 +1,44 @@
package org.jeecg.modules.supervision.bgpartymatter.service.impl;
import org.jeecg.modules.supervision.bgpartymatter.entity.BgPartymatterFeedback;
import org.jeecg.modules.supervision.bgpartymatter.mapper.BgPartymatterFeedbackMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* BgPartymatterFeedbackServiceImpl 测试(简单转发)。
*/
@ExtendWith(MockitoExtension.class)
class BgPartymatterFeedbackServiceImplTest {
@Mock private BgPartymatterFeedbackMapper bgPartymatterFeedbackMapper;
private BgPartymatterFeedbackServiceImpl service;
@BeforeEach
void setUp() {
service = new BgPartymatterFeedbackServiceImpl();
ReflectionTestUtils.setField(service, "bgPartymatterFeedbackMapper", bgPartymatterFeedbackMapper);
}
@Test
void should_select_by_main_id() {
BgPartymatterFeedback feedback = new BgPartymatterFeedback();
when(bgPartymatterFeedbackMapper.selectByMainId("m-1")).thenReturn(List.of(feedback));
List<BgPartymatterFeedback> result = service.selectByMainId("m-1");
assertEquals(1, result.size());
verify(bgPartymatterFeedbackMapper).selectByMainId("m-1");
}
}
@@ -0,0 +1,62 @@
package org.jeecg.modules.supervision.bgpartymatter.service.impl;
import org.flowable.engine.RuntimeService;
import org.jeecg.modules.supervision.bgpartymatter.entity.BgPartymatter;
import org.jeecg.modules.supervision.bgpartymatter.mapper.BgPartymatterFeedbackMapper;
import org.jeecg.modules.supervision.bgpartymatter.mapper.BgPartymatterMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.mockito.Mockito.verify;
/**
* BgPartymatterServiceImpl 测试(主从删除 + 流程变量同步)。
*/
@ExtendWith(MockitoExtension.class)
class BgPartymatterServiceImplTest {
@Mock private BgPartymatterMapper bgPartymatterMapper;
@Mock private BgPartymatterFeedbackMapper bgPartymatterFeedbackMapper;
@Mock private RuntimeService runtimeService;
private BgPartymatterServiceImpl service;
@BeforeEach
void setUp() {
service = new BgPartymatterServiceImpl();
ReflectionTestUtils.setField(service, "bgPartymatterMapper", bgPartymatterMapper);
ReflectionTestUtils.setField(service, "baseMapper", bgPartymatterMapper);
ReflectionTestUtils.setField(service, "bgPartymatterFeedbackMapper", bgPartymatterFeedbackMapper);
ReflectionTestUtils.setField(service, "runtimeService", runtimeService);
}
@Test
void should_delete_main_with_feedback() {
service.delMain("m-1");
verify(bgPartymatterFeedbackMapper).deleteByMainId("m-1");
verify(bgPartymatterMapper).deleteById("m-1");
}
@Test
void should_delete_batch_with_feedback() {
service.delBatchMain(List.of("m-1", "m-2"));
verify(bgPartymatterFeedbackMapper).deleteByMainId("m-1");
verify(bgPartymatterMapper).deleteById("m-1");
verify(bgPartymatterFeedbackMapper).deleteByMainId("m-2");
verify(bgPartymatterMapper).deleteById("m-2");
}
@Test
void should_update_form_and_sync_variable() {
BgPartymatter form = new BgPartymatter();
service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}");
verify(bgPartymatterMapper).updateById(form);
verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}");
}
}
@@ -0,0 +1,41 @@
package org.jeecg.modules.supervision.bqtakepulse.service.impl;
import org.jeecg.modules.supervision.bqtakepulse.entity.BgTakepulseFeedback;
import org.jeecg.modules.supervision.bqtakepulse.mapper.BgTakepulseFeedbackMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* BgTakepulseFeedbackServiceImpl 测试(简单转发)。
*/
@ExtendWith(MockitoExtension.class)
class BgTakepulseFeedbackServiceImplTest {
@Mock private BgTakepulseFeedbackMapper bgTakepulseFeedbackMapper;
private BgTakepulseFeedbackServiceImpl service;
@BeforeEach
void setUp() {
service = new BgTakepulseFeedbackServiceImpl();
ReflectionTestUtils.setField(service, "bgTakepulseFeedbackMapper", bgTakepulseFeedbackMapper);
}
@Test
void should_select_by_main_id() {
BgTakepulseFeedback feedback = new BgTakepulseFeedback();
when(bgTakepulseFeedbackMapper.selectByMainId("m-1")).thenReturn(List.of(feedback));
assertEquals(1, service.selectByMainId("m-1").size());
verify(bgTakepulseFeedbackMapper).selectByMainId("m-1");
}
}
@@ -0,0 +1,103 @@
package org.jeecg.modules.supervision.bqtakepulse.service.impl;
import org.flowable.engine.RuntimeService;
import org.jeecg.modules.supervision.bqtakepulse.entity.BgTakepulse;
import org.jeecg.modules.supervision.bqtakepulse.entity.BgTakepulseFeedback;
import org.jeecg.modules.supervision.bqtakepulse.mapper.BgTakepulseFeedbackMapper;
import org.jeecg.modules.supervision.bqtakepulse.mapper.BgTakepulseMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* BgTakepulseServiceImpl 测试。
* 核心:saveMain 默认 del_flag 补齐(防 @TableLogic 过滤)+ 主从 CRUD。
*/
@ExtendWith(MockitoExtension.class)
class BgTakepulseServiceImplTest {
@Mock private BgTakepulseMapper bgTakepulseMapper;
@Mock private BgTakepulseFeedbackMapper bgTakepulseFeedbackMapper;
@Mock private RuntimeService runtimeService;
private BgTakepulseServiceImpl service;
@BeforeEach
void setUp() {
service = new BgTakepulseServiceImpl();
ReflectionTestUtils.setField(service, "bgTakepulseMapper", bgTakepulseMapper);
ReflectionTestUtils.setField(service, "baseMapper", bgTakepulseMapper);
ReflectionTestUtils.setField(service, "bgTakepulseFeedbackMapper", bgTakepulseFeedbackMapper);
ReflectionTestUtils.setField(service, "runtimeService", runtimeService);
}
@Test
void should_fill_default_del_flag_on_save() {
BgTakepulse main = new BgTakepulse();
main.setId("biz-1");
BgTakepulseFeedback sub = new BgTakepulseFeedback();
service.saveMain(main, List.of(sub));
assertEquals("0", main.getDelFlag());
assertEquals("0", sub.getDelFlag());
assertEquals("biz-1", sub.getMainId());
verify(bgTakepulseMapper).insert(main);
verify(bgTakepulseFeedbackMapper).insert(sub);
}
@Test
void should_keep_existing_del_flag() {
BgTakepulse main = new BgTakepulse();
main.setDelFlag("1");
service.saveMain(main, null);
assertEquals("1", main.getDelFlag());
verify(bgTakepulseMapper).insert(main);
}
@Test
void should_update_main_and_replace_sub() {
BgTakepulse main = new BgTakepulse();
main.setId("biz-1");
BgTakepulseFeedback sub = new BgTakepulseFeedback();
service.updateMain(main, List.of(sub));
verify(bgTakepulseMapper).updateById(main);
verify(bgTakepulseFeedbackMapper).deleteByMainId("biz-1");
assertEquals("biz-1", sub.getMainId());
verify(bgTakepulseFeedbackMapper).insert(sub);
}
@Test
void should_delete_main_with_sub() {
service.delMain("biz-1");
verify(bgTakepulseFeedbackMapper).deleteByMainId("biz-1");
verify(bgTakepulseMapper).deleteById("biz-1");
}
@Test
void should_delete_batch() {
service.delBatchMain(List.of("biz-1", "biz-2"));
verify(bgTakepulseFeedbackMapper).deleteByMainId("biz-2");
verify(bgTakepulseMapper).deleteById("biz-2");
}
@Test
void should_update_form_and_sync_variable() {
BgTakepulse form = new BgTakepulse();
service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}");
verify(bgTakepulseMapper).updateById(form);
verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}");
}
}
@@ -0,0 +1,32 @@
package org.jeecg.modules.supervision.common;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* BpmStatus 枚举解析测试。
*/
class BpmStatusTest {
@Test
void should_parse_known_codes() {
assertEquals(BpmStatus.NOT_START, BpmStatus.fromCode("1"));
assertEquals(BpmStatus.SUPERVISING, BpmStatus.fromCode("2"));
assertEquals(BpmStatus.FINISH, BpmStatus.fromCode("3"));
}
@ParameterizedTest
@ValueSource(strings = {"0", "4", "abc", ""})
void should_return_null_for_unknown_code(String code) {
assertNull(BpmStatus.fromCode(code));
}
@Test
void should_return_null_for_null_code() {
assertNull(BpmStatus.fromCode(null));
}
}
@@ -1,4 +1,4 @@
package org.jeecg.modules.supervision.fixcontact.listener; package org.jeecg.modules.supervision.common.flow;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@@ -9,29 +9,29 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
/** /**
* FixContactApprovalHelper.resolveTargetExecutionId 规则测试TDD -绿-重构 演示 * FlowExecutionSupport.resolveTargetExecutionId 规则测试TDD -绿-重构 演示
* *
* 业务规则来自 FixContactDeptLeaderApproveListener 53-69 * 业务规则来自 fixcontact/xispeak 监听器内联重复逻辑
* 部门领导审批后领导变量要写入"上两级"执行实例 * 部门领导/经办人审批后审批人变量要写入"上两级"执行实例
* - 存在第二层父级大会签容器/外层作用域 写入第二层 grandParentId * - 存在第二层父级大会签容器/外层作用域 写入第二层 grandParentId
* - 不存在第二层父级 写入第一层 parentId * - 不存在第二层父级 写入第一层 parentId
* - 第一层 parentId 为空 无目标返回 null调用方跳过写入 * - 第一层 parentId 为空 无目标返回 null调用方跳过写入
*/ */
class FixContactApprovalHelperTest { class FlowExecutionSupportTest {
// ============ 正常路径 ============ // ============ 正常路径 ============
@Test @Test
void should_return_grandParent_when_both_levels_exist() { void should_return_grandParent_when_both_levels_exist() {
// 两级父级都存在子流程内会签容器场景 // 两级父级都存在子流程内会签容器场景
String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", "grand-1"); String target = FlowExecutionSupport.resolveTargetExecutionId("parent-1", "grand-1");
assertEquals("grand-1", target); assertEquals("grand-1", target);
} }
@Test @Test
void should_return_parent_when_no_grand_parent() { void should_return_parent_when_no_grand_parent() {
// 只有第一层父级无外层容器 // 只有第一层父级无外层容器
String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", null); String target = FlowExecutionSupport.resolveTargetExecutionId("parent-1", null);
assertEquals("parent-1", target); assertEquals("parent-1", target);
} }
@@ -42,7 +42,7 @@ class FixContactApprovalHelperTest {
@ValueSource(strings = {" ", " "}) @ValueSource(strings = {" ", " "})
void should_return_parent_when_grand_parent_blank(String grandParentId) { void should_return_parent_when_grand_parent_blank(String grandParentId) {
// 第二层为空串/空白串视为不存在退回第一层原逻辑 isNotBlank 判定 // 第二层为空串/空白串视为不存在退回第一层原逻辑 isNotBlank 判定
String target = FixContactApprovalHelper.resolveTargetExecutionId("parent-1", grandParentId); String target = FlowExecutionSupport.resolveTargetExecutionId("parent-1", grandParentId);
assertEquals("parent-1", target); assertEquals("parent-1", target);
} }
@@ -53,6 +53,6 @@ class FixContactApprovalHelperTest {
@ValueSource(strings = {" ", " "}) @ValueSource(strings = {" ", " "})
void should_return_null_when_parent_blank(String parentId) { void should_return_null_when_parent_blank(String parentId) {
// 第一层父级都没有无目标执行实例返回 null 供调用方跳过 // 第一层父级都没有无目标执行实例返回 null 供调用方跳过
assertNull(FixContactApprovalHelper.resolveTargetExecutionId(parentId, "grand-1")); assertNull(FlowExecutionSupport.resolveTargetExecutionId(parentId, "grand-1"));
} }
} }
@@ -1,4 +1,4 @@
package org.jeecg.modules.supervision.fixcontact.flow; package org.jeecg.modules.supervision.common.flow;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@@ -11,31 +11,31 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
/** /**
* FixContactFlowSupport 纯函数规则测试TDD -绿-重构阶段 A 示范 * FlowVariableSupport 纯函数规则测试TDD -绿-重构阶段 A 示范
* *
* 业务规则来自 FixContactFlow 内联逻辑3 处重复的逗号分割 + 部门 ID 清洗 * 业务规则来自 fixcontact/xispeak 处重复的逗号分割 + 部门 ID 清洗
* - splitToTrimmedList逗号分割 去首尾空白 过滤空白元素null/ 空列表 * - splitToTrimmedList逗号分割 去首尾空白 过滤空白元素null/ 空列表
* - countTokens分割后元素个数等价于 splitToTrimmedList().size() * - countTokens分割后元素个数等价于 splitToTrimmedList().size()
* - cleanDeptId执行实例部门变量清洗去掉 [] 与首尾空白 XiSpeak 监听器同款规则 * - cleanDeptId执行实例部门变量清洗去掉 [] 与首尾空白
*/ */
class FixContactFlowSupportTest { class FlowVariableSupportTest {
// ============ splitToTrimmedList正常路径 ============ // ============ splitToTrimmedList正常路径 ============
@Test @Test
void should_split_multi_tokens() { void should_split_multi_tokens() {
assertEquals(List.of("a", "b", "c"), FixContactFlowSupport.splitToTrimmedList("a,b,c")); assertEquals(List.of("a", "b", "c"), FlowVariableSupport.splitToTrimmedList("a,b,c"));
} }
@Test @Test
void should_trim_tokens_with_spaces() { void should_trim_tokens_with_spaces() {
// 逗号前后有空格 token trim // 逗号前后有空格 token trim
assertEquals(List.of("a", "b"), FixContactFlowSupport.splitToTrimmedList(" a , b ")); assertEquals(List.of("a", "b"), FlowVariableSupport.splitToTrimmedList(" a , b "));
} }
@Test @Test
void should_return_single_token() { void should_return_single_token() {
assertEquals(List.of("a"), FixContactFlowSupport.splitToTrimmedList("a")); assertEquals(List.of("a"), FlowVariableSupport.splitToTrimmedList("a"));
} }
// ============ splitToTrimmedList边界路径 ============ // ============ splitToTrimmedList边界路径 ============
@@ -43,61 +43,61 @@ class FixContactFlowSupportTest {
@Test @Test
void should_filter_empty_tokens() { void should_filter_empty_tokens() {
// 空元素连续逗号/全空白元素被过滤 // 空元素连续逗号/全空白元素被过滤
assertEquals(List.of("a", "b"), FixContactFlowSupport.splitToTrimmedList("a,,b")); assertEquals(List.of("a", "b"), FlowVariableSupport.splitToTrimmedList("a,,b"));
} }
@ParameterizedTest @ParameterizedTest
@NullAndEmptySource @NullAndEmptySource
@ValueSource(strings = {" ", " ", " , , "}) @ValueSource(strings = {" ", " ", " , , "})
void should_return_empty_list_when_no_valid_token(String raw) { void should_return_empty_list_when_no_valid_token(String raw) {
assertEquals(List.of(), FixContactFlowSupport.splitToTrimmedList(raw)); assertEquals(List.of(), FlowVariableSupport.splitToTrimmedList(raw));
} }
// ============ countTokens正常 + 边界 ============ // ============ countTokens正常 + 边界 ============
@Test @Test
void should_count_tokens() { void should_count_tokens() {
assertEquals(3, FixContactFlowSupport.countTokens("a,b,c")); assertEquals(3, FlowVariableSupport.countTokens("a,b,c"));
} }
@Test @Test
void should_count_trimmed_tokens() { void should_count_trimmed_tokens() {
assertEquals(2, FixContactFlowSupport.countTokens(" a , b ")); assertEquals(2, FlowVariableSupport.countTokens(" a , b "));
} }
@ParameterizedTest @ParameterizedTest
@NullAndEmptySource @NullAndEmptySource
@ValueSource(strings = {" ", " , , "}) @ValueSource(strings = {" ", " , , "})
void should_return_zero_when_no_valid_token(String raw) { void should_return_zero_when_no_valid_token(String raw) {
assertEquals(0, FixContactFlowSupport.countTokens(raw)); assertEquals(0, FlowVariableSupport.countTokens(raw));
} }
// ============ cleanDeptId正常 + 边界 ============ // ============ cleanDeptId正常 + 边界 ============
@Test @Test
void should_remove_square_brackets() { void should_remove_square_brackets() {
assertEquals("dept-1", FixContactFlowSupport.cleanDeptId("[dept-1]")); assertEquals("dept-1", FlowVariableSupport.cleanDeptId("[dept-1]"));
} }
@Test @Test
void should_remove_brackets_and_trim() { void should_remove_brackets_and_trim() {
// 多值列表变量场景整体去括号 + trim // 多值列表变量场景整体去括号 + trim
assertEquals("dept-1, dept-2", FixContactFlowSupport.cleanDeptId("[dept-1, dept-2]")); assertEquals("dept-1, dept-2", FlowVariableSupport.cleanDeptId("[dept-1, dept-2]"));
} }
@Test @Test
void should_keep_plain_id_unchanged() { void should_keep_plain_id_unchanged() {
assertEquals("dept-1", FixContactFlowSupport.cleanDeptId("dept-1")); assertEquals("dept-1", FlowVariableSupport.cleanDeptId("dept-1"));
} }
@Test @Test
void should_handle_non_string_object() { void should_handle_non_string_object() {
// 流程变量可能是任意对象toString 后处理 // 流程变量可能是任意对象toString 后处理
assertEquals("123", FixContactFlowSupport.cleanDeptId(123L)); assertEquals("123", FlowVariableSupport.cleanDeptId(123L));
} }
@Test @Test
void should_return_null_when_raw_null() { void should_return_null_when_raw_null() {
assertNull(FixContactFlowSupport.cleanDeptId(null)); assertNull(FlowVariableSupport.cleanDeptId(null));
} }
} }
@@ -0,0 +1,71 @@
package org.jeecg.modules.supervision.dashboard.service.impl;
import org.jeecg.modules.supervision.dashboard.dto.ProcessPortalStatsDTO;
import org.jeecg.modules.supervision.dashboard.mapper.ProcessPortalMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* ProcessPortalServiceImpl 测试。
* 核心:formatDuration 时长格式化规则(0/分钟/小时/天),经 getStats 间接覆盖。
*/
@ExtendWith(MockitoExtension.class)
class ProcessPortalServiceImplTest {
@Mock private ProcessPortalMapper processPortalMapper;
private ProcessPortalServiceImpl service;
@BeforeEach
void setUp() {
service = new ProcessPortalServiceImpl();
ReflectionTestUtils.setField(service, "processPortalMapper", processPortalMapper);
}
private ProcessPortalStatsDTO statsWithAvg(Long avgMillis) {
when(processPortalMapper.countTodo(eq("u1"))).thenReturn(3L);
when(processPortalMapper.countDoneThisMonth(eq("u1"), org.mockito.ArgumentMatchers.anyString())).thenReturn(10L);
when(processPortalMapper.avgDurationMillis(eq("u1"))).thenReturn(avgMillis);
when(processPortalMapper.countOverdue(eq("u1"))).thenReturn(1L);
return service.getStats("u1");
}
@Test
void should_return_zero_hours_when_null() {
assertEquals("0小时", statsWithAvg(null).getAvgDuration());
}
@Test
void should_format_minutes_when_less_than_hour() {
// 30 分钟
assertEquals("30分钟", statsWithAvg(30L * 60 * 1000).getAvgDuration());
}
@Test
void should_format_hours_when_less_than_day() {
// 2.5 小时
assertEquals("2.5小时", statsWithAvg((long) (2.5 * 3600 * 1000)).getAvgDuration());
}
@Test
void should_format_days_when_ge_day() {
// 48 小时 → 2 天
assertEquals("2.0天", statsWithAvg(48L * 3600 * 1000).getAvgDuration());
}
@Test
void should_fill_all_stats() {
ProcessPortalStatsDTO dto = statsWithAvg(null);
assertEquals(3, dto.getTodoCount());
assertEquals(10, dto.getDoneThisMonth());
assertEquals(1, dto.getOverdueCount());
}
}
@@ -0,0 +1,210 @@
package org.jeecg.modules.supervision.dqinspectproblem.service.impl;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.modules.supervision.dqinspectproblem.entity.DqInspectProblem;
import org.jeecg.modules.supervision.dqinspectproblem.mapper.DqInspectProblemMapper;
import org.jeecg.modules.supervision.dqinspectproblem.service.IDqInspectProblemService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* DqInspectProblemServiceImpl 测试。
* 核心:validateAndNormalizeNode 三层问题清单校验(分类→面上→具体),经 add/update 触发。
*/
@ExtendWith(MockitoExtension.class)
class DqInspectProblemServiceImplTest {
@Mock private DqInspectProblemMapper baseMapper;
private DqInspectProblemServiceImpl service;
@BeforeEach
void setUp() {
service = new DqInspectProblemServiceImpl();
ReflectionTestUtils.setField(service, "baseMapper", baseMapper);
}
private DqInspectProblem problem(String nodeType, String pid) {
DqInspectProblem p = new DqInspectProblem();
p.setNodeType(nodeType);
p.setPid(pid);
return p;
}
private DqInspectProblem category(String id) {
DqInspectProblem c = new DqInspectProblem();
c.setId(id);
c.setNodeType(IDqInspectProblemService.NODE_TYPE_CATEGORY);
c.setPid(IDqInspectProblemService.ROOT_PID_VALUE);
return c;
}
private DqInspectProblem surface(String id) {
DqInspectProblem s = new DqInspectProblem();
s.setId(id);
s.setNodeType(IDqInspectProblemService.NODE_TYPE_SURFACE);
s.setPid("c-1");
return s;
}
// ==================== validateAndNormalizeNode:基础校验 ====================
@Nested
class BasicValidation {
@Test
void should_throw_when_form_null() {
assertThrows(JeecgBootException.class, () -> service.addDqInspectProblem(null));
}
@Test
void should_throw_when_node_type_invalid() {
assertThrows(JeecgBootException.class,
() -> service.addDqInspectProblem(problem("invalid", "c-1")));
}
}
// ==================== 分类节点 ====================
@Nested
class CategoryNode {
@Test
void should_force_root_pid_and_clear_specific_fields() {
DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_CATEGORY, "c-2");
p.setInspectAdvice("advice");
p.setQuestionResLeader("leader");
p.setQuestionResDept("dept");
service.addDqInspectProblem(p);
assertEquals(IDqInspectProblemService.ROOT_PID_VALUE, p.getPid());
assertNull(p.getInspectAdvice());
assertNull(p.getQuestionResLeader());
assertNull(p.getQuestionResDept());
verify(baseMapper).insert(p);
}
}
// ==================== 面上/具体节点:父级校验 ====================
@Nested
class ParentValidation {
@Test
void should_throw_when_pid_empty() {
assertThrows(JeecgBootException.class,
() -> service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, "")));
}
@Test
void should_throw_when_pid_is_root() {
assertThrows(JeecgBootException.class,
() -> service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE,
IDqInspectProblemService.ROOT_PID_VALUE)));
}
@Test
void should_throw_when_parent_not_exists() {
when(baseMapper.selectById("c-9")).thenReturn(null);
assertThrows(JeecgBootException.class,
() -> service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, "c-9")));
}
@Test
void should_throw_when_parent_type_mismatch() {
// 面上问题的父必须是分类;父是面上 → 抛
DqInspectProblem wrongParent = surface("s-1");
when(baseMapper.selectById("s-1")).thenReturn(wrongParent);
assertThrows(JeecgBootException.class,
() -> service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, "s-1")));
}
@Test
void should_insert_surface_under_category() {
when(baseMapper.selectById("c-1")).thenReturn(category("c-1"));
service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SURFACE, "c-1"));
verify(baseMapper).insert(any());
}
@Test
void should_insert_specific_under_surface() {
when(baseMapper.selectById("s-1")).thenReturn(surface("s-1"));
service.addDqInspectProblem(problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1"));
verify(baseMapper).insert(any());
}
}
// ==================== updateDqInspectProblem:自身/下级校验 ====================
@Nested
class UpdateValidation {
@Test
void should_throw_when_entity_not_found() {
DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1");
p.setId("x-1");
when(baseMapper.selectById("x-1")).thenReturn(null);
assertThrows(JeecgBootException.class, () -> service.updateDqInspectProblem(p));
}
@Test
void should_throw_when_pid_is_self() {
DqInspectProblem existing = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1");
existing.setId("x-1");
when(baseMapper.selectById("x-1")).thenReturn(existing);
DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "x-1");
p.setId("x-1");
assertThrows(JeecgBootException.class, () -> service.updateDqInspectProblem(p));
}
@Test
void should_throw_when_pid_is_descendant() {
// x-1 的下级是 d-2:把 x-1 的父设为 d-2 非法
DqInspectProblem existing = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1");
existing.setId("x-1");
when(baseMapper.selectById("x-1")).thenReturn(existing);
DqInspectProblem child = new DqInspectProblem();
child.setId("d-2");
child.setPid("x-1");
when(baseMapper.selectById("d-2")).thenReturn(child);
when(baseMapper.selectById("x-1")).thenReturn(existing); // isDescendantOf 起点
DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "d-2");
p.setId("x-1");
assertThrows(JeecgBootException.class, () -> service.updateDqInspectProblem(p));
}
@Test
void should_update_normally_when_pid_unchanged() {
DqInspectProblem existing = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1");
existing.setId("x-1");
existing.setHasChild(IDqInspectProblemService.NOCHILD);
when(baseMapper.selectById("x-1")).thenReturn(existing);
when(baseMapper.selectById("s-1")).thenReturn(surface("s-1"));
DqInspectProblem p = problem(IDqInspectProblemService.NODE_TYPE_SPECIFIC, "s-1");
p.setId("x-1");
service.updateDqInspectProblem(p);
// hasChild 由树结构维护,忽略前端提交值
assertEquals(IDqInspectProblemService.NOCHILD, p.getHasChild());
verify(baseMapper, never()).updateTreeNodeStatus(any(), any());
verify(baseMapper).updateById(p);
}
}
}
@@ -0,0 +1,103 @@
package org.jeecg.modules.supervision.dqinspecttask.listener;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.jeecg.modules.supervision.dqinspecttask.config.InspectImproveConfig;
import org.jeecg.modules.supervision.dqinspecttask.config.InspectImproveConfig.InspectImproveProperties;
import org.jeecg.modules.supervision.dqinspecttask.entity.DqInspectTask;
import org.jeecg.modules.supervision.dqinspecttask.service.IDqInspectTaskService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* DqInspectTaskStageIncrementListener 阶段递增监听器测试。
* 规则:流程结束时 completedStage 原子递增 1businessKey 缺失跳过;幂等标记 stage_increment_done 防重复递增。
*/
@ExtendWith(MockitoExtension.class)
class DqInspectTaskStageIncrementListenerTest {
private static final String INCREMENT_FLAG = "stage_increment_done";
private static final String BUSINESS_KEY_VAR = "businessKey";
@Mock private InspectImproveConfig inspectImproveConfig;
@Mock private IDqInspectTaskService dqInspectTaskService;
@Mock private RuntimeService runtimeService;
@Mock private DelegateExecution execution;
@Mock private LambdaUpdateChainWrapper<DqInspectTask> chainWrapper;
private DqInspectTaskStageIncrementListener listener;
@BeforeEach
void setUp() {
InspectImproveProperties props = new InspectImproveProperties();
props.setBusinessKey(BUSINESS_KEY_VAR);
when(inspectImproveConfig.getInspectImprove()).thenReturn(props);
listener = new DqInspectTaskStageIncrementListener(inspectImproveConfig, dqInspectTaskService, runtimeService);
}
@Test
void should_skip_when_business_key_missing() {
when(execution.getVariable(BUSINESS_KEY_VAR)).thenReturn(null);
listener.notify(execution);
verify(runtimeService, never()).setVariable(any(), any(), any());
verify(dqInspectTaskService, never()).lambdaUpdate();
}
@Test
void should_skip_when_already_incremented() {
// 幂等:同一流程实例结束仅递增一次
when(execution.getVariable(BUSINESS_KEY_VAR)).thenReturn("biz-1");
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", INCREMENT_FLAG)).thenReturn(Boolean.TRUE);
listener.notify(execution);
verify(dqInspectTaskService, never()).lambdaUpdate();
}
@Test
void should_increment_stage_and_set_flag() {
when(execution.getVariable(BUSINESS_KEY_VAR)).thenReturn("biz-1");
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", INCREMENT_FLAG)).thenReturn(null);
when(dqInspectTaskService.lambdaUpdate()).thenReturn(chainWrapper);
when(chainWrapper.setSql("completed_stage = completed_stage + 1")).thenReturn(chainWrapper);
// 方法引用 DqInspectTask::getId 在测试类与生产类编译为不同 Lambda 实例,用 any() 匹配函数式参数
when(chainWrapper.eq(any(), eq("biz-1"))).thenReturn(chainWrapper);
when(chainWrapper.update()).thenReturn(true);
listener.notify(execution);
verify(runtimeService).setVariable("proc-1", INCREMENT_FLAG, Boolean.TRUE);
verify(chainWrapper).setSql("completed_stage = completed_stage + 1");
verify(chainWrapper).eq(any(), eq("biz-1"));
verify(chainWrapper).update();
}
@Test
void should_not_throw_when_update_affects_zero_rows() {
// 记录可能已删除:update 返回 false 仅 warn,不抛异常
when(execution.getVariable(BUSINESS_KEY_VAR)).thenReturn("biz-1");
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", INCREMENT_FLAG)).thenReturn(null);
when(dqInspectTaskService.lambdaUpdate()).thenReturn(chainWrapper);
when(chainWrapper.setSql("completed_stage = completed_stage + 1")).thenReturn(chainWrapper);
when(chainWrapper.eq(any(), eq("biz-1"))).thenReturn(chainWrapper);
when(chainWrapper.update()).thenReturn(false);
// 不抛异常即通过
listener.notify(execution);
}
}
@@ -0,0 +1,44 @@
package org.jeecg.modules.supervision.dqinspecttask.service.impl;
import org.jeecg.modules.supervision.dqinspecttask.entity.DqInspectProgress;
import org.jeecg.modules.supervision.dqinspecttask.mapper.DqInspectProgressMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* DqInspectProgressServiceImpl 测试(简单转发)。
*/
@ExtendWith(MockitoExtension.class)
class DqInspectProgressServiceImplTest {
@Mock private DqInspectProgressMapper dqInspectProgressMapper;
private DqInspectProgressServiceImpl service;
@BeforeEach
void setUp() {
service = new DqInspectProgressServiceImpl();
ReflectionTestUtils.setField(service, "dqInspectProgressMapper", dqInspectProgressMapper);
}
@Test
void should_select_by_main_id() {
DqInspectProgress progress = new DqInspectProgress();
when(dqInspectProgressMapper.selectByMainId("main-1")).thenReturn(List.of(progress));
List<DqInspectProgress> result = service.selectByMainId("main-1");
assertEquals(1, result.size());
verify(dqInspectProgressMapper).selectByMainId("main-1");
}
}
@@ -0,0 +1,185 @@
package org.jeecg.modules.supervision.dqinspecttask.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.modules.supervision.dqinspectproblem.entity.DqInspectProblem;
import org.jeecg.modules.supervision.dqinspectproblem.service.IDqInspectProblemService;
import org.jeecg.modules.supervision.dqinspecttask.entity.DqInspectProgress;
import org.jeecg.modules.supervision.dqinspecttask.entity.DqInspectTask;
import org.jeecg.modules.supervision.dqinspecttask.mapper.DqInspectProgressMapper;
import org.jeecg.modules.supervision.dqinspecttask.mapper.DqInspectTaskMapper;
import org.jeecg.modules.supervision.dqinspecttask.vo.DqInspectTaskLedgerVo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* DqInspectTaskServiceImpl 测试。
* 核心:validateProblemId 业务规则(整改任务必须挂具体问题节点下)+ saveMain 默认值 + 主从 CRUD。
*/
@ExtendWith(MockitoExtension.class)
class DqInspectTaskServiceImplTest {
@Mock private DqInspectTaskMapper dqInspectTaskMapper;
@Mock private DqInspectProgressMapper dqInspectProgressMapper;
@Mock private IDqInspectProblemService dqInspectProblemService;
private DqInspectTaskServiceImpl service;
@BeforeEach
void setUp() {
service = new DqInspectTaskServiceImpl();
ReflectionTestUtils.setField(service, "dqInspectTaskMapper", dqInspectTaskMapper);
ReflectionTestUtils.setField(service, "dqInspectProgressMapper", dqInspectProgressMapper);
ReflectionTestUtils.setField(service, "dqInspectProblemService", dqInspectProblemService);
}
private DqInspectProblem specificProblem() {
DqInspectProblem p = new DqInspectProblem();
p.setId("prob-1");
p.setNodeType(IDqInspectProblemService.NODE_TYPE_SPECIFIC);
return p;
}
// ==================== validateProblemId 业务规则 ====================
@Test
void should_throw_when_problem_id_blank() {
DqInspectTask task = new DqInspectTask();
assertThrows(JeecgBootException.class, () -> service.saveMain(task, null));
}
@Test
void should_throw_when_problem_not_exists() {
DqInspectTask task = new DqInspectTask();
task.setProblemId("no-such");
when(dqInspectProblemService.getById("no-such")).thenReturn(null);
assertThrows(JeecgBootException.class, () -> service.saveMain(task, null));
}
@Test
void should_throw_when_problem_not_specific_node() {
DqInspectTask task = new DqInspectTask();
task.setProblemId("prob-1");
DqInspectProblem category = new DqInspectProblem();
category.setNodeType("category");
when(dqInspectProblemService.getById("prob-1")).thenReturn(category);
assertThrows(JeecgBootException.class, () -> service.saveMain(task, null));
}
// ==================== saveMain ====================
@Test
void should_set_default_bpm_status_and_stage() {
DqInspectTask task = new DqInspectTask();
task.setProblemId("prob-1");
when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem());
service.saveMain(task, null);
assertEquals("1", task.getBpmStatus());
assertEquals(0, task.getCompletedStage());
verify(dqInspectTaskMapper).insert(task);
}
@Test
void should_save_sub_progress_with_main_id() {
DqInspectTask task = new DqInspectTask();
task.setId("task-1");
task.setProblemId("prob-1");
when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem());
DqInspectProgress progress = new DqInspectProgress();
service.saveMain(task, List.of(progress));
assertEquals("task-1", progress.getMainId());
verify(dqInspectProgressMapper).insert(progress);
}
@Test
void should_not_save_sub_when_list_null() {
DqInspectTask task = new DqInspectTask();
task.setProblemId("prob-1");
when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem());
service.saveMain(task, null);
verify(dqInspectProgressMapper, never()).insert(any());
}
// ==================== updateMain ====================
@Test
void should_not_touch_sub_when_list_null() {
// 关键行为:子表 null 表示"不修改子表",不得删除子表数据
DqInspectTask task = new DqInspectTask();
task.setId("task-1");
task.setProblemId("prob-1");
when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem());
service.updateMain(task, null);
verify(dqInspectTaskMapper).updateById(task);
verify(dqInspectProgressMapper, never()).deleteByMainId(any());
}
@Test
void should_replace_sub_when_list_provided() {
DqInspectTask task = new DqInspectTask();
task.setId("task-1");
task.setProblemId("prob-1");
when(dqInspectProblemService.getById("prob-1")).thenReturn(specificProblem());
DqInspectProgress progress = new DqInspectProgress();
service.updateMain(task, List.of(progress));
verify(dqInspectProgressMapper).deleteByMainId("task-1");
assertEquals("task-1", progress.getMainId());
verify(dqInspectProgressMapper).insert(progress);
}
// ==================== 删除 ====================
@Test
void should_delete_main_with_sub() {
service.delMain("task-1");
verify(dqInspectProgressMapper).deleteByMainId("task-1");
verify(dqInspectTaskMapper).deleteById("task-1");
}
@Test
void should_delete_batch_with_sub() {
service.delBatchMain(List.of("task-1", "task-2"));
verify(dqInspectProgressMapper).deleteByMainId("task-1");
verify(dqInspectTaskMapper).deleteById("task-1");
verify(dqInspectProgressMapper).deleteByMainId("task-2");
verify(dqInspectTaskMapper).deleteById("task-2");
}
// ==================== 台账查询 ====================
@Test
void should_query_ledger_page() {
Page<DqInspectTaskLedgerVo> page = new Page<>(1, 10);
DqInspectTask query = new DqInspectTask();
when(dqInspectTaskMapper.queryLedgerPage(page, query)).thenReturn(page);
IPage<DqInspectTaskLedgerVo> result = service.queryLedgerPage(page, query);
assertEquals(page, result);
verify(dqInspectTaskMapper).queryLedgerPage(page, query);
}
}
@@ -0,0 +1,42 @@
package org.jeecg.modules.supervision.fixcontact.service.impl;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730;
import org.jeecg.modules.supervision.fixcontact.mapper.FixedContactFeedback20260730Mapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* FixedContactFeedback20260730ServiceImpl 测试(简单转发)。
*/
@ExtendWith(MockitoExtension.class)
class FixedContactFeedback20260730ServiceImplTest {
@Mock private FixedContactFeedback20260730Mapper fixedContactFeedback20260730Mapper;
private FixedContactFeedback20260730ServiceImpl service;
@BeforeEach
void setUp() {
service = new FixedContactFeedback20260730ServiceImpl();
// 该类直接使用 ServiceImpl 的 baseMapper(无自有 mapper 字段)
ReflectionTestUtils.setField(service, "baseMapper", fixedContactFeedback20260730Mapper);
}
@Test
void should_select_by_main_id() {
FixedContactFeedback20260730 feedback = new FixedContactFeedback20260730();
when(fixedContactFeedback20260730Mapper.selectByMainId("m-1")).thenReturn(List.of(feedback));
assertEquals(1, service.selectByMainId("m-1").size());
verify(fixedContactFeedback20260730Mapper).selectByMainId("m-1");
}
}
@@ -0,0 +1,41 @@
package org.jeecg.modules.supervision.improvementproposal.service.impl;
import org.jeecg.modules.supervision.improvementproposal.entity.ImprovementDeptScore;
import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementDeptScoreMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* ImprovementDeptScoreServiceImpl 测试(简单转发)。
*/
@ExtendWith(MockitoExtension.class)
class ImprovementDeptScoreServiceImplTest {
@Mock private ImprovementDeptScoreMapper improvementDeptScoreMapper;
private ImprovementDeptScoreServiceImpl service;
@BeforeEach
void setUp() {
service = new ImprovementDeptScoreServiceImpl();
ReflectionTestUtils.setField(service, "improvementDeptScoreMapper", improvementDeptScoreMapper);
}
@Test
void should_select_by_main_id() {
ImprovementDeptScore score = new ImprovementDeptScore();
when(improvementDeptScoreMapper.selectByMainId("m-1")).thenReturn(List.of(score));
assertEquals(1, service.selectByMainId("m-1").size());
verify(improvementDeptScoreMapper).selectByMainId("m-1");
}
}
@@ -0,0 +1,41 @@
package org.jeecg.modules.supervision.improvementproposal.service.impl;
import org.jeecg.modules.supervision.improvementproposal.entity.ImprovementInstituteVote;
import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementInstituteVoteMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* ImprovementInstituteVoteServiceImpl 测试(简单转发)。
*/
@ExtendWith(MockitoExtension.class)
class ImprovementInstituteVoteServiceImplTest {
@Mock private ImprovementInstituteVoteMapper improvementInstituteVoteMapper;
private ImprovementInstituteVoteServiceImpl service;
@BeforeEach
void setUp() {
service = new ImprovementInstituteVoteServiceImpl();
ReflectionTestUtils.setField(service, "improvementInstituteVoteMapper", improvementInstituteVoteMapper);
}
@Test
void should_select_by_main_id() {
ImprovementInstituteVote vote = new ImprovementInstituteVote();
when(improvementInstituteVoteMapper.selectByMainId("m-1")).thenReturn(List.of(vote));
assertEquals(1, service.selectByMainId("m-1").size());
verify(improvementInstituteVoteMapper).selectByMainId("m-1");
}
}
@@ -0,0 +1,233 @@
package org.jeecg.modules.supervision.improvementproposal.service.impl;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.extbpm.process.service.impl.BpmBaseExtApiImpl;
import org.jeecg.modules.extbpm.process.service.IExtActBpmFileService;
import org.jeecg.modules.extbpm.process.service.IExtActBpmLogService;
import org.jeecg.modules.extbpm.process.service.IExtActFlowDataService;
import org.jeecg.modules.extbpm.process.service.IExtActTaskCcService;
import org.jeecg.modules.system.service.ISysUserDepartService;
import org.jeecg.modules.system.service.ISysRoleService;
import org.jeecg.modules.supervision.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.modules.supervision.improvementproposal.dto.ImprovementDeptScoreBpmDTO;
import org.jeecg.modules.supervision.improvementproposal.entity.ImprovementProposal;
import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementDeptScoreMapper;
import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementInstituteVoteMapper;
import org.jeecg.modules.supervision.improvementproposal.mapper.ImprovementProposalMapper;
import org.jeecg.modules.taskapprovalopinion.service.ITaskApprovalOpinionService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* ImprovementProposalServiceImpl 测试(mock mapper/流程服务)。
* 核心:流水号生成、初评奖级与 needDeptScore 联动、部门评分计算与落库。
*/
@ExtendWith(MockitoExtension.class)
class ImprovementProposalServiceImplTest {
@Mock private ImprovementProposalMapper improvementProposalMapper;
@Mock private ImprovementDeptScoreMapper improvementDeptScoreMapper;
@Mock private ImprovementInstituteVoteMapper improvementInstituteVoteMapper;
@Mock private BpmBaseExtApiImpl bpmBaseExtApi;
@Mock private RuntimeService runtimeService;
@Mock private HistoryService historyService;
@Mock private TaskService taskService;
@Mock private ISysRoleService sysRoleService;
@Mock private ISysUserDepartService sysUserDepartService;
@Mock private IExtActTaskCcService extActTaskCcService;
@Mock private ITaskApprovalOpinionService taskApprovalOpinionService;
@Mock private IExtActFlowDataService extActFlowDataService;
@Mock private IExtActBpmLogService extActBpmLogService;
@Mock private IExtActBpmFileService extActBpmFileService;
private ImprovementProposalServiceImpl service;
@BeforeEach
void setUp() {
service = new ImprovementProposalServiceImpl();
ReflectionTestUtils.setField(service, "improvementProposalMapper", improvementProposalMapper);
ReflectionTestUtils.setField(service, "improvementDeptScoreMapper", improvementDeptScoreMapper);
ReflectionTestUtils.setField(service, "improvementInstituteVoteMapper", improvementInstituteVoteMapper);
ReflectionTestUtils.setField(service, "bpmBaseExtApi", bpmBaseExtApi);
ReflectionTestUtils.setField(service, "runtimeService", runtimeService);
ReflectionTestUtils.setField(service, "historyService", historyService);
ReflectionTestUtils.setField(service, "taskService", taskService);
ReflectionTestUtils.setField(service, "sysRoleService", sysRoleService);
ReflectionTestUtils.setField(service, "sysUserDepartService", sysUserDepartService);
ReflectionTestUtils.setField(service, "extActTaskCcService", extActTaskCcService);
ReflectionTestUtils.setField(service, "taskApprovalOpinionService", taskApprovalOpinionService);
ReflectionTestUtils.setField(service, "extActFlowDataService", extActFlowDataService);
ReflectionTestUtils.setField(service, "extActBpmLogService", extActBpmLogService);
ReflectionTestUtils.setField(service, "extActBpmFileService", extActBpmFileService);
}
private LoginUser user(String id) {
LoginUser u = new LoginUser();
u.setId(id);
u.setUsername("u" + id);
u.setRealname("用户" + id);
return u;
}
/** mock 用户有效部门:orgType=2 + orgCategory=2 + delFlag=0 才进入可提报部门 */
private void mockUserDepartments(String userId, String deptId, String deptName) {
org.jeecg.modules.system.entity.SysDepart depart = new org.jeecg.modules.system.entity.SysDepart();
depart.setId(deptId);
depart.setDepartName(deptName);
depart.setOrgCode("org-" + deptId);
depart.setOrgType("2");
depart.setOrgCategory("2");
depart.setDelFlag("0");
when(sysUserDepartService.getOrderedDepartListById(userId)).thenReturn(java.util.List.of(depart));
}
// ==================== createNewProposal(流水号) ====================
@Nested
class CreateNewProposal {
@Test
void should_generate_proposal_no_with_sequence() {
ImprovementProposal proposal = new ImprovementProposal();
proposal.setProposalDeptId("dept-1");
mockUserDepartments("1", "dept-1", "研发部");
when(improvementProposalMapper.incrementProposalYearSequence(2026)).thenReturn(1);
when(improvementProposalMapper.selectProposalYearSequenceForUpdate(2026)).thenReturn(12);
service.createNewProposal(proposal, user("1"));
assertEquals("研发部-2026年-0012", proposal.getProposalNo());
verify(improvementProposalMapper).insert(any());
}
@Test
void should_throw_when_sequence_missing() {
ImprovementProposal proposal = new ImprovementProposal();
proposal.setProposalDeptId("dept-1");
mockUserDepartments("1", "dept-1", "研发部");
when(improvementProposalMapper.incrementProposalYearSequence(any())).thenReturn(1);
when(improvementProposalMapper.selectProposalYearSequenceForUpdate(any())).thenReturn(null);
assertThrows(IllegalStateException.class, () -> service.createNewProposal(proposal, user("1")));
}
}
// ==================== saveInitialReview(奖级 → needDeptScore ====================
@Nested
class SaveInitialReview {
private ImprovementProposal proposal(String id, String procId) {
ImprovementProposal p = new ImprovementProposal();
p.setId(id);
p.setProcessInstanceId(procId);
return p;
}
@Test
void should_set_need_dept_score_when_gold() {
ImprovementProposalBpmActionDTO action = new ImprovementProposalBpmActionDTO();
action.setMainId("p-1");
action.setProcessInstanceId("proc-1");
action.setTaskId("t-1");
action.setInitialAward("0"); // 金 → 需部门评分
when(improvementProposalMapper.selectById("p-1")).thenReturn(proposal("p-1", "proc-1"));
org.flowable.task.api.Task task = org.mockito.Mockito.mock(org.flowable.task.api.Task.class);
org.flowable.task.api.TaskQuery taskQuery = org.mockito.Mockito.mock(org.flowable.task.api.TaskQuery.class);
when(taskService.createTaskQuery()).thenReturn(taskQuery);
when(taskQuery.taskId("t-1")).thenReturn(taskQuery);
when(taskQuery.singleResult()).thenReturn(task);
when(task.getProcessInstanceId()).thenReturn("proc-1");
when(task.getTaskDefinitionKey()).thenReturn("Task_improve001");
when(task.getAssignee()).thenReturn("u1");
service.saveInitialReview(action, user("1"));
verify(improvementProposalMapper).updateById(any());
verify(runtimeService).setVariable("proc-1", "need_dept_score", "1");
}
@Test
void should_not_need_dept_score_when_bronze() {
ImprovementProposalBpmActionDTO action = new ImprovementProposalBpmActionDTO();
action.setMainId("p-1");
action.setProcessInstanceId("proc-1");
action.setTaskId("t-1");
action.setInitialAward("2"); // 铜 → 不需部门评分
when(improvementProposalMapper.selectById("p-1")).thenReturn(proposal("p-1", "proc-1"));
org.flowable.task.api.Task task = org.mockito.Mockito.mock(org.flowable.task.api.Task.class);
org.flowable.task.api.TaskQuery taskQuery = org.mockito.Mockito.mock(org.flowable.task.api.TaskQuery.class);
when(taskService.createTaskQuery()).thenReturn(taskQuery);
when(taskQuery.taskId("t-1")).thenReturn(taskQuery);
when(taskQuery.singleResult()).thenReturn(task);
when(task.getProcessInstanceId()).thenReturn("proc-1");
when(task.getTaskDefinitionKey()).thenReturn("Task_improve001");
when(task.getAssignee()).thenReturn("u1");
service.saveInitialReview(action, user("1"));
verify(runtimeService).setVariable("proc-1", "need_dept_score", "0");
}
}
// ==================== saveDeptScore(评分计算 + 落库) ====================
@Nested
class SaveDeptScore {
@Test
void should_calculate_and_insert_score() {
ImprovementDeptScoreBpmDTO scoreAction = new ImprovementDeptScoreBpmDTO();
scoreAction.setMainId("p-1");
scoreAction.setProcessInstanceId("proc-1");
scoreAction.setTaskId("t-1");
scoreAction.setSafetyScore(BigDecimal.valueOf(80));
scoreAction.setQualityScore(BigDecimal.valueOf(90));
scoreAction.setEfficiencyScore(BigDecimal.valueOf(70));
scoreAction.setCostScore(BigDecimal.valueOf(60));
scoreAction.setPromotionScore(BigDecimal.valueOf(80));
scoreAction.setOriginalityScore(BigDecimal.valueOf(90));
scoreAction.setEffortScore(BigDecimal.valueOf(60));
ImprovementProposal proposal = new ImprovementProposal();
proposal.setId("p-1");
proposal.setProcessInstanceId("proc-1");
when(improvementProposalMapper.selectById("p-1")).thenReturn(proposal);
when(improvementProposalMapper.selectByIdForUpdate("p-1")).thenReturn(proposal);
when(improvementDeptScoreMapper.selectByMainId("p-1")).thenReturn(java.util.List.of());
org.flowable.task.api.Task task = org.mockito.Mockito.mock(org.flowable.task.api.Task.class);
org.flowable.task.api.TaskQuery taskQuery = org.mockito.Mockito.mock(org.flowable.task.api.TaskQuery.class);
when(taskService.createTaskQuery()).thenReturn(taskQuery);
when(taskQuery.taskId("t-1")).thenReturn(taskQuery);
when(taskQuery.singleResult()).thenReturn(task);
when(task.getProcessInstanceId()).thenReturn("proc-1");
when(task.getTaskDefinitionKey()).thenReturn("Task_improve002");
when(task.getAssignee()).thenReturn("u1");
service.saveDeptScore(scoreAction, user("1"));
org.mockito.ArgumentCaptor<org.jeecg.modules.supervision.improvementproposal.entity.ImprovementDeptScore> captor =
org.mockito.ArgumentCaptor.forClass(org.jeecg.modules.supervision.improvementproposal.entity.ImprovementDeptScore.class);
verify(improvementDeptScoreMapper).insert(captor.capture());
// 效果分 (80+90+70+60)/4=75;总分 75*0.5+80*0.2+90*0.2+60*0.1=77.5
assertEquals("75.00", captor.getValue().getEffectScore().toPlainString());
assertEquals("77.50", captor.getValue().getTotalScore().toPlainString());
assertEquals("p-1", captor.getValue().getMainId());
}
}
}
@@ -0,0 +1,97 @@
package org.jeecg.modules.supervision.improvementproposal.service.impl;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* ImprovementProposalSupport 纯函数测试(TDD 提炼自 ImprovementProposalServiceImpl)。
*/
class ImprovementProposalSupportTest {
// ============ formatProposalNo ============
@Test
void should_format_sequence_with_4_digits() {
// 原格式:部门-年份年-4位序号(含连字符)
assertEquals("研发部-2026年-0001",
ImprovementProposalSupport.formatProposalNo("研发部", 2026, 1));
assertEquals("研发部-2026年-0012",
ImprovementProposalSupport.formatProposalNo(" 研发部 ", 2026, 12));
}
// ============ calculateEffectScore ============
@Test
void should_calculate_effect_average() {
// (80+90+70+60)/4 = 75
BigDecimal score = ImprovementProposalSupport.calculateEffectScore(
BigDecimal.valueOf(80), BigDecimal.valueOf(90),
BigDecimal.valueOf(70), BigDecimal.valueOf(60));
assertEquals(0, score.compareTo(BigDecimal.valueOf(75)));
}
@Test
void should_round_half_up_to_2_digits() {
// (85+85+85+86)/4 = 85.25
BigDecimal score = ImprovementProposalSupport.calculateEffectScore(
BigDecimal.valueOf(85), BigDecimal.valueOf(85),
BigDecimal.valueOf(85), BigDecimal.valueOf(86));
assertEquals("85.25", score.toPlainString());
}
// ============ calculateTotalScore ============
@Test
void should_calculate_weighted_total() {
// 效果75*0.5 + 推广80*0.2 + 独创90*0.2 + 努力60*0.1 = 37.5+16+18+6 = 77.5
BigDecimal total = ImprovementProposalSupport.calculateTotalScore(
BigDecimal.valueOf(75), BigDecimal.valueOf(80),
BigDecimal.valueOf(90), BigDecimal.valueOf(60));
assertEquals("77.50", total.toPlainString());
}
// ============ resolveSuggestedAward ============
@Test
void should_map_award_by_average_score() {
assertEquals("0", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(85))); // 金
assertEquals("1", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(70))); // 银
assertEquals("2", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(50))); // 铜
assertEquals("3", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(30))); // 参与
}
@Test
void should_handle_boundaries() {
assertEquals("0", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(80))); // 80→金
assertEquals("1", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(60))); // 60→银
assertEquals("2", ImprovementProposalSupport.resolveSuggestedAward(BigDecimal.valueOf(40))); // 40→铜
}
// ============ award 校验/排名/部门评分判断 ============
@Test
void should_validate_award_values() {
assertTrue(ImprovementProposalSupport.isValidAward("0"));
assertTrue(ImprovementProposalSupport.isValidAward("3"));
assertFalse(ImprovementProposalSupport.isValidAward("4"));
assertFalse(ImprovementProposalSupport.isValidAward(null));
}
@Test
void should_rank_award() {
assertEquals(2, ImprovementProposalSupport.awardRank("2"));
}
@Test
void should_need_dept_score_only_for_gold_silver() {
assertTrue(ImprovementProposalSupport.needsDeptScore("0"));
assertTrue(ImprovementProposalSupport.needsDeptScore("1"));
assertFalse(ImprovementProposalSupport.needsDeptScore("2"));
assertFalse(ImprovementProposalSupport.needsDeptScore("3"));
}
}
@@ -0,0 +1,88 @@
package org.jeecg.modules.supervision.tasklist.service.impl;
import org.junit.jupiter.api.Test;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* TaskListDetailSupport 纯函数测试(TDD 提炼自 TaskListDetialServiceImpl)。
*/
class TaskListDetailSupportTest {
// ============ normalizePid ============
@Test
void should_keep_non_empty_pid() {
assertEquals("p-1", TaskListDetailSupport.normalizePid("p-1"));
}
@Test
void should_normalize_empty_pid_to_null() {
assertNull(TaskListDetailSupport.normalizePid(""));
assertNull(TaskListDetailSupport.normalizePid(null));
}
// ============ collectUserIds ============
@Test
void should_collect_trimmed_ids() {
Set<String> set = new LinkedHashSet<>();
TaskListDetailSupport.collectUserIds(" u1 , u2 ,u1 ", set);
assertEquals(Set.of("u1", "u2"), set);
}
@Test
void should_skip_empty_ids_str() {
Set<String> set = new LinkedHashSet<>();
TaskListDetailSupport.collectUserIds("", set);
TaskListDetailSupport.collectUserIds(null, set);
assertEquals(0, set.size());
}
// ============ buildNamesFromMap ============
@Test
void should_build_names_in_order() {
String names = TaskListDetailSupport.buildNamesFromMap("u1,u2",
Map.of("u1", "张三", "u2", "李四"));
assertEquals("张三,李四", names);
}
@Test
void should_skip_ids_without_mapping() {
String names = TaskListDetailSupport.buildNamesFromMap("u1,u3",
Map.of("u1", "张三"));
assertEquals("张三", names);
}
@Test
void should_return_null_when_no_match_or_empty() {
assertNull(TaskListDetailSupport.buildNamesFromMap("u9", Map.of("u1", "张三")));
assertNull(TaskListDetailSupport.buildNamesFromMap("", Map.of("u1", "张三")));
assertNull(TaskListDetailSupport.buildNamesFromMap("u1", Map.of()));
}
// ============ mergeIds ============
@Test
void should_merge_deduplicate_keep_order() {
assertEquals("u1,u2,u3", TaskListDetailSupport.mergeIds("u1,u2", "u2,u3"));
}
@Test
void should_merge_single_side() {
assertEquals("u1,u2", TaskListDetailSupport.mergeIds("u1,u2", null));
assertEquals("u3", TaskListDetailSupport.mergeIds(null, "u3"));
}
@Test
void should_return_null_when_both_empty() {
assertNull(TaskListDetailSupport.mergeIds("", ""));
assertNull(TaskListDetailSupport.mergeIds(null, null));
}
}
@@ -0,0 +1,193 @@
package org.jeecg.modules.supervision.tasklist.service.impl;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.supervision.tasklist.entity.TaskListDetial;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListDetialMapper;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListMapper;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListPermissionMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* TaskListDetialServiceImpl 测试(Shiro ThreadContext.bind 提供登录用户 + mock mapper/sysBaseAPI)。
* 核心:deleteTask/toggleStatus/moveTask 的权限与父子计数规则 + 查询方法。
*/
@ExtendWith(MockitoExtension.class)
class TaskListDetialServiceImplTest {
@Mock private TaskListDetialMapper taskListDetialMapper;
@Mock private TaskListMapper taskListMapper;
@Mock private TaskListPermissionMapper taskListPermissionMapper;
@Mock private ISysBaseAPI sysBaseAPI;
@Mock private Subject subject;
private TaskListDetialServiceImpl service;
private final LoginUser loginUser = new LoginUser();
@BeforeEach
void setUp() {
loginUser.setId("user-1");
loginUser.setUsername("admin");
loginUser.setRealname("管理员");
service = new TaskListDetialServiceImpl();
ReflectionTestUtils.setField(service, "taskListDetialMapper", taskListDetialMapper);
ReflectionTestUtils.setField(service, "taskListMapper", taskListMapper);
ReflectionTestUtils.setField(service, "taskListPermissionMapper", taskListPermissionMapper);
ReflectionTestUtils.setField(service, "sysBaseAPI", sysBaseAPI);
// Shiro 官方测试机制:绑定 Subject 到当前线程,SecurityUtils.getSubject() 即可取到
ThreadContext.bind(subject);
// lenient:查询方法不经过 SecurityUtils,仅写方法用到
lenient().when(subject.getPrincipal()).thenReturn(loginUser);
}
@AfterEach
void tearDown() {
// Shiro 1.13 无 unbind(),用 remove() 清除线程上下文
ThreadContext.remove();
}
private TaskListDetial task(String id, String mainId, String pid, String type, Integer isDefault) {
TaskListDetial t = new TaskListDetial();
t.setId(id);
t.setMainId(mainId);
t.setPid(pid);
t.setType(type);
t.setIsDefault(isDefault);
return t;
}
// ==================== deleteTask ====================
@Test
void should_throw_when_task_not_exists() {
when(taskListDetialMapper.selectById("t1")).thenReturn(null);
assertThrows(RuntimeException.class, () -> service.deleteTask("t1"));
}
@Test
void should_throw_when_default_group() {
TaskListDetial group = task("g1", "m1", null, "0", 1);
when(taskListDetialMapper.selectById("g1")).thenReturn(group);
when(taskListPermissionMapper.selectOne(any())).thenReturn(null);
assertThrows(RuntimeException.class, () -> service.deleteTask("g1"));
}
@Test
void should_delete_task_and_update_parent_counts() {
TaskListDetial task = task("t1", "m1", "p1", "1", null);
task.setTaskStatus(1);
when(taskListDetialMapper.selectById("t1")).thenReturn(task);
when(taskListPermissionMapper.selectOne(any())).thenReturn(perm("1"));
when(taskListDetialMapper.selectChildrenByPid("t1")).thenReturn(List.of());
TaskListDetial parent = task("p1", "m1", null, "1", null);
when(taskListDetialMapper.selectById("p1")).thenReturn(parent);
when(taskListDetialMapper.countChildrenByPid("p1")).thenReturn(0);
service.deleteTask("t1");
verify(taskListDetialMapper).deleteById("t1");
verify(taskListDetialMapper).decrementSubTaskCount("p1");
verify(taskListDetialMapper).decrementCompletedSubTaskCount("p1");
verify(taskListDetialMapper).updateHasChild("p1", "0");
}
// ==================== toggleStatus ====================
@Test
void should_toggle_status_and_increment_completed() {
TaskListDetial task = task("t1", "m1", "p1", "1", null);
task.setTaskStatus(0);
when(taskListDetialMapper.selectById("t1")).thenReturn(task);
when(taskListPermissionMapper.selectOne(any())).thenReturn(perm("2"));
TaskListDetial parent = task("p1", "m1", null, "1", null);
when(taskListDetialMapper.selectById("p1")).thenReturn(parent);
service.toggleStatus("t1");
verify(taskListDetialMapper).toggleTaskStatus(eq("t1"), eq("admin"), eq(1), any());
verify(taskListDetialMapper).incrementCompletedSubTaskCount("p1");
}
@Test
void should_reject_reader_operating_others_task() {
TaskListDetial task = task("t1", "m1", null, "1", null);
task.setAssigneeId("other-user");
when(taskListDetialMapper.selectById("t1")).thenReturn(task);
when(taskListPermissionMapper.selectOne(any())).thenReturn(perm("3"));
assertThrows(RuntimeException.class, () -> service.toggleStatus("t1"));
}
// ==================== moveTask ====================
@Test
void should_move_task_and_update_parents_when_pid_changed() {
TaskListDetial task = task("t1", "m1", "old-p", "1", null);
when(taskListDetialMapper.selectById("t1")).thenReturn(task);
when(taskListPermissionMapper.selectOne(any())).thenReturn(perm("2"));
when(taskListDetialMapper.selectList(any())).thenReturn(List.of());
TaskListDetial oldParent = task("old-p", "m1", null, "1", null);
TaskListDetial newParent = task("new-p", "m1", null, "1", null);
when(taskListDetialMapper.selectById("old-p")).thenReturn(oldParent);
when(taskListDetialMapper.selectById("new-p")).thenReturn(newParent);
when(taskListDetialMapper.countChildrenByPid("old-p")).thenReturn(0);
org.jeecg.modules.supervision.tasklist.vo.MoveTaskReq req = new org.jeecg.modules.supervision.tasklist.vo.MoveTaskReq();
req.setTaskId("t1");
req.setTargetPid("new-p");
req.setTargetSortOrder(1);
service.moveTask(req);
verify(taskListDetialMapper).decrementSubTaskCount("old-p");
verify(taskListDetialMapper).incrementSubTaskCount("new-p");
verify(taskListDetialMapper).updateHasChild("new-p", "1");
}
// ==================== 查询方法 ====================
@Test
void should_query_all_by_main_id() {
// task 无 createBy/assigneeId 等字段 → fillCreateByName 不触发用户查询
TaskListDetial task = task("t1", "m1", null, "1", null);
when(taskListDetialMapper.selectAllByMainId("m1")).thenReturn(List.of(task));
List<TaskListDetial> result = service.queryAllByMainId("m1");
assertEquals(1, result.size());
verify(taskListDetialMapper).selectAllByMainId("m1");
}
@Test
void should_query_top_level() {
when(taskListDetialMapper.selectTopLevelByMainId("m1")).thenReturn(List.of());
assertEquals(0, service.queryTopLevelByMainId("m1").size());
}
// ==================== 工具 ====================
private org.jeecg.modules.supervision.tasklist.entity.TaskListPermission perm(String permission) {
org.jeecg.modules.supervision.tasklist.entity.TaskListPermission p =
new org.jeecg.modules.supervision.tasklist.entity.TaskListPermission();
p.setPermission(permission);
return p;
}
}
@@ -0,0 +1,127 @@
package org.jeecg.modules.supervision.tasklist.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import org.jeecg.modules.supervision.tasklist.entity.TaskListFavorite;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListFavoriteMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* TaskListFavoriteServiceImpl 测试(排序规则 reorderItem 为核心)。
*/
@ExtendWith(MockitoExtension.class)
class TaskListFavoriteServiceImplTest {
@Mock private TaskListFavoriteMapper taskListFavoriteMapper;
private TaskListFavoriteServiceImpl service;
@BeforeEach
void setUp() {
service = new TaskListFavoriteServiceImpl();
ReflectionTestUtils.setField(service, "taskListFavoriteMapper", taskListFavoriteMapper);
}
@Nested
class MaxSortOrder {
@Test
void should_default_zero_when_null() {
when(taskListFavoriteMapper.selectMaxSortOrder("u1", "p1")).thenReturn(null);
assertEquals(0, service.getMaxSortOrder("u1", "p1"));
}
@Test
void should_return_max() {
when(taskListFavoriteMapper.selectMaxSortOrder("u1", "p1")).thenReturn(5);
assertEquals(5, service.getMaxSortOrder("u1", "p1"));
}
}
@Nested
class ReorderItem {
private TaskListFavorite favorite(String id, Integer sort) {
TaskListFavorite f = new TaskListFavorite();
f.setId(id);
f.setSortOrder(sort);
return f;
}
@Test
void should_throw_when_moved_not_found() {
when(taskListFavoriteMapper.selectById("m1")).thenReturn(null);
assertThrows(RuntimeException.class,
() -> service.reorderItem("m1", "u1", "p1", null, 1, null));
}
@Test
void should_reorder_siblings_within_same_group() {
// 同组移动:m1 拖到位置 1,兄弟重排
TaskListFavorite moved = favorite("m1", 3);
when(taskListFavoriteMapper.selectById("m1")).thenReturn(moved);
when(taskListFavoriteMapper.selectList(any(Wrapper.class)))
.thenReturn(List.of(favorite("a", 1), favorite("b", 2)));
service.reorderItem("m1", "u1", "p1", null, 1, null);
// 兄弟按新位置重排:pos=1 → a 挪到 2,b 挪到 3
verify(taskListFavoriteMapper).updateSortOrder(eq("a"), eq(2));
verify(taskListFavoriteMapper).updateSortOrder(eq("b"), eq(3));
// moved 更新 pid 与位置
verify(taskListFavoriteMapper).updatePidAndSort("m1", "p1", 1);
}
@Test
void should_use_new_pid_when_cross_group() {
TaskListFavorite moved = favorite("m1", 1);
when(taskListFavoriteMapper.selectById("m1")).thenReturn(moved);
when(taskListFavoriteMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
service.reorderItem("m1", "u1", "p1", "type1", null, "new-group");
verify(taskListFavoriteMapper).updatePidAndSort("m1", "new-group", 1);
}
@Test
void should_set_null_pid_when_new_pid_empty() {
// newPid="" 表示拖到根级别(无分组)→ pid 置 null
TaskListFavorite moved = favorite("m1", 1);
when(taskListFavoriteMapper.selectById("m1")).thenReturn(moved);
when(taskListFavoriteMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
service.reorderItem("m1", "u1", "p1", null, null, "");
verify(taskListFavoriteMapper).updatePidAndSort("m1", null, 1);
}
@Test
void should_not_update_sibling_when_sort_unchanged() {
TaskListFavorite moved = favorite("m1", 2);
when(taskListFavoriteMapper.selectById("m1")).thenReturn(moved);
when(taskListFavoriteMapper.selectList(any(Wrapper.class)))
.thenReturn(List.of(favorite("a", 1)));
// pos=2(默认 targetPosition null → siblings.size()+1=2):a 保持 1,不更新
service.reorderItem("m1", "u1", "p1", null, null, null);
verify(taskListFavoriteMapper, never()).updateSortOrder(eq("a"), any());
verify(taskListFavoriteMapper).updatePidAndSort("m1", "p1", 2);
}
}
}
@@ -0,0 +1,41 @@
package org.jeecg.modules.supervision.tasklist.service.impl;
import org.jeecg.modules.supervision.tasklist.entity.TaskListPermission;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListPermissionMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* TaskListPermissionServiceImpl 测试(简单转发)。
*/
@ExtendWith(MockitoExtension.class)
class TaskListPermissionServiceImplTest {
@Mock private TaskListPermissionMapper taskListPermissionMapper;
private TaskListPermissionServiceImpl service;
@BeforeEach
void setUp() {
service = new TaskListPermissionServiceImpl();
ReflectionTestUtils.setField(service, "taskListPermissionMapper", taskListPermissionMapper);
}
@Test
void should_select_by_main_id() {
TaskListPermission perm = new TaskListPermission();
when(taskListPermissionMapper.selectByMainId("m-1")).thenReturn(List.of(perm));
assertEquals(1, service.selectByMainId("m-1").size());
verify(taskListPermissionMapper).selectByMainId("m-1");
}
}
@@ -0,0 +1,236 @@
package org.jeecg.modules.supervision.tasklist.service.impl;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.supervision.tasklist.entity.TaskList;
import org.jeecg.modules.supervision.tasklist.entity.TaskListFavorite;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListDetialMapper;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListFavoriteMapper;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListMapper;
import org.jeecg.modules.supervision.tasklist.mapper.TaskListPermissionMapper;
import org.jeecg.modules.supervision.tasklist.service.ITaskListFavoriteService;
import org.jeecg.modules.supervision.tasklist.vo.AddCollaboratorReq;
import org.jeecg.modules.supervision.tasklist.vo.CreateTaskListGroupReq;
import org.jeecg.modules.supervision.tasklist.vo.CreateTaskListReq;
import org.jeecg.modules.supervision.tasklist.vo.MoveTaskListReq;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* TaskListServiceImpl 测试(Shiro ThreadContext.bind + mock mapper/service/sysBaseAPI)。
* 核心:创建清单的密级/分组校验、创建分组排序、移动清单权限、添加协作人权限。
*/
@ExtendWith(MockitoExtension.class)
class TaskListServiceImplTest {
@Mock private TaskListMapper taskListMapper;
@Mock private TaskListDetialMapper taskListDetialMapper;
@Mock private TaskListPermissionMapper taskListPermissionMapper;
@Mock private TaskListFavoriteMapper taskListFavoriteMapper;
@Mock private ITaskListFavoriteService taskListFavoriteService;
@Mock private ISysBaseAPI sysBaseAPI;
@Mock private Subject subject;
private TaskListServiceImpl service;
private final LoginUser loginUser = new LoginUser();
@BeforeEach
void setUp() {
loginUser.setId("user-1");
loginUser.setUsername("admin");
loginUser.setRealname("管理员");
loginUser.setUserSecurityLevel(3);
service = new TaskListServiceImpl();
ReflectionTestUtils.setField(service, "taskListMapper", taskListMapper);
ReflectionTestUtils.setField(service, "taskListDetialMapper", taskListDetialMapper);
ReflectionTestUtils.setField(service, "taskListPermissionMapper", taskListPermissionMapper);
ReflectionTestUtils.setField(service, "taskListFavoriteMapper", taskListFavoriteMapper);
ReflectionTestUtils.setField(service, "taskListFavoriteService", taskListFavoriteService);
ReflectionTestUtils.setField(service, "sysBaseAPI", sysBaseAPI);
ThreadContext.bind(subject);
lenient().when(subject.getPrincipal()).thenReturn(loginUser);
}
@AfterEach
void tearDown() {
ThreadContext.remove();
}
// ==================== createTaskList ====================
@Nested
class CreateTaskList {
@Test
void should_reject_when_secret_level_too_high() {
CreateTaskListReq req = new CreateTaskListReq();
req.setSecretLevel(5); // userSecLevel=35>=3 → 拒绝
assertThrows(RuntimeException.class, () -> service.createTaskList(req));
}
@Test
void should_reject_when_group_invalid() {
CreateTaskListReq req = new CreateTaskListReq();
req.setSecretLevel(1);
req.setPid("g-1");
when(taskListFavoriteMapper.selectById("g-1")).thenReturn(null);
assertThrows(RuntimeException.class, () -> service.createTaskList(req));
}
@Test
void should_create_list_with_default_group() {
CreateTaskListReq req = new CreateTaskListReq();
req.setTasklistName("我的清单");
req.setSecretLevel(1);
// mock 模拟 MyBatis Plus insert 主键回填(ASSIGN_ID
when(taskListMapper.insert(any())).thenAnswer(inv -> {
TaskList tl = inv.getArgument(0);
tl.setId("tl-1");
return 1;
});
String id = service.createTaskList(req);
assertEquals("tl-1", id);
verify(taskListDetialMapper).insert(any()); // 默认分组
}
}
// ==================== createTaskListGroup ====================
@Nested
class CreateTaskListGroup {
@Test
void should_use_max_sort_plus_one() {
CreateTaskListGroupReq req = new CreateTaskListGroupReq();
req.setTasklistName("新分组");
when(taskListFavoriteService.getMaxSortOrderByType("user-1", null, "0")).thenReturn(5);
String id = service.createTaskListGroup(req);
assertEquals(id, id);
org.mockito.ArgumentCaptor<TaskListFavorite> captor =
org.mockito.ArgumentCaptor.forClass(TaskListFavorite.class);
verify(taskListFavoriteMapper).insert(captor.capture());
assertEquals(6, captor.getValue().getSortOrder());
assertEquals("user-1", captor.getValue().getUserId());
assertEquals("0", captor.getValue().getType());
}
}
// ==================== moveTaskList ====================
@Nested
class MoveTaskList {
@Test
void should_reject_when_favorite_not_owner() {
MoveTaskListReq req = new MoveTaskListReq();
req.setFavoriteId("f-1");
when(taskListFavoriteMapper.selectById("f-1")).thenReturn(null);
assertThrows(RuntimeException.class, () -> service.moveTaskList(req));
}
@Test
void should_reject_when_favorite_is_group() {
TaskListFavorite group = new TaskListFavorite();
group.setId("f-1");
group.setUserId("user-1");
group.setType("0");
MoveTaskListReq req = new MoveTaskListReq();
req.setFavoriteId("f-1");
when(taskListFavoriteMapper.selectById("f-1")).thenReturn(group);
assertThrows(RuntimeException.class, () -> service.moveTaskList(req));
}
@Test
void should_move_list_and_reorder() {
TaskListFavorite favorite = new TaskListFavorite();
favorite.setId("f-1");
favorite.setUserId("user-1");
favorite.setType("1");
favorite.setPid("g-1");
when(taskListFavoriteMapper.selectById("f-1")).thenReturn(favorite);
MoveTaskListReq req = new MoveTaskListReq();
req.setFavoriteId("f-1");
req.setTargetGroupId("g-2");
req.setSortOrder(1);
TaskListFavorite targetGroup = new TaskListFavorite();
targetGroup.setUserId("user-1");
targetGroup.setType("0");
when(taskListFavoriteMapper.selectById("g-2")).thenReturn(targetGroup);
when(taskListFavoriteService.countChildren("user-1", "g-1")).thenReturn(0);
service.moveTaskList(req);
verify(taskListFavoriteService).reorderItem("f-1", "user-1", "g-1", "1", 1, "g-2");
// 旧父 hasChild 置 0 + 新父 hasChild 置 1(实体 updateById
verify(taskListFavoriteMapper, times(2)).updateById(any());
}
}
// ==================== addCollaborator ====================
@Nested
class AddCollaborator {
@Test
void should_reject_when_not_owner() {
AddCollaboratorReq req = new AddCollaboratorReq();
req.setTaskListId("tl-1");
req.setUserId("u-9");
req.setPermission("2");
when(taskListPermissionMapper.selectCount(any())).thenReturn(0L);
assertThrows(RuntimeException.class, () -> service.addCollaborator(req));
}
@Test
void should_reject_when_permission_type_invalid() {
AddCollaboratorReq req = new AddCollaboratorReq();
req.setTaskListId("tl-1");
req.setPermission("1");
when(taskListPermissionMapper.selectCount(any())).thenReturn(1L);
assertThrows(RuntimeException.class, () -> service.addCollaborator(req));
}
@Test
void should_add_collaborator() {
AddCollaboratorReq req = new AddCollaboratorReq();
req.setTaskListId("tl-1");
req.setUserId("u-9");
req.setPermission("2");
// selectCount 调 2 次:ownerCheck=1(通过)+ existCheck=0(不存在则新增)
when(taskListPermissionMapper.selectCount(any())).thenReturn(1L, 0L);
TaskList taskList = new TaskList();
taskList.setSecretLevel(1);
when(taskListMapper.selectById("tl-1")).thenReturn(taskList);
LoginUser target = new LoginUser();
target.setUserSecurityLevel(3); // 密级须 > 清单密级(1) 才能添加
when(sysBaseAPI.getUserById("u-9")).thenReturn(target);
service.addCollaborator(req);
verify(taskListPermissionMapper).insert(any());
}
}
}
@@ -0,0 +1,259 @@
package org.jeecg.modules.supervision.xispeak.flow;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.supervision.common.OrgConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakFeedbackConfig;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.tasktask.entity.TaskTask;
import org.jeecg.modules.tasktask.service.ITaskTaskService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
/**
* XiSpeakFeedbackFlow 反馈流程表达式测试(mock 外部依赖)。
*/
@ExtendWith(MockitoExtension.class)
class XiSpeakFeedbackFlowTest {
@Mock private XiSpeakFeedbackConfig xiSpeakFeedbackConfig;
@Mock private OrgConfig orgConfig;
@Mock private ISysBaseAPI iSysBaseAPI;
@Mock private RuntimeService runtimeService;
@Mock private IBgXiSpeakService bgXiSpeakService;
@Mock private ITaskTaskService taskTaskService;
@Mock private DelegateExecution execution;
private XiSpeakFeedbackFlow flow;
@BeforeEach
void setUp() {
OrgConfig.Dept dept = new OrgConfig.Dept();
dept.setJj("dept-jj");
OrgConfig.Role role = new OrgConfig.Role();
role.setJjWorker("role-jj-worker");
role.setLd("role-ld");
lenient().when(orgConfig.getDept()).thenReturn(dept);
lenient().when(orgConfig.getRole()).thenReturn(role);
flow = new XiSpeakFeedbackFlow(xiSpeakFeedbackConfig, orgConfig, iSysBaseAPI, runtimeService, bgXiSpeakService, taskTaskService);
}
// ==================== getImplDeptList ====================
@Nested
class ImplDeptList {
@Test
void should_return_empty_when_keys_missing() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(execution.getProcessInstanceBusinessKey()).thenReturn("");
assertEquals(List.of(), flow.getImplDeptList(execution));
}
@Test
void should_return_empty_when_entity_not_found() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(execution.getProcessInstanceBusinessKey()).thenReturn("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(null);
assertEquals(List.of(), flow.getImplDeptList(execution));
}
@Test
void should_return_empty_when_approve_info_empty() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(execution.getProcessInstanceBusinessKey()).thenReturn("biz-1");
BgXiSpeak entity = new BgXiSpeak();
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
assertEquals(List.of(), flow.getImplDeptList(execution));
}
@Test
void should_return_dept_keys() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(execution.getProcessInstanceBusinessKey()).thenReturn("biz-1");
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", new DeptApproveDetail());
map.put("dept-2", new DeptApproveDetail());
BgXiSpeak entity = new BgXiSpeak();
entity.setApproveInfo(map);
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
assertEquals(List.of("dept-1", "dept-2"), flow.getImplDeptList(execution));
}
}
// ==================== getImplDeptApprove ====================
@Nested
class ImplDeptApprove {
private TaskTask taskWithHandler(String handler) {
TaskTask tt = new TaskTask();
tt.setBusinessId("xi-biz");
tt.setDeptHandlerName(handler);
return tt;
}
@Test
void should_return_empty_when_business_key_blank() {
when(execution.getProcessInstanceBusinessKey()).thenReturn(" ");
assertEquals("", flow.getImplDeptApprove(execution));
}
@Test
void should_return_empty_when_task_missing() {
when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1");
when(taskTaskService.getById("tt-1")).thenReturn(null);
assertEquals("", flow.getImplDeptApprove(execution));
}
@Test
void should_return_matched_approver_name() {
when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1");
when(taskTaskService.getById("tt-1")).thenReturn(taskWithHandler("worker1"));
DeptApproveDetail detail = new DeptApproveDetail();
detail.setImplUserNameList(List.of("worker1"));
detail.setApproverName("leader1");
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
BgXiSpeak entity = new BgXiSpeak();
entity.setApproveInfo(map);
when(bgXiSpeakService.getById("xi-biz")).thenReturn(entity);
assertEquals("leader1", flow.getImplDeptApprove(execution));
}
@Test
void should_return_empty_when_no_match() {
when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1");
when(taskTaskService.getById("tt-1")).thenReturn(taskWithHandler("nobody"));
DeptApproveDetail detail = new DeptApproveDetail();
detail.setImplUserNameList(List.of("worker1"));
detail.setApproverName("leader1");
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
BgXiSpeak entity = new BgXiSpeak();
entity.setApproveInfo(map);
when(bgXiSpeakService.getById("xi-biz")).thenReturn(entity);
assertEquals("", flow.getImplDeptApprove(execution));
}
}
// ==================== 部门审批详情(经 taskTask 链路) ====================
@Nested
class DeptDetailByTask {
private void mockDetailChain(String deptId, DeptApproveDetail detail) {
when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1");
TaskTask tt = new TaskTask();
tt.setBusinessId("xi-biz");
tt.setDeptId(deptId);
when(taskTaskService.getById("tt-1")).thenReturn(tt);
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put(deptId, detail);
BgXiSpeak entity = new BgXiSpeak();
entity.setApproveInfo(map);
when(bgXiSpeakService.getById("xi-biz")).thenReturn(entity);
}
@Test
void should_return_zero_when_detail_missing() {
when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1");
when(taskTaskService.getById("tt-1")).thenReturn(new TaskTask());
assertEquals(0, flow.getImplDeptIsEnd(execution));
}
@Test
void should_read_is_end() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(1);
mockDetailChain("dept-1", detail);
assertEquals(1, flow.getImplDeptIsEnd(execution));
}
@Test
void should_read_impl_worker() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setImplWorker("w1");
mockDetailChain("dept-1", detail);
assertEquals("w1", flow.getImplLeaderAsWorker(execution));
}
@Test
void should_return_empty_when_worker_blank() {
mockDetailChain("dept-1", new DeptApproveDetail());
assertEquals("", flow.getImplLeaderAsWorker(execution));
}
}
// ==================== getImplDeptWorker ====================
@Nested
class ImplDeptWorker {
@Test
void should_return_empty_when_business_key_blank() {
when(execution.getProcessInstanceBusinessKey()).thenReturn(" ");
assertEquals(List.of(), flow.getImplDeptWorker(execution));
}
@Test
void should_return_empty_when_task_missing() {
when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1");
when(taskTaskService.getById("tt-1")).thenReturn(null);
assertEquals(List.of(), flow.getImplDeptWorker(execution));
}
@Test
void should_split_worker_names() {
when(execution.getProcessInstanceBusinessKey()).thenReturn("tt-1");
TaskTask tt = new TaskTask();
tt.setBusinessId("xi-biz");
tt.setDeptHandlerName("张三, 李四 ,,王五");
when(taskTaskService.getById("tt-1")).thenReturn(tt);
assertEquals(List.of("张三", "李四", "王五"), flow.getImplDeptWorker(execution));
}
}
// ==================== JJ 部门查询 ====================
@Nested
class JJDeptQuery {
@Test
void should_return_jj_workers() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-jj-worker"))
.thenReturn(List.of("jj1", "jj2"));
assertEquals(List.of("jj1", "jj2"), flow.getJJDeptWorkerList());
}
@Test
void should_return_jj_leaders() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-ld"))
.thenReturn(List.of("ld1"));
assertEquals(List.of("ld1"), flow.getJJDeptLeaderList());
}
@Test
void should_return_empty_when_no_worker() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-jj-worker"))
.thenReturn(List.of());
assertEquals(0, flow.getJJDeptWorkerListLength());
}
}
}
@@ -0,0 +1,300 @@
package org.jeecg.modules.supervision.xispeak.flow;
import com.alibaba.fastjson.JSONObject;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression;
import org.jeecg.modules.supervision.common.OrgConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
/**
* XiSpeakFlow 流程表达式测试(mock config/orgConfig/iSysBaseAPI/flowNodeExpression/service)。
*/
@ExtendWith(MockitoExtension.class)
class XiSpeakFlowTest {
@Mock private XiSpeakConfig xiSpeakConfig;
@Mock private OrgConfig orgConfig;
@Mock private ISysBaseAPI iSysBaseAPI;
@Mock private RuntimeService runtimeService;
@Mock private FlowNodeExpression flowNodeExpression;
@Mock private IBgXiSpeakService bgXiSpeakService;
@Mock private DelegateExecution execution;
private XiSpeakFlow flow;
private XiSpeakProperties props;
@BeforeEach
void setUp() {
props = new XiSpeakProperties();
props.setNeedSdwApproveCode("need_sdw_approval");
props.setFeedbackRightNowCode("feedback_right_now");
props.setSdwLeaderListKey("sdw_leader_list");
props.setTemImplWorkerKey("tem_impl_worker");
props.setTemImplLeaderKey("tem_impl_leader");
props.setTemBgLeaderKey("tem_bg_leader");
props.setIsEndKey("is_end");
props.setBusinessKey("business_id");
props.setImplDeptCollectionUsedKey("impl_dept");
props.setDeptWorkerKey("dept_worker");
props.setImplDeptKey("impl_dept");
// lenient:部分测试(getListSize/getImplDeptNums 等)不触碰 config/orgConfig
lenient().when(xiSpeakConfig.getXiSpeak()).thenReturn(props);
OrgConfig.Dept dept = new OrgConfig.Dept();
dept.setBg("dept-bg");
OrgConfig.Role role = new OrgConfig.Role();
role.setLd("role-ld");
role.setSdw("role-sdw");
lenient().when(orgConfig.getDept()).thenReturn(dept);
lenient().when(orgConfig.getRole()).thenReturn(role);
flow = new XiSpeakFlow(xiSpeakConfig, orgConfig, iSysBaseAPI, runtimeService, flowNodeExpression, bgXiSpeakService);
}
// ==================== 部门角色查询 ====================
@Nested
class DeptRoleQuery {
@Test
void should_return_bg_dept_ld_users() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-bg", "role-ld"))
.thenReturn(List.of("u1", "u2"));
assertEquals(List.of("u1", "u2"), flow.getBgDeptLdUserIdList());
}
@Test
void should_return_sdw_leaders() {
when(iSysBaseAPI.getUserByRoleIdLocalApi("role-sdw")).thenReturn(List.of("sdw1"));
assertEquals(List.of("sdw1"), flow.getSDWLeader());
}
@Test
void should_return_impl_dept_lds() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-x", "role-ld"))
.thenReturn(List.of("ld1"));
assertEquals(List.of("ld1"), flow.getImplDeptLdsList("dept-x"));
}
}
// ==================== JSON Integer 解析 ====================
@Nested
class JsonIntegerParse {
@Test
void should_parse_int_from_json_object() {
assertEquals(1, flow.getNeedSdwApproval(new JSONObject().fluentPut("need_sdw_approval", 1)));
}
@Test
void should_parse_int_from_json_string() {
assertEquals(1, flow.getNeedSdwApproval("{\"need_sdw_approval\":1}"));
}
@Test
void should_return_zero_when_json_null() {
assertEquals(0, flow.getNeedSdwApproval(null));
}
@Test
void should_return_zero_when_key_missing() {
assertEquals(0, flow.getFeedBackRightNow(new JSONObject().fluentPut("other", 1)));
}
@Test
void should_return_zero_when_invalid_json() {
assertEquals(0, flow.getNeedSdwApproval("not-json"));
}
}
// ==================== JSON List 解析 ====================
@Nested
class JsonListParse {
@Test
void should_split_list_from_json_object() {
assertEquals(List.of("sdw1", "sdw2"),
flow.getSdwLeaderList(new JSONObject().fluentPut("sdw_leader_list", "sdw1,sdw2")));
}
@Test
void should_trim_tokens() {
assertEquals(List.of("sdw1", "sdw2"),
flow.getSdwLeaderList(new JSONObject().fluentPut("sdw_leader_list", " sdw1 , sdw2 ")));
}
@Test
void should_return_empty_when_json_null() {
assertEquals(List.of(), flow.getSdwLeaderList(null));
}
}
// ==================== flowNodeExpression 转发 ====================
@Nested
class FlowNodeForward {
@Test
void should_forward_impl_worker_list() {
when(flowNodeExpression.getSonProcessHqVariableList(execution, "tem_impl_worker"))
.thenReturn(List.of("w1"));
assertEquals(List.of("w1"), flow.getTemImplWorkerList(execution));
}
@Test
void should_forward_impl_leader_list() {
when(flowNodeExpression.getSonProcessHqVariableList(execution, "tem_impl_leader"))
.thenReturn(List.of("l1", "l2"));
assertEquals(2, flow.getTemImplLeaderListLength(execution));
}
@Test
void should_forward_bg_leader_list() {
when(flowNodeExpression.getSonProcessHqVariableList(execution, "tem_bg_leader"))
.thenReturn(List.of("b1"));
assertEquals(List.of("b1"), flow.getTemBgLeaderList(execution));
}
}
// ==================== 部门审批详情读取 ====================
@Nested
class DeptApproveDetailRead {
private void mockEntityWithDetail(Integer isEnd, String implWorker, List<String> workers) {
when(execution.getVariable("business_id")).thenReturn("biz-1");
when(execution.getVariableLocal("impl_dept")).thenReturn("[dept-1]");
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(isEnd);
detail.setImplWorker(implWorker);
detail.setImplUserNameList(workers);
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
BgXiSpeak entity = new BgXiSpeak();
entity.setApproveInfo(map);
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
}
@Test
void should_read_is_end_from_detail() {
mockEntityWithDetail(1, "w1", null);
assertEquals("1", flow.getImplDeptLeaderIsEnd(execution));
assertEquals(1, flow.getImplDeptIsEnd(execution));
}
@Test
void should_read_impl_worker_from_detail() {
mockEntityWithDetail(1, "w1", null);
assertEquals("w1", flow.getImplDeptLeader(execution));
}
@Test
void should_read_worker_list_from_detail() {
mockEntityWithDetail(0, null, List.of("w1", "w2"));
assertEquals(List.of("w1", "w2"), flow.getImplDeptWorkerListFromJson(execution));
assertEquals(List.of("w1", "w2"), flow.getDeptWorkerList(execution));
}
@Test
void should_fallback_to_variable_when_no_detail() {
// 无 detail:isEnd 从流程变量兜底(兼容旧流程)
// ⚠️ 现状行为:getSonProcessVariable 返回 List<String>splitUsernames),
// value.toString() 得到 "[1]" 而非 "1"getImplDeptIsEnd 的 parseInt("[1]") 失败返回 0。
// 测试锁定现状,若后续修复 isEnd 兜底解析需同步更新。
when(execution.getVariable("business_id")).thenReturn("biz-1");
when(execution.getVariableLocal("impl_dept")).thenReturn("[dept-1]");
BgXiSpeak entity = new BgXiSpeak();
entity.setApproveInfo(new DeptApproveDetailMap());
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
when(flowNodeExpression.getSonProcessVariable(execution, "is_end")).thenReturn(List.of("1"));
assertEquals("[1]", flow.getImplDeptLeaderIsEnd(execution));
assertEquals(0, flow.getImplDeptIsEnd(execution));
assertEquals("", flow.getImplDeptLeader(execution));
}
@Test
void should_return_list_to_string_when_variable_empty() {
// getSonProcessVariable 从不返回 null(空时返回 emptyList),value.toString() = "[]"
when(execution.getVariable("business_id")).thenReturn("biz-1");
when(execution.getVariableLocal("impl_dept")).thenReturn("[dept-1]");
BgXiSpeak entity = new BgXiSpeak();
entity.setApproveInfo(new DeptApproveDetailMap());
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
when(flowNodeExpression.getSonProcessVariable(execution, "is_end")).thenReturn(List.of());
assertEquals("[]", flow.getImplDeptLeaderIsEnd(execution));
assertEquals(0, flow.getImplDeptIsEnd(execution));
}
@Test
void should_return_zero_when_is_end_not_number() {
when(execution.getVariable("business_id")).thenReturn("biz-1");
when(execution.getVariableLocal("impl_dept")).thenReturn("[dept-1]");
BgXiSpeak entity = new BgXiSpeak();
entity.setApproveInfo(new DeptApproveDetailMap());
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
when(flowNodeExpression.getSonProcessVariable(execution, "is_end")).thenReturn(List.of("abc"));
assertEquals(0, flow.getImplDeptIsEnd(execution));
}
}
// ==================== implDeptNums ====================
@Nested
class ImplDeptNums {
@Test
void should_count_json_array_value() {
assertEquals(2, flow.getImplDeptNums("{\"impl_dept\":[\"d1\",\"d2\"]}"));
}
@Test
void should_count_comma_string_value() {
assertEquals(3, flow.getImplDeptNums("{\"impl_dept\":\"d1,d2,d3\"}"));
}
@Test
void should_return_zero_when_json_null() {
assertEquals(0, flow.getImplDeptNums(null));
}
}
// ==================== getListSize ====================
@Nested
class ListSize {
@Test
void should_count_tokens() {
assertEquals(3, flow.getListSize("a,b,c"));
}
@Test
void should_return_zero_when_blank() {
assertEquals(0, flow.getListSize(" "));
}
}
}
@@ -0,0 +1,128 @@
package org.jeecg.modules.supervision.xispeak.listener;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ExecutionQuery;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.tasktask.service.ITaskTaskService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* AfterBgLeaderApproveListener 测试:将 bg 领导写入上两级执行实例局部变量。
*/
@ExtendWith(MockitoExtension.class)
class AfterBgLeaderApproveListenerTest {
@Mock private FlowNodeExpression flowNodeExpression;
@Mock private RuntimeService runtimeService;
@Mock private XiSpeakConfig xiSpeakConfig;
@Mock private ITaskTaskService taskTaskService;
@Mock private IBgXiSpeakService bgXiSpeakService;
@Mock private DelegateTask delegateTask;
@Mock private Execution currentExecution;
@Mock private Execution parentExecution;
@Mock private ExecutionQuery query;
@Mock private ExecutionQuery parentQuery;
private AfterBgLeaderApproveListener listener;
@BeforeEach
void setUp() {
XiSpeakProperties props = new XiSpeakProperties();
props.setTemBgLeaderKey("tem_bg_leader");
when(xiSpeakConfig.getXiSpeak()).thenReturn(props);
listener = new AfterBgLeaderApproveListener(flowNodeExpression, runtimeService, xiSpeakConfig, taskTaskService, bgXiSpeakService);
}
private void mockExecutionChain(String parentId, String grandParentId) {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query, parentQuery);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn(parentId);
when(parentQuery.executionId(parentId)).thenReturn(parentQuery);
when(parentQuery.singleResult()).thenReturn(parentExecution);
when(parentExecution.getParentId()).thenReturn(grandParentId);
}
@Test
void should_return_when_task_null() {
listener.notify(null);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_execution_id_blank() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_current_execution_not_found() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(null);
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_parent_id_blank() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_write_to_grand_parent_when_both_levels_exist() {
mockExecutionChain("parent-1", "grand-1");
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("grand-1", "tem_bg_leader", "leader1");
}
@Test
void should_write_to_parent_when_no_grand_parent() {
mockExecutionChain("parent-1", null);
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("parent-1", "tem_bg_leader", "leader1");
}
@Test
void should_throw_when_set_variable_fails() {
// 与 fixcontact 不同:xispeak 监听器 catch 后重新抛出 RuntimeException(写入失败即中断流程)
mockExecutionChain("parent-1", "grand-1");
doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", "tem_bg_leader", "leader1");
org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class, () -> listener.notify(delegateTask));
}
}
@@ -0,0 +1,61 @@
package org.jeecg.modules.supervision.xispeak.listener;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties;
import org.jeecg.modules.supervision.xispeak.constant.XiSpeakConstant;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* AfterBgWorkerFinalApproveListener 测试:最终审批将业务表单 completionStatus 置为已完成。
*/
@ExtendWith(MockitoExtension.class)
class AfterBgWorkerFinalApproveListenerTest {
@Mock private XiSpeakConfig xiSpeakConfig;
@Mock private IBgXiSpeakService bgXiSpeakService;
@Mock private DelegateTask delegateTask;
private AfterBgWorkerFinalApproveListener listener;
@BeforeEach
void setUp() {
XiSpeakProperties props = new XiSpeakProperties();
props.setBusinessKey("business_id");
when(xiSpeakConfig.getXiSpeak()).thenReturn(props);
listener = new AfterBgWorkerFinalApproveListener(xiSpeakConfig, bgXiSpeakService);
}
@Test
void should_skip_when_business_key_missing() {
when(delegateTask.getVariable("business_id")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_update_completion_status_finished() {
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(bgXiSpeakService.updateById(any())).thenReturn(true);
listener.notify(delegateTask);
ArgumentCaptor<BgXiSpeak> captor = ArgumentCaptor.forClass(BgXiSpeak.class);
verify(bgXiSpeakService).updateById(captor.capture());
assertEquals("biz-1", captor.getValue().getId());
assertEquals(XiSpeakConstant.CompletionStatus.FINISHED, captor.getValue().getCompletionStatus());
}
}
@@ -0,0 +1,126 @@
package org.jeecg.modules.supervision.xispeak.listener;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakFeedbackConfig;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakFeedbackService;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.tasktask.service.ITaskTaskService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* AfterImplDeptLeaderApproveFeedbackListener 测试(反馈流程部门领导审批)。
* 规则:将部门领导写入 approveInfo 对应部门的 approverName(幂等)。
* parseDeptId 语义:Collection 取第一个;String 去 [] 保留全部(与 StoreJson 版不同)。
*/
@ExtendWith(MockitoExtension.class)
class AfterImplDeptLeaderApproveFeedbackListenerTest {
@Mock private XiSpeakFeedbackConfig xiSpeakFeedbackConfig;
@Mock private IBgXiSpeakFeedbackService bgXiSpeakFeedbackService;
@Mock private ITaskTaskService taskTaskService;
@Mock private XiSpeakConfig xiSpeakConfig;
@Mock private IBgXiSpeakService bgXiSpeakService;
@Mock private DelegateTask delegateTask;
private AfterImplDeptLeaderApproveFeedbackListener listener;
@BeforeEach
void setUp() {
XiSpeakProperties props = new XiSpeakProperties();
props.setBusinessKey("business_id");
props.setImplDeptCollectionUsedKey("impl_dept");
when(xiSpeakConfig.getXiSpeak()).thenReturn(props);
listener = new AfterImplDeptLeaderApproveFeedbackListener(
xiSpeakFeedbackConfig, bgXiSpeakFeedbackService, taskTaskService, xiSpeakConfig, bgXiSpeakService);
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_skip_when_business_key_null() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_skip_when_entity_not_found() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_store_approver_name_when_changed() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[dept-1]");
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
// parseDeptId("[dept-1]")String 形式去 [] 保留全部 → "dept-1"
assertEquals("leader1", entity.getApproveInfo().get("dept-1").getApproverName());
verify(bgXiSpeakService).updateById(entity);
}
@Test
void should_skip_update_when_approver_name_unchanged() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[dept-1]");
DeptApproveDetail detail = new DeptApproveDetail();
detail.setApproverName("leader1");
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
entity.setApproveInfo(map);
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_parse_first_element_from_collection() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(delegateTask.getVariableLocal("impl_dept")).thenReturn(List.of("dept-a", "dept-b"));
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals("leader1", entity.getApproveInfo().get("dept-a").getApproverName());
}
}
@@ -0,0 +1,116 @@
package org.jeecg.modules.supervision.xispeak.listener;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ExecutionQuery;
import org.flowable.task.service.delegate.DelegateTask;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* AfterImplDeptLeaderApproveListener 测试:将部门领导写入上两级执行实例局部变量(tem_impl_leader)。
*/
@ExtendWith(MockitoExtension.class)
class AfterImplDeptLeaderApproveListenerTest {
@Mock private RuntimeService runtimeService;
@Mock private DelegateTask delegateTask;
@Mock private Execution currentExecution;
@Mock private Execution parentExecution;
@Mock private ExecutionQuery query;
@Mock private ExecutionQuery parentQuery;
private AfterImplDeptLeaderApproveListener listener;
@BeforeEach
void setUp() {
listener = new AfterImplDeptLeaderApproveListener(runtimeService);
}
private void mockExecutionChain(String parentId, String grandParentId) {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query, parentQuery);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn(parentId);
when(parentQuery.executionId(parentId)).thenReturn(parentQuery);
when(parentQuery.singleResult()).thenReturn(parentExecution);
when(parentExecution.getParentId()).thenReturn(grandParentId);
}
@Test
void should_return_when_task_null() {
listener.notify(null);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_execution_id_blank() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_current_execution_not_found() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(null);
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_parent_id_blank() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_write_to_grand_parent_when_both_levels_exist() {
mockExecutionChain("parent-1", "grand-1");
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("grand-1", "tem_impl_leader", "leader1");
}
@Test
void should_write_to_parent_when_no_grand_parent() {
mockExecutionChain("parent-1", null);
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("parent-1", "tem_impl_leader", "leader1");
}
@Test
void should_throw_when_set_variable_fails() {
// 与 fixcontact 不同:xispeak 监听器 catch 后重新抛出 RuntimeException(写入失败即中断流程)
mockExecutionChain("parent-1", "grand-1");
doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", "tem_impl_leader", "leader1");
org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class, () -> listener.notify(delegateTask));
}
}
@@ -0,0 +1,148 @@
package org.jeecg.modules.supervision.xispeak.listener;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* AfterImplDeptLeaderStoreJsonListener 测试。
* 规则:将部门领导写入 approveInfo 对应部门的 approverName(幂等:值相同跳过更新)。
* parseDeptId 语义:取第一个部门 ID(与 FlowVariableSupport.cleanDeptId 不同,保留单独实现)。
*/
@ExtendWith(MockitoExtension.class)
class AfterImplDeptLeaderStoreJsonListenerTest {
@Mock private XiSpeakConfig xiSpeakConfig;
@Mock private IBgXiSpeakService bgXiSpeakService;
@Mock private DelegateTask delegateTask;
private AfterImplDeptLeaderStoreJsonListener listener;
@BeforeEach
void setUp() {
XiSpeakProperties props = new XiSpeakProperties();
props.setBusinessKey("business_id");
props.setImplDeptCollectionUsedKey("impl_dept");
when(xiSpeakConfig.getXiSpeak()).thenReturn(props);
listener = new AfterImplDeptLeaderStoreJsonListener(xiSpeakConfig, bgXiSpeakService);
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_skip_when_business_key_null() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_skip_when_entity_not_found() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_skip_when_dept_var_missing() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(new BgXiSpeak());
when(delegateTask.getVariableLocal("impl_dept")).thenReturn(null);
when(delegateTask.getVariable("impl_dept")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_store_approver_name_when_changed() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[dept-1]");
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
// parseDeptId("[dept-1]"):字符串形式,取第一个元素 → "dept-1"
DeptApproveDetail detail = entity.getApproveInfo().get("dept-1");
assertEquals("leader1", detail.getApproverName());
verify(bgXiSpeakService).updateById(entity);
}
@Test
void should_skip_update_when_approver_name_unchanged() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[dept-1]");
DeptApproveDetail detail = new DeptApproveDetail();
detail.setDeptId("dept-1");
detail.setApproverName("leader1");
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
entity.setApproveInfo(map);
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_parse_first_element_from_list_like_string() {
// 字符串 "[d1,d2]":只取第一个元素 d1(与 cleanDeptId 保留全部不同)
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(delegateTask.getVariableLocal("impl_dept")).thenReturn("[d1,d2]");
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals("leader1", entity.getApproveInfo().get("d1").getApproverName());
assertEquals(1, entity.getApproveInfo().size());
}
@Test
void should_parse_first_element_from_collection() {
when(delegateTask.getAssignee()).thenReturn("leader1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(delegateTask.getVariableLocal("impl_dept")).thenReturn(List.of("dept-a", "dept-b"));
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
when(bgXiSpeakService.getById("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals("leader1", entity.getApproveInfo().get("dept-a").getApproverName());
}
}
@@ -0,0 +1,134 @@
package org.jeecg.modules.supervision.xispeak.listener;
import org.flowable.engine.RuntimeService;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig;
import org.jeecg.modules.supervision.xispeak.config.XiSpeakConfig.XiSpeakProperties;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* AfterImplWorkerApproveListener 经办人会签监听器测试。
* 规则:写子流程局部变量 tem_impl_worker + 记录经办人到 approveInfo 对应部门(去重)。
*/
@ExtendWith(MockitoExtension.class)
class AfterImplWorkerApproveListenerTest {
@Mock private XiSpeakConfig xiSpeakConfig;
@Mock private IBgXiSpeakService bgXiSpeakService;
@Mock private RuntimeService runtimeService;
@Mock private DelegateTask delegateTask;
private AfterImplWorkerApproveListener listener;
@BeforeEach
void setUp() {
XiSpeakProperties props = new XiSpeakProperties();
props.setBusinessKey("business_id");
props.setTemImplWorkerKey("tem_impl_worker");
props.setImplDeptCollectionUsedKey("impl_dept");
// lenientnotify(null) 用例在 NPE 前不会用到 config
lenient().when(xiSpeakConfig.getXiSpeak()).thenReturn(props);
listener = new AfterImplWorkerApproveListener(xiSpeakConfig, bgXiSpeakService, runtimeService);
}
private void mockNormalTask(String deptVar) {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
// lenientgetVariableLocal 在 getByIdForUpdate 之后才调用,entity 为 null 时不会走到(短路径)
lenient().when(delegateTask.getVariableLocal("impl_dept")).thenReturn(deptVar);
}
@Test
void should_throw_npe_when_task_null() {
// 现状行为:该监听器没有 null 防御(与 FixContactWorkerApproveListener 不同),notify(null) 抛 NPE
org.junit.jupiter.api.Assertions.assertThrows(NullPointerException.class, () -> listener.notify(null));
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_skip_record_when_business_key_missing() {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable("business_id")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_skip_record_when_dept_var_missing() {
// deptVar 读取在 getByIdForUpdate 之后:须先返回 entity 才能走到 deptVar 检查
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable("business_id")).thenReturn("biz-1");
when(bgXiSpeakService.getByIdForUpdate("biz-1")).thenReturn(new BgXiSpeak());
when(delegateTask.getVariableLocal("impl_dept")).thenReturn(null);
when(delegateTask.getVariable("impl_dept")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_skip_record_when_entity_not_found() {
mockNormalTask("dept-1");
when(bgXiSpeakService.getByIdForUpdate("biz-1")).thenReturn(null);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
@Test
void should_write_var_and_record_worker() {
mockNormalTask("dept-1");
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
entity.setApproveInfo(new DeptApproveDetailMap());
when(bgXiSpeakService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("exec-1", "tem_impl_worker", "worker1");
assertEquals(List.of("worker1"), entity.getApproveInfo().get("dept-1").getImplUserNameList());
verify(bgXiSpeakService).updateById(entity);
}
@Test
void should_not_update_when_worker_already_in_list() {
mockNormalTask("dept-1");
DeptApproveDetail detail = new DeptApproveDetail();
detail.setImplUserNameList(new ArrayList<>(List.of("worker1")));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
entity.setApproveInfo(map);
when(bgXiSpeakService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
verify(bgXiSpeakService, never()).updateById(any());
}
}
@@ -0,0 +1,116 @@
package org.jeecg.modules.supervision.xispeak.listener;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ExecutionQuery;
import org.flowable.task.service.delegate.DelegateTask;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* AfterImplWorkerTemStoreListener 测试:将部门经办人写入上两级执行实例局部变量(tem_impl_worker)。
*/
@ExtendWith(MockitoExtension.class)
class AfterImplWorkerTemStoreListenerTest {
@Mock private RuntimeService runtimeService;
@Mock private DelegateTask delegateTask;
@Mock private Execution currentExecution;
@Mock private Execution parentExecution;
@Mock private ExecutionQuery query;
@Mock private ExecutionQuery parentQuery;
private AfterImplWorkerTemStoreListener listener;
@BeforeEach
void setUp() {
listener = new AfterImplWorkerTemStoreListener(runtimeService);
}
private void mockExecutionChain(String parentId, String grandParentId) {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query, parentQuery);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn(parentId);
when(parentQuery.executionId(parentId)).thenReturn(parentQuery);
when(parentQuery.singleResult()).thenReturn(parentExecution);
when(parentExecution.getParentId()).thenReturn(grandParentId);
}
@Test
void should_return_when_task_null() {
listener.notify(null);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_execution_id_blank() {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_current_execution_not_found() {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(null);
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_skip_when_parent_id_blank() {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(runtimeService.createExecutionQuery()).thenReturn(query);
when(query.executionId("exec-1")).thenReturn(query);
when(query.singleResult()).thenReturn(currentExecution);
when(currentExecution.getParentId()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(any(), any(), any());
}
@Test
void should_write_to_grand_parent_when_both_levels_exist() {
mockExecutionChain("parent-1", "grand-1");
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("grand-1", "tem_impl_worker", "worker1");
}
@Test
void should_write_to_parent_when_no_grand_parent() {
mockExecutionChain("parent-1", null);
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("parent-1", "tem_impl_worker", "worker1");
}
@Test
void should_throw_when_set_variable_fails() {
// 与 fixcontact 不同:xispeak 监听器 catch 后重新抛出 RuntimeException(写入失败即中断流程)
mockExecutionChain("parent-1", "grand-1");
doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", "tem_impl_worker", "worker1");
org.junit.jupiter.api.Assertions.assertThrows(RuntimeException.class, () -> listener.notify(delegateTask));
}
}
@@ -0,0 +1,62 @@
package org.jeecg.modules.supervision.xispeak.service.impl;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeakFeedback;
import org.jeecg.modules.supervision.xispeak.mapper.BgXiSpeakFeedbackMapper;
import org.jeecg.modules.supervision.xispeak.mapper.BgXiSpeakMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* BgXiSpeakFeedbackServiceImpl 测试。
* 核心:syncFeedbackCounts 统计反馈总数/已闭环数并回写主表 implCount/closedCount。
*/
@ExtendWith(MockitoExtension.class)
class BgXiSpeakFeedbackServiceImplTest {
@Mock private BgXiSpeakFeedbackMapper baseMapper;
@Mock private BgXiSpeakMapper bgXiSpeakMapper;
private BgXiSpeakFeedbackServiceImpl service;
@BeforeEach
void setUp() {
service = new BgXiSpeakFeedbackServiceImpl();
ReflectionTestUtils.setField(service, "baseMapper", baseMapper);
ReflectionTestUtils.setField(service, "bgXiSpeakMapper", bgXiSpeakMapper);
}
@Test
void should_return_when_main_id_empty() {
// oConvertUtils.isEmpty 只认 null/空串(不认空格)
service.syncFeedbackCounts("");
verify(baseMapper, never()).selectCount(any());
verify(bgXiSpeakMapper, never()).updateById(any());
}
@Test
void should_sync_counts_to_main() {
when(baseMapper.selectCount(any())).thenReturn(5L, 2L);
service.syncFeedbackCounts("biz-1");
// 两次 count:全部反馈 + 已闭环
verify(baseMapper, times(2)).selectCount(any());
org.mockito.ArgumentCaptor<BgXiSpeak> captor = org.mockito.ArgumentCaptor.forClass(BgXiSpeak.class);
verify(bgXiSpeakMapper).updateById(captor.capture());
assertEquals("biz-1", captor.getValue().getId());
assertEquals(5, captor.getValue().getImplCount());
assertEquals(2, captor.getValue().getClosedCount());
}
}
@@ -0,0 +1,345 @@
package org.jeecg.modules.supervision.xispeak.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.runtime.ProcessInstanceQuery;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.modules.extbpm.process.entity.ExtActTaskCc;
import org.jeecg.modules.extbpm.process.service.IExtActTaskCcService;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeakFeedback;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.mapper.BgXiSpeakMapper;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakFeedbackService;
import org.jeecg.modules.supervision.xispeak.service.IBgXiSpeakService;
import org.jeecg.modules.taskapprovalopinion.entity.TaskApprovalOpinion;
import org.jeecg.modules.taskapprovalopinion.service.ITaskApprovalOpinionService;
import org.jeecg.modules.tasktask.entity.TaskTask;
import org.jeecg.modules.tasktask.service.impl.TaskTaskServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* BgXiSpeakServiceImpl 测试(树节点 CRUD 规则 + 流程清理 + 审批保存)。
*/
@ExtendWith(MockitoExtension.class)
class BgXiSpeakServiceImplTest {
@Mock private TaskTaskServiceImpl taskTaskServiceImpl;
@Mock private RuntimeService runtimeService;
@Mock private HistoryService historyService;
@Mock private ITaskApprovalOpinionService taskApprovalOpinionService;
@Mock private IExtActTaskCcService extActTaskCcService;
@Mock private IBgXiSpeakFeedbackService bgXiSpeakFeedbackService;
@Mock private BgXiSpeakMapper baseMapper;
@Mock private ProcessInstanceQuery processInstanceQuery;
private BgXiSpeakServiceImpl service;
@BeforeEach
void setUp() {
service = new BgXiSpeakServiceImpl(taskTaskServiceImpl, runtimeService, historyService,
taskApprovalOpinionService, extActTaskCcService, bgXiSpeakFeedbackService);
ReflectionTestUtils.setField(service, "baseMapper", baseMapper);
}
// ==================== addBgXiSpeak 树节点规则 ====================
@Nested
class AddNode {
@Test
void should_set_root_values_when_pid_empty() {
BgXiSpeak node = new BgXiSpeak();
service.addBgXiSpeak(node);
assertEquals(IBgXiSpeakService.NOCHILD, node.getHasChild());
assertEquals(IBgXiSpeakService.ROOT_PID_VALUE, node.getPid());
assertEquals(0, node.getTreeDepth());
assertEquals("1", node.getBpmStatus());
verify(baseMapper).insert(node);
}
@Test
void should_mark_parent_has_child_and_increment_depth() {
BgXiSpeak parent = new BgXiSpeak();
parent.setPid(IBgXiSpeakService.ROOT_PID_VALUE);
parent.setHasChild(IBgXiSpeakService.NOCHILD);
parent.setTreeDepth(1);
when(baseMapper.selectById("p-1")).thenReturn(parent);
BgXiSpeak node = new BgXiSpeak();
node.setPid("p-1");
service.addBgXiSpeak(node);
assertEquals("1", parent.getHasChild());
verify(baseMapper).updateById(parent);
assertEquals(2, node.getTreeDepth());
verify(baseMapper).insert(node);
}
@Test
void should_set_depth_zero_when_parent_missing() {
when(baseMapper.selectById("p-1")).thenReturn(null);
BgXiSpeak node = new BgXiSpeak();
node.setPid("p-1");
service.addBgXiSpeak(node);
assertEquals(0, node.getTreeDepth());
verify(baseMapper).insert(node);
}
}
// ==================== updateBgXiSpeak ====================
@Nested
class UpdateNode {
@Test
void should_throw_when_entity_not_found() {
BgXiSpeak node = new BgXiSpeak();
node.setId("n-1");
when(baseMapper.selectById("n-1")).thenReturn(null);
assertThrows(JeecgBootException.class, () -> service.updateBgXiSpeak(node));
}
@Test
void should_update_new_parent_status_when_pid_changed() {
BgXiSpeak old = new BgXiSpeak();
old.setId("n-1");
old.setPid("old-p");
when(baseMapper.selectById("n-1")).thenReturn(old);
// 旧父节点无其他子节点 → 置 NOCHILD
when(baseMapper.selectCount(any(Wrapper.class))).thenReturn(1L);
BgXiSpeak node = new BgXiSpeak();
node.setId("n-1");
node.setPid("new-p");
service.updateBgXiSpeak(node);
verify(baseMapper).updateTreeNodeStatus("old-p", IBgXiSpeakService.NOCHILD);
verify(baseMapper).updateTreeNodeStatus("new-p", IBgXiSpeakService.HASCHILD);
verify(baseMapper).updateById(node);
}
@Test
void should_not_touch_parents_when_pid_unchanged() {
BgXiSpeak old = new BgXiSpeak();
old.setId("n-1");
old.setPid("p-1");
when(baseMapper.selectById("n-1")).thenReturn(old);
BgXiSpeak node = new BgXiSpeak();
node.setId("n-1");
node.setPid("p-1");
service.updateBgXiSpeak(node);
verify(baseMapper, never()).updateTreeNodeStatus(any(), any());
verify(baseMapper).updateById(node);
}
}
// ==================== deleteBgXiSpeak ====================
@Nested
class DeleteNode {
@Test
void should_delete_single_node_and_cleanup() {
BgXiSpeak node = new BgXiSpeak();
node.setId("n-1");
node.setPid("p-1");
when(baseMapper.selectById("n-1")).thenReturn(node);
when(baseMapper.selectCount(any(Wrapper.class))).thenReturn(0L);
// 无关联 task_taskcleanup 只删反馈子表
when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of());
service.deleteBgXiSpeak("n-1");
verify(baseMapper).updateTreeNodeStatus("p-1", IBgXiSpeakService.NOCHILD);
verify(baseMapper).deleteById("n-1");
verify(bgXiSpeakFeedbackService).remove(any(Wrapper.class));
}
@Test
void should_throw_when_single_node_not_found() {
when(baseMapper.selectById("n-1")).thenReturn(null);
assertThrows(JeecgBootException.class, () -> service.deleteBgXiSpeak("n-1"));
}
@Test
void should_delete_tree_with_children() {
// n-1 有两个子节点 n-2/n-3:递归收集后批量删除
BgXiSpeak n1 = new BgXiSpeak();
n1.setId("n-1");
n1.setPid("p-1");
BgXiSpeak n2 = new BgXiSpeak();
n2.setId("n-2");
n2.setPid("n-1");
BgXiSpeak n3 = new BgXiSpeak();
n3.setId("n-3");
n3.setPid("n-1");
when(baseMapper.selectById("n-1")).thenReturn(n1);
when(baseMapper.selectById("n-2")).thenReturn(n2);
when(baseMapper.selectById("n-3")).thenReturn(n3);
// selectList 按调用序列:查 n-1 子→[n2,n3];递归查 n-2/n-3 子→空(终止递归);查 p-1 其他子→空
when(baseMapper.selectList(any(Wrapper.class)))
.thenReturn(List.of(n2, n3), List.of(), List.of(), List.of());
service.deleteBgXiSpeak("n-1");
verify(baseMapper).deleteBatchIds(any());
}
}
// ==================== withDrawXiSpeak ====================
@Nested
class WithDraw {
@Test
void should_return_false_when_no_related_task() {
when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of());
assertFalse(service.withDrawXiSpeak("biz-1"));
}
@Test
void should_terminate_process_and_cleanup() {
TaskTask tt = new TaskTask();
tt.setBusinessId("biz-1");
tt.setProcessInstId("proc-1");
when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of(tt));
when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery);
when(processInstanceQuery.processInstanceId("proc-1")).thenReturn(processInstanceQuery);
when(processInstanceQuery.count()).thenReturn(1L);
assertTrue(service.withDrawXiSpeak("biz-1"));
verify(runtimeService).deleteProcessInstance(eq("proc-1"), any());
verify(taskApprovalOpinionService).remove(any(Wrapper.class));
verify(extActTaskCcService).remove(any(Wrapper.class));
verify(taskTaskServiceImpl).remove(any(Wrapper.class));
}
}
// ==================== checkAndMarkCompleted ====================
@Nested
class CheckCompleted {
@Test
void should_return_when_process_inst_id_empty() {
service.checkAndMarkCompletedByProcessInstId("");
verify(taskTaskServiceImpl, never()).getOne(any(Wrapper.class), anyBoolean());
}
@Test
void should_not_mark_when_any_process_running() {
when(taskTaskServiceImpl.getOne(any(Wrapper.class), anyBoolean())).thenReturn(taskWithProc("biz-1", "proc-1"));
when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of(taskWithProc("biz-1", "proc-1")));
when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery);
when(processInstanceQuery.processInstanceId("proc-1")).thenReturn(processInstanceQuery);
when(processInstanceQuery.count()).thenReturn(1L);
service.checkAndMarkCompletedByProcessInstId("proc-1");
verify(baseMapper, never()).updateById(any());
}
@Test
void should_mark_completed_when_all_processes_finished() {
when(taskTaskServiceImpl.getOne(any(Wrapper.class), anyBoolean())).thenReturn(taskWithProc("biz-1", "proc-1"));
when(taskTaskServiceImpl.list(any(Wrapper.class))).thenReturn(List.of(taskWithProc("biz-1", "proc-1")));
when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery);
when(processInstanceQuery.processInstanceId("proc-1")).thenReturn(processInstanceQuery);
when(processInstanceQuery.count()).thenReturn(0L);
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
when(baseMapper.selectById("biz-1")).thenReturn(entity);
service.checkAndMarkCompletedByProcessInstId("proc-1");
assertEquals(1, entity.getCompletionStatus());
verify(baseMapper).updateById(entity);
}
private TaskTask taskWithProc(String biz, String proc) {
TaskTask tt = new TaskTask();
tt.setBusinessId(biz);
tt.setProcessInstId(proc);
return tt;
}
}
// ==================== saveSubApprove ====================
@Nested
class SaveSubApprove {
@Test
void should_throw_when_main_not_exists() {
when(baseMapper.selectById("biz-1")).thenReturn(null);
assertThrows(JeecgBootException.class, () -> service.saveSubApprove("biz-1", "dept-1", 1, "w1", null));
}
@Test
void should_store_impl_worker_when_is_end_one() {
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
when(baseMapper.selectById("biz-1")).thenReturn(entity);
service.saveSubApprove("biz-1", "dept-1", 1, "w1", List.of("w2"));
DeptApproveDetail detail = entity.getApproveInfo().get("dept-1");
assertEquals("w1", detail.getImplWorker());
assertNull(detail.getImplUserNameList());
verify(baseMapper).updateById(entity);
}
@Test
void should_store_impl_user_name_list_when_is_end_zero() {
BgXiSpeak entity = new BgXiSpeak();
entity.setId("biz-1");
when(baseMapper.selectById("biz-1")).thenReturn(entity);
service.saveSubApprove("biz-1", "dept-1", 0, "w1", List.of("w2", "w3"));
DeptApproveDetail detail = entity.getApproveInfo().get("dept-1");
assertNull(detail.getImplWorker());
assertEquals(List.of("w2", "w3"), detail.getImplUserNameList());
verify(baseMapper).updateById(entity);
}
}
// ==================== saveBpmFormAndSyncVariable ====================
@Nested
class SaveBpmForm {
@Test
void should_update_form_and_sync_variable() {
BgXiSpeak form = new BgXiSpeak();
service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}");
verify(baseMapper).updateById(form);
verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}");
}
}
}