Merge branch 'refs/heads/master' into wsq-20260727

This commit is contained in:
王淑勤
2026-07-30 08:28:56 +08:00
28 changed files with 1093 additions and 26 deletions
+321
View File
@@ -0,0 +1,321 @@
-- ============================================================
-- 数据库全量清理脚本
-- 目标:清除所有业务数据与流程运行时数据,
-- 保留用户/部门/角色/菜单/权限/字典等系统配置
-- 保留已设计的流程定义(可重新发布)
-- 效果:登录后可看到菜单,点进去是空的业务界面
--
-- 表来源:代码 @TableName 注解 + 数据库已存在
-- 部分表可能在某些环境中尚未创建,执行时若"doesn't exist"
-- 错误可忽略(该表当前环境无数据需清理)
--
-- ============================================================
-- 执行后必须执行的恢复操作:
-- 1. 同步用户到 Flowable 引擎:
-- PUT /act/process/extActProcess/doSyncUser
-- (act_id_* 表已清空,不执行此步则按角色/部门分配任务会失败)
-- 2. 重新发布流程定义(如有需要):
-- 进入流程设计器 → 发布已设计好的流程
-- ext_act_process.process_status 已重置为 0
-- ============================================================
SET FOREIGN_KEY_CHECKS = 0;
-- ============================================================
-- 1. Flowable 引擎运行时表(当前运行中的流程实例数据)
-- ============================================================
TRUNCATE TABLE act_ru_actinst;
TRUNCATE TABLE act_ru_deadletter_job;
TRUNCATE TABLE act_ru_entitylink;
TRUNCATE TABLE act_ru_event_subscr;
TRUNCATE TABLE act_ru_execution;
TRUNCATE TABLE act_ru_external_job;
TRUNCATE TABLE act_ru_history_job;
TRUNCATE TABLE act_ru_identitylink;
TRUNCATE TABLE act_ru_job;
TRUNCATE TABLE act_ru_suspended_job;
TRUNCATE TABLE act_ru_task;
TRUNCATE TABLE act_ru_timer_job;
TRUNCATE TABLE act_ru_variable;
-- ============================================================
-- 2. Flowable 引擎历史表(已完成的流程实例归档数据)
-- ============================================================
TRUNCATE TABLE act_hi_actinst;
TRUNCATE TABLE act_hi_attachment;
TRUNCATE TABLE act_hi_comment;
TRUNCATE TABLE act_hi_detail;
TRUNCATE TABLE act_hi_entitylink;
TRUNCATE TABLE act_hi_identitylink;
TRUNCATE TABLE act_hi_procinst;
TRUNCATE TABLE act_hi_taskinst;
TRUNCATE TABLE act_hi_tsk_log;
TRUNCATE TABLE act_hi_varinst;
TRUNCATE TABLE act_evt_log;
-- ============================================================
-- 3. Flowable 引擎部署表(已发布的流程定义快照,重新发布会自动重建)
-- ============================================================
TRUNCATE TABLE act_re_procdef;
TRUNCATE TABLE act_re_deployment;
TRUNCATE TABLE act_re_model;
TRUNCATE TABLE act_ge_bytearray;
TRUNCATE TABLE act_procdef_info;
-- ============================================================
-- 4. Flowable 事件注册表运行时数据
-- flw_channel_definition / flw_event_* / flw_ev_* 保留
-- ============================================================
TRUNCATE TABLE flw_ru_batch;
TRUNCATE TABLE flw_ru_batch_part;
-- ============================================================
-- 5. Flowable 身份表(从 sys_user/sys_role 同步的缓存,清空后需重新同步)
-- act_id_property 保留(引擎配置元数据)
-- ============================================================
TRUNCATE TABLE act_id_user;
TRUNCATE TABLE act_id_group;
TRUNCATE TABLE act_id_membership;
TRUNCATE TABLE act_id_info;
TRUNCATE TABLE act_id_bytearray;
TRUNCATE TABLE act_id_priv;
TRUNCATE TABLE act_id_priv_mapping;
TRUNCATE TABLE act_id_token;
-- ============================================================
-- 6. 自定义 BPM 运行时数据
-- ============================================================
TRUNCATE TABLE ext_act_bpm_log;
TRUNCATE TABLE ext_act_bpm_file;
TRUNCATE TABLE ext_act_flow_data;
TRUNCATE TABLE ext_act_task_cc;
TRUNCATE TABLE ext_act_task_notification;
TRUNCATE TABLE ext_act_mutil_flow_data;
TRUNCATE TABLE task_approval_opinion;
-- ============================================================
-- 7. 流程部署快照(与 act_re_* 对应,重新发布流程时自动重建)
-- ============================================================
TRUNCATE TABLE ext_act_process_node_deploy;
-- ============================================================
-- 8. 表单设计器运行时数据
-- design_form 及其他 design_form_* 表保留(表单定义)
-- ============================================================
TRUNCATE TABLE ext_act_design_flow_data;
TRUNCATE TABLE design_form_data;
-- ============================================================
-- 9. 流程定义状态重置为"未发布"
-- 以下流程定义配置表保留不动:
-- ext_act_process / ext_act_process_node / ext_act_process_form
-- ext_act_process_node_auth / ext_act_process_node_reject_target
-- ext_act_listener / ext_act_expression
-- ============================================================
UPDATE ext_act_process SET process_status = 0;
-- ============================================================
-- 10. 监督模块业务表(代码中有 @TableName 定义)
-- ============================================================
-- 习语讲话
TRUNCATE TABLE bg_xi_speak;
TRUNCATE TABLE bg_xi_speak_feedback;
-- 把脉问诊
TRUNCATE TABLE bg_takepulse;
TRUNCATE TABLE bg_takepulse_feedback;
-- 党务管理
TRUNCATE TABLE bg_partymatter;
TRUNCATE TABLE bg_partymatter_feedback;
-- 党建检查与整改
TRUNCATE TABLE dj_inspect_improve;
TRUNCATE TABLE dj_inspection_rectify_list;
TRUNCATE TABLE dj_check;
TRUNCATE TABLE dj_meeting_record;
TRUNCATE TABLE dj_demo_life_meeting;
TRUNCATE TABLE dj_people_service;
TRUNCATE TABLE dj_officialdoc_distribute;
-- 纪律检查
TRUNCATE TABLE dq_inspect_task;
TRUNCATE TABLE dq_inspect_progress;
TRUNCATE TABLE dq_inspect_problem;
-- 改善提案
TRUNCATE TABLE improvement_proposal;
TRUNCATE TABLE improvement_institute_vote;
TRUNCATE TABLE improvement_dept_score;
-- 任务清单
TRUNCATE TABLE task_task;
TRUNCATE TABLE task_list;
TRUNCATE TABLE task_list_detial;
TRUNCATE TABLE task_list_favorite;
TRUNCATE TABLE task_list_permission;
-- 监督测试
TRUNCATE TABLE supervision_test;
-- ============================================================
-- 11. JOA 流程业务演示表
-- ============================================================
TRUNCATE TABLE ext_biz_leave;
TRUNCATE TABLE joa_busines_strip;
TRUNCATE TABLE joa_doc_receiving;
TRUNCATE TABLE joa_doc_sending;
TRUNCATE TABLE joa_employee_leave;
TRUNCATE TABLE joa_loan;
TRUNCATE TABLE joa_overtime;
TRUNCATE TABLE joa_overtime_detail;
TRUNCATE TABLE joa_overtime_leave;
TRUNCATE TABLE joa_demo;
-- ============================================================
-- 12. Demo 模块测试表
-- ============================================================
TRUNCATE TABLE demo;
TRUNCATE TABLE demo_staff;
TRUNCATE TABLE demo_staff_group;
TRUNCATE TABLE demo_field_def_val_main;
TRUNCATE TABLE demo_field_def_val_sub;
TRUNCATE TABLE demo_sub_flow_hq;
TRUNCATE TABLE jeecg_order_main;
TRUNCATE TABLE jeecg_order_customer;
TRUNCATE TABLE jeecg_order_ticket;
TRUNCATE TABLE jeecg_tree_demo;
-- ============================================================
-- 13. OA 办公业务表
-- ============================================================
-- WPS
TRUNCATE TABLE oa_wps_file;
TRUNCATE TABLE oa_wps_file_version;
TRUNCATE TABLE oa_wps_file_watermark;
TRUNCATE TABLE oa_wps_user_acl;
-- 人事
TRUNCATE TABLE oa_become_regular;
TRUNCATE TABLE oa_dimission;
TRUNCATE TABLE oa_employee_award;
TRUNCATE TABLE oa_employee_certificate;
TRUNCATE TABLE oa_employee_education;
TRUNCATE TABLE oa_employee_experience;
TRUNCATE TABLE oa_employee_family;
TRUNCATE TABLE oa_employee_info;
TRUNCATE TABLE oa_employee_relations;
TRUNCATE TABLE oa_employment;
TRUNCATE TABLE oa_employment_apply;
TRUNCATE TABLE oa_interview;
TRUNCATE TABLE oa_promotion;
-- 出差/用车/请假/加班
TRUNCATE TABLE oa_business_travel;
TRUNCATE TABLE oa_business_travel_accompanied;
TRUNCATE TABLE oa_car_info;
TRUNCATE TABLE oa_car_use;
TRUNCATE TABLE oa_leave;
TRUNCATE TABLE oa_overtime;
TRUNCATE TABLE oa_security_vehicle;
-- 签到
TRUNCATE TABLE oa_sign_info;
TRUNCATE TABLE oa_sign_out;
TRUNCATE TABLE oa_sign_patch;
TRUNCATE TABLE oa_sign_rule;
-- 用品
TRUNCATE TABLE oa_supplies_detail;
TRUNCATE TABLE oa_supplies_use;
-- 公文
TRUNCATE TABLE oa_officialdoc_depart_user;
TRUNCATE TABLE oa_officialdoc_distribute;
TRUNCATE TABLE oa_officialdoc_issued;
TRUNCATE TABLE oa_officialdoc_organcode;
TRUNCATE TABLE oa_officialdoc_received;
TRUNCATE TABLE oa_officialdoc_seal;
TRUNCATE TABLE oa_officialdoc_temp;
-- ============================================================
-- 14. EOA 在线办公表
-- ============================================================
-- 聊天
TRUNCATE TABLE eoa_chat_group;
TRUNCATE TABLE eoa_chat_off_message;
TRUNCATE TABLE eoa_chat_on_message;
TRUNCATE TABLE eoa_chat_on_message_his;
TRUNCATE TABLE eoa_chat_session;
TRUNCATE TABLE eoa_chat_user;
TRUNCATE TABLE eoa_chat_user_group;
-- CMS
TRUNCATE TABLE eoa_cms_article;
TRUNCATE TABLE eoa_cms_article_read;
TRUNCATE TABLE eoa_cms_menu;
TRUNCATE TABLE eoa_cms_site;
-- 文件
TRUNCATE TABLE eoa_file;
TRUNCATE TABLE eoa_file_log;
-- 邮箱
TRUNCATE TABLE eoa_mailbox_attach;
TRUNCATE TABLE eoa_mailbox_category;
TRUNCATE TABLE eoa_mailbox_info;
TRUNCATE TABLE eoa_mailbox_receiver;
-- 会议
TRUNCATE TABLE eoa_metting;
TRUNCATE TABLE eoa_metting_device;
TRUNCATE TABLE eoa_metting_room;
TRUNCATE TABLE eoa_metting_sign;
-- 计划/门户
TRUNCATE TABLE eoa_plan;
TRUNCATE TABLE eoa_portal_site;
TRUNCATE TABLE eoa_portal_site_auth;
-- ============================================================
-- 15. 测试表(代码 @TableName + DB 中存在)
-- ============================================================
TRUNCATE TABLE ceshi_des_qingjia;
TRUNCATE TABLE ceshi_nongmin;
TRUNCATE TABLE ceshi_qingjia_dan;
TRUNCATE TABLE ceshi_ruzhi;
TRUNCATE TABLE ceshi_subqt;
TRUNCATE TABLE ce_fenzhi_zx;
TRUNCATE TABLE ce_paita_wangguan;
TRUNCATE TABLE joa_onl_leave;
TRUNCATE TABLE joa_onl_bthuiq;
TRUNCATE TABLE joa_onl_sxhuiq;
TRUNCATE TABLE oa_demo_bhan_wguan;
TRUNCATE TABLE rep_demo_dxtj;
TRUNCATE TABLE rep_demo_employee;
TRUNCATE TABLE rep_demo_gongsi;
TRUNCATE TABLE rep_demo_huizong;
TRUNCATE TABLE rep_demo_jianpiao;
TRUNCATE TABLE rep_demo_order_main;
TRUNCATE TABLE rep_demo_order_product;
TRUNCATE TABLE test_demo;
TRUNCATE TABLE test_enhance_select;
TRUNCATE TABLE test_main_table;
TRUNCATE TABLE test_note;
TRUNCATE TABLE test_online_link;
TRUNCATE TABLE test_order_customer;
TRUNCATE TABLE test_order_main;
TRUNCATE TABLE test_order_product;
TRUNCATE TABLE test_order_user;
TRUNCATE TABLE test_person;
TRUNCATE TABLE test_shoptype_tree;
TRUNCATE TABLE test_son_table;
TRUNCATE TABLE test_third_link;
TRUNCATE TABLE test_tree;
-- ============================================================
-- 16. 系统运行日志 / 消息 / API 调用记录
-- sys_files / sys_file_attachment 保留(头像、系统图标等文件)
-- jimu_report_export_log 保留(可选清除,见下方)
-- ============================================================
TRUNCATE TABLE sys_log;
TRUNCATE TABLE sys_data_log;
TRUNCATE TABLE sys_announcement;
TRUNCATE TABLE sys_announcement_send;
TRUNCATE TABLE sys_sms;
TRUNCATE TABLE sys_comment;
TRUNCATE TABLE sys_files_operate;
TRUNCATE TABLE open_api_log;
-- ============================================================
-- 17. (可选)取消注释以清除更多内容
-- ============================================================
-- TRUNCATE TABLE jimu_report_export_log;
-- TRUNCATE TABLE sys_file_attachment;
-- TRUNCATE TABLE sys_files;
-- TRUNCATE TABLE flyway_schema_history_1;
SET FOREIGN_KEY_CHECKS = 1;
@@ -105,9 +105,7 @@ public class KsimTestController {
if (msgRespBody != null) {
int code = msgRespBody.getIntValue("code");
result.put("success", code == 0 || code == 200);
if (msgRespBody.getJSONObject("dataBody") != null) {
result.put("messageId", msgRespBody.getJSONObject("dataBody").getString("messageId"));
}
result.put("requestId", msgRespBody.getString("requestId"));
result.put("responseCode", code);
} else {
result.put("success", false);
@@ -66,10 +66,9 @@ public class KsimMessageServiceImpl implements IKsimMessageService {
return Result.error(apiResult.getCode(), "即时通发送失败: " + apiResult.getMessage());
}
String messageId = apiResult.getDataBody() != null
? apiResult.getDataBody().getString("messageId") : "";
log.info("【即时通推送-sendBatch】成功, messageId: {}, toUserCount: {}, title: {}", messageId,
String requestId = apiResult.getRequestId() != null ? apiResult.getRequestId() : "";
log.info("【即时通推送-sendBatch】成功, requestId: {}, toUserCount: {}, title: {}", requestId,
toUserValueList != null ? toUserValueList.size() : 0, title);
return Result.OK(messageId);
return Result.OK(requestId);
}
}
@@ -33,7 +33,7 @@ import java.util.Map;
public class TaskCreatedAutoSubmitListener implements TaskListener {
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 IExtActBpmLogService extActBpmLogService;
protected static RuntimeService runtimeService;
@@ -6,6 +6,7 @@ import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.common.exception.JeecgBootException;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.Execution;
@@ -276,6 +277,28 @@ public class FlowNodeExpression {
return sysbase.getUserByRoleIdLocalApi(roleId);
}
/**
* Get required usernames for a Flowable multi-instance task.
* An empty collection would otherwise make Flowable skip the task, so fail fast instead.
* Example: ${flowNodeExpression.getRequiredUsersListByRole('roleIdValue')}
*
* @param roleId system role id
* @return non-empty username list
*/
public List<String> getRequiredUsersListByRole(String roleId) {
if (StringUtils.isBlank(roleId)) {
throw new JeecgBootException("流程角色ID不能为空");
}
List<String> usernames = ISysBaseAPI.getUsernamesByRoleIdLocalApi(roleId);
log.info("流程多实例按角色取人,roleId={}, usernames={}, count={}", roleId, usernames,
usernames == null ? 0 : usernames.size());
if (CollectionUtils.isEmpty(usernames)) {
throw new JeecgBootException("角色[" + roleId + "]未配置有效用户,流程无法继续");
}
return usernames;
}
/**
* Get subprocess variable.
* Example: ${flowNodeExpression.getSonProcessVariable(execution, 'subApproveUser')}
@@ -37,6 +37,8 @@ import org.jeecg.modules.extbpm.util.JSONHelper;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -60,6 +62,7 @@ import java.util.stream.Collectors;
* @date 2019-03-15
* @version V1.0
*/
@Tag(name = "流程定义")
@RestController("extActProcessController")
@RequestMapping("/act/process/extActProcess")
@Slf4j
@@ -570,6 +573,7 @@ public class ExtActProcessController {
* @param extActProcess
* @return
*/
@Operation(summary = "同步用户到Flowable引擎")
@PutMapping(value = "/doSyncUser")
public Result<ExtActProcess> doSyncUser(HttpServletRequest request) {
Result<ExtActProcess> result = new Result<ExtActProcess>();
@@ -27,6 +27,8 @@ 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.ImprovementDeptScoreBpmDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementInstituteVoteCandidateDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.modules.improvementproposal.vo.ImprovementProposalPage;
import org.jeecg.modules.improvementproposal.service.IImprovementProposalService;
@@ -157,12 +159,60 @@ public class ImprovementProposalController {
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")
@Operation(summary = "保存部门负责人确认奖项")
public Result<String> saveLeaderAward(@RequestBody ImprovementProposalBpmActionDTO action) {
return handleBpmAction(() -> improvementProposalService.saveLeaderAward(action, currentUser()));
}
@GetMapping(value = "/bpm/instituteVoteCandidates")
@Operation(summary = "查询所级评议投票候选人")
public Result<List<ImprovementInstituteVoteCandidateDTO>> getInstituteVoteCandidates(@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.getInstituteVoteCandidates(action, currentUser()));
} catch (IllegalStateException e) {
return Result.error(e.getMessage());
}
}
@PostMapping(value = "/bpm/instituteVoteMembers")
@Operation(summary = "保存所级评议投票人员")
public Result<String> saveInstituteVoteMembers(@RequestBody ImprovementProposalBpmActionDTO action) {
return handleBpmAction(() -> improvementProposalService.saveInstituteVoteMembers(action, currentUser()));
}
@PostMapping(value = "/bpm/archiveAward")
@Operation(summary = "保存归档最终奖项")
public Result<String> saveArchiveAward(@RequestBody ImprovementProposalBpmActionDTO action) {
@@ -175,6 +225,12 @@ public class ImprovementProposalController {
return handleBpmAction(() -> improvementProposalService.castInstituteVote(action, currentUser()));
}
@PostMapping(value = "/bpm/submitInstituteVote")
@Operation(summary = "确认所级评委投票并直接完成流程任务")
public Result<String> submitInstituteVote(@RequestBody ImprovementProposalBpmActionDTO action, HttpServletRequest request) {
return handleBpmAction(() -> improvementProposalService.submitInstituteVoteAndComplete(action, currentUser(), request));
}
private LoginUser currentUser() {
return (LoginUser) SecurityUtils.getSubject().getPrincipal();
}
@@ -0,0 +1,46 @@
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 safetyScore;
@Schema(description = "质量得分,0-100")
private BigDecimal qualityScore;
@Schema(description = "效率得分,0-100")
private BigDecimal efficiencyScore;
@Schema(description = "成本得分,0-100")
private BigDecimal costScore;
@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;
}
@@ -0,0 +1,25 @@
package org.jeecg.modules.improvementproposal.dto;
import java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 所级评议投票候选人。
* 仅暴露名单选择和 Flowable 多实例所需的最小用户信息。
*/
@Data
@Schema(description = "所级评议投票候选人")
public class ImprovementInstituteVoteCandidateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private String id;
@Schema(description = "登录用户名")
private String username;
@Schema(description = "真实姓名")
private String realname;
}
@@ -1,6 +1,7 @@
package org.jeecg.modules.improvementproposal.dto;
import java.io.Serializable;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -35,4 +36,11 @@ public class ImprovementProposalBpmActionDTO implements Serializable {
@Schema(description = "投票结果:agree/disagree")
private String voteResult;
/**
* 精益管理部门经办人在所级评议节点选择的投票人 ID 列表。
* 服务端会按固定角色重新校验该列表,不能将其作为投票人身份的信任来源。
*/
@Schema(description = "所级评议投票人ID列表")
private List<String> voterIds;
}
@@ -50,6 +50,22 @@ public class ImprovementDeptScore implements Serializable {
@Excel(name = "评分状态:pending/completed", width = 15)
@Schema(description = "评分状态:pending/completed")
private String scoreStatus;
/**安全得分*/
@Excel(name = "安全得分", width = 15)
@Schema(description = "安全得分")
private java.math.BigDecimal safetyScore;
/**质量得分*/
@Excel(name = "质量得分", width = 15)
@Schema(description = "质量得分")
private java.math.BigDecimal qualityScore;
/**效率得分*/
@Excel(name = "效率得分", width = 15)
@Schema(description = "效率得分")
private java.math.BigDecimal efficiencyScore;
/**成本得分*/
@Excel(name = "成本得分", width = 15)
@Schema(description = "成本得分")
private java.math.BigDecimal costScore;
/**改善效果得分*/
@Excel(name = "改善效果得分", width = 15)
@Schema(description = "改善效果得分")
@@ -125,6 +125,10 @@ public class ImprovementProposal implements Serializable {
@Schema(description = "最终奖项")
@Dict(dicCode = "proposal_prize")
private String finalAward;
/**所级评议领导名单*/
@Excel(name = "所级评议领导名单", width = 15)
@Schema(description = "所级评议领导名单")
private Integer instituteVoters;
/**所级评议应投票人数*/
@Excel(name = "所级评议应投票人数", width = 15)
@Schema(description = "所级评议应投票人数")
@@ -137,9 +141,9 @@ public class ImprovementProposal implements Serializable {
@Excel(name = "所级评议不同意票数", width = 15)
@Schema(description = "所级评议不同意票数")
private Integer instituteDisagreeCount;
/**所级评议结果:passed/failed*/
@Excel(name = "所级评议结果:passed/failed", width = 15)
@Schema(description = "所级评议结果:passed/failed")
/**所级评议结果:通过/不通过*/
@Excel(name = "所级评议结果:通过/不通过", width = 15)
@Schema(description = "所级评议结果:通过/不通过")
private String instituteVoteResult;
/**金奖未通过后的经办人手动确认结果*/
@Excel(name = "金奖未通过后的经办人手动确认结果", width = 15)
@@ -28,4 +28,7 @@ public interface ImprovementInstituteVoteMapper extends BaseMapper<ImprovementIn
* @return List<ImprovementInstituteVote>
*/
public List<ImprovementInstituteVote> selectByMainId(@Param("mainId") String mainId);
/** 任务尚未进入投票节点时,仅失效本轮待投票记录,保留已投票历史。 */
int logicalDeletePendingByMainId(@Param("mainId") String mainId);
}
@@ -2,8 +2,10 @@ package org.jeecg.modules.improvementproposal.mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.jeecg.modules.improvementproposal.dto.ImprovementInstituteVoteCandidateDTO;
import org.jeecg.modules.improvementproposal.entity.ImprovementProposal;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
/**
* @Description: improvement_proposal
@@ -20,4 +22,7 @@ public interface ImprovementProposalMapper extends BaseMapper<ImprovementProposa
@Select("SELECT * FROM improvement_proposal WHERE id = #{id} AND del_flag = '0' FOR UPDATE")
ImprovementProposal selectByIdForUpdate(@Param("id") String id);
/** 查询指定角色下可供所级评议选择的有效用户。 */
List<ImprovementInstituteVoteCandidateDTO> selectInstituteVoteCandidates(@Param("roleCode") String roleCode);
}
@@ -12,5 +12,16 @@
SELECT *
FROM improvement_institute_vote
WHERE
main_id = #{mainId} </select>
main_id = #{mainId}
AND del_flag = '0'
</select>
<!-- Task_improve004 重试保存名单时,只替换尚未投票的有效记录。 -->
<update id="logicalDeletePendingByMainId" parameterType="java.lang.String">
UPDATE improvement_institute_vote
SET del_flag = '1'
WHERE main_id = #{mainId}
AND del_flag = '0'
AND vote_status = 'pending'
</update>
</mapper>
@@ -10,4 +10,17 @@
LIMIT 1
</select>
<!-- 角色名单由服务端查询,前端只能从该受控集合中选择。 -->
<select id="selectInstituteVoteCandidates" resultType="org.jeecg.modules.improvementproposal.dto.ImprovementInstituteVoteCandidateDTO">
SELECT DISTINCT u.id, u.username, u.realname
FROM sys_role r
INNER JOIN sys_user_role ur ON ur.role_id = r.id
INNER JOIN sys_user u ON u.id = ur.user_id
WHERE r.role_code = #{roleCode}
AND u.del_flag = 0
AND u.username IS NOT NULL
AND u.username != ''
ORDER BY u.realname, u.username
</select>
</mapper>
@@ -3,9 +3,12 @@ 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.ImprovementDeptScoreBpmDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementInstituteVoteCandidateDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.common.system.vo.LoginUser;
import com.baomidou.mybatisplus.extension.service.IService;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
@@ -62,15 +65,36 @@ public interface IImprovementProposalService extends IService<ImprovementProposa
/** 保存部门精益专员初审结果,并写入部门评分网关变量。 */
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);
/** 查询所级评议节点可选择的角色成员。 */
List<ImprovementInstituteVoteCandidateDTO> getInstituteVoteCandidates(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 保存所级评议名单、待投票记录及多实例流程变量。 */
void saveInstituteVoteMembers(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 保存经办人归档确认的最终奖项。 */
void saveArchiveAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 记录当前所级评委的投票结果。 */
void castInstituteVote(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 锁定当前所级评委的投票并在全员完成后汇总投票结果。 */
void submitInstituteVote(ImprovementProposalBpmActionDTO action, LoginUser loginUser);
/** 锁定投票后,使用通用流程办理能力直接完成当前所级评委任务。 */
void submitInstituteVoteAndComplete(ImprovementProposalBpmActionDTO action, LoginUser loginUser, HttpServletRequest request);
/**
* 获取指定年度最新提案编号中的XXXX部分
*
@@ -3,6 +3,8 @@ 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.ImprovementDeptScoreBpmDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementInstituteVoteCandidateDTO;
import org.jeecg.modules.improvementproposal.dto.ImprovementProposalBpmActionDTO;
import org.jeecg.modules.improvementproposal.mapper.ImprovementDeptScoreMapper;
import org.jeecg.modules.improvementproposal.mapper.ImprovementInstituteVoteMapper;
@@ -12,22 +14,31 @@ 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.jeecg.modules.extbpm.process.service.IExtActTaskCcService;
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.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.List;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.HistoryService;
import org.flowable.engine.TaskService;
import org.flowable.task.api.Task;
import org.jeecg.modules.system.entity.SysRole;
import org.jeecg.modules.system.service.ISysRoleService;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import javax.servlet.http.HttpServletRequest;
/**
* @Description: improvement_proposal
@@ -43,13 +54,18 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
private static final String FLOW_NAME = "改善提案审批";
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_DEPT_SCORE = "Task_improve002";
private static final String TASK_LEADER_AWARD = "Task_improve003";
private static final String TASK_INSTITUTE_REVIEW = "Task_improve004";
private static final String TASK_INSTITUTE_VOTE = "Task_improve005";
private static final String TASK_ARCHIVE = "Task_improve006";
private static final String INSTITUTE_VOTE_ROLE_CODE = "improve_suoleader";
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";
private static final String INSTITUTE_VOTE_RESULT_PASSED = "通过";
private static final String INSTITUTE_VOTE_RESULT_FAILED = "不通过";
@Autowired
private ImprovementProposalMapper improvementProposalMapper;
@@ -62,7 +78,13 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
@Autowired
private RuntimeService runtimeService;
@Autowired
private HistoryService historyService;
@Autowired
private TaskService taskService;
@Autowired
private ISysRoleService sysRoleService;
@Autowired
private IExtActTaskCcService extActTaskCcService;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -201,11 +223,104 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
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 effectScore = calculateEffectScore(scoreAction);
BigDecimal totalScore = calculateTotalScore(scoreAction, effectScore);
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, effectScore, totalScore);
// 草稿阶段记录最近保存时间,供评委在当前节点实时查看。
newScore.setScoreTime(new Date());
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::getSafetyScore, scoreAction.getSafetyScore())
.set(ImprovementDeptScore::getQualityScore, scoreAction.getQualityScore())
.set(ImprovementDeptScore::getEfficiencyScore, scoreAction.getEfficiencyScore())
.set(ImprovementDeptScore::getCostScore, scoreAction.getCostScore())
.set(ImprovementDeptScore::getEffectScore, effectScore)
.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, new Date()));
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
@Transactional(rollbackFor = Exception.class)
public void saveLeaderAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
validateAward(action.getLeaderFinalAward());
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_LEADER_AWARD);
String awardBase = resolveLeaderAwardBase(proposal);
if (awardRank(action.getLeaderFinalAward()) < awardRank(awardBase)) {
throw new IllegalStateException("负责人确认奖项只能选择当前档位或降档奖项");
}
String needInstituteVote = AWARD_GOLD.equals(action.getLeaderFinalAward()) ? "1" : "0";
ImprovementProposal update = new ImprovementProposal();
update.setId(proposal.getId());
@@ -215,6 +330,75 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
runtimeService.setVariable(action.getProcessInstanceId(), "isneedvote", needInstituteVote);
}
@Override
public List<ImprovementInstituteVoteCandidateDTO> getInstituteVoteCandidates(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
validateRunningTask(action, loginUser, TASK_INSTITUTE_REVIEW);
return loadInstituteVoteCandidates();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveInstituteVoteMembers(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_INSTITUTE_REVIEW);
ImprovementProposal lockedProposal = improvementProposalMapper.selectByIdForUpdate(proposal.getId());
if (lockedProposal == null || !action.getProcessInstanceId().equals(lockedProposal.getProcessInstanceId())) {
throw new IllegalStateException("提案流程状态已变化,请刷新后重试");
}
List<ImprovementInstituteVoteCandidateDTO> candidates = loadInstituteVoteCandidates();
Map<String, ImprovementInstituteVoteCandidateDTO> candidateById = new LinkedHashMap<>();
for (ImprovementInstituteVoteCandidateDTO candidate : candidates) {
candidateById.put(candidate.getId(), candidate);
}
if (action.getVoterIds() == null || action.getVoterIds().isEmpty()) {
throw new IllegalStateException("请至少选择一名所级评议人员");
}
LinkedHashSet<String> selectedUserIds = new LinkedHashSet<>();
for (String voterId : action.getVoterIds()) {
if (oConvertUtils.isEmpty(voterId) || !selectedUserIds.add(voterId)) {
throw new IllegalStateException("所级评议人员选择数据不合法");
}
}
LinkedHashSet<String> usernames = new LinkedHashSet<>();
for (String voterId : selectedUserIds) {
ImprovementInstituteVoteCandidateDTO candidate = candidateById.get(voterId);
if (candidate == null) {
throw new IllegalStateException("所选人员不属于所级评议小组");
}
if (!usernames.add(candidate.getUsername())) {
throw new IllegalStateException("所级评议人员用户名重复");
}
}
List<ImprovementInstituteVote> activeVotes = improvementInstituteVoteMapper.selectByMainId(lockedProposal.getId());
if (activeVotes.stream().anyMatch(item -> !"pending".equals(item.getVoteStatus()))) {
throw new IllegalStateException("所级评议已开始投票,不能重新选择人员");
}
// beforeHandle 可能因网络重试重复调用;先失效未投票记录再重建,确保有效名单始终和流程变量一致。
improvementInstituteVoteMapper.logicalDeletePendingByMainId(lockedProposal.getId());
for (String voterId : selectedUserIds) {
ImprovementInstituteVoteCandidateDTO candidate = candidateById.get(voterId);
ImprovementInstituteVote vote = new ImprovementInstituteVote();
vote.setMainId(lockedProposal.getId());
vote.setVoterId(candidate.getId());
vote.setVoterName(oConvertUtils.isEmpty(candidate.getRealname()) ? candidate.getUsername() : candidate.getRealname());
vote.setVoteStatus("pending");
vote.setSysOrgCode(lockedProposal.getSysOrgCode());
vote.setDelFlag(NORMAL_DEL_FLAG);
improvementInstituteVoteMapper.insert(vote);
}
ImprovementProposal update = new ImprovementProposal();
update.setId(lockedProposal.getId());
update.setInstituteTotalVoters(selectedUserIds.size());
update.setInstituteAgreeCount(null);
update.setInstituteDisagreeCount(null);
update.setInstituteVoteResult(null);
improvementProposalMapper.updateById(update);
runtimeService.setVariable(action.getProcessInstanceId(), "suoleadervotelist", String.join(",", usernames));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveArchiveAward(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
@@ -225,7 +409,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
update.setFinalAward(action.getFinalAward());
update.setArchiveBy(loginUser.getId());
update.setArchiveTime(new Date());
if (AWARD_GOLD.equals(proposal.getLeaderFinalAward()) && "failed".equals(proposal.getInstituteVoteResult())) {
if (AWARD_GOLD.equals(proposal.getLeaderFinalAward()) && INSTITUTE_VOTE_RESULT_FAILED.equals(proposal.getInstituteVoteResult())) {
update.setManualConfirmResult("confirmed");
}
improvementProposalMapper.updateById(update);
@@ -238,25 +422,252 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
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("当前用户不在所级评议投票名单中"));
ImprovementInstituteVote vote = findInstituteVote(proposal.getId(), loginUser.getId());
if ("voted".equals(vote.getVoteStatus())) {
throw new IllegalStateException("您已完成投票,不能重复修改");
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::getVoteTime, null)
.set(ImprovementInstituteVote::getProcessTaskId, action.getTaskId()));
if (updated != 1) {
throw new IllegalStateException("投票状态已变化,请刷新后重试");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submitInstituteVote(ImprovementProposalBpmActionDTO action, LoginUser loginUser) {
if (!"agree".equals(action.getVoteResult()) && !"disagree".equals(action.getVoteResult())) {
throw new IllegalStateException("投票结果只能是同意或不同意");
}
ImprovementProposal proposal = validateRunningTask(action, loginUser, TASK_INSTITUTE_VOTE);
ImprovementProposal lockedProposal = improvementProposalMapper.selectByIdForUpdate(proposal.getId());
if (lockedProposal == null || !action.getProcessInstanceId().equals(lockedProposal.getProcessInstanceId())) {
throw new IllegalStateException("提案流程状态已变化,请刷新后重试");
}
ImprovementInstituteVote vote = findInstituteVote(lockedProposal.getId(), loginUser.getId());
if ("voted".equals(vote.getVoteStatus())) {
// 网络异常导致流程任务未完成时,允许使用已确认的同一结果重试办理;不允许改票。
if (!action.getVoteResult().equals(vote.getVoteResult())) {
throw new IllegalStateException("投票已确认,不能修改投票结果");
}
} else {
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("投票状态已变化,请刷新后重试");
}
}
updateInstituteVoteSummaryIfComplete(lockedProposal, action.getProcessInstanceId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void submitInstituteVoteAndComplete(ImprovementProposalBpmActionDTO action, LoginUser loginUser, HttpServletRequest request) {
submitInstituteVote(action, loginUser);
// 复用通用办理服务,确保审批日志、流程状态及多实例收敛逻辑与普通办理保持一致。
HashMap<String, String> processParams = new HashMap<>(4);
processParams.put("taskId", action.getTaskId());
processParams.put("processModel", "1");
processParams.put("nextCodeCount", "1");
// Task5 只有正常流转分支;通用办理服务以 end + 单分支执行 taskService.complete。
processParams.put("nextnode", "end");
Result<Object> result = extActTaskCcService.processComplete(processParams, request);
if (result == null || !result.isSuccess()) {
throw new IllegalStateException(result == null || oConvertUtils.isEmpty(result.getMessage())
? "投票已确认,但流程办理失败"
: result.getMessage());
}
}
private ImprovementInstituteVote findInstituteVote(String mainId, String voterId) {
return improvementInstituteVoteMapper.selectByMainId(mainId).stream()
.filter(item -> voterId.equals(item.getVoterId()))
.findFirst()
.orElseThrow(() -> new IllegalStateException("当前用户不在所级评议投票名单中"));
}
/**
* 所级评议以名单确定时写入的应投票人数为分母。赞成票满足 agree * 3 >= total * 2 即通过,避免小数取整歧义。
*/
private void updateInstituteVoteSummaryIfComplete(ImprovementProposal proposal, String processInstanceId) {
Integer totalVoters = proposal.getInstituteTotalVoters();
if (totalVoters == null || totalVoters <= 0) {
throw new IllegalStateException("所级评议应投票人数未初始化");
}
List<ImprovementInstituteVote> votes = improvementInstituteVoteMapper.selectByMainId(proposal.getId());
if (votes.size() != totalVoters) {
throw new IllegalStateException("所级评议投票名单与应投票人数不一致");
}
if (votes.stream().anyMatch(item -> !"voted".equals(item.getVoteStatus()))) {
return;
}
long agreeCount = votes.stream().filter(item -> "agree".equals(item.getVoteResult())).count();
long disagreeCount = votes.stream().filter(item -> "disagree".equals(item.getVoteResult())).count();
if (agreeCount + disagreeCount != totalVoters) {
throw new IllegalStateException("所级评议投票结果不完整");
}
String voteResult = agreeCount * 3 >= (long) totalVoters * 2
? INSTITUTE_VOTE_RESULT_PASSED
: INSTITUTE_VOTE_RESULT_FAILED;
ImprovementProposal update = new ImprovementProposal();
update.setId(proposal.getId());
update.setInstituteAgreeCount((int) agreeCount);
update.setInstituteDisagreeCount((int) disagreeCount);
update.setInstituteVoteResult(voteResult);
improvementProposalMapper.updateById(update);
runtimeService.setVariable(processInstanceId, "institute_vote_result", voteResult);
}
/**
* 固定按角色编码读取名单,保存时再次查询以防前端伪造用户 ID 或角色配置已变更。
*/
private List<ImprovementInstituteVoteCandidateDTO> loadInstituteVoteCandidates() {
SysRole role = sysRoleService.getRoleNoTenant(INSTITUTE_VOTE_ROLE_CODE);
if (role == null) {
throw new IllegalStateException("未配置所级评议小组角色:" + INSTITUTE_VOTE_ROLE_CODE);
}
List<ImprovementInstituteVoteCandidateDTO> candidates = improvementProposalMapper.selectInstituteVoteCandidates(INSTITUTE_VOTE_ROLE_CODE);
if (candidates == null || candidates.isEmpty()) {
throw new IllegalStateException("所级评议小组角色未配置有效用户");
}
return candidates;
}
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.getSafetyScore(), "安全");
validateScoreValue(scoreAction.getQualityScore(), "质量");
validateScoreValue(scoreAction.getEfficiencyScore(), "效率");
validateScoreValue(scoreAction.getCostScore(), "成本");
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 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());
target.setQualityScore(source.getQualityScore());
target.setEfficiencyScore(source.getEfficiencyScore());
target.setCostScore(source.getCostScore());
target.setEffectScore(effectScore);
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.getSafetyScore(), "安全");
validateScoreValue(score.getQualityScore(), "质量");
validateScoreValue(score.getEfficiencyScore(), "效率");
validateScoreValue(score.getCostScore(), "成本");
validateScoreValue(score.getPromotionScore(), "可推广度");
validateScoreValue(score.getOriginalityScore(), "独创性");
validateScoreValue(score.getEffortScore(), "努力度");
BigDecimal calculatedEffectScore = calculateEffectScore(score.getSafetyScore(), score.getQualityScore(), score.getEfficiencyScore(), score.getCostScore());
if (score.getEffectScore() == null || score.getEffectScore().compareTo(calculatedEffectScore) != 0 || 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) {
if (action == null || oConvertUtils.isEmpty(action.getMainId()) || oConvertUtils.isEmpty(action.getProcessInstanceId())
|| oConvertUtils.isEmpty(action.getTaskId())) {
@@ -278,11 +689,30 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
}
private void validateAward(String award) {
if (!AWARD_GOLD.equals(award) && !AWARD_SILVER.equals(award) && !AWARD_BRONZE.equals(award) && !AWARD_PARTICIPATION.equals(award)) {
if (!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)) {
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);
}
@@ -128,9 +128,9 @@ public class ImprovementProposalPage {
@Excel(name = "所级评议不同意票数", width = 15)
@Schema(description = "所级评议不同意票数")
private Integer instituteDisagreeCount;
/**所级评议结果:passed/failed*/
@Excel(name = "所级评议结果:passed/failed", width = 15)
@Schema(description = "所级评议结果:passed/failed")
/**所级评议结果:通过/不通过*/
@Excel(name = "所级评议结果:通过/不通过", width = 15)
@Schema(description = "所级评议结果:通过/不通过")
private String instituteVoteResult;
/**金奖未通过后的经办人手动确认结果*/
@Excel(name = "金奖未通过后的经办人手动确认结果", width = 15)
@@ -78,6 +78,15 @@ public interface ISysBaseAPI extends CommonAPI {
@GetMapping("/sys/api/getUserById")
LoginUser getUserById(@RequestParam("id") String id);
/**
* 查询角色下未逻辑删除用户的登录用户名,供流程多实例任务分配使用。
*
* @param roleId 系统角色 ID
* @return 用户名列表
*/
@GetMapping("/sys/api/getUsernamesByRoleId")
List<String> getUsernamesByRoleIdLocalApi(@RequestParam("roleId") String roleId);
/**
* 7通过用户账号查询角色集合
* @param username
@@ -10,6 +10,7 @@ import org.jeecg.common.constant.enums.EmailTemplateEnum;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -24,6 +25,12 @@ public class SysBaseAPIFallback implements ISysBaseAPI {
@Setter
private Throwable cause;
@Override
public List<String> getUsernamesByRoleIdLocalApi(String roleId) {
log.error("Query usernames by role failed, roleId={}", roleId, cause);
return Collections.emptyList();
}
@Override
public void sendSysAnnouncement(MessageDTO message) {
log.error("发送消息失败 {}", cause);
@@ -575,4 +575,13 @@ public interface ISysBaseAPI extends CommonAPI {
List<String> getUsersListByDeptIdAndRoleIdLocalApi(String deptId, String roleId);
List<String> getUserByRoleIdLocalApi(String roleId);
}
/**
* Query active usernames by role for Flowable multi-instance tasks.
* This method intentionally uses an independent query path so existing role queries remain unchanged.
*
* @param roleId system role id
* @return active username list
*/
List<String> getUsernamesByRoleIdLocalApi(String roleId);
}
@@ -131,6 +131,14 @@ public class SystemApiController {
return loginUser;
}
/**
* 流程多实例节点只接受 username 作为办理人标识,统一由系统服务按角色查询。
*/
@GetMapping("/getUsernamesByRoleId")
List<String> getUsernamesByRoleId(@RequestParam("roleId") String roleId) {
return sysBaseApi.getUsernamesByRoleIdLocalApi(roleId);
}
/**
* 通过用户账号查询角色集合
* @param username
@@ -251,4 +251,13 @@ public interface SysUserMapper extends BaseMapper<SysUser> {
* @return
*/
List<SysUser> getUserByRoleId(String roleId);
/**
* Query active usernames by role through a dedicated statement.
* It must not reuse the overloaded getUserByRoleId statement used by the user management page.
*
* @param roleId role id
* @return username list
*/
List<String> getUsernamesByRoleId(@Param("roleId") String roleId);
}
@@ -59,6 +59,17 @@
</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
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}
and u.username is not null
and u.username != ''
</select>
<select id="getUserByRoleIdWithDeptIds" resultType="org.jeecg.modules.system.vo.SysUserRoleDeptsVo">
SELECT
u.id as user_id,
@@ -498,4 +498,13 @@ public interface ISysUserService extends IService<SysUser> {
List<String> queryUserByDeptAndROle(@NotBlank(message = "deptId不为空") String deptId, @NotBlank(message = "角色id不为空") String roleId, @NotBlank(message = "secretLevel不为空") Integer secretLevel);
List<String> getUserByRoleId(String roleId);
/**
* Query active usernames by role for Flowable multi-instance task assignment.
* This is separate from the legacy role-user query to avoid changing its behavior.
*
* @param roleId system role id
* @return active username list
*/
List<String> getUsernamesByRoleId(String roleId);
}
@@ -2161,4 +2161,15 @@ public class SysBaseApiImpl implements ISysBaseAPI {
.stream()
.collect(Collectors.toList());
}
}
@Override
public List<String> getUsernamesByRoleIdLocalApi(String roleId) {
List<String> usernames = sysUserService.getUsernamesByRoleId(roleId);
return Optional.ofNullable(usernames)
.orElse(Collections.emptyList())
.stream()
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList());
}
}
@@ -2424,4 +2424,12 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
public List<String> getUserByRoleId(String roleId){
return sysUserMapper.getUserByRoleId(roleId).stream().map(SysUser::getUsername).collect(Collectors.toList()) ;
}
@Override
public List<String> getUsernamesByRoleId(String roleId) {
if (StringUtils.isBlank(roleId)) {
return Collections.emptyList();
}
return sysUserMapper.getUsernamesByRoleId(roleId);
}
}