yxk-20260727

改善提案
重构 ImprovementProposalBPMForm.vue 为“基本信息”和“流程办理”两个区域。节点识别统一使用 formData.taskDefKey,不通过 PC 表单 URL 参数判断节点;URL 参数仅保留给将来固定开关类需求。
奖项字段统一按 金奖=0、银奖=1、铜奖=2、参与奖=3 存储,页面展示中文。need_dept_score、isneedvote 均使用字符串 1/0。
Key Changes
基本信息区域在所有流程节点统一展示,使用只读组件展示提案申请字段、改善前后说明及三个附件分组;不复用生成式编辑表单,也不允许流程办理人修改申请内容。

流程办理区域仅在运行中的待办显示:formBpm=true、PROCESS_TAB_TYPE='run' 且存在 taskId。提取统一 processInfo,包含 taskId、taskDefKey、processInstanceId、mainId。

继续使用 TaskHandleInnerContent 的 beforeHandle 钩子;按当前 taskDefKey 完成节点专属数据校验、保存和流程变量写入后,再调用通用提交办理。

Task_improve001:
提供初步评定奖项 initial_award 必填选择。
金奖、银奖自动保存 need_dept_score='1';铜奖、参与奖保存 '0'。
后端在同一节点保存接口中更新主表,并通过 Flowable runtimeService.setVariable 写入全局流程变量 need_dept_score。
BPMN 排他网关配置:${need_dept_score == '1'} 流向 Task_improve002,${need_dept_score == '0'} 流向 Task_improve003。

Task_improve002:
展示部门评分记录及“打分”入口,入口按当前登录评委定位其 improvement_dept_score 记录。
本轮仅建立评分弹窗/页面入口和子表查询展示;四维度录入、加权计算、全部评委完成校验、平均分和建议奖项计算留待下一轮实现。

Task_improve003:
只读展示初步奖项;存在评分结果时展示 dept_avg_score、suggested_award。
部门负责人必选 leader_final_award;金奖写入 isneedvote='1',其他奖项写入 '0',并同步为全局流程变量。
BPMN 排他网关配置:${isneedvote == '1'} 流向 Task_improve004,${isneedvote == '0'} 流向 Task_improve006。

Task_improve004:
本轮预留“所级评议人员”办理区域和投票名单展示,不开发专用选人组件及投票记录初始化。
后续选人功能负责创建每位评委一条 improvement_institute_vote 待投票记录,并设置 institute_total_voters;流程需同时配置所级投票任务的多实例人员集合。

Task_improve005:
提供“同意”“不同意”操作,按当前登录用户更新其已有的待投票记录,写入 vote_result、vote_status='voted'、vote_time 与当前 process_task_id。
不信任前端传入的投票人 ID;后端以登录用户定位记录,并禁止非名单人员、重复投票或已完成记录再次修改。
提交办理前校验当前评委已投票。投票统计和三分之二结果计算留待选人及多实例规则完成后统一实现。

Task_improve006:
展示部门负责人确认奖项;如经过所级投票,同时展示投票统计和投票结果。
经办人必须选择 final_award。若金奖所级投票未通过,同时将 manual_confirm_result 固定保存为 confirmed,表示已由经办人手动确认最终奖项。

后端新增流程节点专用 DTO 与接口,分别处理初审奖项、负责人奖项、归档最终奖项和个人投票;接口校验主表、流程实例、活动任务定义键及当前办理人,避免通过普通 /edit 篡改流程数据。

不使用生成代码的 updateMain 保存流程数据,避免删除并重建评分、投票子表,也避免草稿状态校验阻断流程办理。

