diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizAction.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizAction.java
new file mode 100644
index 0000000..6a04fb6
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizAction.java
@@ -0,0 +1,38 @@
+package org.jeecg.modules.xslmes.approval.action;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 审批联动业务动作标注。
+ *
+ *
把本注解打在业务 Controller 的处理方法上,即声明「该按钮/接口可被审批流程作为回调动作选择」。
+ * 系统启动时会反射扫描所有带本注解的接口方法,取其 {@code @RequestMapping} 真实路径与 HTTP 方法,
+ * 按 {@link #table()} 归类,供审批流设计器的节点「回调接口」下拉选择。
+ *
+ * 因为基于 Spring 运行时反射 + 真实映射路径,生产环境天然可用,URL 绝对准确,无需任何源码解析。
+ *
+ * @author GHT
+ * @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注解
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ApprovalBizAction {
+
+ /** 动作展示名(如「批准」「反审核」「启用」),供设计器下拉显示 */
+ String name();
+
+ /** 所属业务表名(如 mes_xsl_rubber_quick_test_std),与审批流绑定的 bizTable 对应 */
+ String table();
+
+ /**
+ * 适用触发时机,可多选;为空表示三种时机均可选用。
+ * 取值:onNodeApprove(本节点通过) / onApprove(最终通过) / onReject(驳回)
+ */
+ String[] phase() default {};
+
+ /** 排序,越小越靠前 */
+ int order() default 0;
+}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizActionRegistry.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizActionRegistry.java
new file mode 100644
index 0000000..ccafe30
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizActionRegistry.java
@@ -0,0 +1,154 @@
+package org.jeecg.modules.xslmes.approval.action;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+
+import jakarta.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 审批联动业务动作注册表。
+ *
+ * 容器启动完成后,反射扫描所有带 {@link ApprovalBizAction} 的接口方法,
+ * 取真实映射路径 + HTTP 方法,按业务表归类缓存,供设计器查询选择。
+ *
+ * @author GHT
+ * @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表
+ */
+@Slf4j
+@Component
+public class ApprovalBizActionRegistry {
+
+ @Resource
+ private ApplicationContext applicationContext;
+
+ /** 业务表 -> 动作列表 */
+ private final Map> byTable = new ConcurrentHashMap<>();
+
+ @EventListener(ContextRefreshedEvent.class)
+ public void onContextRefreshed() {
+ try {
+ scan();
+ } catch (Exception e) {
+ log.warn("[审批联动] 扫描 @ApprovalBizAction 失败", e);
+ }
+ }
+
+ private synchronized void scan() {
+ byTable.clear();
+ RequestMappingHandlerMapping mapping;
+ try {
+ mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
+ } catch (Exception e) {
+ log.warn("[审批联动] 未取到 RequestMappingHandlerMapping,跳过扫描", e);
+ return;
+ }
+ Map handlerMethods = mapping.getHandlerMethods();
+ for (Map.Entry entry : handlerMethods.entrySet()) {
+ HandlerMethod hm = entry.getValue();
+ ApprovalBizAction ann = hm.getMethodAnnotation(ApprovalBizAction.class);
+ if (ann == null || ann.table() == null || ann.table().isEmpty()) {
+ continue;
+ }
+ RequestMappingInfo info = entry.getKey();
+ String url = resolveUrl(info);
+ if (url == null || url.isEmpty()) {
+ continue;
+ }
+ ApprovalBizActionVo vo = new ApprovalBizActionVo();
+ vo.setName(ann.name());
+ vo.setTable(ann.table());
+ vo.setPhase(ann.phase());
+ vo.setOrder(ann.order());
+ vo.setUrl(url);
+ vo.setMethod(resolveMethod(info));
+ vo.setPerms(resolvePerms(hm));
+ byTable.computeIfAbsent(ann.table(), k -> new ArrayList<>()).add(vo);
+ }
+ // 排序
+ for (List list : byTable.values()) {
+ list.sort(Comparator.comparingInt(ApprovalBizActionVo::getOrder));
+ }
+ int total = byTable.values().stream().mapToInt(List::size).sum();
+ log.info("[审批联动] 已注册业务动作 {} 个,覆盖 {} 张业务表", total, byTable.size());
+ }
+
+ /** 取第一个映射路径(不含 context-path) */
+ private String resolveUrl(RequestMappingInfo info) {
+ Set patterns = info.getPatternValues();
+ if (patterns == null || patterns.isEmpty()) {
+ return null;
+ }
+ return patterns.iterator().next();
+ }
+
+ /** 取 HTTP 方法,默认 POST */
+ private String resolveMethod(RequestMappingInfo info) {
+ RequestMethodsRequestCondition cond = info.getMethodsCondition();
+ if (cond == null || cond.getMethods().isEmpty()) {
+ return "POST";
+ }
+ return cond.getMethods().iterator().next().name();
+ }
+
+ /** 取接口权限标识(@RequiresPermissions) */
+ private String resolvePerms(HandlerMethod hm) {
+ RequiresPermissions rp = hm.getMethodAnnotation(RequiresPermissions.class);
+ if (rp != null && rp.value().length > 0) {
+ return rp.value()[0];
+ }
+ return null;
+ }
+
+ /** 按业务表取可选动作 */
+ public List getByTable(String table) {
+ if (table == null) {
+ return Collections.emptyList();
+ }
+ List list = byTable.get(table);
+ return list == null ? Collections.emptyList() : new ArrayList<>(list);
+ }
+
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表+时机自动取业务动作-----------
+ /**
+ * 按业务表 + 时机取动作(如驳回统一执行 onReject 动作,无需在每个流程节点手动配置)。
+ */
+ public List getByTableAndPhase(String table, String phase) {
+ if (table == null || phase == null) {
+ return Collections.emptyList();
+ }
+ List list = byTable.get(table);
+ if (list == null || list.isEmpty()) {
+ return Collections.emptyList();
+ }
+ List result = new ArrayList<>();
+ for (ApprovalBizActionVo vo : list) {
+ String[] phases = vo.getPhase();
+ if (phases == null) {
+ continue;
+ }
+ for (String p : phases) {
+ if (phase.equals(p)) {
+ result.add(vo);
+ break;
+ }
+ }
+ }
+ return result;
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表+时机自动取业务动作-----------
+}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizActionVo.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizActionVo.java
new file mode 100644
index 0000000..df4e23d
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizActionVo.java
@@ -0,0 +1,38 @@
+package org.jeecg.modules.xslmes.approval.action;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 审批联动业务动作(供设计器节点「回调接口」下拉选择)。
+ *
+ * @author GHT
+ * @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作VO
+ */
+@Data
+public class ApprovalBizActionVo implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 动作名(如「批准」) */
+ private String name;
+
+ /** 接口真实路径(取自 @RequestMapping,不含 context-path),如 /xslmes/xxx/updateStatus */
+ private String url;
+
+ /** HTTP 方法:GET/POST/PUT/DELETE */
+ private String method;
+
+ /** 所属业务表 */
+ private String table;
+
+ /** 适用时机:onNodeApprove/onApprove/onReject,空表示均可 */
+ private String[] phase;
+
+ /** 接口权限标识(取自 @RequiresPermissions,可空) */
+ private String perms;
+
+ /** 排序 */
+ private int order;
+}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/callback/ApprovalActionHttpExecutor.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/callback/ApprovalActionHttpExecutor.java
index ace8db7..ac2d1f7 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/callback/ApprovalActionHttpExecutor.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/callback/ApprovalActionHttpExecutor.java
@@ -50,6 +50,27 @@ public class ApprovalActionHttpExecutor {
* @param phase 时机:onNodeApprove / onApprove / onReject
* @param inst 审批实例
*/
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】判断节点某阶段是否配置了业务回调-----------
+ /**
+ * 判断节点在指定时机是否配置了回调接口(用于决定是否由业务回调全权负责回退)。
+ */
+ public boolean hasActions(JSONObject node, String phase) {
+ if (node == null) {
+ return false;
+ }
+ JSONObject propsObj = node.getJSONObject("props");
+ if (propsObj == null) {
+ return false;
+ }
+ JSONObject callbackActions = propsObj.getJSONObject("callbackActions");
+ if (callbackActions == null) {
+ return false;
+ }
+ JSONArray actions = callbackActions.getJSONArray(phase);
+ return actions != null && !actions.isEmpty();
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】判断节点某阶段是否配置了业务回调-----------
+
public void run(JSONObject node, String phase, MesXslApprovalInstance inst) {
if (node == null || inst == null) {
return;
@@ -86,6 +107,25 @@ public class ApprovalActionHttpExecutor {
}
}
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表注解自动调用业务接口-----------
+ /**
+ * 直接按 url+method 调用业务接口(用于驳回统一回退,动作来自 @ApprovalBizAction 注解而非节点配置)。
+ * 无当前处理人登录态时降级跳过。
+ */
+ public void runByUrl(String method, String url, MesXslApprovalInstance inst) {
+ if (oConvertUtils.isEmpty(url) || inst == null) {
+ return;
+ }
+ String token = currentToken();
+ if (oConvertUtils.isEmpty(token)) {
+ log.warn("[审批回调] 无当前处理人登录态,跳过驳回业务回退 url={}, bizId={}", url, inst.getBizDataId());
+ return;
+ }
+ String httpMethod = oConvertUtils.getString(method, "POST").toUpperCase();
+ invoke(httpMethod, url, null, inst.getBizDataId(), token);
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表注解自动调用业务接口-----------
+
private void invoke(String method, String url, JSONObject recordedBody, String bizDataId, String token) {
String fullUrl = buildFullUrl(url);
HttpMethod httpMethod = HttpMethod.valueOf(method);
@@ -95,13 +135,15 @@ public class ApprovalActionHttpExecutor {
headers.add(CommonConstant.X_ACCESS_TOKEN, token);
headers.add(HttpHeaders.AUTHORIZATION, token);
+ // 统一在 url 上附带单据ID的常见参数名,兼容 @RequestParam(id/ids/dataId) 形式的接口
+ // (如密炼PS的 /proofread、/audit、/approve 用的是 @RequestParam ids)
+ String idValue = oConvertUtils.getString(bizDataId, "");
+ String sep = fullUrl.contains("?") ? "&" : "?";
+ fullUrl = fullUrl + sep + "id=" + idValue + "&ids=" + idValue + "&dataId=" + idValue;
+
Object bodyToSend = null;
- if ("GET".equals(method) || "DELETE".equals(method)) {
- // 查询/删除:单据ID拼到 url
- String sep = fullUrl.contains("?") ? "&" : "?";
- fullUrl = fullUrl + sep + "id=" + bizDataId + "&dataId=" + bizDataId;
- } else {
- // 写操作:合并录制的 body,并用单据ID覆盖 id
+ if (!("GET".equals(method) || "DELETE".equals(method))) {
+ // 写操作:合并录制的 body,并用单据ID覆盖 id(兼容 @RequestBody 形式)
JSONObject body = recordedBody == null ? new JSONObject() : new JSONObject(recordedBody);
body.put("id", bizDataId);
bodyToSend = body;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalFlowController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalFlowController.java
index 80ebc07..14358ef 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalFlowController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalFlowController.java
@@ -15,6 +15,8 @@ import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.util.oConvertUtils;
+import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionRegistry;
+import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionVo;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
@@ -47,6 +49,11 @@ public class MesXslApprovalFlowController extends JeecgController> bizActions(@RequestParam(name = "table") String table) {
+ return Result.OK(approvalBizActionRegistry.getByTable(table));
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按业务表查可选回调动作-----
+
/**
* 根据前端路由反查业务表名。
* 约定:jeecg 代码生成的列表组件名为 表名驼峰 + List,sys_permission.component 形如
* xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,url 即路由。
* 反查:url=routePath -> component -> 末段组件名去掉 List -> 驼峰转下划线得到表名。
*/
- private String resolveTableByRoutePath(String routePath) {
+ /** 根据前端路由反查页面组件路径(sys_permission.component),用于前端按 key 取该页面按钮接口映射 */
+ private String resolveComponentByRoutePath(String routePath) {
if (oConvertUtils.isEmpty(routePath)) {
return null;
}
@@ -272,17 +294,20 @@ public class MesXslApprovalFlowController extends JeecgController list = jdbcTemplate.queryForList(sql, String.class, path);
- if (list.isEmpty()) {
- return null;
- }
- component = list.get(0);
+ return list.isEmpty() ? null : list.get(0);
} catch (Exception e) {
log.warn("反查菜单组件失败 routePath={}", routePath, e);
return null;
}
+ }
+
+ private String resolveTableByRoutePath(String routePath) {
+ if (oConvertUtils.isEmpty(routePath)) {
+ return null;
+ }
+ String component = resolveComponentByRoutePath(routePath);
if (oConvertUtils.isEmpty(component)) {
return null;
}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java
index 2a57020..6860c3e 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalHandleController.java
@@ -11,6 +11,7 @@ import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
+import java.util.List;
import java.util.Map;
/**
@@ -71,4 +72,40 @@ public class MesXslApprovalHandleController {
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
return approvalHandleService.reject(instanceId, reason, user);
}
+
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
+ @Operation(summary = "审批办理-撤销(发起人撤回)")
+ @PostMapping("/cancel")
+ public Result cancel(@RequestBody Map body) {
+ String instanceId = (String) body.get("instanceId");
+ String reason = body.get("reason") == null ? null : String.valueOf(body.get("reason"));
+ if (oConvertUtils.isEmpty(instanceId)) {
+ return Result.error("缺少审批实例ID");
+ }
+ LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
+ return approvalHandleService.cancel(instanceId, reason, user);
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
+
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
+ @Operation(summary = "审批办理-催办(发起人催处理人)")
+ @PostMapping("/urge")
+ public Result urge(@RequestBody Map body) {
+ String instanceId = (String) body.get("instanceId");
+ if (oConvertUtils.isEmpty(instanceId)) {
+ return Result.error("缺少审批实例ID");
+ }
+ LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
+ return approvalHandleService.urge(instanceId, user);
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
+
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
+ @Operation(summary = "审批办理-我的待办列表")
+ @GetMapping("/pendingList")
+ public Result>> pendingList() {
+ LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
+ return Result.OK(approvalHandleService.pendingList(user));
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
index 9e9e663..59e8245 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
@@ -1,5 +1,6 @@
package org.jeecg.modules.xslmes.approval.controller;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -165,6 +166,16 @@ public class MesXslApprovalLaunchController {
return Result.error("该审批流未发布,无法发起");
}
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】防止同一单据重复发起审批-----
+ long active = approvalInstanceService.count(new LambdaQueryWrapper()
+ .eq(MesXslApprovalInstance::getBizTable, flow.getBizTable())
+ .eq(MesXslApprovalInstance::getBizDataId, bizDataId)
+ .eq(MesXslApprovalInstance::getStatus, "0"));
+ if (active > 0) {
+ return Result.error("该单据已有审批中的流程,请勿重复发起");
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】防止同一单据重复发起审批-----
+
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
@@ -196,6 +207,7 @@ public class MesXslApprovalLaunchController {
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
int count = 0;
+ int skipCount = 0;
for (Object o : (List>) itemsObj) {
if (!(o instanceof Map)) {
continue;
@@ -206,15 +218,29 @@ public class MesXslApprovalLaunchController {
if (oConvertUtils.isEmpty(bizDataId)) {
continue;
}
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
+ long active = approvalInstanceService.count(new LambdaQueryWrapper()
+ .eq(MesXslApprovalInstance::getBizTable, flow.getBizTable())
+ .eq(MesXslApprovalInstance::getBizDataId, bizDataId)
+ .eq(MesXslApprovalInstance::getStatus, "0"));
+ if (active > 0) {
+ skipCount++;
+ continue;
+ }
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
approvalInstanceService.save(inst);
approvalHandleService.enterFirstNode(inst, loginUser);
count++;
}
if (count == 0) {
- return Result.error("没有有效的单据数据");
+ return Result.error(skipCount > 0 ? "所选单据均已有审批中的流程,无需重复发起" : "没有有效的单据数据");
}
- return Result.OK("已发起 " + count + " 条审批!");
+ String msg = "已发起 " + count + " 条审批!";
+ if (skipCount > 0) {
+ msg += "(" + skipCount + " 条已有审批中流程,已跳过)";
+ }
+ return Result.OK(msg);
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
index 378c8fc..f0f59cb 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
@@ -43,6 +43,16 @@ public class MesXslApprovalFlow extends JeecgEntity implements Serializable {
@Schema(description = "对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)")
private String routePath;
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
+ @Schema(description = "业务单据状态字段名(驳回/撤销时回写其发起时原值)")
+ private String statusField;
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
+
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒配置-----
+ @Schema(description = "超时提醒小时数(0=不提醒),默认24")
+ private Integer timeoutHours;
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒配置-----
+
@Schema(description = "流程设计JSON(钉钉式节点树)")
private String flowConfig;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalInstance.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalInstance.java
index dac24c1..92ea05e 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalInstance.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalInstance.java
@@ -46,6 +46,14 @@ public class MesXslApprovalInstance extends JeecgEntity implements Serializable
@Schema(description = "业务单据展示标题")
private String bizTitle;
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
+ @Schema(description = "业务单据状态字段名(发起时快照)")
+ private String statusField;
+
+ @Schema(description = "发起审批时业务状态原值(驳回/撤销回写)")
+ private String originStatus;
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
+
@Schema(description = "当前节点ID")
private String currentNodeId;
@@ -83,6 +91,12 @@ public class MesXslApprovalInstance extends JeecgEntity implements Serializable
@Schema(description = "备注")
private String remark;
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】乐观锁防并发写-----
+ @com.baomidou.mybatisplus.annotation.Version
+ @Schema(description = "乐观锁版本号")
+ private Integer version;
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】乐观锁防并发写-----
+
@Schema(description = "逻辑删除:0正常 1已删除")
@TableLogic
private Integer delFlag;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/scheduler/MesXslApprovalTimeoutScheduler.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/scheduler/MesXslApprovalTimeoutScheduler.java
new file mode 100644
index 0000000..2ad3dfd
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/scheduler/MesXslApprovalTimeoutScheduler.java
@@ -0,0 +1,40 @@
+package org.jeecg.modules.xslmes.approval.scheduler;
+
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 审批超时提醒定时任务。
+ * 每小时扫描一次所有审批中的实例,对超过各流程配置 timeout_hours 仍未处理的节点
+ * 向当前处理人发送 IM 提醒,同一实例在同一超时周期内只提醒一次。
+ *
+ * @author GHT
+ * @date 2026-05-29 for:【QH-MES审批流完善】超时提醒调度器
+ */
+//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒调度器-----
+@Component
+@Slf4j
+public class MesXslApprovalTimeoutScheduler {
+
+ @Autowired
+ private IMesXslApprovalHandleService approvalHandleService;
+
+ /**
+ * 每小时整点执行:扫描超时未处理的审批实例并发送提醒。
+ * 使用 fixedDelay 避免并发执行;应用启动 5 分钟后开始首次执行。
+ */
+ @Scheduled(initialDelayString = "PT5M", fixedDelayString = "PT1H")
+ public void remindTimeout() {
+ log.info("开始扫描审批超时实例...");
+ try {
+ approvalHandleService.remindTimeoutInstances();
+ } catch (Exception e) {
+ log.warn("审批超时扫描异常", e);
+ }
+ log.info("审批超时实例扫描完成");
+ }
+}
+//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒调度器-----
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java
index b66d60b..4740346 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalHandleService.java
@@ -4,6 +4,7 @@ import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
+import java.util.List;
import java.util.Map;
/**
@@ -31,6 +32,13 @@ public interface IMesXslApprovalHandleService {
*/
Result reject(String instanceId, String reason, LoginUser user);
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
+ /**
+ * 撤销:仅发起人本人在审批中可撤回,流程终止并将业务单据恢复到发起时状态。
+ */
+ Result cancel(String instanceId, String reason, LoginUser user);
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
+
/**
* 查看单据全部字段 + 审批进度/历史,供 IM 卡片"查看详情"弹窗使用。
*/
@@ -41,4 +49,25 @@ public interface IMesXslApprovalHandleService {
* 返回 status/statusText/currentNodeId/currentNodeName/canApprove。
*/
Map statusInfo(String instanceId, LoginUser user);
+
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
+ /**
+ * 催办:发起人向当前处理人发送一次性催办提醒,同一实例每小时最多催一次。
+ */
+ Result urge(String instanceId, LoginUser user);
+ //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
+
+ //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表查询-----
+ /**
+ * 查询当前用户的待办审批列表(状态为审批中且当前处理人包含该用户)。
+ */
+ List