Compare commits

..
10 Commits
Author SHA1 Message Date
wsm 4d006e16d1 !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 规则锁定)
2026-08-19 08:55:37 +00:00
wsm 41546cfc17 !134 fix(supervision): 修正 InspectCloseoutFlowTest 角色参数匹配实际调用
* fix(supervision): 修正 InspectCloseoutFlowTest 角色参数匹配实际调用
* test(fixcontact): 新增流程表达式/监听器/Service 单测,fixcontact 可测代码接近全覆盖
* refactor(fixcontact): 提炼上两级解析与部门ID清洗纯函数,监听器依赖注入化
* chore(bpm): 安装 JaCoCo 覆盖率插件并修复构建链(补齐 base-core 依赖、排除环境依赖测试)
2026-08-19 00:44:24 +00:00
wsm 4e9a57651b !133 fix(supervision): 修正 InspectCloseoutFlowTest 角色参数匹配实际调用
* fix(supervision): 修正 InspectCloseoutFlowTest 角色参数匹配实际调用
* test(fixcontact): 新增流程表达式/监听器/Service 单测,fixcontact 可测代码接近全覆盖
* refactor(fixcontact): 提炼上两级解析与部门ID清洗纯函数,监听器依赖注入化
* chore(bpm): 安装 JaCoCo 覆盖率插件并修复构建链(补齐 base-core 依赖、排除环境依赖测试)
2026-08-18 08:04:26 +00:00
wsmandGitee aea4a0e312 !132 chore(supervision): 删除 src/test 下误放的代码生成器脚手架文件
Merge pull request !132 from wsm/chore/remove_test
2026-08-17 08:47:33 +00:00
wsm 5367a25130 chore(supervision): 删除 src/test 下误放的代码生成器脚手架文件 2026-08-17 16:46:54 +08:00
wsm 1662936f7e !131 chore: gitignore 忽略 weekly-summary 目录
* chore: gitignore 忽略 weekly-summary 目录
* feat(bpm): 流程回归支持驳回路径驱动
* feat(bpm): 4个流程happy path回归测试全部跑通
* feat(bpm): 新增流程回归测试框架(连运行后端全路径驱动+自动清理)
2026-08-17 07:28:24 +00:00
wsm d66788eb94 !130 chore: gitignore 忽略 weekly-summary 目录
* chore: gitignore 忽略 weekly-summary 目录
* feat(bpm): 流程回归支持驳回路径驱动
* feat(bpm): 4个流程happy path回归测试全部跑通
* feat(bpm): 新增流程回归测试框架(连运行后端全路径驱动+自动清理)
2026-08-17 07:24:40 +00:00
wsm a5e4eebc71 !129 feat(sysuser): 新增按角色查询用户接口并使用优化版JOIN查询
* feat(sysuser): 新增按角色查询用户接口并使用优化版JOIN查询
* fix(bgpartymatter): 移除后端自动生成业务序号逻辑
2026-08-14 07:15:16 +00:00
wsmandGitee 06e253b18d !128 fix(dq): 反馈记录补齐进展状态字典与纪检反馈字段
Merge pull request !128 from wsm/fix/inspect_feedback
2026-08-13 07:12:30 +00:00
wsmandClaude Opus 4.7 cfcc0e016c fix(dq): 反馈记录补齐进展状态字典与纪检反馈字段
- bpmStatus 补 @Dict(feedback_status) 注解,修复进展状态列表不显示
- 新增检查情况/监督意见/措施数量字段及表结构增量脚本,修复纪检字段保存丢失

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-13 15:10:33 +08:00
96 changed files with 6391 additions and 1627 deletions
+1
View File
@@ -3,6 +3,7 @@
*.iml
rebel.xml
localDocs
weekly-summary
## backend
**/target
@@ -31,6 +31,11 @@
</repositories>
<dependencies>
<!-- 基础核心(代码直接使用 base-core 的 Result 等) -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
<!-- system单体 api-->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
@@ -189,4 +194,20 @@
</dependencies>
<build>
<plugins>
<!-- 排除 airag/test 包:历史遗留的调试型测试(TestFlows 连本地 7008 AI 服务、TestFileParse 依赖测试文件),
不属于可自动化单测,避免 mvn test 全量时因环境缺失而失败;IDE 中仍可手动单独运行 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/airag/test/**</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -57,6 +57,11 @@
</repositories>
<dependencies>
<!-- 基础核心(代码直接使用 base-core 的 Result/SymbolConstant/QueryGenerator 等) -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
<!-- 单体system api -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
+16
View File
@@ -30,5 +30,21 @@
<artifactId>jeecg-system-biz</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<!-- flowregression 是 E2E 回归测试(HTTP 驱动真实后端 localhost:8080),
后端未启动时必然失败,从全量 mvn test 排除;需要时启动后端或 -Dtest=*RegressionTest 单独跑 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/flowregression/**</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -49,7 +49,6 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -155,7 +154,6 @@ public class BgPartymatterController extends JeecgController<BgPartymatter, IBgP
bgPartymatter.setBpmStatus("1");
}
fillDefaultMeetingTypeForCreate(bgPartymatter);
fillGeneratedNumber(bgPartymatter);
bgPartymatterService.save(bgPartymatter);
return Result.OK("添加成功!");
}
@@ -172,7 +170,6 @@ public class BgPartymatterController extends JeecgController<BgPartymatter, IBgP
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody BgPartymatter bgPartymatter) {
fillMeetingTypeForUpdate(bgPartymatter);
fillGeneratedNumber(bgPartymatter);
bgPartymatterService.updateById(bgPartymatter);
return Result.OK("编辑成功!");
}
@@ -195,7 +192,6 @@ public class BgPartymatterController extends JeecgController<BgPartymatter, IBgP
}
String varField = oConvertUtils.isEmpty(dto.getVarField()) ? "json_data" : dto.getVarField();
fillMeetingTypeForUpdate(dto.getFormData());
fillGeneratedNumber(dto.getFormData());
bgPartymatterService.saveBpmFormAndSyncVariable(dto.getFormData(), dto.getProcessInstanceId(), varField,
JSONObject.toJSONString(dto.getFormData()));
return Result.OK("保存成功");
@@ -227,18 +223,6 @@ public class BgPartymatterController extends JeecgController<BgPartymatter, IBgP
bgPartymatter.setMeetingType(BgPartymatterConstant.MeetingType.PARTY);
}
/**
* 根据会议时间和议题序号生成业务序号,规则:yyyyMMdd-议题序号。
* 前端会即时回显,后端保存前再次生成,保证接口调用和流程保存数据一致。
*/
private void fillGeneratedNumber(BgPartymatter bgPartymatter) {
if (bgPartymatter == null || bgPartymatter.getMatterTime() == null || bgPartymatter.getItemNumber() == null) {
return;
}
String dateText = new SimpleDateFormat("yyyyMMdd").format(bgPartymatter.getMatterTime());
bgPartymatter.setNumber(dateText + "-" + bgPartymatter.getItemNumber());
}
/**
* 通过id删除
*
@@ -0,0 +1,33 @@
package org.jeecg.modules.supervision.common.flow;
import org.apache.commons.lang3.StringUtils;
/**
* Flowable 流程通用支撑:执行实例层级解析(无外部依赖,可独立测试)。
*
* 规则(提炼自 fixcontact/xispeak 多处重复内联逻辑):
* 部门领导/经办人审批后,将审批人写入"上两级"执行实例;
* 存在第二层父级存第二层(大会签容器/外层作用域),否则存第一层。
*/
public final class FlowExecutionSupport {
private FlowExecutionSupport() {
}
/**
* 解析审批人应写入的目标执行实例 ID。
*
* @param parentId 第一层父级执行实例 ID
* @param grandParentId 第二层父级执行实例 ID(可能为空)
* @return 有第二层返回 grandParentId;否则返回 parentIdparentId 为空返回 null(调用方跳过)
*/
public static String resolveTargetExecutionId(String parentId, String grandParentId) {
if (StringUtils.isBlank(parentId)) {
return null;
}
if (StringUtils.isNotBlank(grandParentId)) {
return grandParentId;
}
return parentId;
}
}
@@ -0,0 +1,43 @@
package org.jeecg.modules.supervision.common.flow;
import org.apache.commons.lang.StringUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* Flowable 流程通用支撑:流程变量字符串处理(无外部依赖,可独立测试)。
*
* 规则(提炼自 fixcontact/xispeak 多处重复内联逻辑):
* - splitToTrimmedList:逗号分割 → trim → 过滤空白元素
* - countTokens:分割后有效元素个数
* - cleanDeptId:流程变量部门 ID 清洗(去 [] 与首尾空白)
*/
public final class FlowVariableSupport {
private FlowVariableSupport() {
}
public static List<String> splitToTrimmedList(String raw) {
if (StringUtils.isBlank(raw)) {
return Collections.emptyList();
}
return Arrays.stream(raw.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
}
public static int countTokens(String raw) {
return splitToTrimmedList(raw).size();
}
public static String cleanDeptId(Object raw) {
if (raw == null) {
return null;
}
return raw.toString().replace("[", "").replace("]", "").trim();
}
}
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableLogic;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
@@ -78,8 +79,21 @@ public class DqInspectProgress implements Serializable {
private java.lang.String workProgress;
/**进展状态*/
@Excel(name = "进展状态", width = 15, dicCode = "feedback_status")
@Dict(dicCode = "feedback_status")
@Schema(description = "进展状态")
private java.lang.String bpmStatus;
/**检查情况*/
@Excel(name = "检查情况", width = 15)
@Schema(description = "检查情况")
private java.lang.String inspectionStatus;
/**监督意见*/
@Excel(name = "监督意见", width = 15)
@Schema(description = "监督意见")
private java.lang.String supervisoryOpinion;
/**措施数量*/
@Excel(name = "措施数量", width = 15)
@Schema(description = "措施数量")
private java.lang.Integer measureCount;
/**删除标识*/
@Excel(name = "删除标识", width = 15)
@Schema(description = "删除标识")
@@ -8,6 +8,7 @@ import org.apache.commons.lang.StringUtils;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
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.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730;
@@ -258,7 +259,7 @@ public class FixContactFlow {
execution.getProcessInstanceId());
return null;
}
return deptVar.toString().replace("[", "").replace("]", "").trim();
return FlowVariableSupport.cleanDeptId(deptVar);
}
// ${fixContactFlow.getCurrentDeptLeaderList(execution, deptId)}
@@ -357,14 +358,7 @@ public class FixContactFlow {
if (value == null) {
return Collections.emptyList();
}
String str = value.toString();
if (StringUtils.isBlank(str)) {
return Collections.emptyList();
}
return Arrays.stream(str.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
return FlowVariableSupport.splitToTrimmedList(value.toString());
}
// ${fixContactFlow.getVarAsListCount(execution, varName)}
@@ -374,13 +368,7 @@ public class FixContactFlow {
// ${fixContactFlow.getListSize(data)}
public int getListSize(String data) {
if (StringUtils.isEmpty(data)) {
return 0;
}
return (int) Arrays.stream(data.split(","))
.map(String::trim)
.filter(StringUtils::isNotEmpty)
.count();
return FlowVariableSupport.countTokens(data);
}
// ${fixContactFlow.getIsNeedLeaderApprove(execution)}
@@ -619,10 +607,7 @@ public class FixContactFlow {
if (StringUtils.isBlank(rawValue)) {
return Collections.emptyList();
}
List<String> result = Arrays.stream(rawValue.split(","))
.map(String::trim)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
List<String> result = FlowVariableSupport.splitToTrimmedList(rawValue);
log.info("【定点联系-流程表达式】{} 解析结果: {}", logLabel, result);
return result;
} catch (Exception e) {
@@ -1,13 +1,15 @@
package org.jeecg.modules.supervision.fixcontact.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.TaskListener;
import org.flowable.engine.runtime.Execution;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.modules.supervision.common.flow.FlowExecutionSupport;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@@ -16,16 +18,13 @@ import org.springframework.stereotype.Component;
*/
@Slf4j
@Component("FixContactDeptLeaderApproveListener")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class FixContactDeptLeaderApproveListener implements TaskListener {
private static final long serialVersionUID = 1L;
private static final String VARIABLE_NAME = FixContactConstant.FlowVarName.TEM_IMPL_LEADER;
private static RuntimeService runtimeService;
static {
runtimeService = SpringContextUtils.getBean(RuntimeService.class);
}
private final RuntimeService runtimeService;
@Override
public void notify(DelegateTask delegateTask) {
@@ -64,9 +63,8 @@ public class FixContactDeptLeaderApproveListener implements TaskListener {
if (parentExecution == null) {
return;
}
String grandParentId = parentExecution.getParentId();
// 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层
String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId;
// 有第二层父级则存入第二层(大会签容器/外层作用域),否则存入第一层(规则见 FlowExecutionSupport
String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, parentExecution.getParentId());
try {
runtimeService.setVariableLocal(targetId, VARIABLE_NAME, assignee);
log.info("【定点联系-部门领导审批监听器】已存入上两级局部变量, targetExecutionId: {}, variableName: {}, assignee: {}",
@@ -8,6 +8,7 @@ import org.flowable.engine.delegate.TaskListener;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.common.flow.FlowVariableSupport;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
@@ -74,7 +75,7 @@ public class FixContactWorkerApproveListener implements TaskListener {
log.warn("【定点联系-经办人会签监听器】未获取到子流程当前部门ID,跳过记录经办人, taskId: {}", delegateTask.getId());
return;
}
String deptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim();
String deptId = FlowVariableSupport.cleanDeptId(rawDeptVar);
FixedContact20260730 fixedContact = fixedContact20260730Service.getByIdForUpdate(rawBusinessKey.toString());
if (fixedContact == null) {
@@ -161,7 +161,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
throw new IllegalStateException("生成提案编号失败,请稍后重试");
}
improvementProposal.setProposalNo(formatProposalNo(improvementProposal.getProposalDeptName(), proposalYear, sequence));
improvementProposal.setProposalNo(ImprovementProposalSupport.formatProposalNo(improvementProposal.getProposalDeptName(), proposalYear, sequence));
saveMain(improvementProposal, null, null);
}
@@ -287,9 +287,6 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
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
@Transactional(rollbackFor = Exception.class)
@@ -382,7 +379,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
public void saveInitialReview(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
validateAward(action.getInitialAward());
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();
update.setId(proposal.getId());
update.setInitialAward(action.getInitialAward());
@@ -416,8 +413,8 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
}
// 改善效果由四个子项在服务端计算,避免前端篡改汇总得分或加权总分。
BigDecimal effectScore = calculateEffectScore(scoreAction);
BigDecimal totalScore = calculateTotalScore(scoreAction, effectScore);
BigDecimal effectScore = ImprovementProposalSupport.calculateEffectScore(scoreAction);
BigDecimal totalScore = ImprovementProposalSupport.calculateTotalScore(scoreAction, effectScore);
if (existingScore == null) {
ImprovementDeptScore newScore = new ImprovementDeptScore();
newScore.setMainId(lockedProposal.getId());
@@ -487,7 +484,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
validateAward(action.getLeaderFinalAward());
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_LEADER_AWARD);
String awardBase = resolveLeaderAwardBase(proposal);
if (awardRank(action.getLeaderFinalAward()) < awardRank(awardBase)) {
if (ImprovementProposalSupport.awardRank(action.getLeaderFinalAward()) < ImprovementProposalSupport.awardRank(awardBase)) {
throw new IllegalStateException("负责人确认奖项只能选择当前档位或降档奖项");
}
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) {
target.setSafetyScore(source.getSafetyScore());
@@ -795,7 +779,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
validateScoreValue(score.getPromotionScore(), "可推广度");
validateScoreValue(score.getOriginalityScore(), "独创性");
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) {
throw new IllegalStateException("请先保存完整个人评分");
}
@@ -820,22 +804,10 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
ImprovementProposal summary = new ImprovementProposal();
summary.setId(proposal.getId());
summary.setDeptAvgScore(averageScore);
summary.setSuggestedAward(resolveSuggestedAward(averageScore));
summary.setSuggestedAward(ImprovementProposalSupport.resolveSuggestedAward(averageScore));
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) {
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) {
if (!isValidAward(award)) {
if (!ImprovementProposalSupport.isValidAward(award)) {
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) {
String awardBase = "1".equals(proposal.getNeedDeptScore()) ? proposal.getSuggestedAward() : proposal.getInitialAward();
if (!isValidAward(awardBase)) {
if (!ImprovementProposalSupport.isValidAward(awardBase)) {
throw new IllegalStateException("部门评议建议奖项尚未生成,不能确认负责人奖项");
}
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.setPriority(req.getPriority());
entity.setType(req.getType() != null ? req.getType() : "1");
entity.setPid(normalizePid(req.getPid()));
entity.setPid(TaskListDetailSupport.normalizePid(req.getPid()));
entity.setTaskStatus(0);
entity.setSubTaskCount(0);
entity.setCompletedSubTaskCount(0);
@@ -98,10 +98,10 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
}
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());
} else {
Integer maxSort = taskListDetialMapper.getMaxSortOrder(req.getMainId(), normalizePid(req.getPid()));
Integer maxSort = taskListDetialMapper.getMaxSortOrder(req.getMainId(), TaskListDetailSupport.normalizePid(req.getPid()));
entity.setSortOrder(maxSort + 1);
}
@@ -366,8 +366,8 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
throw new RuntimeException("可阅读者无法移动任务");
}
String oldPid = normalizePid(task.getPid());
String newPid = normalizePid(req.getTargetPid());
String oldPid = TaskListDetailSupport.normalizePid(task.getPid());
String newPid = TaskListDetailSupport.normalizePid(req.getTargetPid());
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;
}
private String normalizePid(String pid) {
return oConvertUtils.isNotEmpty(pid) ? pid : null;
}
private void fillCreateByName(List<TaskListDetial> list) {
if (list == null || list.isEmpty()) {
@@ -568,9 +565,9 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
if (oConvertUtils.isNotEmpty(task.getCreateBy())) {
allUsernames.add(task.getCreateBy());
}
collectUserIds(task.getAssigneeId(), allUserIds);
collectUserIds(task.getParticipantId(), allUserIds);
collectUserIds(task.getFollowersId(), allUserIds);
TaskListDetailSupport.collectUserIds(task.getAssigneeId(), allUserIds);
TaskListDetailSupport.collectUserIds(task.getParticipantId(), allUserIds);
TaskListDetailSupport.collectUserIds(task.getFollowersId(), allUserIds);
}
// 2. 批量查询,构建映射表
@@ -603,48 +600,18 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
}
}
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())) {
task.setParticipantName(buildNamesFromMap(task.getParticipantId(), idToRealname));
task.setParticipantName(TaskListDetailSupport.buildNamesFromMap(task.getParticipantId(), idToRealname));
}
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) {
if (oConvertUtils.isEmpty(ids)) {
@@ -664,26 +631,6 @@ public class TaskListDetialServiceImpl extends ServiceImpl<TaskListDetialMapper,
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) {
if (oConvertUtils.isEmpty(userIds)) return userIds;
@@ -233,7 +233,7 @@ public class TaskListServiceImpl extends ServiceImpl<TaskListMapper, TaskList> i
throw new RuntimeException("分组不支持移动操作");
}
String oldPid = normalizePid(favorite.getPid());
String oldPid = TaskListDetailSupport.normalizePid(favorite.getPid());
String newPid = 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())) {
throw new RuntimeException("目标分组不存在或无权限");
}
newPid = normalizePid(req.getTargetGroupId());
newPid = TaskListDetailSupport.normalizePid(req.getTargetGroupId());
} else {
newPid = "";
}
@@ -700,9 +700,6 @@ public class TaskListServiceImpl extends ServiceImpl<TaskListMapper, TaskList> i
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) {
if (lists == null || lists.isEmpty()) {
@@ -10,6 +10,7 @@ import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.jeecg.modules.supervision.common.OrgConfig;
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.system.api.ISysBaseAPI;
import org.jeecg.modules.supervision.xispeak.entity.BgXiSpeak;
@@ -198,10 +199,7 @@ public class XiSpeakFeedbackFlow {
}
// 将 "张三, 李四 " 转换为 ["张三", "李四"]
return Arrays.stream(implDeptWorker.split(","))
.map(String::trim) // 去掉前后空格
.filter(StringUtils::isNotBlank) // 过滤掉空字符串(防止出现 "张三,,李四"
.collect(Collectors.toList());
return FlowVariableSupport.splitToTrimmedList(implDeptWorker);
}
/**
@@ -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.DeptApproveDetail;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -125,16 +126,7 @@ public class XiSpeakFlow {
String rawValue = data.getString(codeName);
// 4. 转换逗号分隔的字符串为 List
List<String> resultList;
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());
}
List<String> resultList = FlowVariableSupport.splitToTrimmedList(rawValue);
log.info("{} 业务解析结果: {}", logLabel, resultList);
return resultList;
@@ -285,7 +277,7 @@ public class XiSpeakFlow {
if (org.apache.commons.lang.StringUtils.isBlank(businessKey) || rawDeptVar == null) {
return null;
}
String deptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim();
String deptId = FlowVariableSupport.cleanDeptId(rawDeptVar);
BgXiSpeak xiSpeak = bgXiSpeakService.getById(businessKey);
if (xiSpeak == null || xiSpeak.getApproveInfo() == null) {
return null;
@@ -340,13 +332,7 @@ public class XiSpeakFlow {
}
public int getListSize(String jsonData){
if (org.apache.commons.lang3.StringUtils.isEmpty(jsonData)) {
return 0;
}
return Math.toIntExact(Arrays.stream(jsonData.split(","))
.map(String::trim)
.filter(org.apache.commons.lang3.StringUtils::isNotEmpty)
.count());
return FlowVariableSupport.countTokens(jsonData);
}
// List<String> getImplDeptWorker(String JG_LOCAL_PROCESS_ID,String deptId){
@@ -421,10 +407,7 @@ public class XiSpeakFlow {
value = execution.getVariable(variableName);
}
if (value != null) {
return Arrays.stream(String.valueOf(value).split(SymbolConstant.COMMA))
.map(String::trim)
.filter(org.apache.commons.lang.StringUtils::isNotBlank)
.collect(Collectors.toList());
return FlowVariableSupport.splitToTrimmedList(String.valueOf(value));
}
} catch (Exception 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.service.ITaskTaskService;
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.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@@ -75,9 +76,8 @@ public class AfterBgLeaderApproveListener implements TaskListener {
// 4. 获取第二层父级 ID (大会签容器/外层作用域)
String grandParentId = parentExecution.getParentId();
// 5. 确定最终存储目标
// 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层
String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId;
// 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层)
String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId);
try {
// 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.service.ITaskTaskService;
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.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@@ -35,11 +36,7 @@ public class AfterImplDeptLeaderApproveListener implements TaskListener {
private static final long serialVersionUID = 1L;
private static final String VARIABLE_NAME = "tem_impl_leader";
private static RuntimeService runtimeService;
static {
runtimeService = SpringContextUtils.getBean(RuntimeService.class);
}
private final RuntimeService runtimeService;
@Override
public void notify(DelegateTask delegateTask) {
@@ -75,9 +72,8 @@ public class AfterImplDeptLeaderApproveListener implements TaskListener {
// 4. 获取第二层父级 ID (大会签容器/外层作用域)
String grandParentId = parentExecution.getParentId();
// 5. 确定最终存储目标
// 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层
String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId;
// 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层)
String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId);
try {
// 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.service.ITaskTaskService;
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.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@@ -74,7 +75,7 @@ public class AfterImplWorkerApproveListener implements TaskListener {
return;
}
String cleanDeptId = rawDeptVar.toString().replace("[", "").replace("]", "").trim();
String cleanDeptId = FlowVariableSupport.cleanDeptId(rawDeptVar);
DeptApproveDetailMap approveMap = xiSpeak.getApproveInfo();
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.service.ITaskTaskService;
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.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@@ -35,11 +36,7 @@ public class AfterImplWorkerTemStoreListener implements TaskListener {
private static final long serialVersionUID = 1L;
private static final String VARIABLE_NAME = "tem_impl_worker";
private static RuntimeService runtimeService;
static {
runtimeService = SpringContextUtils.getBean(RuntimeService.class);
}
private final RuntimeService runtimeService;
@Override
public void notify(DelegateTask delegateTask) {
@@ -75,9 +72,8 @@ public class AfterImplWorkerTemStoreListener implements TaskListener {
// 4. 获取第二层父级 ID (大会签容器/外层作用域)
String grandParentId = parentExecution.getParentId();
// 5. 确定最终存储目标
// 如果有第二层父级,则存入第二层;如果没有,则退而求其次存入第一层
String targetId = StringUtils.isNotBlank(grandParentId) ? grandParentId : parentId;
// 5. 确定最终存储目标(有第二层父级则存入第二层,否则存入第一层)
String targetId = FlowExecutionSupport.resolveTargetExecutionId(parentId, grandParentId);
try {
// 6. 执行存储
@@ -9,6 +9,7 @@ import org.flowable.engine.delegate.ExecutionListener;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals;
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.stereotype.Component;
@@ -65,10 +66,7 @@ public class AfterSDWApproveHqListener implements ExecutionListener {
// 3. 只有当字段存在且内容不为空时,才进行解析
if (oConvertUtils.isNotEmpty(jsonStr)) {
List<String> deptIds = Arrays.stream(jsonStr.split(","))
.map(String::trim) // 去掉可能存在的空格
.filter(s -> !s.isEmpty()) // 过滤掉空字符串
.collect(Collectors.toList());
List<String> deptIds = FlowVariableSupport.splitToTrimmedList(jsonStr);
if (deptIds != null && !deptIds.isEmpty()) {
// 4. 将提取出的 List 存入子流程变量(供多实例 Collection 使用)
@@ -1,270 +0,0 @@
package org.jeecg.modules.test.controller;
import java.io.UnsupportedEncodingException;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.jeecgframework.poi.excel.ExcelImportUtil;
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
import org.jeecgframework.poi.excel.entity.ExportParams;
import org.jeecgframework.poi.excel.entity.ImportParams;
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
import org.jeecg.common.system.vo.LoginUser;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.query.QueryRuleEnum;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.test.entity.TestSonTable;
import org.jeecg.modules.test.entity.TestMainTable;
import org.jeecg.modules.test.vo.TestMainTablePage;
import org.jeecg.modules.test.service.ITestMainTableService;
import org.jeecg.modules.test.service.ITestSonTableService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import com.alibaba.fastjson.JSON;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.apache.shiro.authz.annotation.RequiresPermissions;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
@Tag(name="test")
@RestController
@RequestMapping("/test/testMainTable")
@Slf4j
public class TestMainTableController {
@Autowired
private ITestMainTableService testMainTableService;
@Autowired
private ITestSonTableService testSonTableService;
/**
* 分页列表查询
*
* @param testMainTable
* @param pageNo
* @param pageSize
* @param req
* @return
*/
//@AutoLog(value = "test-分页列表查询")
@Operation(summary="test-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<TestMainTable>> queryPageList(TestMainTable testMainTable,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<TestMainTable> queryWrapper = QueryGenerator.initQueryWrapper(testMainTable, req.getParameterMap());
Page<TestMainTable> page = new Page<TestMainTable>(pageNo, pageSize);
IPage<TestMainTable> pageList = testMainTableService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param testMainTablePage
* @return
*/
@AutoLog(value = "test-添加")
@Operation(summary="test-添加")
@RequiresPermissions("test:test_main_table:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody TestMainTablePage testMainTablePage) {
TestMainTable testMainTable = new TestMainTable();
BeanUtils.copyProperties(testMainTablePage, testMainTable);
testMainTableService.saveMain(testMainTable, testMainTablePage.getTestSonTableList());
return Result.OK("添加成功!");
}
/**
* 编辑
*
* @param testMainTablePage
* @return
*/
@AutoLog(value = "test-编辑")
@Operation(summary="test-编辑")
@RequiresPermissions("test:test_main_table:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
public Result<String> edit(@RequestBody TestMainTablePage testMainTablePage) {
TestMainTable testMainTable = new TestMainTable();
BeanUtils.copyProperties(testMainTablePage, testMainTable);
TestMainTable testMainTableEntity = testMainTableService.getById(testMainTable.getId());
if(testMainTableEntity==null) {
return Result.error("未找到对应数据");
}
testMainTableService.updateMain(testMainTable, testMainTablePage.getTestSonTableList());
return Result.OK("编辑成功!");
}
/**
* 通过id删除
*
* @param id
* @return
*/
@AutoLog(value = "test-通过id删除")
@Operation(summary="test-通过id删除")
@RequiresPermissions("test:test_main_table:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
testMainTableService.delMain(id);
return Result.OK("删除成功!");
}
/**
* 批量删除
*
* @param ids
* @return
*/
@AutoLog(value = "test-批量删除")
@Operation(summary="test-批量删除")
@RequiresPermissions("test:test_main_table:deleteBatch")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
this.testMainTableService.delBatchMain(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "test-通过id查询")
@Operation(summary="test-通过id查询")
@GetMapping(value = "/queryById")
public Result<TestMainTable> queryById(@RequestParam(name="id",required=true) String id) {
TestMainTable testMainTable = testMainTableService.getById(id);
if(testMainTable==null) {
return Result.error("未找到对应数据");
}
return Result.OK(testMainTable);
}
/**
* 通过id查询
*
* @param id
* @return
*/
//@AutoLog(value = "test通过主表ID查询")
@Operation(summary="test主表ID查询")
@GetMapping(value = "/queryTestSonTableByMainId")
public Result<List<TestSonTable>> queryTestSonTableListByMainId(@RequestParam(name="id",required=true) String id) {
List<TestSonTable> testSonTableList = testSonTableService.selectByMainId(id);
return Result.OK(testSonTableList);
}
/**
* 导出excel
*
* @param request
* @param testMainTable
*/
@RequiresPermissions("test:test_main_table:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, TestMainTable testMainTable) {
// Step.1 组装查询条件查询数据
QueryWrapper<TestMainTable> queryWrapper = QueryGenerator.initQueryWrapper(testMainTable, request.getParameterMap());
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
//配置选中数据查询条件
String selections = request.getParameter("selections");
if(oConvertUtils.isNotEmpty(selections)) {
List<String> selectionList = Arrays.asList(selections.split(","));
queryWrapper.in("id",selectionList);
}
//Step.2 获取导出数据
List<TestMainTable> testMainTableList = testMainTableService.list(queryWrapper);
// Step.3 组装pageList
List<TestMainTablePage> pageList = new ArrayList<TestMainTablePage>();
for (TestMainTable main : testMainTableList) {
TestMainTablePage vo = new TestMainTablePage();
BeanUtils.copyProperties(main, vo);
List<TestSonTable> testSonTableList = testSonTableService.selectByMainId(main.getId());
vo.setTestSonTableList(testSonTableList);
pageList.add(vo);
}
// Step.4 AutoPoi 导出Excel
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
mv.addObject(NormalExcelConstants.FILE_NAME, "test列表");
mv.addObject(NormalExcelConstants.CLASS, TestMainTablePage.class);
mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("test数据", "导出人:"+sysUser.getRealname(), "test"));
mv.addObject(NormalExcelConstants.DATA_LIST, pageList);
return mv;
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequiresPermissions("test:test_main_table:importExcel")
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
// 获取上传文件对象
MultipartFile file = entity.getValue();
ImportParams params = new ImportParams();
params.setTitleRows(2);
params.setHeadRows(1);
params.setNeedSave(true);
try {
List<TestMainTablePage> list = ExcelImportUtil.importExcel(file.getInputStream(), TestMainTablePage.class, params);
for (TestMainTablePage page : list) {
TestMainTable po = new TestMainTable();
BeanUtils.copyProperties(page, po);
testMainTableService.saveMain(po, page.getTestSonTableList());
}
return Result.OK("文件导入成功!数据行数:" + list.size());
} catch (Exception e) {
log.error(e.getMessage(),e);
return Result.error("文件导入失败:"+e.getMessage());
} finally {
try {
file.getInputStream().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return Result.OK("文件导入失败!");
}
}
@@ -1,65 +0,0 @@
package org.jeecg.modules.test.entity;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableLogic;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.jeecg.common.aspect.annotation.Dict;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
@Schema(description="test")
@Data
@TableName("test_main_table")
public class TestMainTable implements Serializable {
private static final long serialVersionUID = 1L;
/**主键*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private java.lang.String id;
/**创建人*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**创建日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private java.util.Date createTime;
/**更新人*/
@Schema(description = "更新人")
private java.lang.String updateBy;
/**更新日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private java.util.Date updateTime;
/**所属部门*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
/**a字段*/
@Excel(name = "a字段", width = 15)
@Schema(description = "a字段")
private java.lang.String fieldA;
/**b字段*/
@Excel(name = "b字段", width = 15)
@Schema(description = "b字段")
private java.lang.String fieldB;
@TableLogic(value = "0", delval = "1")
private int delFlag = 0;
}
@@ -1,62 +0,0 @@
package org.jeecg.modules.test.entity;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableLogic;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import org.jeecgframework.poi.excel.annotation.Excel;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.UnsupportedEncodingException;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
@Schema(description="test")
@Data
@TableName("test_son_table")
public class TestSonTable implements Serializable {
private static final long serialVersionUID = 1L;
/**主键*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private java.lang.String id;
/**创建人*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**创建日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private java.util.Date createTime;
/**更新人*/
@Schema(description = "更新人")
private java.lang.String updateBy;
/**更新日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private java.util.Date updateTime;
/**所属部门*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
/**字段c*/
@Excel(name = "字段c", width = 15)
@Schema(description = "字段c")
private java.lang.String fieldC;
/**主表id*/
@Schema(description = "主表id")
private java.lang.String mainTableId;
@TableLogic(value = "0", delval = "1")
private int delFlag = 0;
}
@@ -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));
}
}
@@ -0,0 +1,58 @@
package org.jeecg.modules.supervision.common.flow;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* FlowExecutionSupport.resolveTargetExecutionId 规则测试(TDD 红-绿-重构 演示)。
*
* 业务规则(来自 fixcontact/xispeak 监听器内联重复逻辑):
* 部门领导/经办人审批后,审批人变量要写入"上两级"执行实例:
* - 存在第二层父级(大会签容器/外层作用域)→ 写入第二层 grandParentId
* - 不存在第二层父级 → 写入第一层 parentId
* - 第一层 parentId 为空 → 无目标,返回 null(调用方跳过写入)
*/
class FlowExecutionSupportTest {
// ============ 正常路径 ============
@Test
void should_return_grandParent_when_both_levels_exist() {
// 两级父级都存在(子流程内会签容器场景)
String target = FlowExecutionSupport.resolveTargetExecutionId("parent-1", "grand-1");
assertEquals("grand-1", target);
}
@Test
void should_return_parent_when_no_grand_parent() {
// 只有第一层父级(无外层容器)
String target = FlowExecutionSupport.resolveTargetExecutionId("parent-1", null);
assertEquals("parent-1", target);
}
// ============ 边界路径 ============
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " "})
void should_return_parent_when_grand_parent_blank(String grandParentId) {
// 第二层为空串/空白串:视为不存在,退回第一层(原逻辑 isNotBlank 判定)
String target = FlowExecutionSupport.resolveTargetExecutionId("parent-1", grandParentId);
assertEquals("parent-1", target);
}
// ============ 异常路径 ============
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " "})
void should_return_null_when_parent_blank(String parentId) {
// 第一层父级都没有:无目标执行实例,返回 null 供调用方跳过
assertNull(FlowExecutionSupport.resolveTargetExecutionId(parentId, "grand-1"));
}
}
@@ -0,0 +1,103 @@
package org.jeecg.modules.supervision.common.flow;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* FlowVariableSupport 纯函数规则测试(TDD 红-绿-重构,阶段 A 示范)。
*
* 业务规则(来自 fixcontact/xispeak 多处重复的逗号分割 + 部门 ID 清洗):
* - splitToTrimmedList:逗号分割 → 去首尾空白 → 过滤空白元素(null/空 → 空列表)
* - countTokens:分割后元素个数(等价于 splitToTrimmedList().size()
* - cleanDeptId:执行实例部门变量清洗(去掉 [] 与首尾空白)
*/
class FlowVariableSupportTest {
// ============ splitToTrimmedList:正常路径 ============
@Test
void should_split_multi_tokens() {
assertEquals(List.of("a", "b", "c"), FlowVariableSupport.splitToTrimmedList("a,b,c"));
}
@Test
void should_trim_tokens_with_spaces() {
// 逗号前后有空格:逐 token trim
assertEquals(List.of("a", "b"), FlowVariableSupport.splitToTrimmedList(" a , b "));
}
@Test
void should_return_single_token() {
assertEquals(List.of("a"), FlowVariableSupport.splitToTrimmedList("a"));
}
// ============ splitToTrimmedList:边界路径 ============
@Test
void should_filter_empty_tokens() {
// 空元素(连续逗号/全空白元素)被过滤
assertEquals(List.of("a", "b"), FlowVariableSupport.splitToTrimmedList("a,,b"));
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " ", " , , "})
void should_return_empty_list_when_no_valid_token(String raw) {
assertEquals(List.of(), FlowVariableSupport.splitToTrimmedList(raw));
}
// ============ countTokens:正常 + 边界 ============
@Test
void should_count_tokens() {
assertEquals(3, FlowVariableSupport.countTokens("a,b,c"));
}
@Test
void should_count_trimmed_tokens() {
assertEquals(2, FlowVariableSupport.countTokens(" a , b "));
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", " , , "})
void should_return_zero_when_no_valid_token(String raw) {
assertEquals(0, FlowVariableSupport.countTokens(raw));
}
// ============ cleanDeptId:正常 + 边界 ============
@Test
void should_remove_square_brackets() {
assertEquals("dept-1", FlowVariableSupport.cleanDeptId("[dept-1]"));
}
@Test
void should_remove_brackets_and_trim() {
// 多值列表变量场景:整体去括号 + trim
assertEquals("dept-1, dept-2", FlowVariableSupport.cleanDeptId("[dept-1, dept-2]"));
}
@Test
void should_keep_plain_id_unchanged() {
assertEquals("dept-1", FlowVariableSupport.cleanDeptId("dept-1"));
}
@Test
void should_handle_non_string_object() {
// 流程变量可能是任意对象,toString 后处理
assertEquals("123", FlowVariableSupport.cleanDeptId(123L));
}
@Test
void should_return_null_when_raw_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);
}
}
}
@@ -105,26 +105,26 @@ class InspectCloseoutFlowTest {
}
@Test
void getJJDeptLdWorkerIdList_shouldReturnUserList() {
void getJJDeptLdList_shouldReturnUserList() {
List<String> users = List.of("worker1");
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-jj-worker"))
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-ld"))
.thenReturn(users);
assertEquals(users, flow.getJJDeptLdWorkerIdList());
assertEquals(users, flow.getJJDeptLdList());
}
@Test
void getJJDeptLdWorkerIdListLength_shouldReturnCount() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-jj-worker"))
void getJJDeptLdListLength_shouldReturnCount() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-ld"))
.thenReturn(List.of("user1", "user2"));
assertEquals(2, flow.getJJDeptLdWorkerIdListLength());
assertEquals(2, flow.getJJDeptLdListLength());
}
@Test
void getDqDeptLdUserIdList_shouldReturnUserList() {
List<String> users = List.of("dq_user1");
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker"))
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld"))
.thenReturn(users);
assertEquals(users, flow.getDqDeptLdUserIdList());
@@ -132,7 +132,7 @@ class InspectCloseoutFlowTest {
@Test
void getDqDeptLdUserIdListLength_shouldReturnCount() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker"))
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld"))
.thenReturn(List.of("user1", "user2"));
assertEquals(2, flow.getDqDeptLdUserIdListLength());
@@ -140,7 +140,7 @@ class InspectCloseoutFlowTest {
@Test
void getDqDeptLdUserIdListLength_shouldReturnZeroWhenEmpty() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-jj-worker"))
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-dq", "role-ld"))
.thenReturn(Collections.emptyList());
assertEquals(0, flow.getDqDeptLdUserIdListLength());
@@ -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,603 @@
package org.jeecg.modules.supervision.fixcontact.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.supervision.fixcontact.constant.FixContactConstant;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContactFeedback20260730Service;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
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.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.Mockito.when;
/**
* FixContactFlow 流程表达式方法测试(mock 外部依赖:service/iSysBaseAPI/runtimeService)。
* 覆盖:业务字段读取、部门角色查询、办结状态、当前部门、反馈、流程变量、JSON 解析、待办人汇总。
*/
@ExtendWith(MockitoExtension.class)
class FixContactFlowTest {
@Mock private IFixedContact20260730Service fixedContactService;
@Mock private IFixedContactFeedback20260730Service feedbackService;
@Mock private ISysBaseAPI iSysBaseAPI;
@Mock private RuntimeService runtimeService;
@Mock private DelegateExecution execution;
private FixContactFlow flow;
@BeforeEach
void setUp() {
flow = new FixContactFlow(fixedContactService, feedbackService, null, iSysBaseAPI, runtimeService);
}
/** stub getFixedContact 读取业务表单的公共链路 */
private void mockEntity(FixedContact20260730 entity) {
when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
when(fixedContactService.getById("biz-1")).thenReturn(entity);
}
// ==================== 业务字段读取 ====================
@Nested
class BusinessFieldRead {
private FixedContact20260730 fullEntity() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setContactLeader("sld_01");
e.setRelatedLeader("sz");
e.setApplicant("admin");
e.setHostDept("dept-host");
e.setCoDept("dept-co");
e.setIsNeedAppro("1");
e.setSupDeptleaderid("bg_ld_01");
e.setContactDept("外单位");
return e;
}
@Test
void should_return_contactLeader_when_entity_exists() {
mockEntity(fullEntity());
assertEquals("sld_01", flow.getContactLeader(execution));
}
@Test
void should_return_null_when_entity_not_found() {
mockEntity(null);
assertNull(flow.getContactLeader(execution));
}
@Test
void should_return_relatedLeader() {
mockEntity(fullEntity());
assertEquals("sz", flow.getRelatedLeader(execution));
}
@Test
void should_return_applicant() {
mockEntity(fullEntity());
assertEquals("admin", flow.getApplicant(execution));
}
@Test
void should_return_hostDept() {
mockEntity(fullEntity());
assertEquals("dept-host", flow.getHostDept(execution));
}
@Test
void should_return_coDept() {
mockEntity(fullEntity());
assertEquals("dept-co", flow.getCoDept(execution));
}
@Test
void should_return_isNeedAppro() {
mockEntity(fullEntity());
assertEquals("1", flow.getIsNeedBgLdApprove(execution));
}
@Test
void should_return_supDeptLeaderId() {
mockEntity(fullEntity());
assertEquals("bg_ld_01", flow.getBgLd(execution));
}
@Test
void should_return_contactDept() {
mockEntity(fullEntity());
assertEquals("外单位", flow.getContactDept(execution));
}
@Test
void should_return_null_when_contactLeader_blank() {
FixedContact20260730 e = fullEntity();
e.setContactLeader(" ");
mockEntity(e);
assertEquals(" ", flow.getContactLeader(execution));
}
}
// ==================== 部门+角色查询 ====================
@Nested
class DeptRoleQuery {
@Test
void should_return_office_leader_list() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi(
FixContactConstant.DeptId.BG_DEPT_ID, FixContactConstant.RoleCode.SLD_ROLE_ID))
.thenReturn(List.of("sld_01", "sld_02"));
assertEquals(List.of("sld_01", "sld_02"), flow.getOfficeLeaderList(execution));
}
@Test
void should_return_office_leader_list_length() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi(
FixContactConstant.DeptId.BG_DEPT_ID, FixContactConstant.RoleCode.SLD_ROLE_ID))
.thenReturn(List.of("sld_01", "sld_02"));
assertEquals(2, flow.getOfficeLeaderListLength(execution));
}
@Test
void should_return_users_by_dept_and_role() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-100", "ld"))
.thenReturn(List.of("u1", "u2"));
assertEquals(List.of("u1", "u2"), flow.getUsersByDeptAndRole("dept-100", "ld"));
}
@Test
void should_return_count_zero_when_no_users() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-100", "ld"))
.thenReturn(List.of());
assertEquals(0, flow.getUsersByDeptAndRoleCount("dept-100", "ld"));
}
@Test
void should_return_host_dept_leader_list() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
mockEntity(e);
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld"))
.thenReturn(List.of("leader1", "leader2"));
assertEquals(List.of("leader1", "leader2"), flow.getHostDeptLeaderList(execution));
}
@Test
void should_return_empty_when_host_dept_has_no_leader() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
mockEntity(e);
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld"))
.thenReturn(List.of());
assertEquals(List.of(), flow.getHostDeptLeaderList(execution));
}
@Test
void should_throw_when_host_dept_blank() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
mockEntity(e);
assertThrows(IllegalStateException.class, () -> flow.getHostDeptLeaderList(execution));
}
@Test
void should_throw_when_entity_not_found_for_host_leader() {
mockEntity(null);
assertThrows(IllegalStateException.class, () -> flow.getHostDeptLeaderList(execution));
}
}
// ==================== 办结状态 ====================
@Nested
class IsEndRead {
@Test
void should_return_null_when_dept_id_blank() {
// deptId 空白直接短路返回,不触达业务表查询
assertNull(flow.getIsEnd(execution, " "));
}
@Test
void should_return_null_when_entity_not_found() {
mockEntity(null);
assertNull(flow.getIsEnd(execution, "dept-1"));
}
@Test
void should_return_null_when_approve_info_empty() {
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(new DeptApproveDetailMap());
mockEntity(e);
assertNull(flow.getIsEnd(execution, "dept-1"));
}
@Test
void should_return_isEnd_one_when_finished() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(1);
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(map);
mockEntity(e);
assertEquals("1", flow.getIsEnd(execution, "dept-1"));
}
@Test
void should_return_isEnd_zero_when_unfinished() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(0);
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(map);
mockEntity(e);
assertEquals("0", flow.getIsEnd(execution, "dept-1"));
}
@Test
void should_return_null_when_detail_missing() {
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(new DeptApproveDetailMap());
mockEntity(e);
assertNull(flow.getIsEnd(execution, "no-such-dept"));
}
}
// ==================== 当前部门 ====================
@Nested
class CurrentDept {
@Test
void should_return_null_when_execution_null() {
assertNull(flow.getCurrentDeptId(null));
}
@Test
void should_clean_local_dept_var() {
when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn("[dept-1]");
assertEquals("dept-1", flow.getCurrentDeptId(execution));
}
@Test
void should_fallback_to_global_var() {
when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
when(execution.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn("dept-2");
assertEquals("dept-2", flow.getCurrentDeptId(execution));
}
@Test
void should_return_null_when_no_dept_var() {
when(execution.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
when(execution.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
assertNull(flow.getCurrentDeptId(execution));
}
}
// ==================== 反馈子表 ====================
@Nested
class Feedback {
private FixedContactFeedback20260730 feedback(String user, String dept, String finishStatus) {
FixedContactFeedback20260730 f = new FixedContactFeedback20260730();
f.setFeedbackUser(user);
f.setFeedbackDept(dept);
f.setFinishStatus(finishStatus);
return f;
}
private void mockEntityWithId() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
mockEntity(e);
}
@Test
void should_return_empty_when_entity_not_found() {
mockEntity(null);
assertEquals(List.of(), flow.getFeedbackList(execution));
}
@Test
void should_return_feedback_list() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of(feedback("u1", "d1", "1")));
assertEquals(1, flow.getFeedbackList(execution).size());
}
@Test
void should_return_null_latest_user_when_no_feedback() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of());
assertNull(flow.getLatestFeedbackUser(execution));
}
@Test
void should_return_latest_feedback_user() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1"))
.thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1")));
assertEquals("u2", flow.getLatestFeedbackUser(execution));
}
@Test
void should_return_latest_feedback_dept() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1"))
.thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1")));
assertEquals("d2", flow.getLatestFeedbackDept(execution));
}
@Test
void should_return_false_when_no_feedback() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1")).thenReturn(List.of());
assertFalse(flow.isAllFeedbackFinished(execution));
}
@Test
void should_return_true_when_all_finished() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1"))
.thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", "1")));
assertTrue(flow.isAllFeedbackFinished(execution));
}
@Test
void should_return_false_when_any_feedback_unfinished() {
mockEntityWithId();
when(feedbackService.selectByMainId("biz-1"))
.thenReturn(List.of(feedback("u1", "d1", "1"), feedback("u2", "d2", " ")));
assertFalse(flow.isAllFeedbackFinished(execution));
}
}
// ==================== 流程变量 ====================
@Nested
class ProcessVariable {
@Test
void should_return_var_value() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", "name")).thenReturn("val");
assertEquals("val", flow.getVar(execution, "name"));
org.mockito.Mockito.verify(execution).getProcessInstanceId();
}
@Test
void should_return_null_when_var_absent() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", "name")).thenReturn(null);
assertNull(flow.getVar(execution, "name"));
}
@Test
void should_default_zero_when_need_leader_approve_blank() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.IS_NEED_LEADER_APPROVE))
.thenReturn(null);
assertEquals("0", flow.getIsNeedLeaderApprove(execution));
}
@Test
void should_return_need_leader_approve_value() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.IS_NEED_LEADER_APPROVE))
.thenReturn("1");
assertEquals("1", flow.getIsNeedLeaderApprove(execution));
}
@Test
void should_split_var_as_list() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.LEADER_APPROVE_USER))
.thenReturn("sld_01, sld_02");
assertEquals(List.of("sld_01", "sld_02"), flow.getLeaderApproveUserList(execution));
}
@Test
void should_return_empty_list_when_var_null() {
when(execution.getProcessInstanceId()).thenReturn("proc-1");
when(runtimeService.getVariable("proc-1", FixContactConstant.FlowVarName.LEADER_APPROVE_USER))
.thenReturn(null);
assertEquals(List.of(), flow.getLeaderApproveUserList(execution));
}
}
// ==================== JSON 解析 ====================
@Nested
class JsonParse {
@Test
void should_parse_from_json_object() {
JSONObject data = new JSONObject().fluentPut("contactLeader", "sld_01");
assertEquals("sld_01", flow.getContactLeaderFromJson(data));
}
@Test
void should_parse_from_json_string() {
assertEquals("sld_01", flow.getContactLeaderFromJson("{\"contactLeader\":\"sld_01\"}"));
}
@Test
void should_return_null_when_json_null() {
assertNull(flow.getContactLeaderFromJson(null));
}
@Test
void should_return_null_when_invalid_json() {
assertNull(flow.getContactLeaderFromJson("not-a-json"));
}
@Test
void should_return_host_dept_leader_list_from_json() {
when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-host", "ld"))
.thenReturn(List.of("leader1"));
assertEquals(List.of("leader1"), flow.getHostDeptLeaderListFromJson(
new JSONObject().fluentPut("hostDept", "dept-host")));
}
@Test
void should_return_empty_when_host_dept_blank_in_json() {
assertEquals(List.of(), flow.getHostDeptLeaderListFromJson(
new JSONObject().fluentPut("hostDept", " ")));
}
}
// ==================== 部门列表组装 ====================
@Nested
class DeptIdList {
@Test
void should_compose_host_and_co_dept() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
e.setCoDept("dept-co");
mockEntity(e);
assertEquals(List.of("dept-host", "dept-co"), flow.getDeptIdList(execution));
}
@Test
void should_compose_only_host_dept() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
mockEntity(e);
assertEquals(List.of("dept-host"), flow.getDeptIdList(execution));
}
@Test
void should_return_empty_when_entity_not_found() {
mockEntity(null);
assertEquals(List.of(), flow.getDeptIdList(execution));
}
}
// ==================== 待办人汇总 ====================
@Nested
class TodoUser {
private FixedContact20260730 entityWithApproveInfo() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
e.setCoDept("dept-co");
mockEntity(e);
return e;
}
@Test
void should_return_empty_when_approve_info_empty() {
FixedContact20260730 e = new FixedContact20260730();
e.setId("biz-1");
e.setHostDept("dept-host");
e.setCoDept("dept-co");
e.setApproveInfo(new DeptApproveDetailMap());
mockEntity(e);
assertEquals(List.of(), flow.getTodoUserList(execution));
}
@Test
void should_use_impl_user_name_list_when_not_finished() {
// isEnd=0:多选经办人 implUserNameList
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(0);
detail.setImplUserNameList(List.of("worker1", "worker2"));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-host", detail);
entityWithApproveInfo().setApproveInfo(map);
assertEquals(List.of("worker1", "worker2"), flow.getTodoUserList(execution));
}
@Test
void should_use_impl_worker_when_finished() {
// isEnd=1:单选经办人 implWorker
DeptApproveDetail detail = new DeptApproveDetail();
detail.setIsEnd(1);
detail.setImplWorker("worker1");
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-host", detail);
entityWithApproveInfo().setApproveInfo(map);
assertEquals(List.of("worker1"), flow.getTodoUserList(execution));
}
@Test
void should_deduplicate_users_across_depts() {
// 两部门分配同一经办人:LinkedHashSet 去重保序
DeptApproveDetail d1 = new DeptApproveDetail();
d1.setIsEnd(0);
d1.setImplUserNameList(List.of("shared"));
DeptApproveDetail d2 = new DeptApproveDetail();
d2.setIsEnd(0);
d2.setImplUserNameList(List.of("shared", "worker2"));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-host", d1);
map.put("dept-co", d2);
entityWithApproveInfo().setApproveInfo(map);
assertEquals(List.of("shared", "worker2"), flow.getTodoUserList(execution));
}
}
// ==================== 当前部门经办人 ====================
@Nested
class CurrentDeptWorker {
@Test
void should_return_empty_when_detail_null() {
assertEquals(List.of(), flow.getCurrentDeptWorkerList(execution, "dept-1"));
}
@Test
void should_return_impl_user_name_list() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setImplUserNameList(List.of("w1", "w2"));
when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(new DeptApproveDetailMap());
e.getApproveInfo().put("dept-1", detail);
when(fixedContactService.getById("biz-1")).thenReturn(e);
assertEquals(List.of("w1", "w2"), flow.getCurrentDeptWorkerList(execution, "dept-1"));
}
@Test
void should_return_singleton_impl_worker() {
DeptApproveDetail detail = new DeptApproveDetail();
detail.setImplWorker("w1");
when(execution.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
FixedContact20260730 e = new FixedContact20260730();
e.setApproveInfo(new DeptApproveDetailMap());
e.getApproveInfo().put("dept-1", detail);
when(fixedContactService.getById("biz-1")).thenReturn(e);
assertEquals(List.of("w1"), flow.getCurrentDeptWorkerList(execution, "dept-1"));
}
}
}
@@ -0,0 +1,134 @@
package org.jeecg.modules.supervision.fixcontact.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.supervision.fixcontact.constant.FixContactConstant;
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;
/**
* FixContactDeptLeaderApproveListener 部门领导审批监听器测试(重构依赖注入后可测)。
* 规则:将 assignee 写入"上两级"执行实例局部变量——有第二层父级存第二层,否则存第一层。
*/
@ExtendWith(MockitoExtension.class)
class FixContactDeptLeaderApproveListenerTest {
@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 FixContactDeptLeaderApproveListener listener;
@BeforeEach
void setUp() {
listener = new FixContactDeptLeaderApproveListener(runtimeService);
}
/** 链式 stub:第一次 createExecutionQuery 查当前执行,第二次查父级执行 */
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_skip_when_parent_execution_not_found() {
// 单独 stub(不复用 mockExecutionChain,避免无用 stub
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("parent-1");
when(parentQuery.executionId("parent-1")).thenReturn(parentQuery);
when(parentQuery.singleResult()).thenReturn(null);
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", FixContactConstant.FlowVarName.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", FixContactConstant.FlowVarName.TEM_IMPL_LEADER, "leader1");
}
@Test
void should_not_throw_when_set_variable_fails() {
mockExecutionChain("parent-1", "grand-1");
doThrow(new RuntimeException("boom")).when(runtimeService).setVariableLocal("grand-1", FixContactConstant.FlowVarName.TEM_IMPL_LEADER, "leader1");
// 不抛异常即通过(异常被 catch)
listener.notify(delegateTask);
}
}
@@ -0,0 +1,179 @@
package org.jeecg.modules.supervision.fixcontact.listener;
import org.flowable.engine.RuntimeService;
import org.flowable.task.service.delegate.DelegateTask;
import org.jeecg.modules.supervision.fixcontact.constant.FixContactConstant;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.service.IFixedContact20260730Service;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap;
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.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* FixContactWorkerApproveListener 经办人会签监听器测试。
* 规则:写入子流程局部变量 tem_impl_worker + 记录经办人到 approveInfo 对应部门(去重)。
*/
@ExtendWith(MockitoExtension.class)
class FixContactWorkerApproveListenerTest {
@Mock private IFixedContact20260730Service fixedContactService;
@Mock private RuntimeService runtimeService;
@Mock private DelegateTask delegateTask;
private FixContactWorkerApproveListener listener;
@BeforeEach
void setUp() {
listener = new FixContactWorkerApproveListener(fixedContactService, runtimeService);
}
/** 正常办理链路:assignee + executionId + businessKey + deptVar 齐备 */
private void mockNormalTask(String deptVar) {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
when(delegateTask.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(deptVar);
}
@Test
void should_return_when_task_null() {
listener.notify(null);
verify(runtimeService, never()).setVariableLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
}
@Test
void should_skip_when_assignee_blank() {
when(delegateTask.getAssignee()).thenReturn(" ");
listener.notify(delegateTask);
verify(runtimeService, never()).setVariableLocal(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_write_worker_local_var() {
mockNormalTask("dept-1");
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(new DeptApproveDetailMap());
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
verify(runtimeService).setVariableLocal("exec-1", FixContactConstant.FlowVarName.TEM_IMPL_WORKER, "worker1");
verify(fixedContactService).updateById(entity);
}
@Test
void should_skip_record_when_business_key_missing() {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn(null);
listener.notify(delegateTask);
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_skip_record_when_dept_var_missing() {
when(delegateTask.getAssignee()).thenReturn("worker1");
when(delegateTask.getExecutionId()).thenReturn("exec-1");
when(delegateTask.getVariable(FixContactConstant.FlowVarName.BUSINESS_KEY)).thenReturn("biz-1");
when(delegateTask.getVariableLocal(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
when(delegateTask.getVariable(FixContactConstant.FlowVarName.IMPL_DEPT_ID)).thenReturn(null);
listener.notify(delegateTask);
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_skip_record_when_entity_not_found() {
mockNormalTask("dept-1");
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(null);
listener.notify(delegateTask);
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_append_worker_to_existing_detail() {
mockNormalTask("dept-1");
DeptApproveDetail detail = new DeptApproveDetail();
detail.setDeptId("dept-1");
detail.setImplUserNameList(new ArrayList<>(List.of("old_worker")));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(map);
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals(List.of("old_worker", "worker1"), detail.getImplUserNameList());
verify(fixedContactService).updateById(entity);
}
@Test
void should_create_detail_when_dept_not_in_map() {
// approveInfo 为 null + dept 无 detail:新建 map + detail
mockNormalTask("dept-1");
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals(List.of("worker1"), entity.getApproveInfo().get("dept-1").getImplUserNameList());
verify(fixedContactService).updateById(entity);
}
@Test
void should_not_update_when_worker_already_in_list() {
mockNormalTask("dept-1");
DeptApproveDetail detail = new DeptApproveDetail();
detail.setDeptId("dept-1");
detail.setImplUserNameList(new ArrayList<>(List.of("worker1")));
DeptApproveDetailMap map = new DeptApproveDetailMap();
map.put("dept-1", detail);
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(map);
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertEquals(1, detail.getImplUserNameList().size());
verify(fixedContactService, never()).updateById(org.mockito.ArgumentMatchers.any());
}
@Test
void should_clean_dept_var_with_brackets() {
// 部门变量带 []:记录到清洗后的 dept key
mockNormalTask("[dept-1]");
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(new DeptApproveDetailMap());
when(fixedContactService.getByIdForUpdate("biz-1")).thenReturn(entity);
listener.notify(delegateTask);
assertTrue(entity.getApproveInfo().containsKey("dept-1"));
assertEquals("dept-1", entity.getApproveInfo().get("dept-1").getDeptId());
}
}
@@ -0,0 +1,178 @@
package org.jeecg.modules.supervision.fixcontact.service.impl;
import org.flowable.engine.RuntimeService;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContact20260730;
import org.jeecg.modules.supervision.fixcontact.entity.FixedContactFeedback20260730;
import org.jeecg.modules.supervision.fixcontact.mapper.FixedContact20260730Mapper;
import org.jeecg.modules.supervision.fixcontact.mapper.FixedContactFeedback20260730Mapper;
import org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetail;
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* FixedContact20260730ServiceImpl 测试(mock mapper + runtimeService)。
* 核心:saveSubApprove 部门审批保存规则(isEnd=1 存 implWorker / isEnd=0 存 implUserNameList+ 主从 CRUD。
*/
@ExtendWith(MockitoExtension.class)
class FixedContact20260730ServiceImplTest {
@Mock private FixedContact20260730Mapper baseMapper;
@Mock private FixedContactFeedback20260730Mapper feedbackMapper;
@Mock private RuntimeService runtimeService;
private FixedContact20260730ServiceImpl service;
@BeforeEach
void setUp() {
service = new FixedContact20260730ServiceImpl();
ReflectionTestUtils.setField(service, "baseMapper", baseMapper);
ReflectionTestUtils.setField(service, "fixedContactFeedback20260730Mapper", feedbackMapper);
ReflectionTestUtils.setField(service, "runtimeService", runtimeService);
}
// ==================== saveSubApprove(核心业务规则) ====================
@Test
void should_throw_when_main_not_exists() {
when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(null);
assertThrows(JeecgBootException.class, () -> service.saveSubApprove("no-such", "dept-1", 1, "w1", null));
}
@Test
void should_store_impl_worker_when_is_end_one() {
// isEnd=1(办结):单选经办人 implWorker,清空 implUserNameList
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
when(baseMapper.selectOne(any(), anyBoolean())).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() {
// isEnd=0(分配经办人):多选 implUserNameList,清空 implWorker
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
when(baseMapper.selectOne(any(), anyBoolean())).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);
}
@Test
void should_merge_into_existing_detail() {
// 同一部门二次保存:覆盖原有 detail 而不重建
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
entity.setApproveInfo(new org.jeecg.modules.supervision.xispeak.entity.DeptApproveDetailMap());
DeptApproveDetail existing = new DeptApproveDetail();
existing.setDeptId("dept-1");
entity.getApproveInfo().put("dept-1", existing);
when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity);
service.saveSubApprove("biz-1", "dept-1", 0, null, List.of("w9"));
DeptApproveDetail detail = entity.getApproveInfo().get("dept-1");
assertEquals(List.of("w9"), detail.getImplUserNameList());
verify(baseMapper).updateById(entity);
}
@Test
void should_create_map_when_approve_info_null() {
FixedContact20260730 entity = new FixedContact20260730();
entity.setId("biz-1");
when(baseMapper.selectOne(any(), anyBoolean())).thenReturn(entity);
service.saveSubApprove("biz-1", "dept-9", 0, null, List.of("w1"));
assertEquals(List.of("w1"), entity.getApproveInfo().get("dept-9").getImplUserNameList());
}
// ==================== 主从 CRUD ====================
@Test
void should_save_main_and_sub_list() {
FixedContact20260730 main = new FixedContact20260730();
main.setId("biz-1");
FixedContactFeedback20260730 sub = new FixedContactFeedback20260730();
service.saveMain(main, List.of(sub));
verify(baseMapper).insert(main);
assertEquals("biz-1", sub.getFixedContact20260730Id());
verify(feedbackMapper).insert(sub);
}
@Test
void should_save_main_without_sub_when_list_null() {
FixedContact20260730 main = new FixedContact20260730();
service.saveMain(main, null);
verify(baseMapper).insert(main);
verify(feedbackMapper, never()).insert(any());
}
@Test
void should_update_main_and_replace_sub() {
FixedContact20260730 main = new FixedContact20260730();
main.setId("biz-1");
FixedContactFeedback20260730 sub = new FixedContactFeedback20260730();
service.updateMain(main, List.of(sub));
verify(baseMapper).updateById(main);
verify(feedbackMapper).deleteByMainId("biz-1");
assertEquals("biz-1", sub.getFixedContact20260730Id());
verify(feedbackMapper).insert(sub);
}
@Test
void should_delete_main_with_sub() {
service.delMain("biz-1");
verify(feedbackMapper).deleteByMainId("biz-1");
verify(baseMapper).deleteById("biz-1");
}
@Test
void should_delete_batch_with_sub() {
service.delBatchMain(List.of("biz-1", "biz-2"));
verify(feedbackMapper).deleteByMainId("biz-1");
verify(baseMapper).deleteById("biz-1");
verify(feedbackMapper).deleteByMainId("biz-2");
verify(baseMapper).deleteById("biz-2");
}
// ==================== 流程变量同步 ====================
@Test
void should_update_form_and_sync_variable() {
FixedContact20260730 form = new FixedContact20260730();
service.saveBpmFormAndSyncVariable(form, "proc-1", "json_data", "{\"a\":1}");
verify(baseMapper).updateById(form);
verify(runtimeService).setVariable("proc-1", "json_data", "{\"a\":1}");
}
}
@@ -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,403 @@
package org.jeecg.modules.supervision.flowregression;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.jeecg.common.util.RestUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* 流程回归测试基类:纯 HTTP 驱动本机运行中的后端(不启 Spring 上下文)。
* 审批人动态发现——每步查询当前任务 assignee,以该用户名 autoLogin 后驱动。
*/
public abstract class FlowRegressionBase {
protected final Logger log = LoggerFactory.getLogger(getClass());
private final Map<String, String> tokenCache = new HashMap<>();
protected final ResourceRegistry registry = new ResourceRegistry();
protected boolean passed = true;
@BeforeEach
void resetState() {
passed = true;
}
/**
* 用例包装:失败时置 passed=false 保留测试数据供排查,成功则清理。
* 不能用 TestWatcher —— 它的 testFailed 在 @AfterEach 之后才触发。
*/
protected void runTestCase(Runnable body) {
try {
body.run();
passed = true;
} catch (Throwable t) {
passed = false;
throw t;
}
}
@AfterEach
void cleanup() {
if (passed) {
cleanupHttp();
try {
registry.cleanupJdbc();
log.info("[回归] 测试通过,测试数据已清理");
} catch (Exception e) {
log.error("[回归] DB 清理失败,请手动清理", e);
}
} else {
registry.printResidue();
}
}
/** 通过 HTTP 清理 Flowable 流程实例 + task_task + 审批意见 */
private void cleanupHttp() {
for (String procId : registry.getProcessInstIds()) {
try {
post("/tasktask/taskTask/deleteSingleProcess",
new JSONObject().fluentPut("processInstId", procId), null, FlowRegressionProps.INITIATOR);
log.info("[回归] 已删除流程实例 procId: {}", procId);
} catch (Exception e) {
log.error("[回归] 删除流程实例失败 procId: {}", procId, e);
}
}
}
// ================= HTTP 基础 =================
protected JSONObject get(String path, JSONObject query, String username) {
ResponseEntity<JSONObject> resp = RestUtil.request(FlowRegressionProps.BASE_URL + path,
HttpMethod.GET, headers(username), query, null, JSONObject.class);
return resp == null ? null : resp.getBody();
}
protected JSONObject post(String path, JSONObject query, JSONObject body, String username) {
ResponseEntity<JSONObject> resp = RestUtil.request(FlowRegressionProps.BASE_URL + path,
HttpMethod.POST, headers(username), query, body, JSONObject.class);
return resp == null ? null : resp.getBody();
}
private HttpHeaders headers(String username) {
HttpHeaders h = new HttpHeaders();
h.setContentType(MediaType.APPLICATION_JSON);
h.set("X-Access-Token", token(username));
return h;
}
protected String token(String username) {
return tokenCache.computeIfAbsent(username, this::login);
}
/** 网关自动登录,跳过密码校验 */
protected String login(String username) {
HttpHeaders h = new HttpHeaders();
h.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<JSONObject> resp = RestUtil.request(FlowRegressionProps.BASE_URL + "/sys/autoLogin",
HttpMethod.POST, h, new JSONObject().fluentPut("username", username), null, JSONObject.class);
JSONObject body = resp == null ? null : resp.getBody();
JSONObject result = body == null ? null : body.getJSONObject("result");
String tk = result == null ? null : result.getString("token");
if (tk == null || tk.isEmpty()) {
throw new IllegalStateException("autoLogin 失败, username=" + username + ", resp=" + body);
}
return tk;
}
// ================= 发起流程 =================
protected String startFlow(FlowRegressionProps.FlowDescriptor fd, String businessId, String initiator) {
return startFlow(fd, businessId, new JSONObject().fluentPut("business_id", businessId), null, initiator);
}
protected String startFlow(FlowRegressionProps.FlowDescriptor fd, String businessId, JSONObject jsonData, String initiator) {
return startFlow(fd, businessId, jsonData, null, initiator);
}
/**
* deptHandlerName:习讲话反馈等流程首节点按 task_task.deptHandlerName 评估,需在发起请求里带上。
*/
protected String startFlow(FlowRegressionProps.FlowDescriptor fd, String businessId, JSONObject jsonData,
String deptHandlerName, String initiator) {
JSONObject taskTask = new JSONObject();
taskTask.put("triggerType", 1);
taskTask.put("flowCode", fd.flowCode);
taskTask.put("flowName", fd.processKey);
taskTask.put("formUrl", fd.formUrl);
taskTask.put("businessTablename", "task_task");
taskTask.put("businessId", businessId);
taskTask.put("jsonData", jsonData == null ? new JSONObject().fluentPut("business_id", businessId) : jsonData);
if (deptHandlerName != null) {
taskTask.put("deptHandlerName", deptHandlerName);
}
JSONObject req = new JSONObject();
req.put("taskTask", taskTask);
JSONObject resp = post("/tasktask/taskTask/flow-schedules", null, req, initiator);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("发起流程失败, flowCode=" + fd.flowCode + ", resp=" + resp);
}
String procId = queryProcessInstIdByBusinessId(businessId, initiator);
if (procId == null) {
throw new IllegalStateException("未查询到流程实例, businessId=" + businessId);
}
registry.registerProcess(procId);
registry.registerBusiness(fd.businessTable, businessId);
log.info("[回归] 流程已发起, flowCode: {}, businessId: {}, procId: {}", fd.flowCode, businessId, procId);
return procId;
}
private String queryProcessInstIdByBusinessId(String businessId, String initiator) {
JSONObject resp = get("/tasktask/taskTask/queryByBusinessId",
new JSONObject().fluentPut("businessId", businessId), initiator);
if (resp == null) {
return null;
}
JSONObject result = resp.getJSONObject("result");
JSONArray arr = result == null ? null : result.getJSONArray("records");
if (arr == null || arr.isEmpty()) {
return null;
}
for (int i = arr.size() - 1; i >= 0; i--) {
JSONObject tt = arr.getJSONObject(i);
if (tt.getString("processInstId") != null) {
return tt.getString("processInstId");
}
}
return null;
}
// ================= 流程变量 / 任务指派 =================
/** 设置流程变量(POST /act/process/setProcessVariable */
protected void setProcessVariable(String procInstId, String varField, Object varVal, String username) {
JSONObject body = new JSONObject();
body.put("processInstanceId", procInstId);
body.put("varField", varField);
body.put("varVal", varVal);
JSONObject resp = post("/act/process/setProcessVariable", null, body, username);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("设置流程变量失败, varField=" + varField + ", resp=" + resp);
}
}
/** 指派任务办理人(GET /act/processInstance/reassign */
protected void assignTask(String taskId, String userName, String operator) {
JSONObject resp = get("/act/processInstance/reassign",
new JSONObject().fluentPut("taskId", taskId).fluentPut("userName", userName), operator);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("指派任务失败, taskId=" + taskId + ", userName=" + userName + ", resp=" + resp);
}
}
/**
* 等待流程自动推进稳定:连续 stableRounds 次查询活跃任务集合不变视为稳定。
* 部分流程节点配置了自动提交(首节点自动完成、后续节点级联),发起后需先等它跑完再驱动。
*/
protected List<ActiveTask> waitSettled(String procInstId, int stableRounds) {
Set<String> last = null;
int stable = 0;
for (int i = 0; i < 30; i++) {
List<ActiveTask> tasks = currentTasks(procInstId);
if (tasks.isEmpty()) {
return tasks;
}
Set<String> current = taskIds(tasks);
if (current.equals(last)) {
stable++;
if (stable >= stableRounds) {
return tasks;
}
} else {
stable = 0;
last = current;
}
try {
Thread.sleep(200);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
throw new IllegalStateException("流程未稳定, procId=" + procInstId);
}
// ================= 当前任务查询 =================
protected List<ActiveTask> currentTasks(String procInstId) {
JSONObject resp = get("/act/task/currentNodeInfo",
new JSONObject().fluentPut("procInstId", procInstId), FlowRegressionProps.INITIATOR);
JSONObject result = resp == null ? null : resp.getJSONObject("result");
List<ActiveTask> tasks = new ArrayList<>();
if (result != null) {
JSONArray arr = result.getJSONArray("currentTasks");
if (arr != null) {
for (int i = 0; i < arr.size(); i++) {
JSONObject t = arr.getJSONObject(i);
tasks.add(new ActiveTask(t.getString("taskId"), t.getString("taskDefinitionKey"),
t.getString("taskName"), t.getString("assignee")));
}
}
}
return tasks;
}
protected boolean isFinished(String procInstId) {
JSONObject resp = get("/act/task/currentNodeInfo",
new JSONObject().fluentPut("procInstId", procInstId), FlowRegressionProps.INITIATOR);
JSONObject result = resp == null ? null : resp.getJSONObject("result");
return result != null && Boolean.TRUE.equals(result.getBoolean("finished"));
}
protected void assertFinished(String procInstId) {
assertTrue(isFinished(procInstId), "流程未按预期结束, procId=" + procInstId + " 当前任务=" + currentTasks(procInstId));
}
/** 驱动流程直到当前活跃任务全部位于指定节点(兼容自动推进);若已结束则返回 */
protected void driveUntil(String procInstId, String taskDefKey) {
for (int i = 0; i < 20 && !isFinished(procInstId); i++) {
List<ActiveTask> tasks = currentTasks(procInstId);
if (!tasks.isEmpty() && tasks.stream().allMatch(t -> taskDefKey.equals(t.taskDefKey))) {
return;
}
completeCurrentNode(procInstId);
}
if (!isFinished(procInstId)) {
throw new IllegalStateException("驱动未到达目标节点 " + taskDefKey + ", procId=" + procInstId
+ " 当前任务=" + currentTasks(procInstId));
}
}
// ================= 审批动作 =================
protected void approve(String taskId, String assignee, String remarks) {
JSONObject body = new JSONObject();
body.put("taskId", taskId);
body.put("processModel", "2");
body.put("remarks", remarks == null ? "同意" : remarks);
JSONObject resp = post("/act/task/processComplete", null, body, assignee);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("审批失败, taskId=" + taskId + ", assignee=" + assignee + ", resp=" + resp);
}
}
protected void reject(String taskId, String assignee, String rejectNodeCode, String remarks) {
JSONObject body = new JSONObject();
body.put("taskId", taskId);
body.put("rejectModelNode", rejectNodeCode);
body.put("remarks", remarks == null ? "驳回" : remarks);
JSONObject resp = post("/act/task/processComplete", null, body, assignee);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("驳回失败, taskId=" + taskId + ", assignee=" + assignee + ", resp=" + resp);
}
}
// ================= 驱动:办完当前节点全部任务直到节点推进 =================
/**
* 办完当前活跃节点上的全部任务(兼容多实例会签/或签)。
* 兼容部分流程节点自动推进:任务可能在查询与办理之间被自动完成,
* 遇失效任务(task is null)跳过重查;等待短暂时间让自动推进稳定后再驱动。
* 若流程已结束返回空列表。
*/
protected List<ActiveTask> completeCurrentNode(String procInstId) {
for (int guard = 0; guard < 20; guard++) {
List<ActiveTask> tasks = currentTasks(procInstId);
if (tasks.isEmpty()) {
return tasks;
}
log.info("[回归] 当前活跃任务: {}", tasks);
boolean anyCompleted = false;
for (ActiveTask t : tasks) {
if (t.assignee == null || t.assignee.isEmpty()) {
throw new IllegalStateException("任务无办理人(可能是候选组待签收), taskId=" + t.taskId
+ ", taskDefKey=" + t.taskDefKey + ", taskName=" + t.taskName);
}
try {
log.info("[回归] 办理节点任务, taskDefKey: {}, taskName: {}, assignee: {}, taskId: {}",
t.taskDefKey, t.taskName, t.assignee, t.taskId);
approve(t.taskId, t.assignee, "同意");
anyCompleted = true;
} catch (IllegalStateException e) {
// 任务可能已被自动推进完成/失效,跳过,下一轮重新查询
log.info("[回归] 任务已失效(可能被自动推进), 跳过重查, taskId: {}, err: {}", t.taskId, e.getMessage());
}
}
sleepQuietly(300);
List<ActiveTask> after = currentTasks(procInstId);
if (after.isEmpty()) {
return after;
}
// 节点已推进(活跃任务集变化)则返回给外层继续驱动
if (!taskIds(after).equals(taskIds(tasks)) || !anyCompleted) {
return after;
}
}
throw new IllegalStateException("完成当前节点超限, procId=" + procInstId);
}
private void sleepQuietly(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/** 断言当前活跃任务恰好在指定节点集合,返回任务列表 */
protected List<ActiveTask> assertCurrentNode(String procInstId, String... taskDefKeys) {
List<ActiveTask> tasks = currentTasks(procInstId);
Set<String> expected = new HashSet<>(List.of(taskDefKeys));
Set<String> actual = new HashSet<>();
for (ActiveTask t : tasks) {
actual.add(t.taskDefKey);
}
if (!actual.equals(expected)) {
fail("节点不符, 期望=" + expected + ", 实际=" + actual + ", tasks=" + tasks);
}
return tasks;
}
private Set<String> taskIds(List<ActiveTask> tasks) {
Set<String> ids = new HashSet<>();
for (ActiveTask t : tasks) {
ids.add(t.taskId);
}
return ids;
}
/** 当前活跃任务 */
public static class ActiveTask {
public final String taskId;
public final String taskDefKey;
public final String taskName;
public final String assignee;
public ActiveTask(String taskId, String taskDefKey, String taskName, String assignee) {
this.taskId = taskId;
this.taskDefKey = taskDefKey;
this.taskName = taskName;
this.assignee = assignee;
}
@Override
public String toString() {
return "{taskId=" + taskId + ", def=" + taskDefKey + ", name=" + taskName + ", assignee=" + assignee + "}";
}
}
}
@@ -0,0 +1,71 @@
package org.jeecg.modules.supervision.flowregression;
/**
* 流程回归测试常量配置。
* 服务器地址/库连接针对本地开发环境(test_src),各流程 flowCode↔process_key 映射
* 来自 test_src.ext_act_process_form / ext_act_process,组织 id 来自 application-flow.yml。
*/
public final class FlowRegressionProps {
private FlowRegressionProps() {
}
/** 后端地址(context-path 为 /jeecg-boot),需本地已运行 */
public static final String BASE_URL = "http://localhost:8080/jeecg-boot";
/** 清理用本地 test_src 连接 */
public static final String JDBC_URL = "jdbc:mysql://127.0.0.1:3306/test_src?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false";
public static final String JDBC_USER = "root";
public static final String JDBC_PASSWORD = "root";
/** 流程发起人(各流程 1.1 节点 assignee = applyUserId = 发起人) */
public static final String INITIATOR = "admin";
// ---- 组织 idapplication-flow.yml flow-biz.org ----
public static final String DEPT_DQ = "2044677628281720834";
public static final String DEPT_JJ = "2054466800692432898";
public static final String DEPT_SLD = "2044677562460508161";
public static final String DEPT_BG = "2044677604785229826";
public static final String ROLE_LD = "2044680793306591234";
public static final String ROLE_JJ_WORKER = "2047511967494213633";
public static final String ROLE_SLD_JWSJ = "2063902766943363073";
public static final String ROLE_SDWSJ = "2062796937607499777";
public static final String ROLE_SDW = "2044676455280570370";
// ---- 4 个流程描述符 ----
/** 巡视整改提升 */
public static final FlowDescriptor IMPROVE = new FlowDescriptor(
"inspect_flow_create_01", "process_1780563034974",
"dj/inspectimprove/components/DjInspectImproveBPMForm", "dq_inspect_task");
/** 巡视整改销号 */
public static final FlowDescriptor CLOSEOUT = new FlowDescriptor(
"dq_delete_01", "partymatter_close",
"dj/inspectcloseout/components/InspectCloseoutBPMForm", "dq_inspect_task");
/** 习讲话审批 */
public static final FlowDescriptor XI_SPEAK = new FlowDescriptor(
"dev_bg_xi_speak_001", "bg_xispeak_process",
"bg/xispeak/components/BgXiSpeakBPMForm?showFeedbackInfo=1&showJiJianFields=1", "bg_xi_speak");
/** 习讲话经办人反馈(process_key 已对齐 BPMN id */
public static final FlowDescriptor XI_SPEAK_FB = new FlowDescriptor(
"bg_xi_speak_fb_01", "process_1778315114392",
"bg/xispeak/components/BgXiSpeakFeedbackBPMForm", "bg_xi_speak");
/** 单个流程的描述与业务表信息 */
public static final class FlowDescriptor {
public final String flowCode;
public final String processKey;
public final String formUrl;
public final String businessTable;
public FlowDescriptor(String flowCode, String processKey, String formUrl, String businessTable) {
this.flowCode = flowCode;
this.processKey = processKey;
this.formUrl = formUrl;
this.businessTable = businessTable;
}
}
}
@@ -0,0 +1,50 @@
package org.jeecg.modules.supervision.flowregression;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Test;
import java.util.List;
/**
* 巡视整改销号流程回归测试。
* flowCode=dq_delete_01 → process_key=partymatter_close
* 节点:1.1 责任部门负责人分配/办结 →(办结分支 is_end=1 跳过 1.2/1.3)→ 1.4 党群经办人 → 1.5 党群领导
* → 1.6 纪检部经办人 → 1.7 纪检部领导 → 1.8 分管所领导 → 1.9 纪委书记 → 1.10 党委书记 → 结束
*/
public class InspectCloseoutRegressionTest extends FlowRegressionBase {
/** 办结 happy path1.1 后 is_end=1 直跳 1.4 */
@Test
void happyPath() {
runTestCase(() -> {
String businessId = createCloseoutBusiness();
String procId = startFlow(FlowRegressionProps.CLOSEOUT, businessId, FlowRegressionProps.INITIATOR);
setProcessVariable(procId, "is_end", "1", FlowRegressionProps.INITIATOR);
List<ActiveTask> settled = waitSettled(procId, 3);
log.info("[回归] 自动推进稳定后活跃任务: {}", settled);
for (int i = 0; i < 20 && !isFinished(procId); i++) {
List<ActiveTask> tasks = completeCurrentNode(procId);
log.info("[回归] 节点推进后活跃任务: {}", tasks);
}
assertFinished(procId);
});
}
private String createCloseoutBusiness() {
String id = "REGX" + System.currentTimeMillis();
JSONObject body = new JSONObject();
body.put("id", id);
// 整改任务必须挂在具体问题节点下
body.put("problemId", "2085316815648165890");
body.put("measureResDept", FlowRegressionProps.DEPT_DQ);
body.put("measureResLeader", "dq_ld_01");
body.put("chargeLeaderId", "sz");
body.put("dqInspectProgressList", new JSONArray());
JSONObject resp = post("/dqinspecttask/dqInspectTask/add", null, body, FlowRegressionProps.INITIATOR);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("创建巡视整改销号任务失败, resp=" + resp);
}
return id;
}
}
@@ -0,0 +1,76 @@
package org.jeecg.modules.supervision.flowregression;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* 巡视整改提升流程回归测试。
* flowCode=inspect_flow_create_01 → process_key=process_1780563034974
* 节点:1.1 发起→1.2 党群负责人→1.3 责任部门领导分配→(经办人 1.4)→1.5 部门领导→1.6 党群经办人→1.7 党群负责人→结束
*/
public class InspectImproveRegressionTest extends FlowRegressionBase {
/** 无经办人(办结)happy path1.1-1.3 自动推进后直跳 1.5 */
@Test
void happyPath() {
runTestCase(() -> {
String businessId = createImproveBusiness();
String procId = startFlow(FlowRegressionProps.IMPROVE, businessId, FlowRegressionProps.INITIATOR);
// 强制走"办结"路径:1.3 后网关 is_end=1 → 直跳 1.5,避免 NULL 经办人节点
setProcessVariable(procId, "is_end", "1", FlowRegressionProps.INITIATOR);
List<ActiveTask> settled = waitSettled(procId, 3);
log.info("[回归] 自动推进稳定后活跃任务: {}", settled);
for (int i = 0; i < 15 && !isFinished(procId); i++) {
List<ActiveTask> tasks = completeCurrentNode(procId);
log.info("[回归] 节点推进后活跃任务: {}", tasks);
}
assertFinished(procId);
});
}
/** 驳回路径:1.6 党群经办人审查 驳回到 1.5 责任部门领导审批,再重走到结束 */
@Test
void rejectPath_atNode16_backTo15() {
runTestCase(() -> {
String businessId = createImproveBusiness();
String procId = startFlow(FlowRegressionProps.IMPROVE, businessId, FlowRegressionProps.INITIATOR);
setProcessVariable(procId, "is_end", "1", FlowRegressionProps.INITIATOR);
// 办结路径:自动推进 + 驱动 1.1-1.3 → 1.6
driveUntil(procId, "Task_14226iy");
List<ActiveTask> tasks = assertCurrentNode(procId, "Task_14226iy");
ActiveTask task16 = tasks.get(0);
log.info("[回归] 1.6 驳回至 1.5, taskId: {}, assignee: {}", task16.taskId, task16.assignee);
reject(task16.taskId, task16.assignee, "Task_0mxtg05", "驳回测试");
// 驳回后回到 1.5,重新驱动到结束
for (int i = 0; i < 15 && !isFinished(procId); i++) {
List<ActiveTask> after = completeCurrentNode(procId);
log.info("[回归] 驳回后节点推进: {}", after);
}
assertFinished(procId);
});
}
private String createImproveBusiness() {
String id = "REGT" + System.currentTimeMillis();
JSONObject body = new JSONObject();
body.put("id", id);
// 整改任务必须挂在具体问题节点下(DqInspectTaskServiceImpl.validateProblemId
body.put("problemId", "2085316815648165890");
body.put("measureResDept", FlowRegressionProps.DEPT_DQ);
body.put("measureResLeader", "dq_ld_01");
body.put("isNeedAppro", "1");
body.put("supDeptleaderid", "dq_ld_01");
body.put("chargeLeaderId", "dq_ld_01");
body.put("dqInspectProgressList", new JSONArray());
JSONObject resp = post("/dqinspecttask/dqInspectTask/add", null, body, FlowRegressionProps.INITIATOR);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("创建巡视整改任务失败, resp=" + resp);
}
return id;
}
}
@@ -0,0 +1,95 @@
package org.jeecg.modules.supervision.flowregression;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 登记本次回归测试创建的资源;通过后由基类清理(Flowable/task_task 走 HTTP
* deleteSingleProcess,此处清理业务表 + 抄送 + 流程关联表残留),失败保留供排查。
*/
public class ResourceRegistry {
private final List<String> processInstIds = new ArrayList<>();
/** 业务主表 table -> ids */
private final Map<String, List<String>> businessMain = new LinkedHashMap<>();
/** 业务子表 subTable -> mainIds(按 main_id 删) */
private final Map<String, List<String>> businessSub = new LinkedHashMap<>();
public void registerProcess(String processInstId) {
if (processInstId != null && !processInstId.isEmpty()) {
processInstIds.add(processInstId);
}
}
public void registerBusiness(String table, String id) {
businessMain.computeIfAbsent(table, k -> new ArrayList<>()).add(id);
}
public void registerSub(String subTable, String mainId) {
businessSub.computeIfAbsent(subTable, k -> new ArrayList<>()).add(mainId);
}
public List<String> getProcessInstIds() {
return processInstIds;
}
/** 清理 DB 业务残留:流程关联表 → 抄送 → 子表 → 主表 */
public void cleanupJdbc() {
try (Connection conn = DriverManager.getConnection(FlowRegressionProps.JDBC_URL,
FlowRegressionProps.JDBC_USER, FlowRegressionProps.JDBC_PASSWORD)) {
conn.setAutoCommit(false);
try {
deleteByIn(conn, "ext_act_flow_data", "process_inst_id", processInstIds);
deleteByIn(conn, "ext_act_task_cc", "proc_inst_id", processInstIds);
// deleteSingleProcess 对 task_task 是逻辑删除(@TableLogic),这里物理清掉
deleteByIn(conn, "task_task", "process_inst_id", processInstIds);
for (Map.Entry<String, List<String>> e : businessSub.entrySet()) {
deleteByIn(conn, e.getKey(), "main_id", e.getValue());
}
for (Map.Entry<String, List<String>> e : businessMain.entrySet()) {
deleteByIn(conn, e.getKey(), "id", e.getValue());
}
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
}
} catch (Exception e) {
throw new IllegalStateException("测试数据清理失败", e);
}
}
private void deleteByIn(Connection conn, String table, String col, List<String> values) throws SQLException {
if (values == null || values.isEmpty()) {
return;
}
StringBuilder sb = new StringBuilder("DELETE FROM ").append(table)
.append(" WHERE ").append(col).append(" IN (");
for (int i = 0; i < values.size(); i++) {
sb.append("?");
if (i < values.size() - 1) {
sb.append(",");
}
}
sb.append(")");
try (PreparedStatement ps = conn.prepareStatement(sb.toString())) {
for (int i = 0; i < values.size(); i++) {
ps.setString(i + 1, values.get(i));
}
ps.executeUpdate();
}
}
public void printResidue() {
System.out.println("[回归] 测试失败,保留以下数据以便排查:");
System.out.println("[回归] processInstIds = " + processInstIds);
System.out.println("[回归] businessMain = " + businessMain);
System.out.println("[回归] businessSub = " + businessSub);
}
}
@@ -0,0 +1,69 @@
package org.jeecg.modules.supervision.flowregression;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Test;
import java.util.List;
/**
* 习讲话经办人反馈流程回归测试。
* flowCode=bg_xi_speak_fb_01 → process_key=xispeak经办人反馈流程(test_src 中文 key
* 节点:1.1 部门经办人反馈完成情况 →1.3 部门负责人审批→1.4 督办经办人核查
* →1.5 督办部门负责人审批→1.6 纪检经办人监察→1.7 纪检负责人审批→结束
*/
public class XiSpeakFeedbackRegressionTest extends FlowRegressionBase {
private static final String DD_DEPT = "2044677731423850498";
@Test
void happyPath() {
runTestCase(() -> {
String businessId = createXiSpeakBusiness();
// 1.1 多实例取 approve_info.implUserNameList1.3 取 detail.approverNamesaveSubApprove 不设 approverName,走 edit
JSONObject approveInfo = new JSONObject();
JSONObject detail = new JSONObject();
detail.put("isEnd", 1);
detail.put("deptId", DD_DEPT);
detail.put("implWorker", "dd_ld_01");
detail.put("implUserNameList", new JSONArray().fluentAdd("dd_ld_01"));
detail.put("approverName", "dd_ld_02");
approveInfo.put(DD_DEPT, detail);
JSONObject editBody = new JSONObject();
editBody.put("id", businessId);
editBody.put("approveInfo", approveInfo);
post("/bg/xispeak/bgXiSpeak/edit", null, editBody, FlowRegressionProps.INITIATOR);
JSONObject jsonData = new JSONObject();
jsonData.put("business_id", businessId);
// 1.1 多实例按 task_task.deptHandlerName 评估,须在发起请求里带上
String procId = startFlow(FlowRegressionProps.XI_SPEAK_FB, businessId, jsonData, "dd_ld_01",
FlowRegressionProps.INITIATOR);
List<ActiveTask> settled = waitSettled(procId, 3);
log.info("[回归] 自动推进稳定后活跃任务: {}", settled);
for (int i = 0; i < 15 && !isFinished(procId); i++) {
List<ActiveTask> tasks = completeCurrentNode(procId);
log.info("[回归] 节点推进后活跃任务: {}", tasks);
}
assertFinished(procId);
});
}
private String createXiSpeakBusiness() {
String id = "REGFB" + System.currentTimeMillis();
JSONObject body = new JSONObject();
body.put("id", id);
body.put("secret_level", 0);
body.put("need_sdw_approval", 0);
body.put("impl_dept", DD_DEPT);
body.put("responsible_person", "dd_ld_01");
body.put("impl_measures", "回归测试反馈");
body.put("completion_status", 0);
body.put("completed_stage", 0);
JSONObject resp = post("/bg/xispeak/bgXiSpeak/add", null, body, FlowRegressionProps.INITIATOR);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("创建习讲话反馈事项失败, resp=" + resp);
}
return id;
}
}
@@ -0,0 +1,66 @@
package org.jeecg.modules.supervision.flowregression;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.junit.jupiter.api.Test;
import java.util.List;
/**
* 习讲话审批流程回归测试。
* flowCode=dev_bg_xi_speak_001 → process_key=bg_xispeak_process
* 收集线:1.1 督办经办人发起→1.2 督办负责人→(needSdw=0 跳过 1.3)→1.4 收集部门负责人分配
* →(isEnd=1 直 1.6 审批)→1.7 收集督办发起人确认
* 反馈线:1.8 反馈部门经办人→1.11 督办经办人核查→1.12 督办部门负责人审批
* →1.13 纪检经办人监察→1.14 纪检负责人审批→1.15 督办部门经办人确认(归档,抄送)→结束
*/
public class XiSpeakRegressionTest extends FlowRegressionBase {
private static final String DD_DEPT = "2044677731423850498";
@Test
void happyPath() {
runTestCase(() -> {
String businessId = createXiSpeakBusiness();
JSONObject jsonData = new JSONObject();
jsonData.put("business_id", businessId);
jsonData.put("needSdwApproval", 0);
jsonData.put("feedback_right_now", 0);
jsonData.put("sdwLeaderList", new JSONArray());
jsonData.put("implDept", DD_DEPT);
String procId = startFlow(FlowRegressionProps.XI_SPEAK, businessId, jsonData, FlowRegressionProps.INITIATOR);
// 写部门审批明细:isEnd=1 收集完成直 1.6implWorker 指定经办人
JSONObject subApprove = new JSONObject();
subApprove.put("mainId", businessId);
subApprove.put("deptId", DD_DEPT);
subApprove.put("isEnd", 1);
subApprove.put("implWorker", "dd_ld_01");
post("/bg/xispeak/bgXiSpeak/saveSubApprove", null, subApprove, FlowRegressionProps.INITIATOR);
List<ActiveTask> settled = waitSettled(procId, 3);
log.info("[回归] 自动推进稳定后活跃任务: {}", settled);
for (int i = 0; i < 20 && !isFinished(procId); i++) {
List<ActiveTask> tasks = completeCurrentNode(procId);
log.info("[回归] 节点推进后活跃任务: {}", tasks);
}
assertFinished(procId);
});
}
private String createXiSpeakBusiness() {
String id = "REGXSP" + System.currentTimeMillis();
JSONObject body = new JSONObject();
body.put("id", id);
body.put("secret_level", 0);
body.put("need_sdw_approval", 0);
body.put("impl_dept", DD_DEPT);
body.put("responsible_person", "dd_ld_01");
body.put("impl_measures", "回归测试措施");
body.put("completion_status", 0);
body.put("completed_stage", 0);
JSONObject resp = post("/bg/xispeak/bgXiSpeak/add", null, body, FlowRegressionProps.INITIATOR);
if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) {
throw new IllegalStateException("创建习讲话事项失败, resp=" + resp);
}
return id;
}
}
@@ -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}");
}
}
}
@@ -1,17 +0,0 @@
package org.jeecg.modules.test.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.jeecg.modules.test.entity.TestMainTable;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
public interface TestMainTableMapper extends BaseMapper<TestMainTable> {
}
@@ -1,31 +0,0 @@
package org.jeecg.modules.test.mapper;
import java.util.List;
import org.jeecg.modules.test.entity.TestSonTable;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
public interface TestSonTableMapper extends BaseMapper<TestSonTable> {
/**
* 通过主表id删除子表数据
*
* @param mainId 主表id
* @return boolean
*/
public boolean deleteByMainId(@Param("mainId") String mainId);
/**
* 通过主表id查询子表数据
*
* @param mainId 主表id
* @return List<TestSonTable>
*/
public List<TestSonTable> selectByMainId(@Param("mainId") String mainId);
}
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.test.mapper.TestMainTableMapper">
</mapper>
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.test.mapper.TestSonTableMapper">
<delete id="deleteByMainId" parameterType="java.lang.String">
DELETE
FROM test_son_table
WHERE
main_table_id = #{mainId} </delete>
<select id="selectByMainId" parameterType="java.lang.String" resultType="org.jeecg.modules.test.entity.TestSonTable">
SELECT *
FROM test_son_table
WHERE
main_table_id = #{mainId} </select>
</mapper>
@@ -1,48 +0,0 @@
package org.jeecg.modules.test.service;
import org.jeecg.modules.test.entity.TestSonTable;
import org.jeecg.modules.test.entity.TestMainTable;
import com.baomidou.mybatisplus.extension.service.IService;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
public interface ITestMainTableService extends IService<TestMainTable> {
/**
* 添加一对多
*
* @param testMainTable
* @param testSonTableList
*/
public void saveMain(TestMainTable testMainTable,List<TestSonTable> testSonTableList) ;
/**
* 修改一对多
*
* @param testMainTable
* @param testSonTableList
*/
public void updateMain(TestMainTable testMainTable,List<TestSonTable> testSonTableList);
/**
* 删除一对多
*
* @param id
*/
public void delMain (String id);
/**
* 批量删除一对多
*
* @param idList
*/
public void delBatchMain (Collection<? extends Serializable> idList);
}
@@ -1,22 +0,0 @@
package org.jeecg.modules.test.service;
import org.jeecg.modules.test.entity.TestSonTable;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
public interface ITestSonTableService extends IService<TestSonTable> {
/**
* 通过主表id查询子表数据
*
* @param mainId 主表id
* @return List<TestSonTable>
*/
public List<TestSonTable> selectByMainId(String mainId);
}
@@ -1,82 +0,0 @@
package org.jeecg.modules.test.service.impl;
import org.jeecg.common.aspect.annotation.CascadeDelete;
import org.jeecg.common.aspect.annotation.SonTable;
import org.jeecg.modules.test.entity.TestMainTable;
import org.jeecg.modules.test.entity.TestSonTable;
import org.jeecg.modules.test.mapper.TestSonTableMapper;
import org.jeecg.modules.test.mapper.TestMainTableMapper;
import org.jeecg.modules.test.service.ITestMainTableService;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.util.List;
import java.util.Collection;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
@Service
public class TestMainTableServiceImpl extends ServiceImpl<TestMainTableMapper, TestMainTable> implements ITestMainTableService {
@Autowired
private TestMainTableMapper testMainTableMapper;
@Autowired
private TestSonTableMapper testSonTableMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void saveMain(TestMainTable testMainTable, List<TestSonTable> testSonTableList) {
testMainTableMapper.insert(testMainTable);
if(testSonTableList!=null && testSonTableList.size()>0) {
for(TestSonTable entity:testSonTableList) {
//外键设置
entity.setMainTableId(testMainTable.getId());
testSonTableMapper.insert(entity);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateMain(TestMainTable testMainTable,List<TestSonTable> testSonTableList) {
testMainTableMapper.updateById(testMainTable);
//1.先删除子表数据
testSonTableMapper.deleteByMainId(testMainTable.getId());
//2.子表数据重新插入
if(testSonTableList!=null && testSonTableList.size()>0) {
for(TestSonTable entity:testSonTableList) {
//外键设置
entity.setMainTableId(testMainTable.getId());
testSonTableMapper.insert(entity);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
@CascadeDelete(sons = {
// 配置子表1:Mapper类 + 子表中关联主表的字段名
@SonTable(mapper = TestSonTableMapper.class, joinColumn = "main_table_id"),
})
public void delMain(String id) {
testMainTableMapper.deleteById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delBatchMain(Collection<? extends Serializable> idList) {
for(Serializable id:idList) {
testSonTableMapper.deleteByMainId(id.toString());
testMainTableMapper.deleteById(id);
}
}
}
@@ -1,27 +0,0 @@
package org.jeecg.modules.test.service.impl;
import org.jeecg.modules.test.entity.TestSonTable;
import org.jeecg.modules.test.mapper.TestSonTableMapper;
import org.jeecg.modules.test.service.ITestSonTableService;
import org.springframework.stereotype.Service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
@Service
public class TestSonTableServiceImpl extends ServiceImpl<TestSonTableMapper, TestSonTable> implements ITestSonTableService {
@Autowired
private TestSonTableMapper testSonTableMapper;
@Override
public List<TestSonTable> selectByMainId(String mainId) {
return testSonTableMapper.selectByMainId(mainId);
}
}
@@ -1,64 +0,0 @@
package org.jeecg.modules.test.vo;
import java.util.List;
import org.jeecg.modules.test.entity.TestMainTable;
import org.jeecg.modules.test.entity.TestSonTable;
import lombok.Data;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.jeecgframework.poi.excel.annotation.ExcelEntity;
import org.jeecgframework.poi.excel.annotation.ExcelCollection;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.constant.ProvinceCityArea;
import org.jeecg.common.util.SpringContextUtils;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* @Description: test
* @Author: jeecg-boot
* @Date: 2026-04-03
* @Version: V1.0
*/
@Data
@Schema(description="test")
public class TestMainTablePage {
/**主键*/
@Schema(description = "主键")
private java.lang.String id;
/**创建人*/
@Schema(description = "创建人")
private java.lang.String createBy;
/**创建日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private java.util.Date createTime;
/**更新人*/
@Schema(description = "更新人")
private java.lang.String updateBy;
/**更新日期*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private java.util.Date updateTime;
/**所属部门*/
@Schema(description = "所属部门")
private java.lang.String sysOrgCode;
/**a字段*/
@Excel(name = "a字段", width = 15)
@Schema(description = "a字段")
private java.lang.String fieldA;
/**b字段*/
@Excel(name = "b字段", width = 15)
@Schema(description = "b字段")
private java.lang.String fieldB;
@ExcelCollection(name="test")
@Schema(description = "test")
private List<TestSonTable> testSonTableList;
}
@@ -1,71 +0,0 @@
import {defHttp} from '/@/utils/http/axios';
import { useMessage } from "/@/hooks/web/useMessage";
const { createConfirm } = useMessage();
enum Api {
list = '/test/testMainTable/list',
save='/test/testMainTable/add',
edit='/test/testMainTable/edit',
deleteOne = '/test/testMainTable/delete',
deleteBatch = '/test/testMainTable/deleteBatch',
importExcel = '/test/testMainTable/importExcel',
exportXls = '/test/testMainTable/exportXls',
testSonTableList = '/test/testMainTable/queryTestSonTableByMainId',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 查询子表数据
* @param params
*/
export const testSonTableList = Api.testSonTableList;
/**
* 列表接口
* @param params
*/
export const list = (params) =>
defHttp.get({url: Api.list, params});
/**
* 删除单个
*/
export const deleteOne = (params,handleSuccess) => {
return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
/**
* 批量删除
* @param params
*/
export const batchDelete = (params, handleSuccess) => {
createConfirm({
iconType: 'warning',
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => {
handleSuccess();
});
}
});
}
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({url: url, params});
}
@@ -1,81 +0,0 @@
import {BasicColumn} from '/@/components/Table';
import {FormSchema} from '/@/components/Table';
import { rules} from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
import {JVxeTypes,JVxeColumn} from '/@/components/jeecg/JVxeTable/types'
import { getWeekMonthQuarterYear } from '/@/utils';
//列表数据
export const columns: BasicColumn[] = [
{
title: 'a字段',
align:"center",
dataIndex: 'fieldA'
},
{
title: 'b字段',
align:"center",
dataIndex: 'fieldB'
},
];
//查询数据
export const searchFormSchema: FormSchema[] = [
];
//表单数据
export const formSchema: FormSchema[] = [
{
label: 'a字段',
field: 'fieldA',
component: 'Input',
},
{
label: 'b字段',
field: 'fieldB',
component: 'Input',
},
// TODO 主键隐藏字段,目前写死为ID
{
label: '',
field: 'id',
component: 'Input',
show: false
},
];
//子表单数据
export const testSonTableFormSchema: FormSchema[] = [
{
label: '字段c',
field: 'fieldC',
component: 'Input',
},
{
label: '',
field: 'id',
component: 'Input',
show: false
},
];
//子表表格配置
// 高级查询数据
export const superQuerySchema = {
fieldA: {title: 'a字段',order: 0,view: 'text', type: 'string',},
fieldB: {title: 'b字段',order: 1,view: 'text', type: 'string',},
//子表高级查询
testSonTable: {
title: 'test',
view: 'table',
fields: {
fieldC: {title: '字段c',order: 0,view: 'text', type: 'string',},
}
},
};
/**
* 流程表单调用这个方法获取formSchema
* @param param
*/
export function getBpmFormSchema(_formData): FormSchema[]{
// 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
return formSchema;
}
@@ -1,207 +0,0 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" v-auth="'test:test_main_table:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
<a-button type="primary" v-auth="'test:test_main_table:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" v-auth="'test:test_main_table:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button v-auth="'test:test_main_table:deleteBatch'">批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<!-- 高级查询 -->
<super-query :config="superQueryConfig" @search="handleSuperQuery" />
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/>
</template>
<!--字段回显插槽-->
<template v-slot:bodyCell="{ column, record, index, text }">
</template>
</BasicTable>
<!-- 表单区域 -->
<TestMainTableModal @register="registerModal" @success="handleSuccess"></TestMainTableModal>
</div>
</template>
<script lang="ts" name="test-testMainTable" setup>
import {ref, reactive, computed, unref} from 'vue';
import {BasicTable, useTable, TableAction} from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage'
import {useModal} from '/@/components/Modal';
import TestMainTableModal from './components/TestMainTableModal.vue'
import {columns, searchFormSchema, superQuerySchema} from './TestMainTable.data';
import {list, deleteOne, batchDelete, getImportUrl,getExportUrl} from './TestMainTable.api';
import {downloadFile} from '/@/utils/common/renderUtils';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({
});
const queryParam = reactive<any>({});
const checkedKeys = ref<Array<string | number>>([]);
const userStore = useUserStore();
const { createMessage } = useMessage();
//注册model
const [registerModal, {openModal}] = useModal();
//注册table数据
const { prefixCls,tableContext,onExportXls,onImportXls } = useListPage({
tableProps:{
title: 'test',
api: list,
columns,
canResize:true,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter:true,
showAdvancedButton:true,
fieldMapToNumber: [
],
fieldMapToTime: [
],
},
actionColumn: {
width: 120,
fixed:'right'
},
beforeFetch: (params) => {
if (params && fieldPickers) {
for (let key in fieldPickers) {
if (params[key]) {
params[key] = getDateByPicker(params[key], fieldPickers[key]);
}
}
}
return Object.assign(params, queryParam);
},
},
exportConfig: {
name:"test",
url: getExportUrl,
params: queryParam,
},
importConfig: {
url: getImportUrl,
success: handleSuccess
},
})
const [registerTable, {reload},{ rowSelection, selectedRowKeys }] = tableContext
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({id: record.id}, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ids: selectedRowKeys.value},handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function getTableAction(record){
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'test:test_main_table:edit'
}
]
}
/**
* 下拉操作栏
*/
function getDropDownAction(record){
return [
{
label: '详情',
onClick: handleDetail.bind(null, record),
}, {
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
placement: 'topLeft'
},
auth: 'test:test_main_table:delete'
}
]
}
</script>
<style lang="less" scoped>
:deep(.ant-picker),:deep(.ant-input-number){
width: 100%;
}
</style>
@@ -1,115 +0,0 @@
<template>
<div>
<BasicForm @register="registerForm" ref="formRef"/>
<!-- 子表单区域 -->
<a-tabs v-model:activeKey="activeKey" animated @change="handleChangeTabs">
<a-tab-pane tab="test" key="testSonTable" :forceRender="true">
<TestSonTableForm ref="testSonTableForm" :disabled="formDisabled"></TestSonTableForm>
</a-tab-pane>
</a-tabs>
<div style="width: 100%;text-align: center" v-if="!formDisabled">
<a-button @click="handleSubmit" pre-icon="ant-design:check" type="primary"> </a-button>
</div>
</div>
</template>
<script lang="ts">
import {BasicForm, useForm} from '/@/components/Form/index';
import { computed, defineComponent, reactive, ref, unref } from 'vue';
import {defHttp} from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods';
import { VALIDATE_FAILED } from '/@/utils/common/vxeUtils';
import TestSonTableForm from './TestSonTableForm.vue'
import {getBpmFormSchema} from '../TestMainTable.data';
import {saveOrUpdate,testSonTableList} from '../TestMainTable.api';
export default defineComponent({
name: "TestMainTableForm",
components:{
BasicForm,
TestSonTableForm,
},
props:{
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
},
setup(props){
const [registerForm, { setFieldsValue, setProps }] = useForm({
labelWidth: 150,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: {span: 24}
});
const formDisabled = computed(()=>{
if(props.formData.disabled === false){
return false;
}
return true;
});
const refKeys = ref(['testSonTable', ]);
const activeKey = ref('testSonTable');
const testSonTableForm = ref();
const tableRefs = {};
const [handleChangeTabs,handleSubmit,requestSubTableData,formRef] = useJvxeMethod(requestAddOrEdit,classifyIntoFormData,tableRefs,activeKey,refKeys,validateSubForm);
function classifyIntoFormData(allValues) {
let main = Object.assign({}, allValues.formValue)
return {
...main, // 展开
testSonTableList: testSonTableForm.value.getFormData(),
}
}
//校验所有一对一子表表单
function validateSubForm(allValues){
return new Promise((resolve, _reject)=>{
Promise.all([
testSonTableForm.value.validateForm(0),
]).then(() => {
resolve(allValues)
}).catch(e => {
if (e.error === VALIDATE_FAILED) {
// 如果有未通过表单验证的子表,就自动跳转到它所在的tab
activeKey.value = e.index == null ? unref(activeKey) : refKeys.value[e.index]
} else {
console.error(e)
}
})
})
}
//表单提交事件
async function requestAddOrEdit(values) {
await saveOrUpdate(values, true);
}
const queryByIdUrl = '/test/testMainTable/queryById';
async function initFormData(){
let params = {id: props.formData.dataId};
const data = await defHttp.get({url: queryByIdUrl, params});
//设置表单的值
await setFieldsValue({...data});
testSonTableForm.value.initFormData(testSonTableList, data.id);
//默认是禁用
await setProps({disabled: formDisabled.value})
}
initFormData();
return {
registerForm,
formDisabled,
formRef,
handleSubmit,
activeKey,
handleChangeTabs,
testSonTableForm,
}
}
});
</script>
@@ -1,144 +0,0 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
<BasicForm @register="registerForm" ref="formRef" name="TestMainTableForm"/>
<!-- 子表单区域 -->
<a-tabs v-model:activeKey="activeKey" animated @change="handleChangeTabs">
<a-tab-pane tab="test" key="testSonTable" :forceRender="true">
<TestSonTableForm ref="testSonTableForm" :disabled="formDisabled"></TestSonTableForm>
</a-tab-pane>
</a-tabs>
</BasicModal>
</template>
<script lang="ts" setup>
import {ref, computed, unref,reactive} from 'vue';
import {BasicModal, useModalInner} from '/@/components/Modal';
import {BasicForm, useForm} from '/@/components/Form/index';
import { JVxeTable } from '/@/components/jeecg/JVxeTable'
import { useJvxeMethod } from '/@/hooks/system/useJvxeMethods.ts'
import TestSonTableForm from './TestSonTableForm.vue'
import {formSchema} from '../TestMainTable.data';
import {saveOrUpdate,testSonTableList} from '../TestMainTable.api';
import { VALIDATE_FAILED } from '/@/utils/common/vxeUtils'
import { useMessage } from '/@/hooks/web/useMessage';
import { getDateByPicker } from '/@/utils';
//日期个性化选择
const fieldPickers = reactive({
});
const { createMessage } = useMessage();
// Emits声明
const emit = defineEmits(['register','success']);
const isUpdate = ref(true);
const formDisabled = ref(false);
const refKeys = ref(['testSonTable', ]);
const activeKey = ref('testSonTable');
const testSonTableForm = ref();
const tableRefs = {};
//表单配置
const [registerForm, {setProps,resetFields, setFieldsValue, validate}] = useForm({
labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: {span: 24}
});
//表单赋值
const [registerModal, {setModalProps, closeModal}] = useModalInner(async (data) => {
//重置表单
await reset();
setModalProps({confirmLoading: false,showCancelBtn:data?.showFooter,showOkBtn:data?.showFooter});
isUpdate.value = !!data?.isUpdate;
formDisabled.value = !data?.showFooter;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
testSonTableForm.value.initFormData(testSonTableList,data?.record?.id)
}
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter })
});
//方法配置
const [handleChangeTabs,handleSubmit,requestSubTableData,formRef] = useJvxeMethod(requestAddOrEdit,classifyIntoFormData,tableRefs,activeKey,refKeys,validateSubForm);
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(formDisabled) ? '编辑' : '详情'));
async function reset(){
await resetFields();
activeKey.value = 'testSonTable';
testSonTableForm.value.resetFields();
}
function classifyIntoFormData(allValues) {
let main = Object.assign({}, allValues.formValue)
return {
...main, // 展开
testSonTableList: testSonTableForm.value.getFormData(),
}
}
//校验所有一对一子表表单
function validateSubForm(allValues){
return new Promise((resolve,reject)=>{
Promise.all([
testSonTableForm.value.validateForm(0),
]).then(() => {
resolve(allValues)
}).catch(e => {
if (e.error === VALIDATE_FAILED) {
// 如果有未通过表单验证的子表,就自动跳转到它所在的tab
activeKey.value = e.index == null ? unref(activeKey) : refKeys.value[e.index]
if (e.errorFields) {
const firstField = e.errorFields[0];
if (firstField) {
e.scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
}
}
} else {
console.error(e)
}
})
})
}
//表单提交事件
async function requestAddOrEdit(values) {
try {
// 预处理日期数据
changeDateValue(values);
setModalProps({confirmLoading: true});
//提交表单
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({confirmLoading: false});
}
}
/**
* 处理日期值
* @param formData 表单数据
*/
const changeDateValue = (formData) => {
if (formData && fieldPickers) {
for (let key in fieldPickers) {
if (formData[key]) {
formData[key] = getDateByPicker(formData[key], fieldPickers[key]);
}
}
}
};
</script>
<style lang="less" scoped>
/** 时间和数字输入框样式 */
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-calendar-picker) {
width: 100%;
}
</style>
@@ -1,79 +0,0 @@
<template>
<BasicForm @register="registerForm" name="TestSonTableForm" class="basic-modal-form"/>
</template>
<script lang="ts">
import {defineComponent} from 'vue';
import {BasicForm, useForm} from '/@/components/Form/index';
import {testSonTableFormSchema} from '../TestMainTable.data';
import {defHttp} from '/@/utils/http/axios';
import { VALIDATE_FAILED } from '/@/utils/common/vxeUtils'
export default defineComponent({
name:"TestSonTableForm",
components: {BasicForm},
emits:['register'],
props:{
disabled: {
type: Boolean,
default: false
}
},
setup(props,{emit}) {
const [registerForm, { setProps, resetFields, setFieldsValue, getFieldsValue, validate, scrollToField }] = useForm({
labelWidth: 150,
schemas: testSonTableFormSchema,
showActionButtonGroup: false,
baseColProps: {span: 24}
});
/**
*初始化加载数据
*/
function initFormData(url,id){
if(id){
defHttp.get({url,params:{id}},{isTransformResponse:false}).then(res=>{
res.success && setFieldsValue({...res.result[0]});
})
}
setProps({disabled: props.disabled})
}
/**
*获取表单数据
*/
function getFormData(){
let formData = getFieldsValue();
Object.keys(formData).map(k=>{
if(formData[k] instanceof Array){
formData[k] = formData[k].join(',')
}
});
return [formData];
}
/**
*表单校验
*/
function validateForm(index){
return new Promise((resolve, reject) => {
// 验证子表表单
validate().then(()=>{
return resolve()
}).catch(({ errorFields }) => {
return reject({ error: VALIDATE_FAILED, index, errorFields: errorFields, scrollToField: scrollToField });
});
})
}
return {
registerForm,
resetFields,
initFormData,
getFormData,
validateForm
}
}
})
</script>
<style lang="less" scoped>
.basic-modal-form {
overflow: auto;
height: 340px;
}
</style>
@@ -12,6 +12,11 @@
<artifactId>jeecg-system-cloud-api</artifactId>
<dependencies>
<!-- 基础核心(代码直接使用 base-core 的 Result/LoginUser/LowAppCopyMenu 等) -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-base-core</artifactId>
</dependency>
<!-- feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
@@ -538,6 +538,31 @@ public class SysUserController {
return Result.OK(pageList);
}
/**
* 用户选择组件 专用 根据角色ID查询用户列表
* @param pageNo
* @param pageSize
* @param roleId
* @return
*/
@RequestMapping(value = "/queryUserByRoleId", method = RequestMethod.GET)
@Operation(summary = "根据角色ID查询用户列表", description = "根据输入的roleId返回该角色下的所有用户(分页)")
public Result<IPage<SysUser>> queryUserByRoleId(
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
@RequestParam(name = "departId", required = false) String departId,
@RequestParam(name = "roleId", required = false) String roleId,
@RequestParam(name="realname",required=false) String realname,
@RequestParam(name="username",required=false) String username,
@RequestParam(name="isMultiTranslate",required=false) String isMultiTranslate,
@RequestParam(name="id",required = false) String id,
@RequestParam(name="searchSecurityLevel",required = false) Integer searchSecurityLevel
) {
Page<SysUser> page = new Page<>(pageNo, pageSize);
IPage<SysUser> pageList = sysUserService.getUserByRoleIdOptimized(page, roleId, username);
return Result.OK(pageList);
}
/**
* 用户选择组件 专用 根据用户账号或部门,角色分页查询
* @param deptRoleVO
@@ -81,6 +81,15 @@ public interface SysUserMapper extends BaseMapper<SysUser> {
*/
IPage<SysUser> getUserByRoleId(Page page, @Param("roleId") String roleId, @Param("username") String username);
/**
* 根据角色Id查询用户信息(优化版:INNER JOIN + DISTINCT,供专用接口使用)
* @param page
* @param roleId 角色id
* @param username 用户登录账户
* @return
*/
IPage<SysUser> getUserByRoleIdOptimized(Page page, @Param("roleId") String roleId, @Param("username") String username);
/**
* 根据角色Id查询带部门的用户信息
* @param page
@@ -59,6 +59,16 @@
</if>
</select>
<!-- 根据角色Id查询(优化版:INNER JOIN + DISTINCT,供专用接口使用) -->
<select id="getUserByRoleIdOptimized" resultType="org.jeecg.modules.system.entity.SysUser">
select distinct u.* from sys_user u
inner join sys_user_role ur on ur.user_id = u.id
where u.del_flag = 0 and ur.role_id = #{roleId}
<if test="username!=null and username!=''">
and u.username = #{username}
</if>
</select>
<!-- Multi-instance flow task query. Keep it separate from the overloaded getUserByRoleId statement. -->
<select id="getUsernamesByRoleId" resultType="java.lang.String">
select distinct u.username
@@ -184,6 +184,15 @@ public interface ISysUserService extends IService<SysUser> {
*/
public IPage<SysUser> getUserByRoleId(Page<SysUser> page,String roleId, String username);
/**
* 根据角色Id查询(优化版:INNER JOIN + DISTINCT,供专用接口使用)
* @param page
* @param roleId 角色id
* @param username 用户账户名称
* @return
*/
public IPage<SysUser> getUserByRoleIdOptimized(Page<SysUser> page,String roleId, String username);
/**
* 根据角色Id查询
* @param page
@@ -584,6 +584,30 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
//update-end---author:wangshuai ---date:20230220 for[QQYUN-3980]组织管理中 职位功能 职位表加租户id 加职位-用户关联表------------
}
/**
* 根据角色Id查询(优化版:INNER JOIN + DISTINCT,供专用接口使用)
* @param page
* @param roleId 角色id
* @param username 用户账户名称
* @return
*/
@Override
public IPage<SysUser> getUserByRoleIdOptimized(Page<SysUser> page, String roleId, String username) {
IPage<SysUser> userRoleList = userMapper.getUserByRoleIdOptimized(page, roleId, username);
List<SysUser> records = userRoleList.getRecords();
if (null != records && records.size() > 0) {
List<String> userIds = records.stream().map(SysUser::getId).collect(Collectors.toList());
Map<String, String> useDepNames = this.getDepNamesByUserIds(userIds);
for (SysUser sysUser : userRoleList.getRecords()) {
//设置部门
sysUser.setOrgCodeTxt(useDepNames.get(sysUser.getId()));
//设置用户职位id
this.userPositionId(sysUser);
}
}
return userRoleList;
}
/**
* 根据角色Id查询
* @param page
+24 -1
View File
@@ -39,6 +39,8 @@
<properties>
<jeecgboot.version>3.8.0</jeecgboot.version>
<!-- 默认跳过单测,需显式 -DskipTests=false 开启(回归测试等场景) -->
<skipTests>false</skipTests>
<!-- JDK版本支持17和1.8 -->
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -551,9 +553,30 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
<!-- JaCoCo 代码覆盖率:prepare-agent 给测试 JVM 插桩,report 在 test 阶段生成 target/site/jacoco 报告 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<id>jacoco-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 避免font文件的二进制文件格式压缩破坏 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
+2
View File
@@ -35,6 +35,8 @@ YYYYMMDD_描述.sql
| 2026-08-12 | DML | `20260812_巡视整改提升流程完整流程更新.sql` | 巡视整改提升流程(process_1780563034974)完整流程更新,全新库可建、已有库可UPDATE |
| 2026-08-12 | DML | `20260812_习讲话反馈流程完整流程更新.sql` | 习讲话经办人反馈流程(xispeak经办人反馈流程)完整流程更新(process_key含中文,需 -f 强制导出),全新库可建、已有库可UPDATE |
| 2026-08-12 | DML | `20260812_习讲话审批流程完整流程更新.sql` | 习讲话审批流程(bg_xispeak_process)完整流程更新,全新库可建、已有库可UPDATE |
| 2026-08-13 | DDL | `20260813_反馈表新增检查情况监督意见措施数量字段.sql` | dq_inspect_progress 新增 inspection_status/supervisory_opinion/measure_count 三列,保存纪检反馈字段 |
| 2026-08-17 | DML | `20260817_习讲话反馈流程process_key对齐.sql` | 习讲话经办人反馈流程 process_key 对齐为 BPMN idprocess_1778315114392),修复按 process_key 无法启动问题 |
## 使用方式
@@ -0,0 +1,9 @@
-- 反馈表 dq_inspect_progress 新增纪检反馈字段
-- 背景:反馈弹窗(DqInspectTaskBPMFeedbackModal)在 showJiJianFields 时展示并提交
-- 检查情况 / 监督意见 / 措施数量 三个字段,但子表实体与表结构均缺失这三列,
-- 导致保存时被 MyBatis-Plus 丢弃。列类型对齐主表 dq_inspect_task。
-- 变更日期:2026-08-13
ALTER TABLE `dq_inspect_progress`
ADD COLUMN `inspection_status` varchar(1000) NULL DEFAULT NULL COMMENT '检查情况' AFTER `bpm_status`,
ADD COLUMN `supervisory_opinion` varchar(1000) NULL DEFAULT NULL COMMENT '监督意见' AFTER `inspection_status`,
ADD COLUMN `measure_count` int NULL DEFAULT NULL COMMENT '措施数量' AFTER `supervisory_opinion`;
@@ -0,0 +1,9 @@
-- 习讲话经办人反馈流程 process_key 对齐
-- 背景:该流程 ext_act_process.process_key 曾为中文(xispeak经办人反馈流程),
-- 与部署的 BPMN <process id>process_1778315114392)不一致,导致 startWorkflow
-- 按 process_key 查不到已部署定义、流程无法发起("立即发起流程失败")。
-- 对齐为 BPMN id 后,按 flowCode 发起即可正常启动。
UPDATE ext_act_process
SET process_key = 'process_1778315114392'
WHERE id = '2058711133876629506'
AND process_key <> 'process_1778315114392';