From 0ff4a201b00c1283630b5b68f92ed9f1a738e82a Mon Sep 17 00:00:00 2001 From: geht <2947093423@qq.com> Date: Fri, 29 May 2026 18:57:09 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84MES=E5=AE=A1=E6=89=B9?= =?UTF-8?q?=E6=B5=81=E8=AE=BE=E8=AE=A1=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=AE=A1=E6=89=B9=E5=8F=AF=E9=80=89=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E3=80=81=E5=8F=91=E8=B5=B7=E4=BA=BA=E6=92=A4?= =?UTF-8?q?=E9=94=80=E5=8F=8A=E5=82=AC=E5=8A=9E=E6=8E=A5=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=AE=A1=E6=89=B9=E7=8A=B6=E6=80=81=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E4=B8=8E=E8=81=94=E5=8A=A8=E5=9B=9E=E9=80=80=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E5=8D=87=E5=AE=A1=E6=89=B9=E6=B5=81=E7=A8=8B=E7=9A=84?= =?UTF-8?q?=E7=81=B5=E6=B4=BB=E6=80=A7=E4=B8=8E=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../approval/action/ApprovalBizAction.java | 38 + .../action/ApprovalBizActionRegistry.java | 154 ++++ .../approval/action/ApprovalBizActionVo.java | 38 + .../callback/ApprovalActionHttpExecutor.java | 54 +- .../MesXslApprovalFlowController.java | 37 +- .../MesXslApprovalHandleController.java | 37 + .../MesXslApprovalLaunchController.java | 30 +- .../approval/entity/MesXslApprovalFlow.java | 10 + .../entity/MesXslApprovalInstance.java | 14 + .../MesXslApprovalTimeoutScheduler.java | 40 ++ .../service/IMesXslApprovalHandleService.java | 29 + .../impl/MesXslApprovalHandleServiceImpl.java | 673 ++++++++++++++++-- .../xslmes/config/XslMesSchedulingConfig.java | 17 + .../MesXslMixerPsCompileController.java | 38 + .../MesXslRubberQuickTestStdController.java | 4 + .../service/IMesXslFormulaSpecService.java | 10 + .../service/IMesXslMixerPsCompileService.java | 18 + .../service/IMesXslMixingSpecService.java | 10 + .../IMesXslRubberQuickTestStdService.java | 7 + .../impl/MesXslFormulaSpecServiceImpl.java | 35 + .../impl/MesXslMixerPsCompileServiceImpl.java | 91 +++ .../impl/MesXslMixingSpecServiceImpl.java | 32 + .../MesXslRubberQuickTestStdServiceImpl.java | 19 + ...2_115__mes_xsl_approval_restore_status.sql | 14 + ...9.2_116__mes_xsl_approval_enhancements.sql | 10 + jeecgboot-vue3/src/utils/flowApiRecorder.ts | 94 --- jeecgboot-vue3/src/utils/http/axios/index.ts | 6 - .../views/approval/flow/approvalFlow.api.ts | 11 + .../views/approval/flow/approvalHandle.api.ts | 8 + .../approval/flow/components/FlowDesign.vue | 7 +- .../flow/components/NodeConfigDrawer.vue | 148 ++-- .../views/approval/flow/components/flow.less | 5 - .../system/im/ImBizRecordMessageContent.vue | 129 +++- 33 files changed, 1617 insertions(+), 250 deletions(-) create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizAction.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizActionRegistry.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/action/ApprovalBizActionVo.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/scheduler/MesXslApprovalTimeoutScheduler.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/config/XslMesSchedulingConfig.java create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_115__mes_xsl_approval_restore_status.sql create mode 100644 jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_116__mes_xsl_approval_enhancements.sql delete mode 100644 jeecgboot-vue3/src/utils/flowApiRecorder.ts 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> pendingList(LoginUser user); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表查询----- + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒(调度器调用)----- + /** + * 扫描超时未处理的审批实例并向当前处理人发送提醒(由定时任务调用)。 + */ + void remindTimeoutInstances(); + //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/impl/MesXslApprovalHandleServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java index 413adbc..c15e895 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java @@ -10,6 +10,8 @@ import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.im.service.ISysImChatService; import org.jeecg.modules.system.entity.SysUser; import org.jeecg.modules.system.service.ISysUserService; +import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionRegistry; +import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionVo; import org.jeecg.modules.xslmes.approval.callback.ApprovalActionHttpExecutor; import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext; import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackDispatcher; @@ -22,13 +24,19 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】通知改为事务提交后发送,解决处理人卡片不实时置灰----- +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】通知改为事务提交后发送,解决处理人卡片不实时置灰----- import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.regex.Pattern; /** @@ -48,6 +56,12 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer /** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */ private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$"); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办频率限制(同一实例1h内只催一次)----- + /** 记录最近一次催办时间,key=instanceId,value=毫秒时间戳。重启后清空,可接受 */ + private final Map urgeTimeMap = new java.util.concurrent.ConcurrentHashMap<>(); + private static final long URGE_MIN_INTERVAL_MS = 60 * 60 * 1000L; // 1小时 + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办频率限制(同一实例1h内只催一次)----- + @Autowired private IMesXslApprovalFlowService flowService; @@ -69,6 +83,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer @Autowired private ApprovalActionHttpExecutor actionHttpExecutor; + + // 驳回统一回退:按业务表自动发现 @ApprovalBizAction(onReject) 动作,无需在每个流程节点配置 + @Autowired + private ApprovalBizActionRegistry bizActionRegistry; //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调----- // ==================== 发起后进入首节点 ==================== @@ -90,7 +108,13 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer log.error("解析流程设计失败 flowId={}", inst.getFlowId(), e); return; } - JSONObject firstApprover = findFirstApprover(root); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起时快照业务状态原值,供驳回/撤销恢复----- + snapshotBizStatus(inst, flow); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起时快照业务状态原值,供驳回/撤销恢复----- + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】发起时读取业务记录用于条件分支评估----- + Map bizRecord = readBizRecord(inst.getBizTable(), inst.getBizDataId()); + JSONObject firstApprover = findFirstApprover(root, bizRecord); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】发起时读取业务记录用于条件分支评估----- if (firstApprover == null) { // 无审批节点 -> 自动通过 inst.setStatus("1"); @@ -168,6 +192,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer inst.setNodeProgress(progress.toJSONString()); inst.setCurrentHandlersText(pendingNames(tasks) + " 待会签"); instanceService.updateById(inst); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】会签模式:通知其他待处理人有人已通过----- + notifyCoApproversProgress(inst, flow, tasks, user, progress.getString("nodeName")); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】会签模式:通知其他待处理人有人已通过----- return Result.OK("已审批,等待其他会签人处理"); } @@ -234,18 +261,90 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer // 驳回 -> 回调业务(可回退业务状态) callbackDispatcher.fireRejected(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, reason)); - MesXslApprovalFlow rejectFlow = flowService.getById(inst.getFlowId()); - JSONObject rejectRoot = rejectFlow == null ? null : safeParse(rejectFlow.getFlowConfig()); - if (rejectRoot != null) { - actionHttpExecutor.run(findNodeById(rejectRoot, progress.getString("nodeId")), "onReject", inst); - } + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退(按表注解自动执行,全局一处维护)----- + revertBizOnReject(inst); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退(按表注解自动执行,全局一处维护)----- - String handlerName = oConvertUtils.getString(user.getRealname(), user.getUsername()); - notifyApplicant(inst, user.getUsername(), - "您发起的「" + safeTitle(inst) + "」被 " + handlerName + " 驳回。\n驳回理由:" + reason); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】驳回通知改为事务提交后发送,发起人/会签人卡片实时置灰----- + // 通知(发起人 + 同节点其他会签人)延迟到事务提交后发送: + // 否则对方收到WS时驳回事务尚未提交,查 getApprovalStatus 仍是「审批中」,卡片按钮不会实时置灰。 + final String nodeNameSnapshot = progress.getString("nodeName"); + final String handlerName = oConvertUtils.getString(user.getRealname(), user.getUsername()); + runAfterCommit(() -> { + notifyCoHandlersOnReject(inst, tasks, user, reason, nodeNameSnapshot); + notifyApplicant(inst, user.getUsername(), + "您发起的「" + safeTitle(inst) + "」被 " + handlerName + " 驳回。\n驳回理由:" + reason); + }); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】驳回通知改为事务提交后发送,发起人/会签人卡片实时置灰----- return Result.OK("已驳回"); } + // ==================== 撤销(发起人撤回) ==================== + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销并恢复业务单据到发起时状态----- + @Override + @Transactional(rollbackFor = Exception.class) + public Result cancel(String instanceId, String reason, LoginUser user) { + MesXslApprovalInstance inst = instanceService.getById(instanceId); + if (inst == null) { + return Result.error("审批实例不存在"); + } + if (!"0".equals(inst.getStatus())) { + return Result.error("该审批已结束,无法撤销"); + } + // 仅发起人本人可撤销 + if (user == null || !user.getUsername().equals(inst.getApplyUser())) { + return Result.error("只有发起人本人可以撤销该审批"); + } + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销仅允许单据仍处于发起前(最初)状态时进行----- + // 撤销 = 撤回整个审批流,只能在单据还是发起前最初状态时执行; + // 一旦下游节点已确认并推进了业务状态(current != 发起时快照originStatus),则不允许撤销。 + String statusField = inst.getStatusField(); + if (oConvertUtils.isNotEmpty(statusField) && inst.getOriginStatus() != null) { + String currentBizStatus = readBizFieldValue(inst.getBizTable(), inst.getBizDataId(), statusField); + if (!Objects.equals(inst.getOriginStatus(), currentBizStatus)) { + return Result.error("审批已进入下一环节并被处理,无法撤销"); + } + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销仅允许单据仍处于发起前(最初)状态时进行----- + String comment = oConvertUtils.getString(reason, "发起人撤销"); + appendHistory(inst, inst.getCurrentNodeId(), inst.getCurrentNodeName(), user, "cancel", comment); + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销通知改为事务提交后发送,处理人卡片实时置灰----- + // 先快照当前处理人(下面会清空)与通知所需信息,通知延迟到事务提交后发送: + // 否则处理人收到WS时撤销事务尚未提交,查 getApprovalStatus 仍是「审批中」,卡片按钮不会实时置灰。 + final String handlersSnapshot = inst.getCurrentHandlers(); + final String applicantName = oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser()); + final String bizTitleSnapshot = safeTitle(inst); + final String fromUserId = user.getId(); + final Integer tenantId = inst.getTenantId(); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销通知改为事务提交后发送,处理人卡片实时置灰----- + + inst.setStatus("3"); + inst.setCurrentHandlers(null); + inst.setCurrentHandlersText("已撤销"); + instanceService.updateById(inst); + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销改为恢复发起前快照,不再走业务「拒绝/驳回」接口----- + // 撤销时单据仍处于发起前最初状态,只需把状态字段恢复到发起时快照(幂等), + // 不能调用业务 onReject「拒绝」接口——该接口语义是从更高环节往回退,单据已是最初状态时会报「无需拒绝」导致撤销失败。 + restoreBizStatus(inst); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销改为恢复发起前快照,不再走业务「拒绝/驳回」接口----- + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销通知改为事务提交后发送,处理人卡片实时置灰----- + runAfterCommit(() -> { + if (oConvertUtils.isNotEmpty(handlersSnapshot)) { + for (String handler : handlersSnapshot.split(",")) { + sendOne(fromUserId, handler, tenantId, + "您待办的「" + bizTitleSnapshot + "」已被发起人 " + applicantName + " 撤销,无需处理。", "text"); + } + } + }); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销通知改为事务提交后发送,处理人卡片实时置灰----- + return Result.OK("已撤销"); + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销并恢复业务单据到发起时状态----- + // ==================== 查看详情 ==================== @Override @@ -326,7 +425,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer instanceService.updateById(inst); // 流程终止(等同驳回) -> 回调业务(可回退业务状态) callbackDispatcher.fireRejected(buildContext(inst, nodeId, nodeName, null, "审批人为空,流程终止")); - actionHttpExecutor.run(node, "onReject", inst); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】流程终止:驳回统一回退(按表注解自动执行)----- + revertBizOnReject(inst); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】流程终止:驳回统一回退(按表注解自动执行)----- notifyApplicant(inst, null, "您发起的「" + safeTitle(inst) + "」在「" + nodeName + "」节点无处理人,流程已终止。"); return; } @@ -354,6 +455,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer progress.put("nodeName", nodeName); progress.put("mode", mode); progress.put("tasks", tasks); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】记录进入节点时间,供超时提醒计算----- + progress.put("enterTime", now()); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】记录进入节点时间,供超时提醒计算----- // 活动处理人:依次审批仅首个,其余全部 List activeUsers = new ArrayList<>(); @@ -378,10 +482,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer sendApprovalCard(inst, flow, nodeName, activeUsers); } - /** 当前节点完成后,查找下一审批节点并推进;无则置为通过并通知发起人。 */ + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支路由替换原始线性flatten----- + /** 当前节点完成后,按条件分支路由查找下一审批节点并推进;无则置为通过并通知发起人。 */ private void advanceAfter(MesXslApprovalInstance inst, MesXslApprovalFlow flow, JSONObject root, String currentNodeId, LoginUser actingUser) { - List chain = new ArrayList<>(); - flatten(root, chain); + Map bizRecord = readBizRecord(inst.getBizTable(), inst.getBizDataId()); + List chain = buildExecSequence(root, bizRecord); int idx = -1; for (int i = 0; i < chain.size(); i++) { if (currentNodeId != null && currentNodeId.equals(chain.get(i).getString("id"))) { @@ -401,6 +506,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer } } if (nextApprover != null) { + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】节点流转时通知发起人进度----- + notifyApplicant(inst, actingUser == null ? null : actingUser.getUsername(), + "您发起的「" + safeTitle(inst) + "」已进入下一审批节点「" + nextApprover.getString("name") + "」,请关注进展。"); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】节点流转时通知发起人进度----- enterNode(inst, flow, root, nextApprover); } else { inst.setStatus("1"); @@ -414,6 +523,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer notifyApplicant(inst, actor, "您发起的「" + safeTitle(inst) + "」审批已全部通过。"); } } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支路由替换原始线性flatten----- // ==================== 处理人解析 ==================== @@ -573,7 +683,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer String content; String msgType; if (oConvertUtils.isNotEmpty(routePath)) { - content = buildCardJson(inst, null, false, routePath); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送卡片不带办理按钮,抄送人无需审核----- + content = buildCardJson(inst, null, false, routePath, false); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送卡片不带办理按钮,抄送人无需审核----- msgType = "biz_record"; } else { content = String.format("【抄送通知】%s 发起的「%s」抄送给您。", @@ -620,11 +732,22 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer /** 构建 biz_record 卡片 JSON(含 instanceId / actionLabel / canApprove,与前端 ImBizRecordPayload 对齐 v=2) */ private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath) { + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片----- + return buildCardJson(inst, actionLabel, canApprove, routePath, true); + } + + /** + * @param approvalCard true=审批卡片(带 instanceId/办理按钮);false=抄送通知卡片(仅展示+定位链接,无办理按钮) + */ + private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath, boolean approvalCard) { + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片----- JSONArray fields = new JSONArray(); addField(fields, "审批流", inst.getFlowName()); addField(fields, "单据", safeTitle(inst)); addField(fields, "发起人", oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser())); - addField(fields, "当前节点", oConvertUtils.getString(inst.getCurrentNodeName(), "审批")); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送卡片标注「抄送知会」----- + addField(fields, approvalCard ? "当前节点" : "知会", oConvertUtils.getString(inst.getCurrentNodeName(), approvalCard ? "审批" : "抄送")); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送卡片标注「抄送知会」----- addField(fields, "状态", statusText(inst.getStatus())); JSONObject item = new JSONObject(); @@ -641,14 +764,18 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer item.put("body", body.toString()); String sep = routePath.contains("?") ? "&" : "?"; item.put("linkPath", routePath + sep + "imRecordId=" + inst.getBizDataId()); - // 审批扩展字段 - item.put("instanceId", inst.getId()); - item.put("canApprove", canApprove); - // 该卡片对应的节点ID(用于前端实时校验是否仍为当前节点,旧节点卡片自动置灰) - item.put("nodeId", inst.getCurrentNodeId()); - if (oConvertUtils.isNotEmpty(actionLabel)) { - item.put("actionLabel", actionLabel); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】仅审批卡片带办理上下文,抄送卡片不可办理----- + if (approvalCard) { + // 审批扩展字段 + item.put("instanceId", inst.getId()); + item.put("canApprove", canApprove); + // 该卡片对应的节点ID(用于前端实时校验是否仍为当前节点,旧节点卡片自动置灰) + item.put("nodeId", inst.getCurrentNodeId()); + if (oConvertUtils.isNotEmpty(actionLabel)) { + item.put("actionLabel", actionLabel); + } } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】仅审批卡片带办理上下文,抄送卡片不可办理----- JSONArray items = new JSONArray(); items.add(item); @@ -668,6 +795,111 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer fields.add(f); } + // ==================== 业务状态快照/恢复 ==================== + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态----- + /** + * 发起审批时快照业务单据状态字段原值,写入实例。 + * 状态字段名来源:审批流配置 status_field 优先,未配置时自动探测单据表是否存在 status 列。 + */ + private void snapshotBizStatus(MesXslApprovalInstance inst, MesXslApprovalFlow flow) { + try { + String statusField = flow == null ? null : flow.getStatusField(); + if (oConvertUtils.isEmpty(statusField) && hasColumn(inst.getBizTable(), "status")) { + statusField = "status"; + } + if (oConvertUtils.isEmpty(statusField)) { + return; + } + inst.setStatusField(statusField); + inst.setOriginStatus(readBizFieldValue(inst.getBizTable(), inst.getBizDataId(), statusField)); + } catch (Exception e) { + log.warn("快照业务状态失败 table={}, id={}", inst.getBizTable(), inst.getBizDataId(), e); + } + } + + /** + * 驳回/撤销/终止统一回退入口(全局一处维护,所有流程通用): + * 按业务表自动发现标注了 {@code @ApprovalBizAction(phase=onReject)} 的业务接口并调用, + * 由业务接口全权负责回退(本单据状态 + 关联单据级联),无需在每个流程节点手动配置「驳回时执行」。 + * 未标注 onReject 业务动作的单据,退化为通用「恢复到发起时状态」。 + */ + private void revertBizOnReject(MesXslApprovalInstance inst) { + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】单据仍在发起前状态时驳回:跳过业务「拒绝」接口,避免「无需拒绝」报错----- + // 若单据当前状态仍等于发起前快照(说明尚未被任何节点审批推进,也没有级联变更), + // 则无需调用业务 onReject「拒绝」接口(其语义是从更高环节往回退,单据已是最初状态会报「无需拒绝」), + // 直接恢复快照(幂等)即可。常见于在第一个审批节点(如校对)就驳回的场景。 + if (isBizAtOriginStatus(inst)) { + restoreBizStatus(inst); + return; + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】单据仍在发起前状态时驳回:跳过业务「拒绝」接口,避免「无需拒绝」报错----- + List rejectActions = bizActionRegistry.getByTableAndPhase(inst.getBizTable(), "onReject"); + if (rejectActions != null && !rejectActions.isEmpty()) { + for (ApprovalBizActionVo action : rejectActions) { + actionHttpExecutor.runByUrl(action.getMethod(), action.getUrl(), inst); + } + } else { + restoreBizStatus(inst); + } + } + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】判断业务单据是否仍处于发起前(最初)状态----- + /** + * 判断业务单据当前状态字段是否仍等于发起审批时的快照原值。 + * true 表示单据尚未被任何节点推进(仍是最初状态);无快照字段时返回 false(交由正常业务回退处理)。 + */ + private boolean isBizAtOriginStatus(MesXslApprovalInstance inst) { + String statusField = inst.getStatusField(); + if (oConvertUtils.isEmpty(statusField) || inst.getOriginStatus() == null) { + return false; + } + String current = readBizFieldValue(inst.getBizTable(), inst.getBizDataId(), statusField); + return Objects.equals(inst.getOriginStatus(), current); + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】判断业务单据是否仍处于发起前(最初)状态----- + + /** + * 驳回/撤销/终止时,将业务单据状态字段直接回写为发起时原值。 + * 直接 UPDATE,不触发业务级联同步,确保单据回到“可重新提交”的初始态。 + */ + private void restoreBizStatus(MesXslApprovalInstance inst) { + String table = inst.getBizTable(); + String field = inst.getStatusField(); + String bizDataId = inst.getBizDataId(); + String origin = inst.getOriginStatus(); + if (oConvertUtils.isEmpty(table) || oConvertUtils.isEmpty(field) + || oConvertUtils.isEmpty(bizDataId) || origin == null) { + return; + } + if (!IDENTIFIER.matcher(table).matches() || !IDENTIFIER.matcher(field).matches()) { + return; + } + try { + jdbcTemplate.update("UPDATE " + table + " SET " + field + " = ? WHERE id = ?", origin, bizDataId); + log.info("审批结束恢复业务状态 table={}, id={}, {}={}", table, bizDataId, field, origin); + } catch (Exception e) { + log.warn("恢复业务状态失败 table={}, id={}, field={}", table, bizDataId, field, e); + } + } + + /** 判断业务表是否存在指定列 */ + private boolean hasColumn(String table, String column) { + if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) { + return false; + } + try { + Integer cnt = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM information_schema.columns " + + "WHERE table_schema = (SELECT DATABASE()) AND table_name = ? AND column_name = ?", + Integer.class, table, column); + return cnt != null && cnt > 0; + } catch (Exception e) { + return false; + } + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态----- + // ==================== 业务表读取 ==================== /** 读取单据指定字段值 */ @@ -768,29 +1000,146 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer } } - // ==================== 流程树遍历 ==================== + // ==================== 流程树遍历(含条件分支评估) ==================== - /** 沿流程树查找第一个审批节点(遇条件分支取首个分支后续) */ - private JSONObject findFirstApprover(JSONObject node) { - if (node == null) { - return null; - } - if ("approver".equals(node.getString("type"))) { - return node; - } - JSONArray branches = node.getJSONArray("conditionNodes"); - if (branches != null) { - for (int i = 0; i < branches.size(); i++) { - JSONObject found = findFirstApprover(branches.getJSONObject(i)); - if (found != null) { - return found; - } - } - } - return findFirstApprover(node.getJSONObject("childNode")); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支评估引擎----- + + /** + * 构建考虑条件分支后的执行序列(approver/cc 节点按执行顺序排列)。 + * 遇到 branch 节点时评估条件,选择唯一匹配的分支路径;无匹配则走最后一个分支(默认分支)。 + */ + private List buildExecSequence(JSONObject root, Map bizRecord) { + List seq = new ArrayList<>(); + buildExecSeq(root, seq, bizRecord); + return seq; } - /** 按节点ID查找节点(含条件分支与子节点) */ + private void buildExecSeq(JSONObject node, List out, Map bizRecord) { + if (node == null) { + return; + } + String type = node.getString("type"); + if ("approver".equals(type) || "cc".equals(type)) { + out.add(node); + } + if ("branch".equals(type)) { + // 条件分支容器:评估后选唯一路径,之后继续 childNode + JSONObject chosen = selectBranch(node, bizRecord); + if (chosen != null) { + buildExecSeq(chosen.getJSONObject("childNode"), out, bizRecord); + } + buildExecSeq(node.getJSONObject("childNode"), out, bizRecord); + return; + } + buildExecSeq(node.getJSONObject("childNode"), out, bizRecord); + } + + /** + * 从 branch 节点的 conditionNodes 中选出第一个满足条件的分支; + * 无匹配时返回最后一个分支(视为"否则"默认分支)。 + */ + private JSONObject selectBranch(JSONObject branchNode, Map bizRecord) { + JSONArray condNodes = branchNode.getJSONArray("conditionNodes"); + if (condNodes == null || condNodes.isEmpty()) { + return null; + } + JSONObject defaultBranch = condNodes.getJSONObject(condNodes.size() - 1); // 最后一个作为默认 + for (int i = 0; i < condNodes.size() - 1; i++) { // 跳过最后一个(默认) + JSONObject branch = condNodes.getJSONObject(i); + JSONObject props = branch.getJSONObject("props"); + if (props == null) { + continue; + } + JSONArray conditions = props.getJSONArray("conditions"); + if (conditions == null || conditions.isEmpty()) { + continue; + } + boolean isAnd = "and".equalsIgnoreCase(oConvertUtils.getString(props.getString("conditionType"), "and")); + if (evaluateConditions(conditions, bizRecord, isAnd)) { + return branch; + } + } + return defaultBranch; + } + + private boolean evaluateConditions(JSONArray conditions, Map bizRecord, boolean isAnd) { + if (conditions == null || conditions.isEmpty()) { + return true; + } + for (int i = 0; i < conditions.size(); i++) { + JSONObject cond = conditions.getJSONObject(i); + String field = cond.getString("field"); + String operator = oConvertUtils.getString(cond.getString("operator"), "eq"); + String expected = cond.getString("value"); + String actual = bizRecord == null ? null : bizRecord.get(field); + boolean match = evaluateCondition(actual, operator, expected); + if (isAnd && !match) { + return false; + } + if (!isAnd && match) { + return true; + } + } + return isAnd; // and:全部通过;or:没有一个通过 + } + + private boolean evaluateCondition(String actual, String operator, String expected) { + switch (operator) { + case "empty": return actual == null || actual.trim().isEmpty(); + case "notEmpty": return actual != null && !actual.trim().isEmpty(); + case "eq": return Objects.equals(actual, expected); + case "ne": return !Objects.equals(actual, expected); + case "contains": return actual != null && expected != null && actual.contains(expected); + case "gt": case "gte": case "lt": case "lte": + try { + double a = Double.parseDouble(actual == null ? "" : actual.trim()); + double e = Double.parseDouble(expected == null ? "" : expected.trim()); + if ("gt".equals(operator)) return a > e; + if ("gte".equals(operator)) return a >= e; + if ("lt".equals(operator)) return a < e; + return a <= e; + } catch (NumberFormatException ex) { + return false; + } + default: return false; + } + } + + /** 读取业务单据全部字段为 Map,用于条件分支评估 */ + private Map readBizRecord(String table, String bizDataId) { + Map result = new HashMap<>(); + if (oConvertUtils.isEmpty(table) || oConvertUtils.isEmpty(bizDataId) || !IDENTIFIER.matcher(table).matches()) { + return result; + } + try { + List> rows = jdbcTemplate.queryForList( + "SELECT * FROM " + table + " WHERE id = ? LIMIT 1", bizDataId); + if (rows.isEmpty()) { + return result; + } + for (Map.Entry e : rows.get(0).entrySet()) { + result.put(e.getKey(), e.getValue() == null ? null : String.valueOf(e.getValue())); + } + } catch (Exception e) { + log.warn("读取业务单据失败(条件分支) table={}, id={}", table, bizDataId, e); + } + return result; + } + + /** 沿执行路径查找第一个审批节点(考虑条件分支) */ + private JSONObject findFirstApprover(JSONObject root, Map bizRecord) { + List seq = buildExecSequence(root, bizRecord); + for (JSONObject n : seq) { + if ("approver".equals(n.getString("type"))) { + return n; + } + } + return null; + } + + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支评估引擎----- + + /** 按节点ID查找节点(含条件分支与子节点,全树搜索) */ private JSONObject findNodeById(JSONObject node, String nodeId) { if (node == null || oConvertUtils.isEmpty(nodeId)) { return null; @@ -810,24 +1159,6 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer return findNodeById(node.getJSONObject("childNode"), nodeId); } - /** 前序遍历收集审批人/抄送节点(条件分支按顺序展开) */ - private void flatten(JSONObject node, List out) { - if (node == null) { - return; - } - String type = node.getString("type"); - if ("approver".equals(type) || "cc".equals(type)) { - out.add(node); - } - JSONArray branches = node.getJSONArray("conditionNodes"); - if (branches != null) { - for (int i = 0; i < branches.size(); i++) { - flatten(branches.getJSONObject(i), out); - } - } - flatten(node.getJSONObject("childNode"), out); - } - // ==================== 进度/历史辅助 ==================== private JSONObject parseProgress(MesXslApprovalInstance inst) { @@ -944,7 +1275,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer JSONObject v = new JSONObject(); v.put("nodeName", h.getString("nodeName")); v.put("name", h.getString("name")); - v.put("actionText", "approve".equals(h.getString("action")) ? "通过" : "驳回"); + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】历史动作文案支持撤销----- + String act = h.getString("action"); + v.put("actionText", "approve".equals(act) ? "通过" : ("cancel".equals(act) ? "撤销" : "驳回")); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】历史动作文案支持撤销----- v.put("comment", h.getString("comment")); v.put("time", h.getString("time")); out.add(v); @@ -994,6 +1328,33 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer } } + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】事务提交后执行(用于IM通知,避免WS先于事务提交导致前端查到旧状态)----- + /** + * 在当前事务提交成功后执行任务(如发送IM/WS通知)。 + * 若当前无活动事务,则立即执行。 + * 用于解决「通知WS先于业务事务提交到达,前端查到旧状态」的时序问题。 + */ + private void runAfterCommit(Runnable task) { + if (task == null) { + return; + } + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + try { + task.run(); + } catch (Exception e) { + log.warn("事务提交后通知任务执行失败", e); + } + } + }); + } else { + task.run(); + } + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】事务提交后执行(用于IM通知,避免WS先于事务提交导致前端查到旧状态)----- + private List singletonList(String s) { List l = new ArrayList<>(); l.add(s); @@ -1020,4 +1381,194 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer private String now() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } + + // ==================== 催办 ==================== + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办:发起人向当前处理人发催办提醒----- + @Override + public Result urge(String instanceId, LoginUser user) { + MesXslApprovalInstance inst = instanceService.getById(instanceId); + if (inst == null) { + return Result.error("审批实例不存在"); + } + if (!"0".equals(inst.getStatus())) { + return Result.error("该审批已结束,无需催办"); + } + if (user == null || !user.getUsername().equals(inst.getApplyUser())) { + return Result.error("只有发起人才可以催办"); + } + Long lastUrge = urgeTimeMap.get(instanceId); + if (lastUrge != null && System.currentTimeMillis() - lastUrge < URGE_MIN_INTERVAL_MS) { + return Result.error("催办过于频繁,请1小时后再试"); + } + if (oConvertUtils.isEmpty(inst.getCurrentHandlers())) { + return Result.error("当前无待处理人"); + } + String applicantName = oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser()); + for (String handler : inst.getCurrentHandlers().split(",")) { + String uname = handler == null ? "" : handler.trim(); + if (oConvertUtils.isEmpty(uname)) { + continue; + } + sendOne(user.getId(), uname, inst.getTenantId(), + "【催办提醒】" + applicantName + " 催促您处理「" + safeTitle(inst) + "」,请尽快审批。", "text"); + } + urgeTimeMap.put(instanceId, System.currentTimeMillis()); + return Result.OK("催办已发送"); + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办:发起人向当前处理人发催办提醒----- + + // ==================== 待办列表 ==================== + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表:查询当前用户的待处理审批实例----- + @Override + public List> pendingList(LoginUser user) { + if (user == null) { + return new ArrayList<>(); + } + String sql = "SELECT id, flow_name, biz_table_name, biz_title, current_node_name, " + + "apply_user_name, apply_time, current_handlers_text " + + "FROM mes_xsl_approval_instance " + + "WHERE status = '0' AND del_flag = 0 " + + "AND FIND_IN_SET(?, current_handlers) " + + "ORDER BY apply_time DESC LIMIT 200"; + try { + return jdbcTemplate.queryForList(sql, user.getUsername()); + } catch (Exception e) { + log.warn("查询待办列表失败 user={}", user.getUsername(), e); + return new ArrayList<>(); + } + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表:查询当前用户的待处理审批实例----- + + // ==================== 超时提醒(调度器调用) ==================== + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒:扫描超时实例并通知处理人----- + @Override + public void remindTimeoutInstances() { + // 查询所有审批中的实例,联查审批流的超时配置 + String sql = "SELECT i.id, i.flow_id, i.biz_title, i.current_handlers, i.node_progress, " + + "i.tenant_id, i.apply_user, i.apply_user_name, i.flow_name, " + + "COALESCE(f.timeout_hours, 24) AS timeout_hours " + + "FROM mes_xsl_approval_instance i " + + "LEFT JOIN mes_xsl_approval_flow f ON f.id = i.flow_id " + + "WHERE i.status = '0' AND i.del_flag = 0 AND COALESCE(f.timeout_hours, 24) > 0"; + List> rows; + try { + rows = jdbcTemplate.queryForList(sql); + } catch (Exception e) { + log.warn("超时提醒扫描失败", e); + return; + } + for (Map row : rows) { + try { + processTimeoutRow(row); + } catch (Exception e) { + log.warn("处理超时实例失败 id={}", row.get("id"), e); + } + } + } + + private void processTimeoutRow(Map row) { + String nodeProgress = (String) row.get("node_progress"); + if (oConvertUtils.isEmpty(nodeProgress)) { + return; + } + JSONObject progress = safeParse(nodeProgress); + if (progress == null) { + return; + } + String enterTimeStr = progress.getString("enterTime"); + if (oConvertUtils.isEmpty(enterTimeStr)) { + return; + } + Date enterTime; + try { + enterTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(enterTimeStr); + } catch (Exception e) { + return; + } + int timeoutHours = row.get("timeout_hours") == null ? 24 : ((Number) row.get("timeout_hours")).intValue(); + long elapsedHours = (System.currentTimeMillis() - enterTime.getTime()) / (1000 * 60 * 60); + if (elapsedHours < timeoutHours) { + return; + } + // 已超时:通知当前处理人 + String currentHandlers = (String) row.get("current_handlers"); + if (oConvertUtils.isEmpty(currentHandlers)) { + return; + } + String instanceId = (String) row.get("id"); + // 避免重复通知:每个超时周期内只通知一次 + String reminderKey = instanceId + "_timeout"; + Long lastRemind = urgeTimeMap.get(reminderKey); + if (lastRemind != null && System.currentTimeMillis() - lastRemind < timeoutHours * 3600_000L) { + return; + } + String bizTitle = oConvertUtils.getString((String) row.get("biz_title"), instanceId); + String flowName = oConvertUtils.getString((String) row.get("flow_name"), ""); + String applyUserName = oConvertUtils.getString((String) row.get("apply_user_name"), ""); + Integer tenantId = row.get("tenant_id") == null ? null : ((Number) row.get("tenant_id")).intValue(); + String msg = String.format("【超时提醒】您有一条审批待处理已超过%d小时,请尽快处理!\n审批流:%s\n单据:%s\n发起人:%s", + elapsedHours, flowName, bizTitle, applyUserName); + String applyUser = (String) row.get("apply_user"); + SysUser fromUser = getUserSafely(applyUser); + String fromId = fromUser == null ? null : fromUser.getId(); + for (String handler : currentHandlers.split(",")) { + String uname = handler == null ? "" : handler.trim(); + if (oConvertUtils.isEmpty(uname)) { + continue; + } + sendOne(fromId, uname, tenantId, msg, "text"); + } + urgeTimeMap.put(reminderKey, System.currentTimeMillis()); + log.info("审批超时提醒已发送 instanceId={}, elapsedHours={}", instanceId, elapsedHours); + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒:扫描超时实例并通知处理人----- + + // ==================== 辅助通知方法 ==================== + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】全链路通知补全----- + /** 会签模式:某人通过后通知其他仍待处理的处理人 */ + private void notifyCoApproversProgress(MesXslApprovalInstance inst, MesXslApprovalFlow flow, + JSONArray tasks, LoginUser actingUser, String nodeName) { + String actorName = actingUser == null ? "某人" + : oConvertUtils.getString(actingUser.getRealname(), actingUser.getUsername()); + String msg = "【会签进度】" + actorName + " 已通过「" + safeTitle(inst) + "」的「" + + oConvertUtils.getString(nodeName, "审批") + "」节点,等待您审批。"; + SysUser from = actingUser == null ? null : getUserSafely(actingUser.getUsername()); + String fromId = from == null ? null : from.getId(); + for (int i = 0; i < tasks.size(); i++) { + JSONObject t = tasks.getJSONObject(i); + if ("pending".equals(t.getString("status")) + && (actingUser == null || !actingUser.getUsername().equals(t.getString("username")))) { + sendOne(fromId, t.getString("username"), inst.getTenantId(), msg, "text"); + } + } + } + + /** 驳回时通知同节点其他待处理人(无需处理,流程已结束) */ + private void notifyCoHandlersOnReject(MesXslApprovalInstance inst, + JSONArray tasks, LoginUser actingUser, String reason, String nodeName) { + if (tasks == null) { + return; + } + String actorName = actingUser == null ? "某人" + : oConvertUtils.getString(actingUser.getRealname(), actingUser.getUsername()); + String msg = "【审批驳回】" + actorName + " 驳回了「" + safeTitle(inst) + "」,流程已终止,无需您继续处理。\n驳回理由:" + reason; + SysUser from = actingUser == null ? null : getUserSafely(actingUser.getUsername()); + String fromId = from == null ? null : from.getId(); + for (int i = 0; i < tasks.size(); i++) { + JSONObject t = tasks.getJSONObject(i); + String uname = t.getString("username"); + if (oConvertUtils.isEmpty(uname)) { + continue; + } + if (actingUser != null && actingUser.getUsername().equals(uname)) { + continue; + } + sendOne(fromId, uname, inst.getTenantId(), msg, "text"); + } + } + //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/config/XslMesSchedulingConfig.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/config/XslMesSchedulingConfig.java new file mode 100644 index 0000000..9158ffb --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/config/XslMesSchedulingConfig.java @@ -0,0 +1,17 @@ +package org.jeecg.modules.xslmes.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 启用 Spring @Scheduled,用于审批超时提醒等定时任务。 + * + * @author GHT + * @date 2026-05-29 for:【QH-MES审批流完善】启用定时任务支持 + */ +//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】启用定时任务支持----- +@Configuration +@EnableScheduling +public class XslMesSchedulingConfig { +} +//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/controller/MesXslMixerPsCompileController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerPsCompileController.java index a1d4d4c..1c6da68 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerPsCompileController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslMixerPsCompileController.java @@ -22,6 +22,7 @@ import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.system.entity.SysDepart; import org.jeecg.modules.system.service.ISysDepartService; +import org.jeecg.modules.xslmes.approval.action.ApprovalBizAction; import org.jeecg.modules.xslmes.entity.MesXslMixerPsCompile; import org.jeecg.modules.xslmes.service.IMesXslMixerPsCompileService; import org.springframework.beans.factory.annotation.Autowired; @@ -168,6 +169,9 @@ public class MesXslMixerPsCompileController extends JeecgController reject(@RequestParam(name = "ids") String ids) { + String err = mesXslMixerPsCompileService.rejectBatch(ids, getOperatorName()); + if (err != null) { + return Result.error(err); + } + return Result.OK("拒绝成功,已退回编制状态!"); + } + + @AutoLog(value = "MES密炼PS编制-撤回") + @Operation(summary = "MES密炼PS编制-撤回(退回上一环节并撤销关联操作)") + @RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:withdraw") + @PostMapping(value = "/withdraw") + public Result withdraw(@RequestParam(name = "ids") String ids) { + String err = mesXslMixerPsCompileService.withdrawBatch(ids, getOperatorName()); + if (err != null) { + return Result.error(err); + } + return Result.OK("撤回成功!"); + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退----------- + private String getOperatorName() { LoginUser loginUser = null; try { diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java index 6f4a26a..00d4a5e 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRubberQuickTestStdController.java @@ -21,6 +21,7 @@ import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.system.base.controller.JeecgController; import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.util.oConvertUtils; +import org.jeecg.modules.xslmes.approval.action.ApprovalBizAction; import org.jeecg.modules.mes.material.entity.MesMaterial; import org.jeecg.modules.mes.material.service.IMesMaterialService; import org.jeecg.modules.system.entity.SysDepart; @@ -162,6 +163,9 @@ public class MesXslRubberQuickTestStdController return Result.OK(mesXslRubberQuickTestStdService.selectLinesByStdId(id)); } + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作----- + @ApprovalBizAction(name = "启用/停用", table = "mes_xsl_rubber_quick_test_std", phase = {"onApprove", "onReject"}) + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作----- @AutoLog(value = "MES胶料快检实验标准-启用/停用") @Operation(summary = "MES胶料快检实验标准-启用/停用(字典 xslmes_rubber_quick_test_std_enable_status:1使用中 0已停用)") @RequiresPermissions("mes:mes_xsl_rubber_quick_test_std:updateStatus") diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslFormulaSpecService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslFormulaSpecService.java index 9cbbdc0..cfb4607 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslFormulaSpecService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslFormulaSpecService.java @@ -39,6 +39,16 @@ public interface IMesXslFormulaSpecService extends IService { void syncFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus); //update-end---author:cursor ---date:20260522 for:【配合示方】密炼PS审批联动同步状态与审批人----------- + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方----------- + /** + * 密炼PS拒绝/撤回时,按发行编号(PS编码)将关联配合示方回退到目标状态,并清空高于目标状态的审批人痕迹。 + * + * @param ps 已回退后的密炼PS编制单 + * @param mixerPsTargetStatus 密炼PS回退后的目标状态:compile / proofread / audit + */ + void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方----------- + //update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A38】配合示方生成混炼示方预览与批量创建----------- /** * 根据配合示方混合段数构建生成混炼示方预览行(示方编号 + 段信息) diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerPsCompileService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerPsCompileService.java index 1c37ebb..b603460 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerPsCompileService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslMixerPsCompileService.java @@ -16,4 +16,22 @@ public interface IMesXslMixerPsCompileService extends IService { * @param mixerPsTargetStatus 密炼PS目标状态:proofread / audit / approve */ void syncFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus); + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方----------- + /** + * 密炼PS拒绝/撤回时,按发行编号(PS编码)清空关联混炼示方高于目标状态的审批人痕迹。 + * + * @param ps 已回退后的密炼PS编制单 + * @param mixerPsTargetStatus 密炼PS回退后的目标状态:compile / proofread / audit + */ + void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方----------- //update-end---author:cursor ---date:20260526 for:【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人----------- } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestStdService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestStdService.java index 89cf541..8d13b8d 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestStdService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRubberQuickTestStdService.java @@ -25,4 +25,11 @@ public interface IMesXslRubberQuickTestStdService extends IService psCompileIds); + + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿----------- + /** + * 密炼PS(原材料检验标准)从已批准回退(拒绝/撤回)时,将关联实验标准审核状态置回草稿(未批准) + */ + void markAuditDraftByPsCompileIds(Collection psCompileIds); + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿----------- } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFormulaSpecServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFormulaSpecServiceImpl.java index d1b1864..e2ea385 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFormulaSpecServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslFormulaSpecServiceImpl.java @@ -499,6 +499,41 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(MesXslFormulaSpec::getIssueNumber, ps.getPsCode()); + switch (mixerPsTargetStatus) { + case "compile": + // 回退到编制:状态回 compile,清空 校对/审核/批准 三组痕迹 + wrapper.set(MesXslFormulaSpec::getStatus, "compile") + .set(MesXslFormulaSpec::getProofreadBy, null).set(MesXslFormulaSpec::getProofreadTime, null) + .set(MesXslFormulaSpec::getAuditBy, null).set(MesXslFormulaSpec::getAuditTime, null) + .set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null); + break; + case "proofread": + // 回退到校对:状态回 submit,清空 审核/批准 痕迹(保留校对) + wrapper.set(MesXslFormulaSpec::getStatus, "submit") + .set(MesXslFormulaSpec::getAuditBy, null).set(MesXslFormulaSpec::getAuditTime, null) + .set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null); + break; + case "audit": + // 回退到审核:状态回 review_pass,清空 批准 痕迹(保留校对/审核) + wrapper.set(MesXslFormulaSpec::getStatus, "review_pass") + .set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null); + break; + default: + return; + } + wrapper.set(MesXslFormulaSpec::getUpdateTime, new Date()); + this.update(wrapper); + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方----------- + //update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A38】配合示方生成混炼示方预览与批量创建----------- @Override public MesXslFormulaMixingGeneratePreviewVO buildMixingGeneratePreview(String formulaSpecId) { diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java index c68ed4c..5c86c0c 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixerPsCompileServiceImpl.java @@ -1,8 +1,10 @@ package org.jeecg.modules.xslmes.service.impl; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import org.jeecg.common.util.oConvertUtils; @@ -96,4 +98,93 @@ public class MesXslMixerPsCompileServiceImpl extends ServiceImpl wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(MesXslMixerPsCompile::getId, entity.getId()) + .set(MesXslMixerPsCompile::getStatus, targetStatus); + if ("compile".equals(targetStatus)) { + wrapper.set(MesXslMixerPsCompile::getProofreadBy, null).set(MesXslMixerPsCompile::getProofreadTime, null) + .set(MesXslMixerPsCompile::getAuditBy, null).set(MesXslMixerPsCompile::getAuditTime, null) + .set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null); + } else if ("proofread".equals(targetStatus)) { + wrapper.set(MesXslMixerPsCompile::getAuditBy, null).set(MesXslMixerPsCompile::getAuditTime, null) + .set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null); + } else if ("audit".equals(targetStatus)) { + wrapper.set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null); + } + update(wrapper); + // 同步实体内存值,供关联单据联动判断使用 + entity.setStatus(targetStatus); + + // 联动回退:配合示方(状态+审批人)、混炼示方(审批人) + mesXslFormulaSpecService.revertFromMixerPsWorkflow(entity, targetStatus); + mesXslMixingSpecService.revertFromMixerPsWorkflow(entity, targetStatus); + + // 离开批准态:原材料检验标准回退为草稿(仅原料检验标准类型) + if (leavingApprove && XslMesBizConstants.PS_TYPE_RAW_INSPECT_STD.equals(entity.getPsType())) { + mesXslRubberQuickTestStdService.markAuditDraftByPsCompileIds(Collections.singletonList(entity.getId())); + } + } + + /** 上一环节映射:approve→audit→proofread→compile,compile 无上一环节返回 null */ + private String prevStatus(String current) { + switch (current) { + case "approve": + return "audit"; + case "audit": + return "proofread"; + case "proofread": + return "compile"; + default: + return null; + } + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退----------- } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java index 3a4ca5b..d5c8e72 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslMixingSpecServiceImpl.java @@ -841,6 +841,38 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl wrapper = new LambdaUpdateWrapper<>(); + wrapper.eq(MesXslMixingSpec::getIssueNumber, ps.getPsCode()); + switch (mixerPsTargetStatus) { + case "compile": + // 回退到编制:清空 校对/审核/批准 三组痕迹 + wrapper.set(MesXslMixingSpec::getProofreadBy, null).set(MesXslMixingSpec::getProofreadTime, null) + .set(MesXslMixingSpec::getAuditBy, null).set(MesXslMixingSpec::getAuditTime, null) + .set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null); + break; + case "proofread": + // 回退到校对:清空 审核/批准 痕迹(保留校对) + wrapper.set(MesXslMixingSpec::getAuditBy, null).set(MesXslMixingSpec::getAuditTime, null) + .set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null); + break; + case "audit": + // 回退到审核:清空 批准 痕迹(保留校对/审核) + wrapper.set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null); + break; + default: + return; + } + wrapper.set(MesXslMixingSpec::getUpdateTime, new Date()); + this.update(wrapper); + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方----------- + //update-begin---author:cursor ---date:20260527 for:【配方日志查询】从入参直接构建快照,避免二次查库----------- private MesXslMixingSpecPage buildPageFromInput( MesXslMixingSpec main, diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java index b259752..6bfb367 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/impl/MesXslRubberQuickTestStdServiceImpl.java @@ -156,5 +156,24 @@ public class MesXslRubberQuickTestStdServiceImpl } //update-end---author:jiangxh ---date:20260525 for:【MES】原材料检验标准密炼PS批准时关联实验标准置已批准----------- + //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿----------- + @Override + public void markAuditDraftByPsCompileIds(Collection psCompileIds) { + if (CollectionUtils.isEmpty(psCompileIds)) { + return; + } + List ids = + psCompileIds.stream().filter(id -> oConvertUtils.isNotEmpty(id)).map(String::trim).distinct().collect(Collectors.toList()); + if (ids.isEmpty()) { + return; + } + this.lambdaUpdate() + .in(MesXslRubberQuickTestStd::getPsCompileId, ids) + .and(w -> w.eq(MesXslRubberQuickTestStd::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslRubberQuickTestStd::getDelFlag)) + .set(MesXslRubberQuickTestStd::getAuditStatus, XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_DRAFT) + .update(); + } + //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿----------- + //update-end---author:jiangxh ---date:20260525 for:【MES】胶料快检实验标准名称同租户唯一、主子保存----------- } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_115__mes_xsl_approval_restore_status.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_115__mes_xsl_approval_restore_status.sql new file mode 100644 index 0000000..f08e5ca --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_115__mes_xsl_approval_restore_status.sql @@ -0,0 +1,14 @@ +-- 【QH-MES审批流设计】驳回/撤销恢复初始状态:审批流配置状态字段名,实例快照发起时业务状态原值 +SET NAMES utf8mb4; + +ALTER TABLE `mes_xsl_approval_flow` + ADD COLUMN `status_field` varchar(64) DEFAULT NULL COMMENT '业务单据状态字段名(驳回/撤销时回写其发起时原值)' AFTER `route_path`; + +ALTER TABLE `mes_xsl_approval_instance` + ADD COLUMN `status_field` varchar(64) DEFAULT NULL COMMENT '业务单据状态字段名(发起时快照)' AFTER `biz_title`, + ADD COLUMN `origin_status` varchar(64) DEFAULT NULL COMMENT '发起审批时业务状态原值(驳回/撤销回写)' AFTER `status_field`; + +-- 密炼PS编制默认以 status 字段作为可恢复状态字段 +UPDATE `mes_xsl_approval_flow` + SET `status_field` = 'status' + WHERE `biz_table` = 'mes_xsl_mixer_ps_compile' AND (`status_field` IS NULL OR `status_field` = ''); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_116__mes_xsl_approval_enhancements.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_116__mes_xsl_approval_enhancements.sql new file mode 100644 index 0000000..7429c42 --- /dev/null +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_116__mes_xsl_approval_enhancements.sql @@ -0,0 +1,10 @@ +-- 【QH-MES审批流完善】①乐观锁防并发 ②超时配置 ③催办记录时间 +SET NAMES utf8mb4; + +-- 审批实例:乐观锁版本号,防止多人同时审批导致进度覆盖写 +ALTER TABLE `mes_xsl_approval_instance` + ADD COLUMN `version` int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' AFTER `remark`; + +-- 审批流定义:超时提醒小时数(0=不提醒),默认24h +ALTER TABLE `mes_xsl_approval_flow` + ADD COLUMN `timeout_hours` int(11) NOT NULL DEFAULT 24 COMMENT '超时提醒小时数(0=不提醒)' AFTER `status_field`; diff --git a/jeecgboot-vue3/src/utils/flowApiRecorder.ts b/jeecgboot-vue3/src/utils/flowApiRecorder.ts deleted file mode 100644 index d437d7f..0000000 --- a/jeecgboot-vue3/src/utils/flowApiRecorder.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { ref } from 'vue'; - -/** - * 审批流「回调接口」录制器。 - * - * 审批流设计器是覆盖在业务页面之上的(同一前端应用、同一个 defHttp 实例), - * 录制时全局拦截器会把每次业务请求的 url/method/参数 上报到这里, - * 捕获用户点击目标按钮所触发的真实请求,自动回填到节点的回调接口配置。 - * - * @author GHT - * @date 2026-05-29 for:【QH-MES审批流设计】可视化录制业务按钮接口 - */ - -export interface FlowCapturedApi { - /** 请求路径(servlet 内路径,如 /xslmes/xxx/approve) */ - url: string; - /** 请求方法 GET/POST/PUT/DELETE */ - method: string; - /** 请求体(POST/PUT 时) */ - data?: any; - /** 查询参数 */ - params?: any; -} - -/** 录制中状态(供设计器隐藏遮罩、显示录制条) */ -export const flowApiRecording = ref(false); - -let resolver: ((c: FlowCapturedApi | null) => void) | null = null; - -/** 开始录制,返回 Promise;捕获到请求或取消时 resolve(取消为 null) */ -export function startFlowApiRecord(): Promise { - // 若已有未完成的录制,先取消 - cancelFlowApiRecord(); - flowApiRecording.value = true; - return new Promise((resolve) => { - resolver = resolve; - }); -} - -/** 取消录制 */ -export function cancelFlowApiRecord() { - if (resolver) { - const r = resolver; - resolver = null; - flowApiRecording.value = false; - r(null); - } else { - flowApiRecording.value = false; - } -} - -/** - * 由全局请求拦截器调用,上报每次请求。 - * 仅在录制中生效,默认只捕获「非 GET」业务请求(按钮动作通常是写操作), - * 并跳过审批流自身接口,避免误捕获。 - */ -export function notifyFlowApiRecorder(config: any) { - if (!flowApiRecording.value || !resolver) { - return; - } - const method = String(config?.method || 'get').toUpperCase(); - if (method === 'GET') { - return; - } - const url = config?.url; - if (!url || typeof url !== 'string') { - return; - } - // 跳过审批流/审批办理自身接口 - if (url.includes('/approvalFlow') || url.includes('/approvalHandle') || url.includes('/sys/dict')) { - return; - } - const captured: FlowCapturedApi = { - url, - method, - data: safeClone(config?.data), - params: safeClone(config?.params), - }; - const r = resolver; - resolver = null; - flowApiRecording.value = false; - r(captured); -} - -function safeClone(v: any) { - if (v == null || typeof v !== 'object') { - return v; - } - try { - return JSON.parse(JSON.stringify(v)); - } catch { - return undefined; - } -} diff --git a/jeecgboot-vue3/src/utils/http/axios/index.ts b/jeecgboot-vue3/src/utils/http/axios/index.ts index f8c0178..757affc 100644 --- a/jeecgboot-vue3/src/utils/http/axios/index.ts +++ b/jeecgboot-vue3/src/utils/http/axios/index.ts @@ -19,9 +19,6 @@ import { useI18n } from '/@/hooks/web/useI18n'; import { joinTimestamp, formatRequestDate } from './helper'; import { useUserStoreWithOut } from '/@/store/modules/user'; import { cloneDeep } from "lodash-es"; -//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】回调接口录制----- -import { notifyFlowApiRecorder } from '/@/utils/flowApiRecorder'; -//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】回调接口录制----- const globSetting = useGlobSetting(); const urlPrefix = globSetting.urlPrefix; const { createMessage, createErrorModal } = useMessage(); @@ -93,9 +90,6 @@ const transform: AxiosTransform = { // 请求之前处理config beforeRequestHook: (config, options) => { - //update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制时捕获原始servlet路径(未加前缀/上下文)----- - notifyFlowApiRecorder({ url: config.url, method: config.method, data: config.data, params: config.params }); - //update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制时捕获原始servlet路径(未加前缀/上下文)----- const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options; // http开头的请求url,不加前缀 diff --git a/jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts b/jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts index 1112e37..7605059 100644 --- a/jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts +++ b/jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts @@ -17,6 +17,9 @@ enum Api { // update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文----- designContext = '/xslmes/approvalFlow/designContext', // update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文----- + // update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作----- + bizActions = '/xslmes/approvalFlow/bizActions', + // update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作----- } /** @@ -73,3 +76,11 @@ export const batchDeleteApprovalFlow = (params, handleSuccess) => { */ export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url: Api.designContext, params: { routePath } }); // update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)----- + +// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作(后端@ApprovalBizAction注解扫描)----- +/** + * 查询某业务表已标注 @ApprovalBizAction 的可选回调动作,供节点「回调接口」下拉选择。 + * 返回 [{ name, url, method, table, phase, perms }] + */ +export const getApprovalBizActions = (table: string) => defHttp.get({ url: Api.bizActions, params: { table } }); +// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作----- diff --git a/jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts b/jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts index 7a5b98f..346414a 100644 --- a/jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts +++ b/jeecgboot-vue3/src/views/approval/flow/approvalHandle.api.ts @@ -10,6 +10,9 @@ enum Api { status = '/xslmes/approvalHandle/status', approve = '/xslmes/approvalHandle/approve', reject = '/xslmes/approvalHandle/reject', + // update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销----- + cancel = '/xslmes/approvalHandle/cancel', + // update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销----- } /** 查看单据全部字段 + 审批进度/历史 */ @@ -23,3 +26,8 @@ export const approveApproval = (params: { instanceId: string; comment?: string } /** 驳回(需填写理由) */ export const rejectApproval = (params: { instanceId: string; reason: string }) => defHttp.post({ url: Api.reject, data: params }); + +// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销----- +/** 撤销(仅发起人本人,审批中可撤回,业务单据恢复到发起时状态) */ +export const cancelApproval = (params: { instanceId: string; reason?: string }) => defHttp.post({ url: Api.cancel, data: params }); +// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销----- diff --git a/jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue b/jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue index ea2d994..057521a 100644 --- a/jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue +++ b/jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue @@ -4,7 +4,7 @@ @date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计 -->