补齐数据库定义:在 improvement_proposal 增加 isneedvote char(1),同步建表 SQL、数据库 Markdown 与实体注释;将 need_dept_score 的说明统一改为 1/0。为已建库环境提供单独的增量 ALTER TABLE 脚本。
This commit is contained in:
ye1023
2026-07-27 15:06:10 +08:00
parent 4b4802bb39
commit bc0ec8f354
5 changed files with 222 additions and 7 deletions
@@ -27,6 +27,7 @@ import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore;
import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote;
import org.jeecg.modules.improvementproposal.entity.ImprovementProposal;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.modules.improvementproposal.vo.ImprovementProposalPage;
import org.jeecg.modules.improvementproposal.service.IImprovementProposalService;
import org.jeecg.modules.improvementproposal.service.IImprovementDeptScoreService;
@@ -150,6 +151,48 @@ public class ImprovementProposalController {
}
}
@PostMapping(value = "/bpm/initialReview")
@Operation(summary = "保存部门精益专员初审结果")
public Result<String> saveInitialReview(@RequestBody ImprovementProposalBpmActionDTO action) {
return handleBpmAction(() -> improvementProposalService.saveInitialReview(action, currentUser()));
}
@PostMapping(value = "/bpm/leaderAward")
@Operation(summary = "保存部门负责人确认奖项")
public Result<String> saveLeaderAward(@RequestBody ImprovementProposalBpmActionDTO action) {
return handleBpmAction(() -> improvementProposalService.saveLeaderAward(action, currentUser()));
}
@PostMapping(value = "/bpm/archiveAward")
@Operation(summary = "保存归档最终奖项")
public Result<String> saveArchiveAward(@RequestBody ImprovementProposalBpmActionDTO action) {
return handleBpmAction(() -> improvementProposalService.saveArchiveAward(action, currentUser()));
}
@PostMapping(value = "/bpm/instituteVote")
@Operation(summary = "记录所级评委投票")
public Result<String> castInstituteVote(@RequestBody ImprovementProposalBpmActionDTO action) {
return handleBpmAction(() -> improvementProposalService.castInstituteVote(action, currentUser()));
}
private LoginUser currentUser() {
return (LoginUser) SecurityUtils.getSubject().getPrincipal();
}
private Result<String> handleBpmAction(BpmAction action) {
try {
action.run();
return Result.OK("保存成功");
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
}
}
@FunctionalInterface
private interface BpmAction {
void run();
}
/**
* 通过id删除
*
@@ -0,0 +1,38 @@
package org.jeecg.modules.improvementproposal.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 改善提案流程节点办理数据。
*
* <p>节点由后端根据 taskId 校验,不允许客户端通过该对象指定节点或办理人。</p>
*/
@Data
@Schema(description = "改善提案流程节点办理数据")
public class ImprovementProposalBpmActionDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "改善提案主表ID")
private String mainId;
@Schema(description = "流程实例ID")
private String processInstanceId;
@Schema(description = "当前流程任务ID")
private String taskId;
@Schema(description = "初步评定奖项:0金奖、1银奖、2铜奖、3参与奖")
private String initialAward;
@Schema(description = "部门负责人确认奖项:0金奖、1银奖、2铜奖、3参与奖")
private String leaderFinalAward;
@Schema(description = "归档最终奖项:0金奖、1银奖、2铜奖、3参与奖")
private String finalAward;
@Schema(description = "投票结果:agree/disagree")
private String voteResult;
}
@@ -100,11 +100,11 @@ public class ImprovementProposal implements Serializable {
@Excel(name = "部门精益专员初步评定奖项", width = 15)
@Schema(description = "部门精益专员初步评定奖项")
private String initialAward;
/**是否需要部门评议打分:Y/N*/
/**是否需要部门评议打分:1需要,0不需要*/
@Excel(name = "是否需要部门评议打分:1/0", width = 15)
@Schema(description = "是否需要部门评议打分:1/0")
private String needDeptScore;
/**是否需要部门评议打分:Y/N*/
/**是否需要所级评议投票:1需要,0不需要*/
@Excel(name = "是否需要所级评议小组投票:1/0", width = 15)
@Schema(description = "是否需要所级评议小组投票:1/0")
private String isneedvote;
@@ -3,6 +3,8 @@ package org.jeecg.modules.improvementproposal.service;
import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore;
import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote;
import org.jeecg.modules.improvementproposal.entity.ImprovementProposal;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.common.system.vo.LoginUser;
import com.baomidou.mybatisplus.extension.service.IService;
import java.io.Serializable;
import java.util.Collection;
@@ -57,6 +59,18 @@ public interface IImprovementProposalService extends IService<ImprovementProposa
*/
public void delBatchMain (Collection<? extends Serializable> idList);
/** 保存部门精益专员初审结果,并写入部门评分网关变量。 */
void saveInitialReview(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 保存部门负责人确认奖项,并写入所级投票网关变量。 */
void saveLeaderAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 保存经办人归档确认的最终奖项。 */
void saveArchiveAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 记录当前所级评委的投票结果。 */
void castInstituteVote(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/**
* 获取指定年度最新提案编号中的XXXX部分
*
@@ -3,12 +3,14 @@ package org.jeecg.modules.improvementproposal.service.impl;
import org.jeecg.modules.improvementproposal.entity.ImprovementProposal;
import org.jeecg.modules.improvementproposal.entity.ImprovementDeptScore;
import org.jeecg.modules.improvementproposal.entity.ImprovementInstituteVote;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.modules.improvementproposal.mapper.ImprovementDeptScoreMapper;
import org.jeecg.modules.improvementproposal.mapper.ImprovementInstituteVoteMapper;
import org.jeecg.modules.improvementproposal.mapper.ImprovementProposalMapper;
import org.jeecg.modules.improvementproposal.service.IImprovementProposalService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.extbpm.process.service.impl.BpmBaseExtApiImpl;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -17,6 +19,11 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.Serializable;
import java.util.List;
import java.util.Collection;
import java.util.Date;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.task.api.Task;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -32,6 +39,14 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
private static final String BPM_STATUS_DRAFT = "1";
private static final String FLOW_CODE = "dev_improvement_proposal_001";
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_LEADER_AWARD = "Task_improve003";
private static final String TASK_INSTITUTE_VOTE = "Task_improve005";
private static final String TASK_ARCHIVE = "Task_improve006";
private static final String AWARD_GOLD = "0";
private static final String AWARD_SILVER = "1";
private static final String AWARD_BRONZE = "2";
private static final String AWARD_PARTICIPATION = "3";
@Autowired
private ImprovementProposalMapper improvementProposalMapper;
@@ -41,6 +56,10 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
private ImprovementInstituteVoteMapper improvementInstituteVoteMapper;
@Autowired
private BpmBaseExtApiImpl bpmBaseExtApi;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -154,6 +173,107 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
}
}
@Override
@Transactional(rollbackFor = Exception.class)
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";
ImprovementProposal update = new ImprovementProposal();
update.setId(proposal.getId());
update.setInitialAward(action.getInitialAward());
update.setNeedDeptScore(needDeptScore);
improvementProposalMapper.updateById(update);
// 网关紧随当前任务;提交任务前必须写入全局变量,不能只依赖主表字段。
runtimeService.setVariable(action.getProcessInstanceId(), "need_dept_score", needDeptScore);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveLeaderAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
validateAward(action.getLeaderFinalAward());
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_LEADER_AWARD);
String needInstituteVote = AWARD_GOLD.equals(action.getLeaderFinalAward()) ? "1" : "0";
ImprovementProposal update = new ImprovementProposal();
update.setId(proposal.getId());
update.setLeaderFinalAward(action.getLeaderFinalAward());
update.setIsneedvote(needInstituteVote);
improvementProposalMapper.updateById(update);
runtimeService.setVariable(action.getProcessInstanceId(), "isneedvote", needInstituteVote);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveArchiveAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
validateAward(action.getFinalAward());
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_ARCHIVE);
ImprovementProposal update = new ImprovementProposal();
update.setId(proposal.getId());
update.setFinalAward(action.getFinalAward());
update.setArchiveBy(loginUser.getId());
update.setArchiveTime(new Date());
if (AWARD_GOLD.equals(proposal.getLeaderFinalAward()) && "failed".equals(proposal.getInstituteVoteResult())) {
update.setManualConfirmResult("confirmed");
}
improvementProposalMapper.updateById(update);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void castInstituteVote(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
if (!"agree".equals(action.getVoteResult()) && !"disagree".equals(action.getVoteResult())) {
throw new IllegalStateException("投票结果只能是同意或不同意");
}
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_INSTITUTE_VOTE);
ImprovementInstituteVote vote = improvementInstituteVoteMapper.selectByMainId(proposal.getId()).stream()
.filter(item -> loginUser.getId().equals(item.getVoterId()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("当前用户不在所级评议投票名单中"));
if ("voted".equals(vote.getVoteStatus())) {
throw new IllegalStateException("您已完成投票,不能重复修改");
}
int updated = improvementInstituteVoteMapper.update(null, new LambdaUpdateWrapper<ImprovementInstituteVote>()
.eq(ImprovementInstituteVote::getId, vote.getId())
.eq(ImprovementInstituteVote::getVoteStatus, "pending")
.set(ImprovementInstituteVote::getVoteResult, action.getVoteResult())
.set(ImprovementInstituteVote::getVoteStatus, "voted")
.set(ImprovementInstituteVote::getVoteTime, new Date())
.set(ImprovementInstituteVote::getProcessTaskId, action.getTaskId()));
if (updated != 1) {
throw new IllegalStateException("投票状态已变化,请刷新后重试");
}
}
private ImprovementProposal validateRunningTask(ImprovementProposalBpmActionDTO action, LoginUser loginUser, String expectedTaskDefKey) {
if (action == null || oConvertUtils.isEmpty(action.getMainId()) || oConvertUtils.isEmpty(action.getProcessInstanceId())
|| oConvertUtils.isEmpty(action.getTaskId())) {
throw new IllegalStateException("流程办理参数不完整");
}
ImprovementProposal proposal = improvementProposalMapper.selectById(action.getMainId());
if (proposal == null || !action.getProcessInstanceId().equals(proposal.getProcessInstanceId())) {
throw new IllegalStateException("提案与流程实例不匹配");
}
Task task = taskService.createTaskQuery().taskId(action.getTaskId()).singleResult();
if (task == null || !action.getProcessInstanceId().equals(task.getProcessInstanceId()) || !expectedTaskDefKey.equals(task.getTaskDefinitionKey())) {
throw new IllegalStateException("当前任务节点已变化,请刷新后重试");
}
// 已签收任务必须由签收人操作;候选任务仍由通用办理接口在提交时执行权限校验。
if (oConvertUtils.isNotEmpty(task.getAssignee()) && !loginUser.getUsername().equals(task.getAssignee())) {
throw new IllegalStateException("当前任务不属于登录用户");
}
return proposal;
}
private void validateAward(String award) {
if (!AWARD_GOLD.equals(award) && !AWARD_SILVER.equals(award) && !AWARD_BRONZE.equals(award) && !AWARD_PARTICIPATION.equals(award)) {
throw new IllegalStateException("请选择有效的奖项");
}
}
private boolean needsDeptScore(String award) {
return AWARD_GOLD.equals(award) || AWARD_SILVER.equals(award);
}
/**
* 任何会改变主表生命周期的操作都必须先锁定并确认仍是未发起草稿,
* 这样并发的“发起流程”和“编辑/删除”不会让流程与业务数据脱节。