完善MES审批流设计功能,新增审批可选回调动作、发起人撤销及催办接口,支持审批状态恢复与联动回退,提升审批流程的灵活性与用户体验。
This commit is contained in:
@@ -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;
|
||||
|
||||
/**
|
||||
* 审批联动业务动作标注。
|
||||
*
|
||||
* <p>把本注解打在业务 Controller 的处理方法上,即声明「该按钮/接口可被审批流程作为回调动作选择」。
|
||||
* 系统启动时会反射扫描所有带本注解的接口方法,取其 {@code @RequestMapping} 真实路径与 HTTP 方法,
|
||||
* 按 {@link #table()} 归类,供审批流设计器的节点「回调接口」下拉选择。</p>
|
||||
*
|
||||
* <p>因为基于 Spring 运行时反射 + 真实映射路径,生产环境天然可用,URL 绝对准确,无需任何源码解析。</p>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 审批联动业务动作注册表。
|
||||
*
|
||||
* <p>容器启动完成后,反射扫描所有带 {@link ApprovalBizAction} 的接口方法,
|
||||
* 取真实映射路径 + HTTP 方法,按业务表归类缓存,供设计器查询选择。</p>
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ApprovalBizActionRegistry {
|
||||
|
||||
@Resource
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/** 业务表 -> 动作列表 */
|
||||
private final Map<String, List<ApprovalBizActionVo>> 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<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> 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<ApprovalBizActionVo> 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<String> 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<ApprovalBizActionVo> getByTable(String table) {
|
||||
if (table == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> 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<ApprovalBizActionVo> getByTableAndPhase(String table, String phase) {
|
||||
if (table == null || phase == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> list = byTable.get(table);
|
||||
if (list == null || list.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> 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审批流设计】驳回统一回退:按表+时机自动取业务动作-----------
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<MesXslApproval
|
||||
@Autowired
|
||||
private ISysBaseAPI sysBaseAPI;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表-----
|
||||
@Autowired
|
||||
private ApprovalBizActionRegistry approvalBizActionRegistry;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页字段解析-----
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@@ -258,13 +265,28 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按业务表查可选回调动作-----
|
||||
/**
|
||||
* 查询某业务表已标注 @ApprovalBizAction 的可选回调动作,供设计器节点「回调接口」下拉选择。
|
||||
*
|
||||
* @param table 业务表名(审批流绑定的 bizTable)
|
||||
*/
|
||||
@Operation(summary = "审批流设计-业务表可选回调动作")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@GetMapping(value = "/bizActions")
|
||||
public Result<List<ApprovalBizActionVo>> 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<MesXslApproval
|
||||
String sql = "SELECT component FROM sys_permission WHERE menu_type IN (0,1) "
|
||||
+ "AND (del_flag = 0 OR del_flag IS NULL) AND url = ? "
|
||||
+ "ORDER BY menu_type DESC LIMIT 1";
|
||||
String component;
|
||||
try {
|
||||
List<String> 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;
|
||||
}
|
||||
|
||||
@@ -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<String> cancel(@RequestBody Map<String, Object> 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<String> urge(@RequestBody Map<String, Object> 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<List<Map<String, Object>>> pendingList() {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return Result.OK(approvalHandleService.pendingList(user));
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
|
||||
}
|
||||
|
||||
@@ -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<MesXslApprovalInstance>()
|
||||
.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<MesXslApprovalInstance>()
|
||||
.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审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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审批流完善】超时提醒调度器-----
|
||||
@@ -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<String> reject(String instanceId, String reason, LoginUser user);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
/**
|
||||
* 撤销:仅发起人本人在审批中可撤回,流程终止并将业务单据恢复到发起时状态。
|
||||
*/
|
||||
Result<String> 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<String, Object> statusInfo(String instanceId, LoginUser user);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
/**
|
||||
* 催办:发起人向当前处理人发送一次性催办提醒,同一实例每小时最多催一次。
|
||||
*/
|
||||
Result<String> 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<Map<String, Object>> 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审批流完善】超时提醒(调度器调用)-----
|
||||
}
|
||||
|
||||
@@ -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<String, Long> 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<String, String> 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<String> 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<String> 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<JSONObject> chain = new ArrayList<>();
|
||||
flatten(root, chain);
|
||||
Map<String, String> bizRecord = readBizRecord(inst.getBizTable(), inst.getBizDataId());
|
||||
List<JSONObject> 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<ApprovalBizActionVo> 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<JSONObject> buildExecSequence(JSONObject root, Map<String, String> bizRecord) {
|
||||
List<JSONObject> seq = new ArrayList<>();
|
||||
buildExecSeq(root, seq, bizRecord);
|
||||
return seq;
|
||||
}
|
||||
|
||||
/** 按节点ID查找节点(含条件分支与子节点) */
|
||||
private void buildExecSeq(JSONObject node, List<JSONObject> out, Map<String, String> 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<String, String> 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<String, String> 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<String, String> readBizRecord(String table, String bizDataId) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (oConvertUtils.isEmpty(table) || oConvertUtils.isEmpty(bizDataId) || !IDENTIFIER.matcher(table).matches()) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM " + table + " WHERE id = ? LIMIT 1", bizDataId);
|
||||
if (rows.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
for (Map.Entry<String, Object> 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<String, String> bizRecord) {
|
||||
List<JSONObject> 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<JSONObject> 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<String> singletonList(String s) {
|
||||
List<String> 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<String> 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<Map<String, Object>> 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<Map<String, Object>> rows;
|
||||
try {
|
||||
rows = jdbcTemplate.queryForList(sql);
|
||||
} catch (Exception e) {
|
||||
log.warn("超时提醒扫描失败", e);
|
||||
return;
|
||||
}
|
||||
for (Map<String, Object> row : rows) {
|
||||
try {
|
||||
processTimeoutRow(row);
|
||||
} catch (Exception e) {
|
||||
log.warn("处理超时实例失败 id={}", row.get("id"), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processTimeoutRow(Map<String, Object> 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审批流完善】全链路通知补全-----
|
||||
}
|
||||
|
||||
@@ -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审批流完善】启用定时任务支持-----
|
||||
@@ -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<MesXslMixerP
|
||||
}
|
||||
|
||||
//update-begin---author:jiangxh ---date:20260520 for:【密炼PS编制】校对/审核/批准-----------
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "校对", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 1)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-校对")
|
||||
@Operation(summary = "MES密炼PS编制-校对")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:proofread")
|
||||
@@ -176,6 +180,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return doChangeStatus(ids, "compile", "proofread", "校对");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "审核", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 2)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-审核")
|
||||
@Operation(summary = "MES密炼PS编制-审核")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:audit")
|
||||
@@ -184,6 +191,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return doChangeStatus(ids, "proofread", "audit", "审核");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "批准", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 3)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-批准")
|
||||
@Operation(summary = "MES密炼PS编制-批准")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:approve")
|
||||
@@ -201,6 +211,34 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return Result.OK(actionLabel + "成功!");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
@ApprovalBizAction(name = "拒绝", table = "mes_xsl_mixer_ps_compile", phase = {"onReject"}, order = 4)
|
||||
@AutoLog(value = "MES密炼PS编制-拒绝")
|
||||
@Operation(summary = "MES密炼PS编制-拒绝(一步退回编制并撤销关联操作)")
|
||||
// 拒绝主要由审批流 onReject 回调触发(执行人为当前审批节点处理人),不强制要求其具备密炼PS业务权限码;
|
||||
// 是否允许驳回已由审批节点本身控制,故此处不加 @RequiresPermissions。
|
||||
@PostMapping(value = "/reject")
|
||||
public Result<String> 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<String> 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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -39,6 +39,16 @@ public interface IMesXslFormulaSpecService extends IService<MesXslFormulaSpec> {
|
||||
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】配合示方生成混炼示方预览与批量创建-----------
|
||||
/**
|
||||
* 根据配合示方混合段数构建生成混炼示方预览行(示方编号 + 段信息)
|
||||
|
||||
@@ -16,4 +16,22 @@ public interface IMesXslMixerPsCompileService extends IService<MesXslMixerPsComp
|
||||
*/
|
||||
String changeStatusBatch(String ids, String expectedStatus, String targetStatus, String actionLabel, String operatorName);
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】批量流转状态-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
/**
|
||||
* 批量拒绝:将单据一步退回到编制(compile),清空本单据校对/审核/批准全部痕迹,
|
||||
* 并联动回退配合示方、混炼示方、原材料检验标准等关联单据。
|
||||
*
|
||||
* @return 失败原因,null 表示成功
|
||||
*/
|
||||
String rejectBatch(String ids, String operatorName);
|
||||
|
||||
/**
|
||||
* 批量撤回:将单据按当前状态退回上一环节(approve→audit→proofread→compile),
|
||||
* 仅清空被撤回那一步的痕迹,并联动回退关联单据。
|
||||
*
|
||||
* @return 失败原因,null 表示成功
|
||||
*/
|
||||
String withdrawBatch(String ids, String operatorName);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
}
|
||||
|
||||
@@ -46,5 +46,15 @@ public interface IMesXslMixingSpecService extends IService<MesXslMixingSpec> {
|
||||
* @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审批联动同步审批人-----------
|
||||
}
|
||||
|
||||
@@ -25,4 +25,11 @@ public interface IMesXslRubberQuickTestStdService extends IService<MesXslRubberQ
|
||||
* 密炼PS(原材料检验标准)批准后,将关联实验标准审核状态置为已批准(仅写入,不随PS反审核回退)
|
||||
*/
|
||||
void markAuditApprovedByPsCompileIds(Collection<String> psCompileIds);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
/**
|
||||
* 密炼PS(原材料检验标准)从已批准回退(拒绝/撤回)时,将关联实验标准审核状态置回草稿(未批准)
|
||||
*/
|
||||
void markAuditDraftByPsCompileIds(Collection<String> psCompileIds);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
}
|
||||
|
||||
@@ -499,6 +499,41 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
}
|
||||
//update-end---author:cursor ---date:20260522 for:【配合示方】密炼PS审批联动同步状态与审批人-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
@Override
|
||||
public void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus) {
|
||||
if (ps == null || oConvertUtils.isEmpty(ps.getPsCode()) || oConvertUtils.isEmpty(mixerPsTargetStatus)) {
|
||||
return;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslFormulaSpec> 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) {
|
||||
|
||||
@@ -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<MesXslMixerPsCo
|
||||
}
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】批量流转状态-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String rejectBatch(String ids, String operatorName) {
|
||||
return doRevertBatch(ids, "拒绝", true, operatorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String withdrawBatch(String ids, String operatorName) {
|
||||
return doRevertBatch(ids, "撤回", false, operatorName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 逆向回退批处理。
|
||||
*
|
||||
* @param toCompile true=拒绝(一步退回编制),false=撤回(退回上一环节)
|
||||
*/
|
||||
private String doRevertBatch(String ids, String actionLabel, boolean toCompile, String operatorName) {
|
||||
if (oConvertUtils.isEmpty(ids)) {
|
||||
return "请选择要" + actionLabel + "的记录";
|
||||
}
|
||||
for (String rawId : ids.split(",")) {
|
||||
if (oConvertUtils.isEmpty(rawId)) {
|
||||
continue;
|
||||
}
|
||||
String id = rawId.trim();
|
||||
MesXslMixerPsCompile entity = getById(id);
|
||||
if (entity == null) {
|
||||
return "记录不存在或已删除";
|
||||
}
|
||||
String current = entity.getStatus() == null ? "" : entity.getStatus();
|
||||
String prev = prevStatus(current);
|
||||
if (prev == null) {
|
||||
String psCode = oConvertUtils.isEmpty(entity.getPsCode()) ? id : entity.getPsCode();
|
||||
return "PS编码[" + psCode + "]当前为编制状态,无需" + actionLabel;
|
||||
}
|
||||
String targetStatus = toCompile ? "compile" : prev;
|
||||
applyRevert(entity, current, targetStatus);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 执行单条单据的逆向回退:本单据状态+痕迹回退,并联动回退关联单据 */
|
||||
private void applyRevert(MesXslMixerPsCompile entity, String currentStatus, String targetStatus) {
|
||||
boolean leavingApprove = "approve".equals(currentStatus);
|
||||
// 本单据:状态回退 + 清空高于目标状态的痕迹(用 UpdateWrapper 显式 set null,绕过 updateById 对 null 不更新)
|
||||
LambdaUpdateWrapper<MesXslMixerPsCompile> 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拒绝/撤回逆向回退-----------
|
||||
}
|
||||
|
||||
@@ -841,6 +841,38 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
}
|
||||
//update-end---author:cursor ---date:20260526 for:【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
@Override
|
||||
public void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus) {
|
||||
if (ps == null || oConvertUtils.isEmpty(ps.getPsCode()) || oConvertUtils.isEmpty(mixerPsTargetStatus)) {
|
||||
return;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslMixingSpec> 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,
|
||||
|
||||
@@ -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<String> psCompileIds) {
|
||||
if (CollectionUtils.isEmpty(psCompileIds)) {
|
||||
return;
|
||||
}
|
||||
List<String> 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】胶料快检实验标准名称同租户唯一、主子保存-----------
|
||||
}
|
||||
|
||||
@@ -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` = '');
|
||||
@@ -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`;
|
||||
Reference in New Issue
Block a user