Merge branch 'master' of gitee.com:sz_30/supervison-java into wsq-20260727

Signed-off-by: wsm <8454518+new-twice@user.noreply.gitee.com>
This commit is contained in:
wsm
2026-07-27 06:29:13 +00:00
committed by Gitee
33 changed files with 724 additions and 132 deletions
+2 -1
View File
@@ -7,8 +7,9 @@ localDocs
## backend
**/target
**/logs
**/yml
**/.claude
**/yml
*.bundle
## front
**/*.lock
+12
View File
@@ -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='流程节点驳回目标配置';
@@ -176,6 +176,14 @@ public class JeecgController<T, S extends IService<T>> {
writeExcel(response, clazz, title, Collections.emptyList());
}
/**
* 导出自定义数据(含表头),供子类导出模板示例行
*/
protected void exportXlsWithData(HttpServletResponse response,
Class<T> clazz, String title, List<T> data) throws IOException {
writeExcel(response, clazz, title, data);
}
private void writeExcel(HttpServletResponse response, Class<T> clazz,
String title, List<T> data) throws IOException {
ExcelAnnotationUtils.write(response, clazz, title, data, dictQuery());
@@ -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<JSONObject> 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;
@@ -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<String> ALLOWED_LINK_OPEN_MODES = Set.of("Exe", "Web", "None");
@PostMapping("/sendBatch")
public Result<Map<String, Object>> sendBatch(@RequestBody Map<String, Object> params) {
Map<String, Object> 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);
@@ -1,6 +1,6 @@
package org.jeecg.modules.ksim.dto;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* 即时通 API 通用响应体
@@ -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<String> 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; }
@@ -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<String> 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<JSONObject> apiResult = apiClient.sendMsg(token, dto);
@@ -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;
@@ -111,6 +111,54 @@ public final class ExcelAnnotationUtils {
return columns;
}
/**
* 根据 @ExcelColumn 注解自动构建一个填充了示例数据的实体实例,用于导出模板。
* String 字段填 "示例" + 列名,dict 字段填 "示例文本",日期填当天,数字填 1。
*/
public static <T> T buildExample(Class<T> 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;
@@ -47,7 +47,13 @@
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
</dependency>
<!-- 即时通消息推送模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-module-im</artifactId>
<version>${jeecgProjectVersion}</version>
</dependency>
<!--flowable 核心依赖-->
<dependency>
<groupId>org.flowable</groupId>
@@ -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】流程节点配置智能化-------------
// 流程审批日志
@@ -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<String> getUsersListByRole(String roleId) {
if (StringUtils.isBlank(roleId)) {
return Collections.emptyList();
}
return sysbase.getUserByRoleIdLocalApi(roleId);
}
/**
* Get subprocess variable.
* Example: ${flowNodeExpression.getSonProcessVariable(execution, 'subApproveUser')}
@@ -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<String> idList = Arrays.asList(ids.split(","));
this.extActProcessNodeService.removeByIds(idList);
result.success("删除成功!");
}
return result;
}
@GetMapping(value = "/queryRejectTargets")
public Result<List<ExtActProcessNodeRejectTarget>> queryRejectTargets(
@RequestParam(name="processId", required=true) String processId,
@RequestParam(name="sourceNodeCode", required=true) String sourceNodeCode) {
Result<List<ExtActProcessNodeRejectTarget>> result = new Result<>();
List<ExtActProcessNodeRejectTarget> 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<ExtActProcessNodeRejectTarget> rejectTargetList) {
// 删除旧配置
LambdaQueryWrapper<ExtActProcessNodeRejectTarget> 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<List<String>> queryRejectTargetCodes(
@RequestParam(name="processKey", required=true) String processKey,
@RequestParam(name="nodeCode", required=true) String nodeCode) {
Result<List<String>> result = new Result<>();
List<String> list = extActProcessNodeRejectTargetService.queryRejectTargetCodesByProcessKeyAndNodeCode(processKey, nodeCode);
result.setResult(list);
result.setSuccess(true);
return result;
}
/**
* 通过id查询
* @param id
@@ -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;
}
@@ -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<ExtActProcessNodeRejectTarget> {
/**
* 通过 processKey + nodeCode 查询可驳回的目标节点CODE列表(运行时过滤用)
*/
List<String> queryRejectTargetCodesByProcessKeyAndNodeCode(
@Param("processKey") String processKey,
@Param("nodeCode") String nodeCode);
/**
* 查询某源节点的所有驳回目标配置(配置页面回显用)
*/
List<ExtActProcessNodeRejectTarget> queryBySourceNode(
@Param("processId") String processId,
@Param("sourceNodeCode") String sourceNodeCode);
}
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.extbpm.process.mapper.ExtActProcessNodeRejectTargetMapper">
<resultMap id="ExtActProcessNodeRejectTarget" type="org.jeecg.modules.extbpm.process.entity.ExtActProcessNodeRejectTarget">
<result column="id" property="id" jdbcType="VARCHAR"/>
<result column="process_id" property="processId" jdbcType="VARCHAR"/>
<result column="source_node_code" property="sourceNodeCode" jdbcType="VARCHAR"/>
<result column="target_node_code" property="targetNodeCode" jdbcType="VARCHAR"/>
<result column="target_node_name" property="targetNodeName" jdbcType="VARCHAR"/>
</resultMap>
<!-- 通过 processKey + nodeCode 查询可驳回目标节点CODE(运行时过滤) -->
<select id="queryRejectTargetCodesByProcessKeyAndNodeCode" resultType="java.lang.String">
SELECT rt.target_node_code
FROM ext_act_process_node_reject_target rt
INNER JOIN ext_act_process_node eapn
ON rt.process_id = eapn.process_id
AND rt.source_node_code = eapn.process_node_code
INNER JOIN ext_act_process eap
ON eapn.process_id = eap.id
WHERE eap.process_key = #{processKey}
AND eapn.process_node_code = #{nodeCode}
</select>
<!-- 查询某源节点的所有驳回目标配置(配置页面回显) -->
<select id="queryBySourceNode" resultMap="ExtActProcessNodeRejectTarget">
SELECT id, process_id, source_node_code, target_node_code, target_node_name
FROM ext_act_process_node_reject_target
WHERE process_id = #{processId}
AND source_node_code = #{sourceNodeCode}
</select>
</mapper>
@@ -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<ExtActProcessNodeRejectTarget> {
List<String> queryRejectTargetCodesByProcessKeyAndNodeCode(String processKey, String nodeCode);
List<ExtActProcessNodeRejectTarget> queryBySourceNode(String processId, String sourceNodeCode);
}
@@ -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<ExtActProcessNodeRejectTargetMapper, ExtActProcessNodeRejectTarget>
implements IExtActProcessNodeRejectTargetService {
@Autowired
private ExtActProcessNodeRejectTargetMapper extActProcessNodeRejectTargetMapper;
@Override
public List<String> queryRejectTargetCodesByProcessKeyAndNodeCode(String processKey, String nodeCode) {
return extActProcessNodeRejectTargetMapper.queryRejectTargetCodesByProcessKeyAndNodeCode(processKey, nodeCode);
}
@Override
public List<ExtActProcessNodeRejectTarget> queryBySourceNode(String processId, String sourceNodeCode) {
return extActProcessNodeRejectTargetMapper.queryBySourceNode(processId, sourceNodeCode);
}
}
@@ -105,9 +105,9 @@ public class TaskApprovalOpinionController extends JeecgController<TaskApprovalO
@AutoLog(value = "task_approval_opinion-增补审批意见")
@Operation(summary="task_approval_opinion-增补审批意见")
@PostMapping(value = "/supplement")
public Result<String> supplement(@RequestBody SupplementApprovalOpinionDTO supplementDTO) {
taskApprovalOpinionService.saveSupplementOpinion(supplementDTO);
return Result.OK("提交成功!");
public Result<Map<String, Object>> supplement(@RequestBody SupplementApprovalOpinionDTO supplementDTO) {
Map<String, Object> pushResult = taskApprovalOpinionService.saveSupplementOpinion(supplementDTO);
return Result.OK(pushResult);
}
/**
@@ -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;
}
@@ -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<TaskApprovalOpinion> {
/**
* 保存增补审批意见,仅写入 task_approval_opinion。
*/
void saveSupplementOpinion(SupplementApprovalOpinionDTO supplementDTO);
Map<String, Object> saveSupplementOpinion(SupplementApprovalOpinionDTO supplementDTO);
}
@@ -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<TaskApprovalOpin
private ExtActFlowDataMapper extActFlowDataMapper;
@Autowired(required = false)
private JdbcTemplate jdbcTemplate;
@Autowired
private IKsimMessageService ksimMessageService;
/**
* 保存增补审批意见。该入口只写 task_approval_opinion,不触碰 Flowable 原生表或旧流程日志表。
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void saveSupplementOpinion(SupplementApprovalOpinionDTO supplementDTO) {
public Map<String, Object> saveSupplementOpinion(SupplementApprovalOpinionDTO supplementDTO) {
if (supplementDTO == null || oConvertUtils.isEmpty(supplementDTO.getProcessInstId())) {
throw new JeecgBootException("流程实例ID不能为空");
}
@@ -87,7 +90,63 @@ public class TaskApprovalOpinionServiceImpl extends ServiceImpl<TaskApprovalOpin
opinion.setSysOrgCode(deptInfo.code);
fillBusinessSnapshot(opinion);
save(opinion);
if (!save(opinion)) {
throw new JeecgBootException("保存增补审批意见失败");
}
// --- 所领导增补意见 → 即时通推送(DB 事务外执行,避免 HTTP 调用占用连接池)---
return doPushNotification(opinion, deptInfo, loginUser, opinionText, supplementDTO.getBizTitle());
}
/**
* 所领导增补意见即时通推送。在 DB 事务外执行,避免 HTTP 调用占用数据库连接。
*/
private Map<String, Object> doPushNotification(TaskApprovalOpinion opinion, ApprovalDeptInfo deptInfo,
LoginUser loginUser, String opinionText, String frontendBizTitle) {
Map<String, Object> 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<String, String> handlerMap = getDistinctHandlerNames(opinion.getProcessInstId(), loginUser.getUsername(), loginUser.getRealname());
List<String> handlerIds = new ArrayList<>(handlerMap.keySet());
List<String> 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<String> 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<TaskApprovalOpin
throw new JeecgBootException("未获取到当前登录用户");
}
/**
* 优先记录当前登录部门,避免多部门用户写入第一个所属部门导致审批历史筛选不准。
*/
private ApprovalDeptInfo resolveCurrentApprovalDept(LoginUser user, String username) {
String deptId = user != null ? user.getOrgId() : null;
String deptCode = user != null ? user.getOrgCode() : null;
String deptName = null;
String departType = null;
if (oConvertUtils.isNotEmpty(deptCode)) {
try {
@@ -118,16 +175,33 @@ public class TaskApprovalOpinionServiceImpl extends ServiceImpl<TaskApprovalOpin
if (oConvertUtils.isEmpty(deptName)) {
deptName = depart.getString("depart_name");
}
// 一次查询同时捕获 departType,避免 isSuoLeader 再查 sys_depart
departType = depart.getString("departType");
if (oConvertUtils.isEmpty(departType)) {
departType = depart.getString("depart_type");
}
}
} catch (Exception e) {
log.debug("获取当前登录部门失败, username={}, orgCode={}", username, deptCode, e);
log.warn("【增补意见-审批意见】获取当前登录部门失败, username={}, orgCode={}", username, deptCode, e);
}
}
if (oConvertUtils.isEmpty(deptName) && oConvertUtils.isNotEmpty(username)) {
deptName = resolveDeptName(username);
}
return new ApprovalDeptInfo(deptId, deptName, deptCode);
// 若 departType 仍未获取到,回退查 sys_depart 表(补回退,通常不会执行)
if (oConvertUtils.isEmpty(departType) && oConvertUtils.isNotEmpty(deptId) && jdbcTemplate != null) {
try {
List<Map<String, Object>> 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<TaskApprovalOpin
return names.get(0);
}
} catch (Exception e) {
log.debug("获取部门名称失败, username={}", username, e);
log.warn("【增补意见-审批意见】获取部门名称失败, username={}", username, e);
}
return null;
}
@@ -165,9 +239,6 @@ public class TaskApprovalOpinionServiceImpl extends ServiceImpl<TaskApprovalOpin
return flowDataList.isEmpty() ? null : flowDataList.get(0);
}
/**
* task_task 属于业务侧表,这里只做可选字段补齐,补齐失败不影响增补意见保存。
*/
private void fillTaskTaskFields(TaskApprovalOpinion opinion) {
if (jdbcTemplate == null || oConvertUtils.isEmpty(opinion.getTaskTaskId())) {
return;
@@ -203,15 +274,92 @@ public class TaskApprovalOpinionServiceImpl extends ServiceImpl<TaskApprovalOpin
return null;
}
private boolean isSuoLeader(ApprovalDeptInfo deptInfo) {
return "1".equals(deptInfo.departType);
}
private String getBizTitle(String taskTaskId, String processInstId) {
if (jdbcTemplate != null && oConvertUtils.isNotEmpty(taskTaskId)) {
try {
List<Map<String, Object>> 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<String, String> getDistinctHandlerNames(String processInstId, String excludeUserId, String excludeRealName) {
if (jdbcTemplate == null || oConvertUtils.isEmpty(processInstId)) {
return new LinkedHashMap<>();
}
try {
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
"SELECT DISTINCT op_user_id, op_user_name FROM task_approval_opinion WHERE process_inst_id = ?",
processInstId);
LinkedHashMap<String, String> handlerMap = new LinkedHashMap<>();
for (Map<String, Object> 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;
}
}
}
@@ -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<BgXiSpeak, IBgXiSpeakSe
@RequiresPermissions("bg.xispeak:bg_xi_speak:exportXls")
@GetMapping(value = "/exportXlsHeaders")
public void exportXlsHeaders(HttpServletResponse response) throws IOException {
super.exportXlsHeadersByEasyExcel(response, BgXiSpeak.class, "习总书记重要讲话指示批示情况");
BgXiSpeak example = ExcelAnnotationUtils.buildExample(BgXiSpeak.class);
super.exportXlsWithData(response, BgXiSpeak.class, "习总书记重要讲话指示批示情况",
java.util.Collections.singletonList(example));
}
/**
@@ -10,6 +10,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.excel.ExcelAnnotationUtils;
import org.jeecg.modules.bg.constant.BpmStatus;
import org.jeecg.modules.bg.constant.SupervisionConstant;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak;
import org.jeecg.modules.demo.dqinspecttask.entity.DqInspectTask;
import org.springframework.beans.factory.annotation.Autowired;
@@ -466,10 +469,11 @@ public class BgPartymatterController extends JeecgController<BgPartymatter, IBgP
return super.importExcelByEasyExcel(request, BgPartymatter.class,
Map.of(
BgPartymatterConstant.ClassFieldName.MEETING_TYPE_NAME, meetingType,
BgPartymatterConstant.ClassFieldName.DEL_FLAG_NAME,
BgPartymatterConstant.DelFlag.NORMAL));
BgPartymatterConstant.ClassFieldName.DEL_FLAG_NAME, BgPartymatterConstant.DelFlag.NORMAL,
SupervisionConstant.ClassFieldName.BPM_STATUS_NAME, BpmStatus.NOT_START.getCode()));
}
/**
* 通过EasyExcel导出数据,根据 meetingType 动态切换标题
*/
@@ -482,14 +486,15 @@ public class BgPartymatterController extends JeecgController<BgPartymatter, IBgP
}
/**
* 导出Excel表头(EasyExcel),通过 meetingType 参数指定模板类型
* 导出Excel模板(含表头和一行示例数据),通过 meetingType 参数指定模板类型
*/
@RequiresPermissions("bgpartymatter:bg_partymatter:exportXls")
@GetMapping(value = "/exportXlsHeaders")
public void exportXlsHeaders(HttpServletResponse response,
@RequestParam(defaultValue = "party") String meetingType) throws IOException {
String title = BgPartymatterConstant.MeetingType.OFFICE.equals(meetingType) ? "所务会" : "党委会";
super.exportXlsHeadersByEasyExcel(response, BgPartymatter.class, title);
BgPartymatter example = ExcelAnnotationUtils.buildExample(BgPartymatter.class);
super.exportXlsWithData(response, BgPartymatter.class, title, java.util.Collections.singletonList(example));
}
@@ -13,6 +13,9 @@ import javax.servlet.http.HttpServletResponse;
import com.alibaba.fastjson.JSONObject;
import org.flowable.engine.RuntimeService;
import org.jeecg.modules.bg.constant.BpmStatus;
import org.jeecg.modules.bg.constant.SupervisionConstant;
import org.jeecg.modules.bg.xispeak.entity.BgXiSpeak;
import org.jeecgframework.poi.excel.ExcelImportUtil;
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
import org.jeecgframework.poi.excel.entity.ExportParams;
@@ -24,6 +27,7 @@ import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.query.QueryRuleEnum;
import org.jeecg.common.util.excel.ExcelAnnotationUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.demo.bqtakepulse.dto.BgTakepulseBpmSaveDTO;
import org.jeecg.modules.demo.bqtakepulse.entity.BgTakepulseFeedback;
@@ -449,7 +453,12 @@ public class BgTakepulseController extends JeecgController<BgTakepulse, IBgTakep
@RequiresPermissions("bqtakepulse:bg_takepulse:importExcel")
@PostMapping(value = "/importExcelByEasyExcel")
public Result<?> 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<BgTakepulse, IBgTakep
@RequiresPermissions("bqtakepulse:bg_takepulse:exportXls")
@GetMapping(value = "/exportXlsHeaders")
public void exportXlsHeaders(HttpServletResponse response) throws IOException {
super.exportXlsHeadersByEasyExcel(response, BgTakepulse.class, "我为四所把把脉");
BgTakepulse example = ExcelAnnotationUtils.buildExample(BgTakepulse.class);
super.exportXlsWithData(response, BgTakepulse.class, "我为四所把把脉",
java.util.Collections.singletonList(example));
}
}
@@ -23,6 +23,7 @@ import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.query.QueryRuleEnum;
import org.jeecg.common.util.excel.ExcelAnnotationUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.modules.demo.dqinspecttask.entity.DqInspectProgress;
@@ -354,7 +355,9 @@ public class DqInspectTaskController extends JeecgController<DqInspectTask, IDqI
@RequiresPermissions("dqinspecttask:dq_inspect_task:exportXls")
@GetMapping(value = "/exportXlsHeaders")
public void exportXlsHeaders(HttpServletResponse response) throws IOException {
super.exportXlsHeadersByEasyExcel(response, DqInspectTask.class, "dq_inspect_task");
DqInspectTask example = ExcelAnnotationUtils.buildExample(DqInspectTask.class);
super.exportXlsWithData(response, DqInspectTask.class, "dq_inspect_task",
java.util.Collections.singletonList(example));
}
}
@@ -13,6 +13,9 @@ import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.common.system.vo.SelectTreeModel;
import org.jeecg.modules.bg.constant.BpmStatus;
import org.jeecg.modules.bg.constant.SupervisionConstant;
import org.jeecg.modules.demo.bqtakepulse.entity.BgTakepulse;
import org.jeecg.modules.dj.inspectimprove.entity.DjInspectImprove;
import org.jeecg.modules.dj.inspectimprove.service.IDjInspectImproveService;
@@ -310,7 +313,12 @@ public class DjInspectImproveController extends JeecgController<DjInspectImprove
@RequiresPermissions("dj.inspectimprove:dj_inspect_improve:importExcel")
@RequestMapping(value = "/importExcelByEasyExcel", method = RequestMethod.POST)
public Result<?> 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));
}
/**
@@ -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<String> 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<String> startProcess(@RequestBody(required = false) Map<String, String> 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<String> 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<String> 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) {
@@ -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 = "部门评议平均分")
@@ -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<ImprovementProposal> {
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);
}
@@ -16,6 +16,15 @@ import java.util.List;
*/
public interface IImprovementProposalService extends IService<ImprovementProposal> {
/**
* 发起改善提案审批流程。
*
* @param id 提案主表 ID
* @param username 当前发起人账号
* @return 流程实例 ID
*/
String startProcess(String id, String username);
/**
* 添加一对多
*
@@ -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<ImprovementProposalMapper, ImprovementProposal> 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<ImprovementPropo
private ImprovementDeptScoreMapper improvementDeptScoreMapper;
@Autowired
private ImprovementInstituteVoteMapper improvementInstituteVoteMapper;
@Autowired
private BpmBaseExtApiImpl bpmBaseExtApi;
@Override
@Transactional(rollbackFor = Exception.class)
public String startProcess(String id, String username) {
ImprovementProposal proposal = getDraftProposalForUpdate(id);
try {
// 改善提案以自身主表 ID 作为流程业务键,不引入 task_task 的重复督办中间表。
Result<String> 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<ImprovementPropo
@Override
@Transactional(rollbackFor = Exception.class)
public void updateMain(ImprovementProposal improvementProposal,List<ImprovementDeptScore> improvementDeptScoreList,List<ImprovementInstituteVote> 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<ImprovementPropo
@Override
@Transactional(rollbackFor = Exception.class)
public void delMain(String id) {
getDraftProposalForUpdate(id);
improvementDeptScoreMapper.deleteByMainId(id);
improvementInstituteVoteMapper.deleteByMainId(id);
improvementProposalMapper.deleteById(id);
@@ -108,11 +147,30 @@ public class ImprovementProposalServiceImpl extends ServiceImpl<ImprovementPropo
@Transactional(rollbackFor = Exception.class)
public void delBatchMain(Collection<? extends Serializable> 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) {