yxk-20260728

改善提案
实现部门评议小组打分功能
This commit is contained in:
ye1023
2026-07-28 14:57:22 +08:00
parent 5d565a3068
commit bbebf12399
5 changed files with 270 additions and 1 deletions
@@ -33,7 +33,7 @@ import java.util.Map;
public class TaskCreatedAutoSubmitListener implements TaskListener { public class TaskCreatedAutoSubmitListener implements TaskListener {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final String DEFAULT_START_APPROVAL_OPINION = "发起督办流程。"; private static final String DEFAULT_START_APPROVAL_OPINION = "发起流程。";
private static ISysBaseAPI sysBaseAPI; private static ISysBaseAPI sysBaseAPI;
private static IExtActBpmLogService extActBpmLogService; private static IExtActBpmLogService extActBpmLogService;
protected static RuntimeService runtimeService; protected static RuntimeService runtimeService;
@@ -27,6 +27,7 @@ import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore; import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore;
import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote; import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote;
import org.jeecg.modules.improvementproposal.entity.ImprovementProposal; import org.jeecg.modules.improvementproposal.entity.ImprovementProposal;
import org.jeecg.modules.improvementproposal.dto.ImprovementDeptScoreBpmDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO; import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.modules.improvementproposal.vo.ImprovementProposalPage; import org.jeecg.modules.improvementproposal.vo.ImprovementProposalPage;
import org.jeecg.modules.improvementproposal.service.IImprovementProposalService; import org.jeecg.modules.improvementproposal.service.IImprovementProposalService;
@@ -157,6 +158,33 @@ public class ImprovementProposalController {
return handleBpmAction(() -> improvementProposalService.saveInitialReview(action, currentUser())); return handleBpmAction(() -> improvementProposalService.saveInitialReview(action, currentUser()));
} }
@GetMapping(value = "/bpm/myDeptScore")
@Operation(summary = "查询当前部门评委个人评分")
public Result<ImprovementDeptScore> getMyDeptScore(@RequestParam String mainId, @RequestParam String processInstanceId,
@RequestParam String taskId) {
ImprovementProposalBpmActionDTO action = new ImprovementProposalBpmActionDTO();
action.setMainId(mainId);
action.setProcessInstanceId(processInstanceId);
action.setTaskId(taskId);
try {
return Result.OK(improvementProposalService.getMyDeptScore(action, currentUser()));
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
}
}
@PostMapping(value = "/bpm/deptScore")
@Operation(summary = "保存当前部门评委个人评分草稿")
public Result<String> saveDeptScore(@RequestBody ImprovementDeptScoreBpmDTO score) {
return handleBpmAction(() -> improvementProposalService.saveDeptScore(score, currentUser()));
}
@PostMapping(value = "/bpm/submitDeptScore")
@Operation(summary = "锁定当前部门评委个人评分")
public Result<String> submitDeptScore(@RequestBody ImprovementProposalBpmActionDTO action) {
return handleBpmAction(() -> improvementProposalService.submitDeptScore(action, currentUser()));
}
@PostMapping(value = "/bpm/leaderAward") @PostMapping(value = "/bpm/leaderAward")
@Operation(summary = "保存部门负责人确认奖项") @Operation(summary = "保存部门负责人确认奖项")
public Result<String> saveLeaderAward(@RequestBody ImprovementProposalBpmActionDTO action) { public Result<String> saveLeaderAward(@RequestBody ImprovementProposalBpmActionDTO action) {
@@ -0,0 +1,37 @@
package org.jeecg.modules.improvementproposal.dto;
import java.math.BigDecimal;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 部门评议评委提交个人评分时使用的流程节点参数。
*/
@Data
public class ImprovementDeptScoreBpmDTO {
@Schema(description = "改善提案主表ID")
private String mainId;
@Schema(description = "流程实例ID")
private String processInstanceId;
@Schema(description = "当前流程任务ID")
private String taskId;
@Schema(description = "改善效果得分,0-100")
private BigDecimal effectScore;
@Schema(description = "可推广度得分,0-100")
private BigDecimal promotionScore;
@Schema(description = "独创性得分,0-100")
private BigDecimal originalityScore;
@Schema(description = "努力度得分,0-100")
private BigDecimal effortScore;
@Schema(description = "评分意见")
private String scoreOpinion;
}
@@ -3,6 +3,7 @@ package org.jeecg.modules.improvementproposal.service;
import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore; import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore;
import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote; import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote;
import org.jeecg.modules.improvementproposal.entity.ImprovementProposal; import org.jeecg.modules.improvementproposal.entity.ImprovementProposal;
import org.jeecg.modules.improvementproposal.dto.ImprovementDeptScoreBpmDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO; import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
@@ -62,6 +63,15 @@ public interface IImprovementProposalService extends IService<ImprovementProposa
/** 保存部门精益专员初审结果,并写入部门评分网关变量。 */ /** 保存部门精益专员初审结果,并写入部门评分网关变量。 */
void saveInitialReview(ImprovementProposalBpmActionDTO action, LoginUser loginUser); void saveInitialReview(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 获取当前部门评委在当前流程任务下的个人评分,不返回其他评委数据。 */
ImprovementDeptScore getMyDeptScore(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 保存当前部门评委的四维度评分草稿,流程任务提交前允许修改。 */
void saveDeptScore(ImprovementDeptScoreBpmDTO score, LoginUser loginUser);
/** 锁定当前评委的已保存评分,并在全部评委完成时汇总部门评议结果。 */
void submitDeptScore(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 保存部门负责人确认奖项,并写入所级投票网关变量。 */ /** 保存部门负责人确认奖项,并写入所级投票网关变量。 */
void saveLeaderAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser); void saveLeaderAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
@@ -3,6 +3,7 @@ package org.jeecg.modules.improvementproposal.service.impl;
import org.jeecg.modules.improvementproposal.entity.ImprovementProposal; import org.jeecg.modules.improvementproposal.entity.ImprovementProposal;
import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore; import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore;
import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote; import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote;
import org.jeecg.modules.improvementproposal.dto.ImprovementDeptScoreBpmDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO; import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.modules.improvementproposal.mapper.ImprovementDeptScoreMapper; import org.jeecg.modules.improvementproposal.mapper.ImprovementDeptScoreMapper;
import org.jeecg.modules.improvementproposal.mapper.ImprovementInstituteVoteMapper; import org.jeecg.modules.improvementproposal.mapper.ImprovementInstituteVoteMapper;
@@ -17,12 +18,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Collection; import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
import org.flowable.engine.RuntimeService; import org.flowable.engine.RuntimeService;
import org.flowable.engine.HistoryService;
import org.flowable.engine.TaskService; import org.flowable.engine.TaskService;
import org.flowable.task.api.Task; import org.flowable.task.api.Task;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
@@ -43,6 +47,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
private static final String FLOW_NAME = "改善提案审批"; private static final String FLOW_NAME = "改善提案审批";
private static final String BPM_FORM_URL = "gh/improvementProposal/components/ImprovementProposalBPMForm"; private static final String BPM_FORM_URL = "gh/improvementProposal/components/ImprovementProposalBPMForm";
private static final String TASK_INITIAL_REVIEW = "Task_improve001"; private static final String TASK_INITIAL_REVIEW = "Task_improve001";
private static final String TASK_DEPT_SCORE = "Task_improve002";
private static final String TASK_LEADER_AWARD = "Task_improve003"; private static final String TASK_LEADER_AWARD = "Task_improve003";
private static final String TASK_INSTITUTE_VOTE = "Task_improve005"; private static final String TASK_INSTITUTE_VOTE = "Task_improve005";
private static final String TASK_ARCHIVE = "Task_improve006"; private static final String TASK_ARCHIVE = "Task_improve006";
@@ -62,6 +67,8 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
@Autowired @Autowired
private RuntimeService runtimeService; private RuntimeService runtimeService;
@Autowired @Autowired
private HistoryService historyService;
@Autowired
private TaskService taskService; private TaskService taskService;
@Override @Override
@@ -201,6 +208,87 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
runtimeService.setVariable(action.getProcessInstanceId(), "need_dept_score", needDeptScore); runtimeService.setVariable(action.getProcessInstanceId(), "need_dept_score", needDeptScore);
} }
@Override
public ImprovementDeptScore getMyDeptScore(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
ImprovementProposal proposal = validateDeptScoreTask(action, loginUser);
return findDeptScore(proposal.getId(), loginUser.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveDeptScore(ImprovementDeptScoreBpmDTO scoreAction, LoginUser loginUser) {
validateScoreValues(scoreAction);
ImprovementProposalBpmActionDTO action = toBpmAction(scoreAction);
ImprovementProposal proposal = validateDeptScoreTask(action, loginUser);
// 锁定主表后再查询个人记录,未设置数据库唯一约束时也能阻止同一评委并发生成重复评分。
ImprovementProposal lockedProposal = improvementProposalMapper.selectByIdForUpdate(proposal.getId());
if (lockedProposal == null || !scoreAction.getProcessInstanceId().equals(lockedProposal.getProcessInstanceId())) {
throw new IllegalStateException("提案流程状态已变化,请刷新后重试");
}
ImprovementDeptScore existingScore = findDeptScore(lockedProposal.getId(), loginUser.getId());
if (existingScore != null && "completed".equals(existingScore.getScoreStatus())) {
throw new IllegalStateException("评分已锁定,请重新提交流程办理");
}
BigDecimal totalScore = calculateTotalScore(scoreAction);
if (existingScore == null) {
ImprovementDeptScore newScore = new ImprovementDeptScore();
newScore.setMainId(lockedProposal.getId());
newScore.setProcessTaskId(scoreAction.getTaskId());
newScore.setScorerId(loginUser.getId());
newScore.setScorerName(loginUser.getRealname());
fillDeptScoreDraft(newScore, scoreAction, totalScore);
improvementDeptScoreMapper.insert(newScore);
} else {
// 草稿保存只能更新 pending 记录,避免流程已提交后通过接口修改最终评分。
int updated = improvementDeptScoreMapper.update(null, new LambdaUpdateWrapper<ImprovementDeptScore>()
.eq(ImprovementDeptScore::getId, existingScore.getId())
.eq(ImprovementDeptScore::getScoreStatus, "pending")
.set(ImprovementDeptScore::getProcessTaskId, scoreAction.getTaskId())
.set(ImprovementDeptScore::getEffectScore, scoreAction.getEffectScore())
.set(ImprovementDeptScore::getPromotionScore, scoreAction.getPromotionScore())
.set(ImprovementDeptScore::getOriginalityScore, scoreAction.getOriginalityScore())
.set(ImprovementDeptScore::getEffortScore, scoreAction.getEffortScore())
.set(ImprovementDeptScore::getTotalScore, totalScore)
.set(ImprovementDeptScore::getScoreOpinion, scoreAction.getScoreOpinion())
.set(ImprovementDeptScore::getScoreStatus, "pending")
.set(ImprovementDeptScore::getScoreTime, null));
if (updated != 1) {
throw new IllegalStateException("评分状态已变化,请刷新后重试");
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submitDeptScore(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
ImprovementProposal proposal = validateDeptScoreTask(action, loginUser);
ImprovementProposal lockedProposal = improvementProposalMapper.selectByIdForUpdate(proposal.getId());
if (lockedProposal == null || !action.getProcessInstanceId().equals(lockedProposal.getProcessInstanceId())) {
throw new IllegalStateException("提案流程状态已变化,请刷新后重试");
}
ImprovementDeptScore score = findDeptScore(lockedProposal.getId(), loginUser.getId());
if (score == null) {
throw new IllegalStateException("请先保存个人评分");
}
validateSavedScore(score);
// 通用流程办理可能因网络重试再次调用 beforeHandle;已锁定时保持幂等,让任务能够再次提交完成。
if (!"completed".equals(score.getScoreStatus())) {
int updated = improvementDeptScoreMapper.update(null, new LambdaUpdateWrapper<ImprovementDeptScore>()
.eq(ImprovementDeptScore::getId, score.getId())
.eq(ImprovementDeptScore::getScoreStatus, "pending")
.set(ImprovementDeptScore::getScoreStatus, "completed")
.set(ImprovementDeptScore::getScoreTime, new Date())
.set(ImprovementDeptScore::getProcessTaskId, action.getTaskId()));
if (updated != 1) {
throw new IllegalStateException("评分状态已变化,请刷新后重试");
}
}
updateDeptScoreSummaryIfComplete(lockedProposal, action.getProcessInstanceId());
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void saveLeaderAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser) { public void saveLeaderAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
@@ -257,6 +345,112 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
} }
} }
private ImprovementProposalBpmActionDTO toBpmAction(ImprovementDeptScoreBpmDTO scoreAction) {
ImprovementProposalBpmActionDTO action = new ImprovementProposalBpmActionDTO();
action.setMainId(scoreAction.getMainId());
action.setProcessInstanceId(scoreAction.getProcessInstanceId());
action.setTaskId(scoreAction.getTaskId());
return action;
}
private ImprovementProposal validateDeptScoreTask(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_DEPT_SCORE);
Task task = taskService.createTaskQuery().taskId(action.getTaskId()).singleResult();
// 部门评议必须使用并行多实例的直接办理人任务,避免候选组任务被未分配人员越权评分。
if (task == null || !loginUser.getUsername().equals(task.getAssignee())) {
throw new IllegalStateException("当前用户不是本评分任务的办理人");
}
return proposal;
}
private ImprovementDeptScore findDeptScore(String mainId, String scorerId) {
return improvementDeptScoreMapper.selectByMainId(mainId).stream()
.filter(item -> scorerId.equals(item.getScorerId()))
.findFirst()
.orElse(null);
}
private void validateScoreValues(ImprovementDeptScoreBpmDTO scoreAction) {
if (scoreAction == null) {
throw new IllegalStateException("评分参数不能为空");
}
validateScoreValue(scoreAction.getEffectScore(), "改善效果");
validateScoreValue(scoreAction.getPromotionScore(), "可推广度");
validateScoreValue(scoreAction.getOriginalityScore(), "独创性");
validateScoreValue(scoreAction.getEffortScore(), "努力度");
}
private void validateScoreValue(BigDecimal score, String dimensionName) {
if (score == null || score.scale() > 2 || score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(BigDecimal.valueOf(100)) > 0) {
throw new IllegalStateException(dimensionName + "得分必须是 0 到 100 之间且最多两位小数的数值");
}
}
private BigDecimal calculateTotalScore(ImprovementDeptScoreBpmDTO scoreAction) {
return scoreAction.getEffectScore().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 totalScore) {
target.setEffectScore(source.getEffectScore());
target.setPromotionScore(source.getPromotionScore());
target.setOriginalityScore(source.getOriginalityScore());
target.setEffortScore(source.getEffortScore());
target.setTotalScore(totalScore);
target.setScoreOpinion(source.getScoreOpinion());
target.setScoreStatus("pending");
target.setScoreTime(null);
}
private void validateSavedScore(ImprovementDeptScore score) {
validateScoreValue(score.getEffectScore(), "改善效果");
validateScoreValue(score.getPromotionScore(), "可推广度");
validateScoreValue(score.getOriginalityScore(), "独创性");
validateScoreValue(score.getEffortScore(), "努力度");
if (score.getTotalScore() == null) {
throw new IllegalStateException("请先保存完整个人评分");
}
}
private void updateDeptScoreSummaryIfComplete(ImprovementProposal proposal, String processInstanceId) {
long expectedScorerCount = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.taskDefinitionKey(TASK_DEPT_SCORE)
.count();
List<ImprovementDeptScore> scoreList = improvementDeptScoreMapper.selectByMainId(proposal.getId());
boolean allCompleted = expectedScorerCount > 0 && scoreList.size() == expectedScorerCount
&& scoreList.stream().allMatch(item -> "completed".equals(item.getScoreStatus()) && item.getTotalScore() != null);
if (!allCompleted) {
return;
}
BigDecimal totalScore = scoreList.stream()
.map(ImprovementDeptScore::getTotalScore)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal averageScore = totalScore.divide(BigDecimal.valueOf(expectedScorerCount), 2, RoundingMode.HALF_UP);
ImprovementProposal summary = new ImprovementProposal();
summary.setId(proposal.getId());
summary.setDeptAvgScore(averageScore);
summary.setSuggestedAward(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) { private ImprovementProposal validateRunningTask(ImprovementProposalBpmActionDTO action, LoginUser loginUser, String expectedTaskDefKey) {
if (action == null || oConvertUtils.isEmpty(action.getMainId()) || oConvertUtils.isEmpty(action.getProcessInstanceId()) if (action == null || oConvertUtils.isEmpty(action.getMainId()) || oConvertUtils.isEmpty(action.getProcessInstanceId())
|| oConvertUtils.isEmpty(action.getTaskId())) { || oConvertUtils.isEmpty(action.getTaskId())) {