diff --git a/.gitignore b/.gitignore index e345e9b..616bb37 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,9 @@ localDocs ## backend **/target **/logs -**/yml **/.claude +**/yml +*.bundle ## front **/*.lock diff --git a/db/upgrade/add_reject_target_node.sql b/db/upgrade/add_reject_target_node.sql new file mode 100644 index 0000000..3eaecfb --- /dev/null +++ b/db/upgrade/add_reject_target_node.sql @@ -0,0 +1,12 @@ +-- 流程节点驳回目标节点配置表 +-- 一对多:一个源节点可配置多个可驳回的目标节点 +CREATE TABLE IF NOT EXISTS `ext_act_process_node_reject_target` ( + `id` varchar(64) NOT NULL COMMENT '主键ID', + `process_id` varchar(64) NOT NULL COMMENT '流程ID(对应ext_act_process_node.process_id)', + `source_node_code` varchar(100) NOT NULL COMMENT '源节点CODE(当前配置驳回的节点)', + `target_node_code` varchar(100) NOT NULL COMMENT '目标节点CODE(可以驳回到的节点)', + `target_node_name` varchar(200) DEFAULT NULL COMMENT '目标节点名称', + PRIMARY KEY (`id`), + KEY `idx_process_source` (`process_id`, `source_node_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流程节点驳回目标配置'; +、 \ No newline at end of file diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java index 13d4d25..360e5e3 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java @@ -176,6 +176,14 @@ public class JeecgController> { writeExcel(response, clazz, title, Collections.emptyList()); } + /** + * 导出自定义数据(含表头),供子类导出模板示例行 + */ + protected void exportXlsWithData(HttpServletResponse response, + Class clazz, String title, List data) throws IOException { + writeExcel(response, clazz, title, data); + } + private void writeExcel(HttpServletResponse response, Class clazz, String title, List data) throws IOException { ExcelAnnotationUtils.write(response, clazz, title, data, dictQuery()); diff --git a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/client/KsimApiClient.java b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/client/KsimApiClient.java index 1188172..bb0528c 100644 --- a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/client/KsimApiClient.java +++ b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/client/KsimApiClient.java @@ -1,7 +1,7 @@ package org.jeecg.modules.ksim.client; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import lombok.extern.slf4j.Slf4j; import org.jeecg.common.util.RestUtil; import org.jeecg.modules.ksim.config.KsimProperties; @@ -58,11 +58,13 @@ public class KsimApiClient { HttpHeaders headers = jsonHeaders(); headers.add("imOpenApiToken", token); - String bodyJson = JSON.toJSONString(dto); - String url = ksimProperties.getHost() + "/api-open/v1/msg/send"; + // 直接 POJO→JSONObject,避免 DTO→String→JSONObject 三重序列化 + JSONObject body = (JSONObject) JSON.toJSON(dto); + String url = ksimProperties.getHost() + "/api-open/v1/msg/sendBatch"; + log.debug("【即时通推送-消息发送】请求体: {}", body.toJSONString()); ResponseEntity resp = RestUtil.request(url, HttpMethod.POST, headers, null, - JSON.parseObject(bodyJson), JSONObject.class); + body, JSONObject.class); JSONObject respBody = resp.getBody(); int code = respBody != null ? respBody.getIntValue("code") : -1; diff --git a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/controller/KsimTestController.java b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/controller/KsimTestController.java index 926fd85..d73a871 100644 --- a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/controller/KsimTestController.java +++ b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/controller/KsimTestController.java @@ -1,7 +1,7 @@ package org.jeecg.modules.ksim.controller; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import lombok.extern.slf4j.Slf4j; import org.jeecg.common.api.vo.Result; import org.jeecg.common.util.RestUtil; @@ -19,7 +19,6 @@ import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; @Slf4j @@ -28,8 +27,6 @@ import java.util.stream.Collectors; @RequestMapping("/ksim/test") public class KsimTestController { - private static final Set ALLOWED_LINK_OPEN_MODES = Set.of("Exe", "Web", "None"); - @PostMapping("/sendBatch") public Result> sendBatch(@RequestBody Map params) { Map result = new LinkedHashMap<>(); @@ -47,8 +44,8 @@ public class KsimTestController { String content = (String) params.getOrDefault("content", ""); String linkUrl = (String) params.getOrDefault("linkUrl", ""); String linkTitle = (String) params.getOrDefault("linkTitle", ""); - String linkOpenMode = (String) params.getOrDefault("linkOpenMode", "Exe"); - String dataFormat = (String) params.getOrDefault("dataFormat", "JsonListView"); + String linkOpenMode = (String) params.getOrDefault("linkOpenMode", ""); + String dataFormat = (String) params.getOrDefault("dataFormat", ""); String data = (String) params.getOrDefault("data", ""); // --- Step 1: 创建 Token --- @@ -82,28 +79,14 @@ public class KsimTestController { dto.setToUserValueList(toUserValueList); dto.setTitle(title); dto.setContent(content); - // 可选字段:传了才设置,不传则不发给 ksim - if (data != null && !data.isEmpty()) { - dto.setData(data); - } - if (linkOpenMode != null && !linkOpenMode.isEmpty()) { - if (!ALLOWED_LINK_OPEN_MODES.contains(linkOpenMode)) { - log.warn("【即时通测试-sendBatch】非法的 linkOpenMode: {}, 已跳过", linkOpenMode); - } else { - dto.setLinkOpenMode(linkOpenMode); - } - } - if (dataFormat != null && !dataFormat.isEmpty()) { - dto.setDataFormat(dataFormat); - } - if (linkTitle != null && !linkTitle.isEmpty()) { - dto.setLinkTitle(linkTitle); - } - if (linkUrl != null && !linkUrl.isEmpty()) { - dto.setLinkUrl(linkUrl); + // 可选字段统一通过 setOptionalFields 设置 + try { + dto.setOptionalFields(data, linkOpenMode, dataFormat, linkTitle, linkUrl); + } catch (IllegalArgumentException e) { + log.warn("【即时通测试-sendBatch】可选字段校验失败: {}", e.getMessage()); } - String msgUrl = host + "/api-open/v1/msg/send"; + String msgUrl = host + "/api-open/v1/msg/sendBatch"; requestInfo.put("msgRequestUrl", msgUrl); requestInfo.put("msgRequestBody", dto); diff --git a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/dto/KsimApiResult.java b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/dto/KsimApiResult.java index c86723d..56aec86 100644 --- a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/dto/KsimApiResult.java +++ b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/dto/KsimApiResult.java @@ -1,6 +1,6 @@ package org.jeecg.modules.ksim.dto; -import com.alibaba.fastjson.annotation.JSONField; +import com.alibaba.fastjson2.annotation.JSONField; /** * 即时通 API 通用响应体 diff --git a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/dto/KsimMsgSendDTO.java b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/dto/KsimMsgSendDTO.java index 995ccab..a5bf238 100644 --- a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/dto/KsimMsgSendDTO.java +++ b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/dto/KsimMsgSendDTO.java @@ -1,12 +1,15 @@ package org.jeecg.modules.ksim.dto; import java.util.List; +import java.util.Set; /** * 对应即时通文档 3.4.3 ReqMsgSendDTO */ public class KsimMsgSendDTO { + public static final Set ALLOWED_LINK_OPEN_MODES = Set.of("Exe", "Web", "None"); + private String title; private String content; private String toUserKeyType; @@ -17,6 +20,33 @@ public class KsimMsgSendDTO { private String linkTitle; private String linkUrl; + /** + * 设置可选字段。仅当值非空时才设置,避免向 ksim 发送 null 字段。 + * linkOpenMode 会校验白名单。 + */ + public void setOptionalFields(String data, String linkOpenMode, String dataFormat, + String linkTitle, String linkUrl) { + if (data != null && !data.isEmpty()) { + this.data = data; + } + if (linkOpenMode != null && !linkOpenMode.isEmpty()) { + if (!ALLOWED_LINK_OPEN_MODES.contains(linkOpenMode)) { + throw new IllegalArgumentException("非法的 linkOpenMode: " + linkOpenMode + + ",允许的值: " + ALLOWED_LINK_OPEN_MODES); + } + this.linkOpenMode = linkOpenMode; + } + if (dataFormat != null && !dataFormat.isEmpty()) { + this.dataFormat = dataFormat; + } + if (linkTitle != null && !linkTitle.isEmpty()) { + this.linkTitle = linkTitle; + } + if (linkUrl != null && !linkUrl.isEmpty()) { + this.linkUrl = linkUrl; + } + } + public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } diff --git a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/service/impl/KsimMessageServiceImpl.java b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/service/impl/KsimMessageServiceImpl.java index 06065da..bf620bd 100644 --- a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/service/impl/KsimMessageServiceImpl.java +++ b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/service/impl/KsimMessageServiceImpl.java @@ -1,8 +1,9 @@ package org.jeecg.modules.ksim.service.impl; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONObject; import lombok.extern.slf4j.Slf4j; import org.jeecg.common.api.vo.Result; +import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.ksim.client.KsimApiClient; import org.jeecg.modules.ksim.dto.KsimApiResult; import org.jeecg.modules.ksim.dto.KsimMsgSendDTO; @@ -12,14 +13,11 @@ import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; -import java.util.Set; @Slf4j @Service public class KsimMessageServiceImpl implements IKsimMessageService { - private static final Set ALLOWED_LINK_OPEN_MODES = Set.of("Exe", "Web", "None"); - @Resource private KsimTokenManager tokenManager; @@ -31,6 +29,16 @@ public class KsimMessageServiceImpl implements IKsimMessageService { String title, String content, String linkUrl, String linkTitle, String data, String linkOpenMode, String dataFormat) { + if (toUserValueList == null || toUserValueList.isEmpty()) { + log.warn("【即时通推送-sendBatch】toUserValueList为空,跳过发送"); + return Result.error("接收人列表为空"); + } + if (oConvertUtils.isEmpty(title)) { + log.warn("【即时通推送-sendBatch】title为空"); + } + if (oConvertUtils.isEmpty(content)) { + log.warn("【即时通推送-sendBatch】content为空"); + } String token = tokenManager.getToken(); KsimMsgSendDTO dto = new KsimMsgSendDTO(); @@ -38,25 +46,11 @@ public class KsimMessageServiceImpl implements IKsimMessageService { dto.setToUserValueList(toUserValueList); dto.setTitle(title); dto.setContent(content); - // 以下可选字段:传了才设置,不传则不发给 ksim - if (data != null && !data.isEmpty()) { - dto.setData(data); - } - if (linkOpenMode != null && !linkOpenMode.isEmpty()) { - if (!ALLOWED_LINK_OPEN_MODES.contains(linkOpenMode)) { - log.warn("【即时通推送-sendBatch】非法的 linkOpenMode: {}, 已跳过", linkOpenMode); - } else { - dto.setLinkOpenMode(linkOpenMode); - } - } - if (dataFormat != null && !dataFormat.isEmpty()) { - dto.setDataFormat(dataFormat); - } - if (linkTitle != null && !linkTitle.isEmpty()) { - dto.setLinkTitle(linkTitle); - } - if (linkUrl != null && !linkUrl.isEmpty()) { - dto.setLinkUrl(linkUrl); + // 可选字段统一通过 setOptionalFields 设置,避免与测试控制器重复 + try { + dto.setOptionalFields(data, linkOpenMode, dataFormat, linkTitle, linkUrl); + } catch (IllegalArgumentException e) { + log.warn("【即时通推送-sendBatch】可选字段校验失败: {}", e.getMessage()); } KsimApiResult apiResult = apiClient.sendMsg(token, dto); diff --git a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/token/KsimTokenManager.java b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/token/KsimTokenManager.java index 0a6cc77..4b607a2 100644 --- a/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/token/KsimTokenManager.java +++ b/jeecg-boot-module/jeecg-module-im/src/main/java/org/jeecg/modules/ksim/token/KsimTokenManager.java @@ -1,6 +1,6 @@ package org.jeecg.modules.ksim.token; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONObject; import lombok.extern.slf4j.Slf4j; import org.jeecg.common.exception.JeecgBootException; import org.jeecg.modules.ksim.client.KsimApiClient; diff --git a/jeecg-boot-module/jeecg-module-tools/src/main/java/org/jeecg/common/util/excel/ExcelAnnotationUtils.java b/jeecg-boot-module/jeecg-module-tools/src/main/java/org/jeecg/common/util/excel/ExcelAnnotationUtils.java index f5fc052..b22802f 100644 --- a/jeecg-boot-module/jeecg-module-tools/src/main/java/org/jeecg/common/util/excel/ExcelAnnotationUtils.java +++ b/jeecg-boot-module/jeecg-module-tools/src/main/java/org/jeecg/common/util/excel/ExcelAnnotationUtils.java @@ -111,6 +111,54 @@ public final class ExcelAnnotationUtils { return columns; } + /** + * 根据 @ExcelColumn 注解自动构建一个填充了示例数据的实体实例,用于导出模板。 + * String 字段填 "示例" + 列名,dict 字段填 "示例文本",日期填当天,数字填 1。 + */ + public static T buildExample(Class clazz) { + try { + T obj = clazz.getDeclaredConstructor().newInstance(); + for (Field field : getAllFields(clazz)) { + ExcelColumn col = field.getAnnotation(ExcelColumn.class); + if (col == null) continue; + field.setAccessible(true); + setExampleValue(field, obj, col); + } + return obj; + } catch (Exception e) { + throw new RuntimeException("【Excel工具】构建示例数据失败, class: " + clazz.getSimpleName(), e); + } + } + + private static void setExampleValue(Field field, Object obj, ExcelColumn col) throws Exception { + Class type = field.getType(); + boolean hasDict = StringUtils.isNotEmpty(col.dictTable()) || StringUtils.isNotEmpty(col.dicCode()); + + if (type == String.class) { + field.set(obj, hasDict ? "示例文本" : "示例" + col.name()); + } else if (type == Date.class) { + field.set(obj, new Date()); + } else if (type == LocalDate.class) { + field.set(obj, LocalDate.now()); + } else if (type == LocalDateTime.class) { + field.set(obj, LocalDateTime.now()); + } else if (type == Integer.class || type == int.class) { + field.set(obj, 1); + } else if (type == Long.class || type == long.class) { + field.set(obj, 1L); + } else if (type == Double.class || type == double.class) { + field.set(obj, 1.0); + } else if (type == Float.class || type == float.class) { + field.set(obj, 1.0f); + } else if (type == Short.class || type == short.class) { + field.set(obj, (short) 1); + } else if (type == BigDecimal.class) { + field.set(obj, BigDecimal.ONE); + } else if (type == Boolean.class || type == boolean.class) { + field.set(obj, false); + } + } + // ==================== 导出 ==================== /** @@ -214,15 +262,16 @@ public final class ExcelAnnotationUtils { // ==================== 内部辅助方法 ==================== static boolean isValidDictDisplay(String[] replace, String value) { + String normalized = normalizeComma(value); for (String s : replace) { String[] arr = s.split(","); - if (arr.length >= 2 && arr[0].equals(value)) { + if (arr.length >= 2 && arr[0].equals(normalized)) { return true; } } - // 逗号分隔多值:逐个验证每个部分是否在字典范围内 - if (value.contains(",")) { - String[] parts = value.split(","); + // 逗号分隔多值:逐个验证每个部分是否在字典范围内(兼容中英文逗号) + if (normalized.contains(",")) { + String[] parts = normalized.split(","); for (String part : parts) { String trimmed = part.trim(); if (trimmed.isEmpty()) continue; @@ -234,7 +283,10 @@ public final class ExcelAnnotationUtils { break; } } - if (!found) return false; + if (!found) { + log.warn("【Excel导入-字典校验】多值中'{}'不在字典范围, 输入值: {}, replace长度: {}", trimmed, value, replace.length); + return false; + } } return true; } @@ -249,9 +301,10 @@ public final class ExcelAnnotationUtils { String resolved = displayToCode.getOrDefault(value, value); if (type == String.class) { - // 逗号分隔多值:逐个翻译后再拼回(如 "部门A,部门B" → "id_a,id_b") - if (resolved.equals(value) && value.contains(",")) { - String[] parts = value.split(","); + // 逗号分隔多值:逐个翻译后再拼回(如 "部门A,部门B" → "id_a,id_b"),兼容中英文逗号 + String normalized = normalizeComma(value); + if (resolved.equals(value) && normalized.contains(",")) { + String[] parts = normalized.split(","); StringBuilder sb = new StringBuilder(); for (int i = 0; i < parts.length; i++) { if (i > 0) sb.append(","); @@ -406,6 +459,14 @@ public final class ExcelAnnotationUtils { return sb.toString(); } + /** + * 将中文逗号归一化为英文逗号,兼容 Excel 中两种逗号混用。 + */ + private static String normalizeComma(String value) { + if (value == null) return null; + return value.replace(',', ','); + } + private static String[] mergeReplace(String[] original, String[] dictReplace) { if (original == null || original.length == 0) return dictReplace; if (dictReplace == null || dictReplace.length == 0) return original; diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/pom.xml b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/pom.xml index 4498b9c..284a9ad 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/pom.xml +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/pom.xml @@ -47,7 +47,13 @@ org.jeecgframework.boot jeecg-system-local-api - + + + org.jeecgframework.boot + jeecg-module-im + ${jeecgProjectVersion} + + org.flowable diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/controller/ActTaskController.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/controller/ActTaskController.java index 396441f..7cf41f0 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/controller/ActTaskController.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/bpm/controller/ActTaskController.java @@ -357,6 +357,9 @@ public class ActTaskController { map.put("defaultOpinionCond0", extActProcessNode.getDefaultOpinion()); map.put("defaultOpinionCond1", extActProcessNode.getDefaultOpinion2()); } + // 流程标识和当前节点编码,供前端做驳回目标过滤 + map.put("processKey", pd.getKey()); + map.put("taskDefinitionKey", task.getTaskDefinitionKey()); //--update--end------author:scott-----date:20220705-----for:【VUEN-1332】流程节点配置智能化------------- // 流程审批日志 diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/common/expression/FlowNodeExpression.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/common/expression/FlowNodeExpression.java index 998d42a..fce908a 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/common/expression/FlowNodeExpression.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/common/expression/FlowNodeExpression.java @@ -262,6 +262,20 @@ public class FlowNodeExpression { return ISysBaseAPI.getUsersListByJsonLocalAPI(json, "deptId", roleId); } + /** + * Get username list by system role id without department filtering. + * Example: ${flowNodeExpression.getUsersListByRole('roleIdValue')} + * + * @param roleId system role id + * @return username list + */ + public List getUsersListByRole(String roleId) { + if (StringUtils.isBlank(roleId)) { + return Collections.emptyList(); + } + return sysbase.getUserByRoleIdLocalApi(roleId); + } + /** * Get subprocess variable. * Example: ${flowNodeExpression.getSonProcessVariable(execution, 'subApproveUser')} diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/controller/ExtActProcessNodeController.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/controller/ExtActProcessNodeController.java index a6717cf..9494d0d 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/controller/ExtActProcessNodeController.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/controller/ExtActProcessNodeController.java @@ -22,12 +22,15 @@ import org.jeecg.modules.extbpm.process.entity.ExtActFlowData; import org.jeecg.modules.extbpm.process.entity.ExtActProcessNode; import org.jeecg.modules.extbpm.process.entity.ExtActProcessNodeDeployment; import org.jeecg.modules.extbpm.process.entity.ExtActProcessNodePermission; +import org.jeecg.modules.extbpm.process.entity.ExtActProcessNodeRejectTarget; import org.jeecg.modules.extbpm.process.pojo.BizTaskDTO; import org.jeecg.modules.extbpm.process.service.IExtActFlowDataService; import org.jeecg.modules.extbpm.process.service.IExtActProcessNodeDeploymentService; import org.jeecg.modules.extbpm.process.service.IExtActProcessNodePermissionService; +import org.jeecg.modules.extbpm.process.service.IExtActProcessNodeRejectTargetService; import org.jeecg.modules.extbpm.process.service.IExtActProcessNodeService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; @@ -67,6 +70,8 @@ public class ExtActProcessNodeController { private IExtActProcessNodePermissionService extActProcessNodePermissionService; @Autowired private IExtActFlowDataService extActFlowDataService; + @Autowired + private IExtActProcessNodeRejectTargetService extActProcessNodeRejectTargetService; /** * 分页列表查询 @@ -142,7 +147,7 @@ public class ExtActProcessNodeController { result.success("删除成功!"); } } - + return result; } @@ -158,12 +163,58 @@ public class ExtActProcessNodeController { if(ids==null || "".equals(ids.trim())) { result.error500("参数不识别!"); }else { - this.extActProcessNodeService.removeByIds(Arrays.asList(ids.split(","))); + List idList = Arrays.asList(ids.split(",")); + this.extActProcessNodeService.removeByIds(idList); result.success("删除成功!"); } return result; } + @GetMapping(value = "/queryRejectTargets") + public Result> queryRejectTargets( + @RequestParam(name="processId", required=true) String processId, + @RequestParam(name="sourceNodeCode", required=true) String sourceNodeCode) { + Result> result = new Result<>(); + List list = extActProcessNodeRejectTargetService.queryBySourceNode(processId, sourceNodeCode); + result.setResult(list); + result.setSuccess(true); + return result; + } + + /** + * 保存节点的驳回目标配置(先删旧、后插新) + */ + @Transactional + @PostMapping(value = "/saveRejectTargets") + public Result saveRejectTargets( + @RequestParam(name="processId", required=true) String processId, + @RequestParam(name="sourceNodeCode", required=true) String sourceNodeCode, + @RequestBody(required = false) List rejectTargetList) { + // 删除旧配置 + LambdaQueryWrapper deleteWrapper = new LambdaQueryWrapper<>(); + deleteWrapper.eq(ExtActProcessNodeRejectTarget::getProcessId, processId); + deleteWrapper.eq(ExtActProcessNodeRejectTarget::getSourceNodeCode, sourceNodeCode); + extActProcessNodeRejectTargetService.remove(deleteWrapper); + // 插入新配置 + if (rejectTargetList != null && !rejectTargetList.isEmpty()) { + extActProcessNodeRejectTargetService.saveBatch(rejectTargetList); + } + return Result.ok("保存成功"); + } + /** + * 根据 processKey + nodeCode 查询可驳回的目标节点CODE列表(运行时过滤用) + */ + @GetMapping(value = "/queryRejectTargetCodes") + public Result> queryRejectTargetCodes( + @RequestParam(name="processKey", required=true) String processKey, + @RequestParam(name="nodeCode", required=true) String nodeCode) { + Result> result = new Result<>(); + List list = extActProcessNodeRejectTargetService.queryRejectTargetCodesByProcessKeyAndNodeCode(processKey, nodeCode); + result.setResult(list); + result.setSuccess(true); + return result; + } + /** * 通过id查询 * @param id diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/entity/ExtActProcessNodeRejectTarget.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/entity/ExtActProcessNodeRejectTarget.java new file mode 100644 index 0000000..b40d568 --- /dev/null +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/entity/ExtActProcessNodeRejectTarget.java @@ -0,0 +1,33 @@ +package org.jeecg.modules.extbpm.process.entity; + +import java.io.Serializable; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +/** + * @Description: 流程节点驳回目标配置 + * @author: jeecg-boot + * @date: 2026-07-27 + * @version: V1.0 + */ +@Data +@TableName("ext_act_process_node_reject_target") +public class ExtActProcessNodeRejectTarget implements Serializable { + private static final long serialVersionUID = 1L; + + /**id*/ + @TableId(type = IdType.ASSIGN_ID) + private java.lang.String id; + /**流程ID*/ + private java.lang.String processId; + /**源节点CODE(当前配置驳回的节点)*/ + private java.lang.String sourceNodeCode; + /**目标节点CODE(可以驳回到的节点)*/ + private java.lang.String targetNodeCode; + /**目标节点名称*/ + private java.lang.String targetNodeName; +} diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/mapper/ExtActProcessNodeRejectTargetMapper.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/mapper/ExtActProcessNodeRejectTargetMapper.java new file mode 100644 index 0000000..08eea21 --- /dev/null +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/mapper/ExtActProcessNodeRejectTargetMapper.java @@ -0,0 +1,31 @@ +package org.jeecg.modules.extbpm.process.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; +import org.jeecg.modules.extbpm.process.entity.ExtActProcessNodeRejectTarget; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @Description: 流程节点驳回目标配置 + * @author: jeecg-boot + * @date: 2026-07-27 + * @version: V1.0 + */ +public interface ExtActProcessNodeRejectTargetMapper extends BaseMapper { + + /** + * 通过 processKey + nodeCode 查询可驳回的目标节点CODE列表(运行时过滤用) + */ + List queryRejectTargetCodesByProcessKeyAndNodeCode( + @Param("processKey") String processKey, + @Param("nodeCode") String nodeCode); + + /** + * 查询某源节点的所有驳回目标配置(配置页面回显用) + */ + List queryBySourceNode( + @Param("processId") String processId, + @Param("sourceNodeCode") String sourceNodeCode); +} diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/mapper/xml/ExtActProcessNodeRejectTargetMapper.xml b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/mapper/xml/ExtActProcessNodeRejectTargetMapper.xml new file mode 100644 index 0000000..bd376bd --- /dev/null +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/mapper/xml/ExtActProcessNodeRejectTargetMapper.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/service/IExtActProcessNodeRejectTargetService.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/service/IExtActProcessNodeRejectTargetService.java new file mode 100644 index 0000000..74dd93f --- /dev/null +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/service/IExtActProcessNodeRejectTargetService.java @@ -0,0 +1,20 @@ +package org.jeecg.modules.extbpm.process.service; + +import java.util.List; + +import org.jeecg.modules.extbpm.process.entity.ExtActProcessNodeRejectTarget; + +import com.baomidou.mybatisplus.extension.service.IService; + +/** + * @Description: 流程节点驳回目标配置 + * @author: jeecg-boot + * @date: 2026-07-27 + * @version: V1.0 + */ +public interface IExtActProcessNodeRejectTargetService extends IService { + + List queryRejectTargetCodesByProcessKeyAndNodeCode(String processKey, String nodeCode); + + List queryBySourceNode(String processId, String sourceNodeCode); +} diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/service/impl/ExtActProcessNodeRejectTargetServiceImpl.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/service/impl/ExtActProcessNodeRejectTargetServiceImpl.java new file mode 100644 index 0000000..036e484 --- /dev/null +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/extbpm/process/service/impl/ExtActProcessNodeRejectTargetServiceImpl.java @@ -0,0 +1,36 @@ +package org.jeecg.modules.extbpm.process.service.impl; + +import java.util.List; + +import org.jeecg.modules.extbpm.process.entity.ExtActProcessNodeRejectTarget; +import org.jeecg.modules.extbpm.process.mapper.ExtActProcessNodeRejectTargetMapper; +import org.jeecg.modules.extbpm.process.service.IExtActProcessNodeRejectTargetService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +/** + * @Description: 流程节点驳回目标配置 + * @author: jeecg-boot + * @date: 2026-07-27 + * @version: V1.0 + */ +@Service("extActProcessNodeRejectTargetService") +public class ExtActProcessNodeRejectTargetServiceImpl + extends ServiceImpl + implements IExtActProcessNodeRejectTargetService { + + @Autowired + private ExtActProcessNodeRejectTargetMapper extActProcessNodeRejectTargetMapper; + + @Override + public List queryRejectTargetCodesByProcessKeyAndNodeCode(String processKey, String nodeCode) { + return extActProcessNodeRejectTargetMapper.queryRejectTargetCodesByProcessKeyAndNodeCode(processKey, nodeCode); + } + + @Override + public List queryBySourceNode(String processId, String sourceNodeCode) { + return extActProcessNodeRejectTargetMapper.queryBySourceNode(processId, sourceNodeCode); + } +} diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/controller/TaskApprovalOpinionController.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/controller/TaskApprovalOpinionController.java index 96fc998..fceaea0 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/controller/TaskApprovalOpinionController.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/controller/TaskApprovalOpinionController.java @@ -105,9 +105,9 @@ public class TaskApprovalOpinionController extends JeecgController supplement(@RequestBody SupplementApprovalOpinionDTO supplementDTO) { - taskApprovalOpinionService.saveSupplementOpinion(supplementDTO); - return Result.OK("提交成功!"); + public Result> supplement(@RequestBody SupplementApprovalOpinionDTO supplementDTO) { + Map pushResult = taskApprovalOpinionService.saveSupplementOpinion(supplementDTO); + return Result.OK(pushResult); } /** diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/dto/SupplementApprovalOpinionDTO.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/dto/SupplementApprovalOpinionDTO.java index 44bb6d3..2f50af4 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/dto/SupplementApprovalOpinionDTO.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/dto/SupplementApprovalOpinionDTO.java @@ -5,9 +5,6 @@ import lombok.Data; import java.io.Serializable; -/** - * 增补审批意见提交参数。 - */ @Data @Schema(description = "增补审批意见提交参数") public class SupplementApprovalOpinionDTO implements Serializable { @@ -15,22 +12,18 @@ public class SupplementApprovalOpinionDTO implements Serializable { @Schema(description = "流程实例ID") private String processInstId; - @Schema(description = "Flowable任务ID") private String flowTaskId; - @Schema(description = "流程节点Key") private String taskDefKey; - - @Schema(description = "事项任务ID,对应task_task.id") + @Schema(description = "事项任务ID") private String taskTaskId; - @Schema(description = "关联业务ID") private String businessId; - @Schema(description = "流程编码") private String flowCode; - + @Schema(description = "业务标题") + private String bizTitle; @Schema(description = "增补审批意见") private String opinion; } diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/service/ITaskApprovalOpinionService.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/service/ITaskApprovalOpinionService.java index c337926..f817a49 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/service/ITaskApprovalOpinionService.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/service/ITaskApprovalOpinionService.java @@ -1,15 +1,10 @@ package org.jeecg.modules.taskapprovalopinion.service; +import java.util.Map; import org.jeecg.modules.taskapprovalopinion.entity.TaskApprovalOpinion; import org.jeecg.modules.taskapprovalopinion.dto.SupplementApprovalOpinionDTO; import com.baomidou.mybatisplus.extension.service.IService; -/** - * 审批意见快照 Service。 - */ public interface ITaskApprovalOpinionService extends IService { - /** - * 保存增补审批意见,仅写入 task_approval_opinion。 - */ - void saveSupplementOpinion(SupplementApprovalOpinionDTO supplementDTO); + Map saveSupplementOpinion(SupplementApprovalOpinionDTO supplementDTO); } diff --git a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/service/impl/TaskApprovalOpinionServiceImpl.java b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/service/impl/TaskApprovalOpinionServiceImpl.java index f286a51..a4779ce 100644 --- a/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/service/impl/TaskApprovalOpinionServiceImpl.java +++ b/jeecg-boot-platform/jeecg-boot-module-bpm-flowable/src/main/java/org/jeecg/modules/taskapprovalopinion/service/impl/TaskApprovalOpinionServiceImpl.java @@ -5,12 +5,14 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; +import org.jeecg.common.api.vo.Result; import org.jeecg.common.exception.JeecgBootException; import org.jeecg.common.system.api.ISysBaseAPI; import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.extbpm.process.entity.ExtActFlowData; import org.jeecg.modules.extbpm.process.mapper.ExtActFlowDataMapper; +import org.jeecg.modules.ksim.service.IKsimMessageService; import org.jeecg.modules.taskapprovalopinion.dto.SupplementApprovalOpinionDTO; import org.jeecg.modules.taskapprovalopinion.entity.TaskApprovalOpinion; import org.jeecg.modules.taskapprovalopinion.mapper.TaskApprovalOpinionMapper; @@ -19,9 +21,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -43,13 +48,11 @@ public class TaskApprovalOpinionServiceImpl extends ServiceImpl saveSupplementOpinion(SupplementApprovalOpinionDTO supplementDTO) { if (supplementDTO == null || oConvertUtils.isEmpty(supplementDTO.getProcessInstId())) { throw new JeecgBootException("流程实例ID不能为空"); } @@ -87,7 +90,63 @@ public class TaskApprovalOpinionServiceImpl extends ServiceImpl doPushNotification(TaskApprovalOpinion opinion, ApprovalDeptInfo deptInfo, + LoginUser loginUser, String opinionText, String frontendBizTitle) { + Map pushResult = new HashMap<>(); + // 确保所有代码路径返回一致的 key 集合 + pushResult.put("pushed", false); + pushResult.put("toUsers", Collections.emptyList()); + pushResult.put("content", ""); + pushResult.put("title", ""); + pushResult.put("error", ""); + + try { + if (!isSuoLeader(deptInfo)) { + return pushResult; + } + String bizTitle = oConvertUtils.isNotEmpty(frontendBizTitle) + ? frontendBizTitle + : getBizTitle(opinion.getTaskTaskId(), opinion.getProcessInstId()); + String content = "所领导" + (oConvertUtils.isNotEmpty(loginUser.getRealname()) ? loginUser.getRealname() : loginUser.getUsername()) + + "增补意见:" + opinionText; + LinkedHashMap handlerMap = getDistinctHandlerNames(opinion.getProcessInstId(), loginUser.getUsername(), loginUser.getRealname()); + List handlerIds = new ArrayList<>(handlerMap.keySet()); + List handlerNames = new ArrayList<>(handlerMap.values()); + pushResult.put("pushed", !handlerIds.isEmpty()); + pushResult.put("toUsers", handlerNames); + pushResult.put("content", content); + pushResult.put("title", bizTitle); + if (!handlerIds.isEmpty()) { + Result sendResult = ksimMessageService.sendBatch("Account", handlerIds, bizTitle, content, + null, null, null, null, null); + if (sendResult != null && sendResult.isSuccess()) { + log.info("【增补意见-即时通推送】已发送, processInstId: {}, toUserCount: {}, toUsers: {}, title: {}, content: {}", + opinion.getProcessInstId(), handlerIds.size(), handlerNames, bizTitle, content); + } else { + String reason = sendResult != null ? sendResult.getMessage() : "null"; + log.warn("【增补意见-即时通推送】发送失败, reason: {}", reason); + pushResult.put("pushed", false); + pushResult.put("error", reason); + } + } + } catch (Exception e) { + String errMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); + log.warn("【增补意见-即时通推送】失败,不影响增补意见保存, error: {}", errMsg); + pushResult.put("pushed", false); + pushResult.put("error", errMsg); + } + return pushResult; } private LoginUser getCurrentLoginUser() { @@ -98,13 +157,11 @@ public class TaskApprovalOpinionServiceImpl extends ServiceImpl> rows = jdbcTemplate.queryForList( + "SELECT depart_type FROM sys_depart WHERE id = ?", deptId); + if (!rows.isEmpty()) { + departType = getStringValue(rows.get(0), "depart_type"); + } + } catch (Exception e) { + log.warn("【增补意见-审批意见】查询部门类型失败, deptId={}", deptId, e); + } + } + return new ApprovalDeptInfo(deptId, deptName, deptCode, departType); } private String resolveDeptName(String username) { @@ -137,7 +211,7 @@ public class TaskApprovalOpinionServiceImpl extends ServiceImpl> rows = jdbcTemplate.queryForList( + "select title from task_task where id = ?", taskTaskId); + if (!rows.isEmpty()) { + Object val = rows.get(0).get("title"); + if (val != null && !String.valueOf(val).trim().isEmpty()) { + return String.valueOf(val).trim(); + } + } + } catch (Exception e) { + log.debug("查询 task_task.title 失败, taskTaskId={}", taskTaskId, e); + } + } + if (oConvertUtils.isNotEmpty(processInstId)) { + try { + ExtActFlowData flowData = queryFlowData(processInstId); + if (flowData != null && oConvertUtils.isNotEmpty(flowData.getProcessExpTitle())) { + return flowData.getProcessExpTitle().trim(); + } + } catch (Exception e) { + log.debug("查询 flow title 失败, processInstId={}", processInstId, e); + } + } + return ""; + } + + /** userId → userName 映射,保持插入顺序 */ + private LinkedHashMap getDistinctHandlerNames(String processInstId, String excludeUserId, String excludeRealName) { + if (jdbcTemplate == null || oConvertUtils.isEmpty(processInstId)) { + return new LinkedHashMap<>(); + } + try { + List> rows = jdbcTemplate.queryForList( + "SELECT DISTINCT op_user_id, op_user_name FROM task_approval_opinion WHERE process_inst_id = ?", + processInstId); + LinkedHashMap handlerMap = new LinkedHashMap<>(); + for (Map row : rows) { + Object idVal = row.get("op_user_id"); + Object nameVal = row.get("op_user_name"); + if (idVal == null) { + continue; + } + String userId = String.valueOf(idVal).trim(); + if (userId.isEmpty()) { + continue; + } + String name = nameVal != null ? String.valueOf(nameVal).trim() : ""; + + // 主判断:工号匹配则排除(最可靠) + if (userId.equals(excludeUserId)) { + log.info("【增补意见-即时通推送】排除本人(工号匹配), userId: {}, name: {}", userId, name); + continue; + } + // 辅助判断:姓名匹配则排除(防止工号异常等边界情况) + if (oConvertUtils.isNotEmpty(excludeRealName) && name.equals(excludeRealName)) { + log.info("【增补意见-即时通推送】排除本人(姓名匹配), name: {}, userId: {}", name, userId); + continue; + } + handlerMap.putIfAbsent(userId, name); + } + log.info("【增补意见-即时通推送】查询经办人完成, processInstId: {}, DB记录数: {}, 排除后: {}", + processInstId, rows.size(), handlerMap.size()); + return handlerMap; + } catch (Exception e) { + log.warn("【增补意见-即时通推送】查询经办人失败, processInstId={}", processInstId, e); + return new LinkedHashMap<>(); + } + } + private static class ApprovalDeptInfo { private final String id; private final String name; private final String code; + private final String departType; - private ApprovalDeptInfo(String id, String name, String code) { + private ApprovalDeptInfo(String id, String name, String code, String departType) { this.id = id; this.name = name; this.code = code; + this.departType = departType; } } } diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java index 8f38989..ce05a92 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/bg/xispeak/controller/BgXiSpeakController.java @@ -18,6 +18,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.query.QueryGenerator; +import org.jeecg.common.util.excel.ExcelAnnotationUtils; import org.jeecg.common.util.oConvertUtils; import org.jeecg.common.system.vo.SelectTreeModel; import org.jeecg.modules.bg.constant.BpmStatus; @@ -456,7 +457,9 @@ public class BgXiSpeakController extends JeecgController importExcelByEasyExcel(HttpServletRequest request) { - return super.importExcelByEasyExcel(request, BgTakepulse.class); + return super.importExcelByEasyExcel(request, BgTakepulse.class, + Map.of( + SupervisionConstant.ClassFieldName.BPM_STATUS_NAME, + BpmStatus.NOT_START.getCode(), + SupervisionConstant.ClassFieldName.DEL_FLAG_NAME, + SupervisionConstant.DelFlag.NORMAL)); } /** @@ -468,7 +477,9 @@ public class BgTakepulseController extends JeecgController importExcelByEasyExcel(HttpServletRequest request) { - return super.importExcelByEasyExcel(request, DjInspectImprove.class); + return super.importExcelByEasyExcel(request, DjInspectImprove.class, + Map.of( + SupervisionConstant.ClassFieldName.BPM_STATUS_NAME, + BpmStatus.NOT_START.getCode(), + SupervisionConstant.ClassFieldName.DEL_FLAG_NAME, + SupervisionConstant.DelFlag.NORMAL)); } /** diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/controller/ImprovementProposalController.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/controller/ImprovementProposalController.java index 46bd949..cfdc25a 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/controller/ImprovementProposalController.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/controller/ImprovementProposalController.java @@ -102,7 +102,8 @@ public class ImprovementProposalController { ImprovementProposal improvementProposal = new ImprovementProposal(); BeanUtils.copyProperties(improvementProposalPage, improvementProposal); improvementProposal.setBpmStatus("1"); - improvementProposalService.saveMain(improvementProposal, improvementProposalPage.getImprovementDeptScoreList(),improvementProposalPage.getImprovementInstituteVoteList()); + // 新增申请阶段只创建主表,评分和投票子表由后续流程节点维护。 + improvementProposalService.saveMain(improvementProposal, null, null); return Result.OK("添加成功!"); } @@ -117,14 +118,36 @@ public class ImprovementProposalController { @RequiresPermissions("improvementproposal:improvement_proposal:edit") @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) public Result edit(@RequestBody ImprovementProposalPage improvementProposalPage) { - ImprovementProposal improvementProposal = new ImprovementProposal(); - BeanUtils.copyProperties(improvementProposalPage, improvementProposal); - ImprovementProposal improvementProposalEntity = improvementProposalService.getById(improvementProposal.getId()); - if(improvementProposalEntity==null) { - return Result.error("未找到对应数据"); + try { + ImprovementProposal improvementProposal = new ImprovementProposal(); + BeanUtils.copyProperties(improvementProposalPage, improvementProposal); + improvementProposalService.updateMain(improvementProposal, improvementProposalPage.getImprovementDeptScoreList(),improvementProposalPage.getImprovementInstituteVoteList()); + return Result.OK("编辑成功!"); + } catch (IllegalStateException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 使用改善提案主表作为流程业务数据发起审批,不复用督办任务表。 + */ + @PostMapping(value = "/startProcess") + public Result startProcess(@RequestBody(required = false) Map requestBody, + @RequestParam(name = "id", required = false) String id) { + // defHttp 的 POST 参数默认在请求体中;同时兼容外部调用将 id 放在 URL 查询串的场景。 + if (oConvertUtils.isEmpty(id) && requestBody != null) { + id = requestBody.get("id"); + } + if (oConvertUtils.isEmpty(id)) { + return Result.error("提案ID不能为空"); + } + LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal(); + try { + String processInstanceId = improvementProposalService.startProcess(id, sysUser.getUsername()); + return Result.OK("流程发起成功!", processInstanceId); + } catch (IllegalStateException e) { + return Result.error(e.getMessage()); } - improvementProposalService.updateMain(improvementProposal, improvementProposalPage.getImprovementDeptScoreList(),improvementProposalPage.getImprovementInstituteVoteList()); - return Result.OK("编辑成功!"); } /** @@ -138,8 +161,12 @@ public class ImprovementProposalController { @RequiresPermissions("improvementproposal:improvement_proposal:delete") @DeleteMapping(value = "/delete") public Result delete(@RequestParam(name="id",required=true) String id) { - improvementProposalService.delMain(id); - return Result.OK("删除成功!"); + try { + improvementProposalService.delMain(id); + return Result.OK("删除成功!"); + } catch (IllegalStateException e) { + return Result.error(e.getMessage()); + } } /** @@ -153,8 +180,12 @@ public class ImprovementProposalController { @RequiresPermissions("improvementproposal:improvement_proposal:deleteBatch") @DeleteMapping(value = "/deleteBatch") public Result deleteBatch(@RequestParam(name="ids",required=true) String ids) { - this.improvementProposalService.delBatchMain(Arrays.asList(ids.split(","))); - return Result.OK("批量删除成功!"); + try { + this.improvementProposalService.delBatchMain(Arrays.asList(ids.split(","))); + return Result.OK("批量删除成功!"); + } catch (IllegalStateException e) { + return Result.error(e.getMessage()); + } } /** @@ -278,7 +309,8 @@ public class ImprovementProposalController { for (ImprovementProposalPage page : list) { ImprovementProposal po = new ImprovementProposal(); BeanUtils.copyProperties(page, po); - improvementProposalService.saveMain(po, page.getImprovementDeptScoreList(),page.getImprovementInstituteVoteList()); + // 导入新提案时同样只创建主表,不提前生成评分或投票数据。 + improvementProposalService.saveMain(po, null, null); } return Result.OK("文件导入成功!数据行数:" + list.size()); } catch (Exception e) { diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/entity/ImprovementProposal.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/entity/ImprovementProposal.java index 0727838..650146a 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/entity/ImprovementProposal.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/entity/ImprovementProposal.java @@ -101,9 +101,13 @@ public class ImprovementProposal implements Serializable { @Schema(description = "部门精益专员初步评定奖项") private String initialAward; /**是否需要部门评议打分:Y/N*/ - @Excel(name = "是否需要部门评议打分:Y/N", width = 15) - @Schema(description = "是否需要部门评议打分:Y/N") + @Excel(name = "是否需要部门评议打分:1/0", width = 15) + @Schema(description = "是否需要部门评议打分:1/0") private String needDeptScore; + /**是否需要部门评议打分:Y/N*/ + @Excel(name = "是否需要所级评议小组投票:1/0", width = 15) + @Schema(description = "是否需要所级评议小组投票:1/0") + private String isneedvote; /**部门评议平均分*/ @Excel(name = "部门评议平均分", width = 15) @Schema(description = "部门评议平均分") diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/mapper/ImprovementProposalMapper.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/mapper/ImprovementProposalMapper.java index d607042..335b2d5 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/mapper/ImprovementProposalMapper.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/mapper/ImprovementProposalMapper.java @@ -1,8 +1,7 @@ package org.jeecg.modules.improvementproposal.mapper; -import java.util.List; - import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.jeecg.modules.improvementproposal.entity.ImprovementProposal; import com.baomidou.mybatisplus.core.mapper.BaseMapper; @@ -15,5 +14,10 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; public interface ImprovementProposalMapper extends BaseMapper { String selectLatestProposalNoByYear(@Param("year") Integer year); + /** + * 锁定提案主表,串行化流程发起、编辑和删除操作,避免同一提案生成多个流程实例。 + */ + @Select("SELECT * FROM improvement_proposal WHERE id = #{id} AND del_flag = '0' FOR UPDATE") + ImprovementProposal selectByIdForUpdate(@Param("id") String id); } diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/service/IImprovementProposalService.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/service/IImprovementProposalService.java index 2c73003..c3b4deb 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/service/IImprovementProposalService.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/service/IImprovementProposalService.java @@ -16,6 +16,15 @@ import java.util.List; */ public interface IImprovementProposalService extends IService { + /** + * 发起改善提案审批流程。 + * + * @param id 提案主表 ID + * @param username 当前发起人账号 + * @return 流程实例 ID + */ + String startProcess(String id, String username); + /** * 添加一对多 * diff --git a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/service/impl/ImprovementProposalServiceImpl.java b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/service/impl/ImprovementProposalServiceImpl.java index 6901dce..8ca1514 100644 --- a/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/service/impl/ImprovementProposalServiceImpl.java +++ b/jeecg-module-supervision/src/main/java/org/jeecg/modules/improvementproposal/service/impl/ImprovementProposalServiceImpl.java @@ -7,6 +7,9 @@ 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.modules.extbpm.process.service.impl.BpmBaseExtApiImpl; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; @@ -26,6 +29,9 @@ import java.util.regex.Pattern; @Service public class ImprovementProposalServiceImpl extends ServiceImpl implements IImprovementProposalService { private static final String NORMAL_DEL_FLAG = "0"; + 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"; @Autowired private ImprovementProposalMapper improvementProposalMapper; @@ -33,6 +39,34 @@ public class ImprovementProposalServiceImpl extends ServiceImpl result = bpmBaseExtApi.startMutilProcess(FLOW_CODE, proposal.getId(), BPM_FORM_URL, BPM_FORM_URL, username, null); + if (result == null || !result.isSuccess() || oConvertUtils.isEmpty(result.getResult())) { + throw new IllegalStateException(result == null || oConvertUtils.isEmpty(result.getMessage()) ? "发起流程失败" : result.getMessage()); + } + + // 流程引擎会按 BPM 配置将 bpm_status 回写为处理中;此处使用局部对象更新,避免旧实体的草稿状态将其覆盖。 + ImprovementProposal processUpdate = new ImprovementProposal(); + processUpdate.setId(proposal.getId()); + processUpdate.setProcessInstanceId(result.getResult()); + if (improvementProposalMapper.updateById(processUpdate) != 1) { + throw new IllegalStateException("流程已启动,但流程实例关联保存失败"); + } + return result.getResult(); + } catch (IllegalStateException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("发起流程失败:" + e.getMessage(), e); + } + } @Override @Transactional(rollbackFor = Exception.class) @@ -67,6 +101,10 @@ public class ImprovementProposalServiceImpl extends ServiceImpl improvementDeptScoreList,List improvementInstituteVoteList) { + ImprovementProposal existingProposal = getDraftProposalForUpdate(improvementProposal.getId()); + // 草稿编辑不能伪造流程状态或覆盖已关联的流程实例。 + improvementProposal.setBpmStatus(existingProposal.getBpmStatus()); + improvementProposal.setProcessInstanceId(existingProposal.getProcessInstanceId()); improvementProposalMapper.updateById(improvementProposal); //1.先删除子表数据 @@ -99,6 +137,7 @@ public class ImprovementProposalServiceImpl extends ServiceImpl idList) { for(Serializable id:idList) { + getDraftProposalForUpdate(id.toString()); improvementDeptScoreMapper.deleteByMainId(id.toString()); improvementInstituteVoteMapper.deleteByMainId(id.toString()); improvementProposalMapper.deleteById(id); } } + + /** + * 任何会改变主表生命周期的操作都必须先锁定并确认仍是未发起草稿, + * 这样并发的“发起流程”和“编辑/删除”不会让流程与业务数据脱节。 + */ + private ImprovementProposal getDraftProposalForUpdate(String id) { + if (oConvertUtils.isEmpty(id)) { + throw new IllegalStateException("提案ID不能为空"); + } + ImprovementProposal proposal = improvementProposalMapper.selectByIdForUpdate(id); + if (proposal == null) { + throw new IllegalStateException("未找到对应提案"); + } + if (!BPM_STATUS_DRAFT.equals(proposal.getBpmStatus()) || oConvertUtils.isNotEmpty(proposal.getProcessInstanceId())) { + throw new IllegalStateException("提案已进入流程,不能编辑、删除或重复发起"); + } + return proposal; + } @Override public Integer getLatestProposalSequence(Integer year) {