完善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 phase 时机:onNodeApprove / onApprove / onReject
|
||||||
* @param inst 审批实例
|
* @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) {
|
public void run(JSONObject node, String phase, MesXslApprovalInstance inst) {
|
||||||
if (node == null || inst == null) {
|
if (node == null || inst == null) {
|
||||||
return;
|
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) {
|
private void invoke(String method, String url, JSONObject recordedBody, String bizDataId, String token) {
|
||||||
String fullUrl = buildFullUrl(url);
|
String fullUrl = buildFullUrl(url);
|
||||||
HttpMethod httpMethod = HttpMethod.valueOf(method);
|
HttpMethod httpMethod = HttpMethod.valueOf(method);
|
||||||
@@ -95,13 +135,15 @@ public class ApprovalActionHttpExecutor {
|
|||||||
headers.add(CommonConstant.X_ACCESS_TOKEN, token);
|
headers.add(CommonConstant.X_ACCESS_TOKEN, token);
|
||||||
headers.add(HttpHeaders.AUTHORIZATION, 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;
|
Object bodyToSend = null;
|
||||||
if ("GET".equals(method) || "DELETE".equals(method)) {
|
if (!("GET".equals(method) || "DELETE".equals(method))) {
|
||||||
// 查询/删除:单据ID拼到 url
|
// 写操作:合并录制的 body,并用单据ID覆盖 id(兼容 @RequestBody 形式)
|
||||||
String sep = fullUrl.contains("?") ? "&" : "?";
|
|
||||||
fullUrl = fullUrl + sep + "id=" + bizDataId + "&dataId=" + bizDataId;
|
|
||||||
} else {
|
|
||||||
// 写操作:合并录制的 body,并用单据ID覆盖 id
|
|
||||||
JSONObject body = recordedBody == null ? new JSONObject() : new JSONObject(recordedBody);
|
JSONObject body = recordedBody == null ? new JSONObject() : new JSONObject(recordedBody);
|
||||||
body.put("id", bizDataId);
|
body.put("id", bizDataId);
|
||||||
bodyToSend = body;
|
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.query.QueryGenerator;
|
||||||
import org.jeecg.common.system.vo.DictModel;
|
import org.jeecg.common.system.vo.DictModel;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
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.entity.MesXslApprovalFlow;
|
||||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||||
@@ -47,6 +49,11 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ISysBaseAPI sysBaseAPI;
|
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审批流设计】当前页字段解析-----
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页字段解析-----
|
||||||
@Autowired
|
@Autowired
|
||||||
private JdbcTemplate jdbcTemplate;
|
private JdbcTemplate jdbcTemplate;
|
||||||
@@ -258,13 +265,28 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
|||||||
return Result.OK(data);
|
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 形如
|
* 约定:jeecg 代码生成的列表组件名为 表名驼峰 + List,sys_permission.component 形如
|
||||||
* xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,url 即路由。
|
* xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,url 即路由。
|
||||||
* 反查:url=routePath -> component -> 末段组件名去掉 List -> 驼峰转下划线得到表名。
|
* 反查:url=routePath -> component -> 末段组件名去掉 List -> 驼峰转下划线得到表名。
|
||||||
*/
|
*/
|
||||||
private String resolveTableByRoutePath(String routePath) {
|
/** 根据前端路由反查页面组件路径(sys_permission.component),用于前端按 key 取该页面按钮接口映射 */
|
||||||
|
private String resolveComponentByRoutePath(String routePath) {
|
||||||
if (oConvertUtils.isEmpty(routePath)) {
|
if (oConvertUtils.isEmpty(routePath)) {
|
||||||
return null;
|
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) "
|
String sql = "SELECT component FROM sys_permission WHERE menu_type IN (0,1) "
|
||||||
+ "AND (del_flag = 0 OR del_flag IS NULL) AND url = ? "
|
+ "AND (del_flag = 0 OR del_flag IS NULL) AND url = ? "
|
||||||
+ "ORDER BY menu_type DESC LIMIT 1";
|
+ "ORDER BY menu_type DESC LIMIT 1";
|
||||||
String component;
|
|
||||||
try {
|
try {
|
||||||
List<String> list = jdbcTemplate.queryForList(sql, String.class, path);
|
List<String> list = jdbcTemplate.queryForList(sql, String.class, path);
|
||||||
if (list.isEmpty()) {
|
return list.isEmpty() ? null : list.get(0);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
component = list.get(0);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("反查菜单组件失败 routePath={}", routePath, e);
|
log.warn("反查菜单组件失败 routePath={}", routePath, e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTableByRoutePath(String routePath) {
|
||||||
|
if (oConvertUtils.isEmpty(routePath)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String component = resolveComponentByRoutePath(routePath);
|
||||||
if (oConvertUtils.isEmpty(component)) {
|
if (oConvertUtils.isEmpty(component)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,4 +72,40 @@ public class MesXslApprovalHandleController {
|
|||||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
return approvalHandleService.reject(instanceId, reason, user);
|
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;
|
package org.jeecg.modules.xslmes.approval.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -165,6 +166,16 @@ public class MesXslApprovalLaunchController {
|
|||||||
return Result.error("该审批流未发布,无法发起");
|
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();
|
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||||
@@ -196,6 +207,7 @@ public class MesXslApprovalLaunchController {
|
|||||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
int skipCount = 0;
|
||||||
for (Object o : (List<?>) itemsObj) {
|
for (Object o : (List<?>) itemsObj) {
|
||||||
if (!(o instanceof Map)) {
|
if (!(o instanceof Map)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -206,15 +218,29 @@ public class MesXslApprovalLaunchController {
|
|||||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||||
continue;
|
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);
|
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||||
approvalInstanceService.save(inst);
|
approvalInstanceService.save(inst);
|
||||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
if (count == 0) {
|
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审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ public class MesXslApprovalFlow extends JeecgEntity implements Serializable {
|
|||||||
@Schema(description = "对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)")
|
@Schema(description = "对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)")
|
||||||
private String routePath;
|
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(钉钉式节点树)")
|
@Schema(description = "流程设计JSON(钉钉式节点树)")
|
||||||
private String flowConfig;
|
private String flowConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ public class MesXslApprovalInstance extends JeecgEntity implements Serializable
|
|||||||
@Schema(description = "业务单据展示标题")
|
@Schema(description = "业务单据展示标题")
|
||||||
private String bizTitle;
|
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")
|
@Schema(description = "当前节点ID")
|
||||||
private String currentNodeId;
|
private String currentNodeId;
|
||||||
|
|
||||||
@@ -83,6 +91,12 @@ public class MesXslApprovalInstance extends JeecgEntity implements Serializable
|
|||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
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已删除")
|
@Schema(description = "逻辑删除:0正常 1已删除")
|
||||||
@TableLogic
|
@TableLogic
|
||||||
private Integer delFlag;
|
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.common.system.vo.LoginUser;
|
||||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +32,13 @@ public interface IMesXslApprovalHandleService {
|
|||||||
*/
|
*/
|
||||||
Result<String> reject(String instanceId, String reason, LoginUser user);
|
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 卡片"查看详情"弹窗使用。
|
* 查看单据全部字段 + 审批进度/历史,供 IM 卡片"查看详情"弹窗使用。
|
||||||
*/
|
*/
|
||||||
@@ -41,4 +49,25 @@ public interface IMesXslApprovalHandleService {
|
|||||||
* 返回 status/statusText/currentNodeId/currentNodeName/canApprove。
|
* 返回 status/statusText/currentNodeId/currentNodeName/canApprove。
|
||||||
*/
|
*/
|
||||||
Map<String, Object> statusInfo(String instanceId, LoginUser user);
|
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.im.service.ISysImChatService;
|
||||||
import org.jeecg.modules.system.entity.SysUser;
|
import org.jeecg.modules.system.entity.SysUser;
|
||||||
import org.jeecg.modules.system.service.ISysUserService;
|
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.ApprovalActionHttpExecutor;
|
||||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
|
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
|
||||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackDispatcher;
|
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.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
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.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +56,12 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
|
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
|
||||||
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
|
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
|
@Autowired
|
||||||
private IMesXslApprovalFlowService flowService;
|
private IMesXslApprovalFlowService flowService;
|
||||||
|
|
||||||
@@ -69,6 +83,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ApprovalActionHttpExecutor actionHttpExecutor;
|
private ApprovalActionHttpExecutor actionHttpExecutor;
|
||||||
|
|
||||||
|
// 驳回统一回退:按业务表自动发现 @ApprovalBizAction(onReject) 动作,无需在每个流程节点配置
|
||||||
|
@Autowired
|
||||||
|
private ApprovalBizActionRegistry bizActionRegistry;
|
||||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调-----
|
//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);
|
log.error("解析流程设计失败 flowId={}", inst.getFlowId(), e);
|
||||||
return;
|
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) {
|
if (firstApprover == null) {
|
||||||
// 无审批节点 -> 自动通过
|
// 无审批节点 -> 自动通过
|
||||||
inst.setStatus("1");
|
inst.setStatus("1");
|
||||||
@@ -168,6 +192,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
inst.setNodeProgress(progress.toJSONString());
|
inst.setNodeProgress(progress.toJSONString());
|
||||||
inst.setCurrentHandlersText(pendingNames(tasks) + " 待会签");
|
inst.setCurrentHandlersText(pendingNames(tasks) + " 待会签");
|
||||||
instanceService.updateById(inst);
|
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("已审批,等待其他会签人处理");
|
return Result.OK("已审批,等待其他会签人处理");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,18 +261,90 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
|
|
||||||
// 驳回 -> 回调业务(可回退业务状态)
|
// 驳回 -> 回调业务(可回退业务状态)
|
||||||
callbackDispatcher.fireRejected(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, reason));
|
callbackDispatcher.fireRejected(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, reason));
|
||||||
MesXslApprovalFlow rejectFlow = flowService.getById(inst.getFlowId());
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退(按表注解自动执行,全局一处维护)-----
|
||||||
JSONObject rejectRoot = rejectFlow == null ? null : safeParse(rejectFlow.getFlowConfig());
|
revertBizOnReject(inst);
|
||||||
if (rejectRoot != null) {
|
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退(按表注解自动执行,全局一处维护)-----
|
||||||
actionHttpExecutor.run(findNodeById(rejectRoot, progress.getString("nodeId")), "onReject", inst);
|
|
||||||
}
|
|
||||||
|
|
||||||
String handlerName = oConvertUtils.getString(user.getRealname(), user.getUsername());
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】驳回通知改为事务提交后发送,发起人/会签人卡片实时置灰-----
|
||||||
notifyApplicant(inst, user.getUsername(),
|
// 通知(发起人 + 同节点其他会签人)延迟到事务提交后发送:
|
||||||
"您发起的「" + safeTitle(inst) + "」被 " + handlerName + " 驳回。\n驳回理由:" + reason);
|
// 否则对方收到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("已驳回");
|
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
|
@Override
|
||||||
@@ -326,7 +425,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
instanceService.updateById(inst);
|
instanceService.updateById(inst);
|
||||||
// 流程终止(等同驳回) -> 回调业务(可回退业务状态)
|
// 流程终止(等同驳回) -> 回调业务(可回退业务状态)
|
||||||
callbackDispatcher.fireRejected(buildContext(inst, nodeId, nodeName, null, "审批人为空,流程终止"));
|
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 + "」节点无处理人,流程已终止。");
|
notifyApplicant(inst, null, "您发起的「" + safeTitle(inst) + "」在「" + nodeName + "」节点无处理人,流程已终止。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -354,6 +455,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
progress.put("nodeName", nodeName);
|
progress.put("nodeName", nodeName);
|
||||||
progress.put("mode", mode);
|
progress.put("mode", mode);
|
||||||
progress.put("tasks", tasks);
|
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<>();
|
List<String> activeUsers = new ArrayList<>();
|
||||||
@@ -378,10 +482,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
sendApprovalCard(inst, flow, nodeName, activeUsers);
|
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) {
|
private void advanceAfter(MesXslApprovalInstance inst, MesXslApprovalFlow flow, JSONObject root, String currentNodeId, LoginUser actingUser) {
|
||||||
List<JSONObject> chain = new ArrayList<>();
|
Map<String, String> bizRecord = readBizRecord(inst.getBizTable(), inst.getBizDataId());
|
||||||
flatten(root, chain);
|
List<JSONObject> chain = buildExecSequence(root, bizRecord);
|
||||||
int idx = -1;
|
int idx = -1;
|
||||||
for (int i = 0; i < chain.size(); i++) {
|
for (int i = 0; i < chain.size(); i++) {
|
||||||
if (currentNodeId != null && currentNodeId.equals(chain.get(i).getString("id"))) {
|
if (currentNodeId != null && currentNodeId.equals(chain.get(i).getString("id"))) {
|
||||||
@@ -401,6 +506,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nextApprover != null) {
|
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);
|
enterNode(inst, flow, root, nextApprover);
|
||||||
} else {
|
} else {
|
||||||
inst.setStatus("1");
|
inst.setStatus("1");
|
||||||
@@ -414,6 +523,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
notifyApplicant(inst, actor, "您发起的「" + safeTitle(inst) + "」审批已全部通过。");
|
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 content;
|
||||||
String msgType;
|
String msgType;
|
||||||
if (oConvertUtils.isNotEmpty(routePath)) {
|
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";
|
msgType = "biz_record";
|
||||||
} else {
|
} else {
|
||||||
content = String.format("【抄送通知】%s 发起的「%s」抄送给您。",
|
content = String.format("【抄送通知】%s 发起的「%s」抄送给您。",
|
||||||
@@ -620,11 +732,22 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
|
|
||||||
/** 构建 biz_record 卡片 JSON(含 instanceId / actionLabel / canApprove,与前端 ImBizRecordPayload 对齐 v=2) */
|
/** 构建 biz_record 卡片 JSON(含 instanceId / actionLabel / canApprove,与前端 ImBizRecordPayload 对齐 v=2) */
|
||||||
private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath) {
|
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();
|
JSONArray fields = new JSONArray();
|
||||||
addField(fields, "审批流", inst.getFlowName());
|
addField(fields, "审批流", inst.getFlowName());
|
||||||
addField(fields, "单据", safeTitle(inst));
|
addField(fields, "单据", safeTitle(inst));
|
||||||
addField(fields, "发起人", oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser()));
|
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()));
|
addField(fields, "状态", statusText(inst.getStatus()));
|
||||||
|
|
||||||
JSONObject item = new JSONObject();
|
JSONObject item = new JSONObject();
|
||||||
@@ -641,14 +764,18 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
item.put("body", body.toString());
|
item.put("body", body.toString());
|
||||||
String sep = routePath.contains("?") ? "&" : "?";
|
String sep = routePath.contains("?") ? "&" : "?";
|
||||||
item.put("linkPath", routePath + sep + "imRecordId=" + inst.getBizDataId());
|
item.put("linkPath", routePath + sep + "imRecordId=" + inst.getBizDataId());
|
||||||
// 审批扩展字段
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】仅审批卡片带办理上下文,抄送卡片不可办理-----
|
||||||
item.put("instanceId", inst.getId());
|
if (approvalCard) {
|
||||||
item.put("canApprove", canApprove);
|
// 审批扩展字段
|
||||||
// 该卡片对应的节点ID(用于前端实时校验是否仍为当前节点,旧节点卡片自动置灰)
|
item.put("instanceId", inst.getId());
|
||||||
item.put("nodeId", inst.getCurrentNodeId());
|
item.put("canApprove", canApprove);
|
||||||
if (oConvertUtils.isNotEmpty(actionLabel)) {
|
// 该卡片对应的节点ID(用于前端实时校验是否仍为当前节点,旧节点卡片自动置灰)
|
||||||
item.put("actionLabel", actionLabel);
|
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();
|
JSONArray items = new JSONArray();
|
||||||
items.add(item);
|
items.add(item);
|
||||||
@@ -668,6 +795,111 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
fields.add(f);
|
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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 流程树遍历 ====================
|
// ==================== 流程树遍历(含条件分支评估) ====================
|
||||||
|
|
||||||
/** 沿流程树查找第一个审批节点(遇条件分支取首个分支后续) */
|
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支评估引擎-----
|
||||||
private JSONObject findFirstApprover(JSONObject node) {
|
|
||||||
if (node == null) {
|
/**
|
||||||
return null;
|
* 构建考虑条件分支后的执行序列(approver/cc 节点按执行顺序排列)。
|
||||||
}
|
* 遇到 branch 节点时评估条件,选择唯一匹配的分支路径;无匹配则走最后一个分支(默认分支)。
|
||||||
if ("approver".equals(node.getString("type"))) {
|
*/
|
||||||
return node;
|
private List<JSONObject> buildExecSequence(JSONObject root, Map<String, String> bizRecord) {
|
||||||
}
|
List<JSONObject> seq = new ArrayList<>();
|
||||||
JSONArray branches = node.getJSONArray("conditionNodes");
|
buildExecSeq(root, seq, bizRecord);
|
||||||
if (branches != null) {
|
return seq;
|
||||||
for (int i = 0; i < branches.size(); i++) {
|
|
||||||
JSONObject found = findFirstApprover(branches.getJSONObject(i));
|
|
||||||
if (found != null) {
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return findFirstApprover(node.getJSONObject("childNode"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按节点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) {
|
private JSONObject findNodeById(JSONObject node, String nodeId) {
|
||||||
if (node == null || oConvertUtils.isEmpty(nodeId)) {
|
if (node == null || oConvertUtils.isEmpty(nodeId)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -810,24 +1159,6 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
return findNodeById(node.getJSONObject("childNode"), nodeId);
|
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) {
|
private JSONObject parseProgress(MesXslApprovalInstance inst) {
|
||||||
@@ -944,7 +1275,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
JSONObject v = new JSONObject();
|
JSONObject v = new JSONObject();
|
||||||
v.put("nodeName", h.getString("nodeName"));
|
v.put("nodeName", h.getString("nodeName"));
|
||||||
v.put("name", h.getString("name"));
|
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("comment", h.getString("comment"));
|
||||||
v.put("time", h.getString("time"));
|
v.put("time", h.getString("time"));
|
||||||
out.add(v);
|
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) {
|
private List<String> singletonList(String s) {
|
||||||
List<String> l = new ArrayList<>();
|
List<String> l = new ArrayList<>();
|
||||||
l.add(s);
|
l.add(s);
|
||||||
@@ -1020,4 +1381,194 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
|||||||
private String now() {
|
private String now() {
|
||||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
|
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.common.util.oConvertUtils;
|
||||||
import org.jeecg.modules.system.entity.SysDepart;
|
import org.jeecg.modules.system.entity.SysDepart;
|
||||||
import org.jeecg.modules.system.service.ISysDepartService;
|
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.entity.MesXslMixerPsCompile;
|
||||||
import org.jeecg.modules.xslmes.service.IMesXslMixerPsCompileService;
|
import org.jeecg.modules.xslmes.service.IMesXslMixerPsCompileService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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: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编制-校对")
|
@AutoLog(value = "MES密炼PS编制-校对")
|
||||||
@Operation(summary = "MES密炼PS编制-校对")
|
@Operation(summary = "MES密炼PS编制-校对")
|
||||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:proofread")
|
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:proofread")
|
||||||
@@ -176,6 +180,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
|||||||
return doChangeStatus(ids, "compile", "proofread", "校对");
|
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编制-审核")
|
@AutoLog(value = "MES密炼PS编制-审核")
|
||||||
@Operation(summary = "MES密炼PS编制-审核")
|
@Operation(summary = "MES密炼PS编制-审核")
|
||||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:audit")
|
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:audit")
|
||||||
@@ -184,6 +191,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
|||||||
return doChangeStatus(ids, "proofread", "audit", "审核");
|
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编制-批准")
|
@AutoLog(value = "MES密炼PS编制-批准")
|
||||||
@Operation(summary = "MES密炼PS编制-批准")
|
@Operation(summary = "MES密炼PS编制-批准")
|
||||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:approve")
|
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:approve")
|
||||||
@@ -201,6 +211,34 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
|||||||
return Result.OK(actionLabel + "成功!");
|
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() {
|
private String getOperatorName() {
|
||||||
LoginUser loginUser = null;
|
LoginUser loginUser = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.jeecg.common.constant.CommonConstant;
|
|||||||
import org.jeecg.common.system.base.controller.JeecgController;
|
import org.jeecg.common.system.base.controller.JeecgController;
|
||||||
import org.jeecg.common.system.query.QueryGenerator;
|
import org.jeecg.common.system.query.QueryGenerator;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
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.entity.MesMaterial;
|
||||||
import org.jeecg.modules.mes.material.service.IMesMaterialService;
|
import org.jeecg.modules.mes.material.service.IMesMaterialService;
|
||||||
import org.jeecg.modules.system.entity.SysDepart;
|
import org.jeecg.modules.system.entity.SysDepart;
|
||||||
@@ -162,6 +163,9 @@ public class MesXslRubberQuickTestStdController
|
|||||||
return Result.OK(mesXslRubberQuickTestStdService.selectLinesByStdId(id));
|
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胶料快检实验标准-启用/停用")
|
@AutoLog(value = "MES胶料快检实验标准-启用/停用")
|
||||||
@Operation(summary = "MES胶料快检实验标准-启用/停用(字典 xslmes_rubber_quick_test_std_enable_status:1使用中 0已停用)")
|
@Operation(summary = "MES胶料快检实验标准-启用/停用(字典 xslmes_rubber_quick_test_std_enable_status:1使用中 0已停用)")
|
||||||
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_std:updateStatus")
|
@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);
|
void syncFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||||
//update-end---author:cursor ---date:20260522 for:【配合示方】密炼PS审批联动同步状态与审批人-----------
|
//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】配合示方生成混炼示方预览与批量创建-----------
|
//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);
|
String changeStatusBatch(String ids, String expectedStatus, String targetStatus, String actionLabel, String operatorName);
|
||||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】批量流转状态-----------
|
//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
|
* @param mixerPsTargetStatus 密炼PS目标状态:proofread / audit / approve
|
||||||
*/
|
*/
|
||||||
void syncFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
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审批联动同步审批人-----------
|
//update-end---author:cursor ---date:20260526 for:【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人-----------
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,11 @@ public interface IMesXslRubberQuickTestStdService extends IService<MesXslRubberQ
|
|||||||
* 密炼PS(原材料检验标准)批准后,将关联实验标准审核状态置为已批准(仅写入,不随PS反审核回退)
|
* 密炼PS(原材料检验标准)批准后,将关联实验标准审核状态置为已批准(仅写入,不随PS反审核回退)
|
||||||
*/
|
*/
|
||||||
void markAuditApprovedByPsCompileIds(Collection<String> psCompileIds);
|
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-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】配合示方生成混炼示方预览与批量创建-----------
|
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A38】配合示方生成混炼示方预览与批量创建-----------
|
||||||
@Override
|
@Override
|
||||||
public MesXslFormulaMixingGeneratePreviewVO buildMixingGeneratePreview(String formulaSpecId) {
|
public MesXslFormulaMixingGeneratePreviewVO buildMixingGeneratePreview(String formulaSpecId) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package org.jeecg.modules.xslmes.service.impl;
|
package org.jeecg.modules.xslmes.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.jeecg.common.util.oConvertUtils;
|
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-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-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:【配方日志查询】从入参直接构建快照,避免二次查库-----------
|
//update-begin---author:cursor ---date:20260527 for:【配方日志查询】从入参直接构建快照,避免二次查库-----------
|
||||||
private MesXslMixingSpecPage buildPageFromInput(
|
private MesXslMixingSpecPage buildPageFromInput(
|
||||||
MesXslMixingSpec main,
|
MesXslMixingSpec main,
|
||||||
|
|||||||
@@ -156,5 +156,24 @@ public class MesXslRubberQuickTestStdServiceImpl
|
|||||||
}
|
}
|
||||||
//update-end---author:jiangxh ---date:20260525 for:【MES】原材料检验标准密炼PS批准时关联实验标准置已批准-----------
|
//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】胶料快检实验标准名称同租户唯一、主子保存-----------
|
//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`;
|
||||||
@@ -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<FlowCapturedApi | null> {
|
|
||||||
// 若已有未完成的录制,先取消
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,9 +19,6 @@ import { useI18n } from '/@/hooks/web/useI18n';
|
|||||||
import { joinTimestamp, formatRequestDate } from './helper';
|
import { joinTimestamp, formatRequestDate } from './helper';
|
||||||
import { useUserStoreWithOut } from '/@/store/modules/user';
|
import { useUserStoreWithOut } from '/@/store/modules/user';
|
||||||
import { cloneDeep } from "lodash-es";
|
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 globSetting = useGlobSetting();
|
||||||
const urlPrefix = globSetting.urlPrefix;
|
const urlPrefix = globSetting.urlPrefix;
|
||||||
const { createMessage, createErrorModal } = useMessage();
|
const { createMessage, createErrorModal } = useMessage();
|
||||||
@@ -93,9 +90,6 @@ const transform: AxiosTransform = {
|
|||||||
|
|
||||||
// 请求之前处理config
|
// 请求之前处理config
|
||||||
beforeRequestHook: (config, options) => {
|
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;
|
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
|
||||||
|
|
||||||
// http开头的请求url,不加前缀
|
// http开头的请求url,不加前缀
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ enum Api {
|
|||||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||||
designContext = '/xslmes/approvalFlow/designContext',
|
designContext = '/xslmes/approvalFlow/designContext',
|
||||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
// 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 } });
|
export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url: Api.designContext, params: { routePath } });
|
||||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
// 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审批流设计】业务表可选回调动作-----
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ enum Api {
|
|||||||
status = '/xslmes/approvalHandle/status',
|
status = '/xslmes/approvalHandle/status',
|
||||||
approve = '/xslmes/approvalHandle/approve',
|
approve = '/xslmes/approvalHandle/approve',
|
||||||
reject = '/xslmes/approvalHandle/reject',
|
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 });
|
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审批流设计】发起人撤销-----
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<BasicModal v-bind="$attrs" @register="registerModal" :title="modalTitle" defaultFullscreen :canFullscreen="false" :showOkBtn="!readonly" :okText="'保存并发布'" @ok="handleSave" :wrapClassName="flowApiRecording ? 'flow-design-recording-hide' : ''">
|
<BasicModal v-bind="$attrs" @register="registerModal" :title="modalTitle" defaultFullscreen :canFullscreen="false" :showOkBtn="!readonly" :okText="'保存并发布'" @ok="handleSave">
|
||||||
<div class="fd-design">
|
<div class="fd-design">
|
||||||
<div class="fd-toolbar">
|
<div class="fd-toolbar">
|
||||||
<span class="fd-tb-item">绑定单据:<b>{{ record.bizTableName || record.bizTable }}</b></span>
|
<span class="fd-tb-item">绑定单据:<b>{{ record.bizTableName || record.bizTable }}</b></span>
|
||||||
@@ -58,7 +58,6 @@
|
|||||||
} from './flowTypes';
|
} from './flowTypes';
|
||||||
import type { FlowNode as FlowNodeType, NodeType, StageField } from './flowTypes';
|
import type { FlowNode as FlowNodeType, NodeType, StageField } from './flowTypes';
|
||||||
import { saveApprovalFlowDesign, getApprovalFlowById } from '../approvalFlow.api';
|
import { saveApprovalFlowDesign, getApprovalFlowById } from '../approvalFlow.api';
|
||||||
import { flowApiRecording } from '/@/utils/flowApiRecorder';
|
|
||||||
|
|
||||||
defineOptions({ name: 'ApprovalFlowDesign' });
|
defineOptions({ name: 'ApprovalFlowDesign' });
|
||||||
|
|
||||||
@@ -69,6 +68,9 @@
|
|||||||
const readonly = ref(false);
|
const readonly = ref(false);
|
||||||
const record = reactive<any>({ id: '', flowName: '', bizTable: '', bizTableName: '' });
|
const record = reactive<any>({ id: '', flowName: '', bizTable: '', bizTableName: '' });
|
||||||
const drawerRef = ref();
|
const drawerRef = ref();
|
||||||
|
// 当前审批流绑定的业务表,供节点配置按表查可选回调动作
|
||||||
|
const bizTableRef = ref('');
|
||||||
|
provide('approvalBizTable', bizTableRef);
|
||||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||||
const paletteStages = ref<StageField[]>([]);
|
const paletteStages = ref<StageField[]>([]);
|
||||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||||
@@ -105,6 +107,7 @@
|
|||||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||||
readonly.value = !!data?.readonly;
|
readonly.value = !!data?.readonly;
|
||||||
flowCtx.readonly = readonly.value;
|
flowCtx.readonly = readonly.value;
|
||||||
|
bizTableRef.value = data?.record?.bizTable || '';
|
||||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||||
paletteStages.value = Array.isArray(data?.paletteStages) ? data.paletteStages : [];
|
paletteStages.value = Array.isArray(data?.paletteStages) ? data.paletteStages : [];
|
||||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<a-drawer :title="title" :width="480" :open="open && !flowApiRecording" @close="onClose" :maskClosable="!readonly">
|
<a-drawer :title="title" :width="480" :open="open" @close="onClose" :maskClosable="!readonly">
|
||||||
<template v-if="form">
|
<template v-if="form">
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
<a-form-item label="节点名称">
|
<a-form-item label="节点名称">
|
||||||
@@ -80,13 +80,17 @@
|
|||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(录制业务按钮接口)----- -->
|
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(自动识别页面按钮)----- -->
|
||||||
<a-divider style="margin: 16px 0 12px">回调接口(审批联动业务)</a-divider>
|
<a-divider style="margin: 16px 0 12px">回调接口(审批联动业务)</a-divider>
|
||||||
<a-alert
|
<a-alert
|
||||||
type="info"
|
type="info"
|
||||||
show-icon
|
show-icon
|
||||||
style="margin-bottom: 12px"
|
style="margin-bottom: 12px"
|
||||||
message="审批到对应时机时,系统会以「当前处理人」身份调用所选业务接口(自动带上单据ID)。点击「录制」后,设计器会临时隐藏,请到页面上点击目标按钮(如“批准”),系统自动识别其接口并回填。"
|
:message="
|
||||||
|
pageActionOptions.length
|
||||||
|
? '以下为该业务已标注(@ApprovalBizAction)的可选接口。审批到对应时机时,系统会以「当前处理人」身份调用所选接口(自动带上单据ID)。'
|
||||||
|
: '该业务暂未标注可选接口(在后端 Controller 方法上加 @ApprovalBizAction 注解即可出现在此),也可手动填写接口路径。'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<div v-for="phase in callbackPhases" :key="phase.key" class="fd-cb-block">
|
<div v-for="phase in callbackPhases" :key="phase.key" class="fd-cb-block">
|
||||||
<div class="fd-cb-title">{{ phase.label }}</div>
|
<div class="fd-cb-title">{{ phase.label }}</div>
|
||||||
@@ -96,15 +100,28 @@
|
|||||||
<a-input v-model:value="a.url" placeholder="接口路径 /xxx" :disabled="readonly" style="flex: 1; min-width: 120px" />
|
<a-input v-model:value="a.url" placeholder="接口路径 /xxx" :disabled="readonly" style="flex: 1; min-width: 120px" />
|
||||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cb-del" @click="removeAction(phase.key, i)" />
|
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cb-del" @click="removeAction(phase.key, i)" />
|
||||||
</div>
|
</div>
|
||||||
<a-space v-if="!readonly" style="margin-top: 4px">
|
<a-space v-if="!readonly" style="margin-top: 4px" :size="6" wrap>
|
||||||
<a-button size="small" @click="recordInto(phase.key)">
|
<a-select
|
||||||
<Icon icon="ant-design:aim-outlined" />
|
v-if="pageActionOptions.length"
|
||||||
<span>录制接口</span>
|
style="width: 240px"
|
||||||
</a-button>
|
placeholder="从页面按钮中选择…"
|
||||||
|
:options="pageActionOptions"
|
||||||
|
v-model:value="actionPicker[phase.key]"
|
||||||
|
show-search
|
||||||
|
option-filter-prop="label"
|
||||||
|
@select="(v) => addFromButton(phase.key, v)"
|
||||||
|
/>
|
||||||
<a-button size="small" @click="addAction(phase.key)">手动添加</a-button>
|
<a-button size="small" @click="addAction(phase.key)">手动添加</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】节点回调接口可视化配置(录制业务按钮接口)----- -->
|
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退,无需逐节点配置----- -->
|
||||||
|
<a-alert
|
||||||
|
type="success"
|
||||||
|
show-icon
|
||||||
|
style="margin-top: 4px"
|
||||||
|
message="驳回 / 撤销 已全局统一:系统会自动执行该业务标注为「驳回时执行(@ApprovalBizAction onReject)」的接口完成回退,无需在此逐节点、逐流程配置。"
|
||||||
|
/>
|
||||||
|
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退,无需逐节点配置----- -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 抄送人 -->
|
<!-- 抄送人 -->
|
||||||
@@ -173,20 +190,11 @@
|
|||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
|
|
||||||
<!-- update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制中悬浮提示条----- -->
|
|
||||||
<Teleport to="body">
|
|
||||||
<div v-if="flowApiRecording" class="flow-record-banner">
|
|
||||||
<Icon icon="ant-design:aim-outlined" class="flow-record-banner-icon" />
|
|
||||||
<span class="flow-record-banner-text">录制中:请点击页面上要绑定的业务按钮(如“批准/反审核”),系统会自动识别其接口</span>
|
|
||||||
<a-button size="small" danger @click="cancelRecord">取消录制</a-button>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
<!-- update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制中悬浮提示条----- -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, inject, watch } from 'vue';
|
||||||
|
import type { Ref } from 'vue';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep } from 'lodash-es';
|
||||||
import { defHttp } from '/@/utils/http/axios';
|
import { defHttp } from '/@/utils/http/axios';
|
||||||
import { ApiSelect } from '/@/components/Form';
|
import { ApiSelect } from '/@/components/Form';
|
||||||
@@ -194,25 +202,63 @@
|
|||||||
import { OPERATOR_OPTIONS } from './flowTypes';
|
import { OPERATOR_OPTIONS } from './flowTypes';
|
||||||
import type { FlowNode } from './flowTypes';
|
import type { FlowNode } from './flowTypes';
|
||||||
import { useMessage } from '/@/hooks/web/useMessage';
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
import { flowApiRecording, startFlowApiRecord, cancelFlowApiRecord } from '/@/utils/flowApiRecorder';
|
import { getApprovalBizActions } from '../approvalFlow.api';
|
||||||
|
|
||||||
const props = defineProps<{ readonly?: boolean }>();
|
const props = defineProps<{ readonly?: boolean }>();
|
||||||
const emit = defineEmits(['confirm']);
|
const emit = defineEmits(['confirm']);
|
||||||
|
|
||||||
const { createMessage } = useMessage();
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
// 当前审批流绑定的业务表(由 FlowDesign 注入),据此向后端查可选回调动作
|
||||||
|
const bizTable = inject<Ref<string>>('approvalBizTable', ref(''));
|
||||||
|
// 后端 @ApprovalBizAction 标注的可选业务动作
|
||||||
|
const bizActions = ref<any[]>([]);
|
||||||
|
const bizActionsTable = ref('');
|
||||||
|
|
||||||
|
async function loadBizActions() {
|
||||||
|
const table = bizTable.value || '';
|
||||||
|
if (!table || bizActionsTable.value === table) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await getApprovalBizActions(table);
|
||||||
|
bizActions.value = Array.isArray(res) ? res : [];
|
||||||
|
bizActionsTable.value = table;
|
||||||
|
} catch (e) {
|
||||||
|
bizActions.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选动作下拉项(含真实 url/method)
|
||||||
|
const pageActionOptions = computed(() =>
|
||||||
|
(bizActions.value || []).map((a) => ({
|
||||||
|
label: `${a.name}(${a.method} ${a.url})`,
|
||||||
|
value: a.url,
|
||||||
|
raw: a,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
bizTable,
|
||||||
|
() => {
|
||||||
|
loadBizActions();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const node = ref<FlowNode | null>(null);
|
const node = ref<FlowNode | null>(null);
|
||||||
const form = ref<any>(null);
|
const form = ref<any>(null);
|
||||||
|
// 「从页面按钮中选择」下拉的临时选中值(仅作选择器,选完即清空,避免跨节点残留上次的选项)
|
||||||
|
const actionPicker = ref<Record<string, any>>({ onNodeApprove: undefined, onApprove: undefined, onReject: undefined });
|
||||||
|
|
||||||
const operatorOptions = OPERATOR_OPTIONS;
|
const operatorOptions = OPERATOR_OPTIONS;
|
||||||
const readonly = computed(() => !!props.readonly);
|
const readonly = computed(() => !!props.readonly);
|
||||||
|
|
||||||
// 回调接口配置:三个触发时机
|
// 回调接口配置:通过类时机需按节点配置;驳回类(onReject)已全局统一(后端按 @ApprovalBizAction 自动执行),无需在此逐节点维护
|
||||||
const callbackPhases = [
|
const callbackPhases = [
|
||||||
{ key: 'onNodeApprove', label: '本节点通过时执行' },
|
{ key: 'onNodeApprove', label: '本节点通过时执行' },
|
||||||
{ key: 'onApprove', label: '流程最终通过时执行' },
|
{ key: 'onApprove', label: '流程最终通过时执行' },
|
||||||
{ key: 'onReject', label: '驳回时执行' },
|
|
||||||
];
|
];
|
||||||
const methodOptions = [
|
const methodOptions = [
|
||||||
{ label: 'POST', value: 'POST' },
|
{ label: 'POST', value: 'POST' },
|
||||||
@@ -230,6 +276,8 @@
|
|||||||
|
|
||||||
function openDrawer(n: FlowNode) {
|
function openDrawer(n: FlowNode) {
|
||||||
node.value = n;
|
node.value = n;
|
||||||
|
// 切换节点时清空下拉选择器的残留值
|
||||||
|
actionPicker.value = { onNodeApprove: undefined, onApprove: undefined, onReject: undefined };
|
||||||
// 编辑副本,确定时回写,避免取消后脏数据
|
// 编辑副本,确定时回写,避免取消后脏数据
|
||||||
form.value = { name: n.name, props: cloneDeep(n.props) };
|
form.value = { name: n.name, props: cloneDeep(n.props) };
|
||||||
// 审批人节点确保回调接口配置结构存在
|
// 审批人节点确保回调接口配置结构存在
|
||||||
@@ -252,17 +300,21 @@
|
|||||||
form.value.props.callbackActions[phaseKey].splice(i, 1);
|
form.value.props.callbackActions[phaseKey].splice(i, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 录制:临时隐藏设计器,捕获用户点击业务按钮的请求并回填 */
|
/** 从「已标注的业务动作」中选择一个接口加入回调(按 url 值查回原始动作) */
|
||||||
async function recordInto(phaseKey: string) {
|
function addFromButton(phaseKey: string, url: string) {
|
||||||
const cap = await startFlowApiRecord();
|
const opt = pageActionOptions.value.find((o) => o.value === url);
|
||||||
if (cap) {
|
const raw = opt?.raw;
|
||||||
form.value.props.callbackActions[phaseKey].push({ name: '', method: cap.method, url: cap.url, body: cap.data });
|
if (!raw || !raw.url) {
|
||||||
createMessage.success(`已录制接口:${cap.method} ${cap.url}`);
|
return;
|
||||||
}
|
}
|
||||||
}
|
const list = form.value.props.callbackActions[phaseKey];
|
||||||
|
// 选完即清空下拉,使其回到 placeholder(仅作选择器用)
|
||||||
function cancelRecord() {
|
actionPicker.value[phaseKey] = undefined;
|
||||||
cancelFlowApiRecord();
|
if (list.some((a) => a.url === raw.url)) {
|
||||||
|
createMessage.info('该接口已添加');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.push({ name: raw.name, method: raw.method || 'POST', url: raw.url });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClose() {
|
function onClose() {
|
||||||
@@ -327,33 +379,3 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- 录制提示条 Teleport 到 body,需非 scoped 全局样式 -->
|
|
||||||
<style lang="less">
|
|
||||||
.flow-record-banner {
|
|
||||||
position: fixed;
|
|
||||||
top: 16px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 99999;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
max-width: 92vw;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fffbe6;
|
|
||||||
border: 1px solid #ffe58f;
|
|
||||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
|
||||||
color: #614700;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
.flow-record-banner-icon {
|
|
||||||
color: #fa8c16;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
.flow-record-banner-text {
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -5,11 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
@line-color: #cacaca;
|
@line-color: #cacaca;
|
||||||
|
|
||||||
/* 录制回调接口时,隐藏整个设计器弹窗(含遮罩),让用户能点击业务页面上的真实按钮 */
|
|
||||||
.flow-design-recording-hide {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fd-design {
|
.fd-design {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -35,6 +35,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<!-- 不可办理(已处理/已流转/非当前处理人):置灰提示 -->
|
<!-- 不可办理(已处理/已流转/非当前处理人):置灰提示 -->
|
||||||
<span v-else-if="!props.mine && disabledText" class="im-biz-record-disabled">{{ disabledText }}</span>
|
<span v-else-if="!props.mine && disabledText" class="im-biz-record-disabled">{{ disabledText }}</span>
|
||||||
|
<!-- 发起人:审批中可撤销 -->
|
||||||
|
<a-button v-if="canCancel" size="small" danger :loading="cancelling" @click="openCancel(singleItem)">
|
||||||
|
<Icon icon="ant-design:rollback-outlined" />
|
||||||
|
<span>撤销</span>
|
||||||
|
</a-button>
|
||||||
|
<!-- 发起人:审批已结束的状态提示 -->
|
||||||
|
<span v-else-if="mineEndedText" class="im-biz-record-disabled">{{ mineEndedText }}</span>
|
||||||
<a v-if="canLocate" class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
<a v-if="canLocate" class="im-biz-record-link" @click.prevent="handleLinkClick(singleItem.linkPath)">
|
||||||
<Icon icon="ant-design:unordered-list-outlined" />
|
<Icon icon="ant-design:unordered-list-outlined" />
|
||||||
<span>跳转至列表</span>
|
<span>跳转至列表</span>
|
||||||
@@ -90,17 +97,30 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 撤销弹窗 -->
|
||||||
|
<a-modal v-model:open="cancelOpen" title="撤销审批" :confirmLoading="cancelling" okText="确认撤销" @ok="confirmCancel">
|
||||||
|
<a-alert type="warning" show-icon :message="'撤销后流程将终止,单据将恢复到发起审批前的状态。'" style="margin-bottom: 12px" />
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item label="撤销原因">
|
||||||
|
<a-textarea v-model:value="cancelReason" :rows="3" placeholder="可选填写撤销原因" :maxlength="500" show-count />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, onMounted, watch } from 'vue';
|
import { computed, ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
import type { ImBizRecordItem, ImBizRecordPayload } from './imBizRecordMessage';
|
import type { ImBizRecordItem, ImBizRecordPayload } from './imBizRecordMessage';
|
||||||
import { getImBizRecordFieldValueByLabel, resolveImBizRecordItemFields, resolveImBizRecordListColumnLabels } from './imBizRecordMessage';
|
import { getImBizRecordFieldValueByLabel, resolveImBizRecordItemFields, resolveImBizRecordListColumnLabels } from './imBizRecordMessage';
|
||||||
import { navigateImBizRecordLink } from './imRecordLocate';
|
import { navigateImBizRecordLink } from './imRecordLocate';
|
||||||
import { hasImBizRecordPagePermission } from './imBizRecordPermission';
|
import { hasImBizRecordPagePermission } from './imBizRecordPermission';
|
||||||
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
|
||||||
|
import { onImMessagesUpdated } from './imCache';
|
||||||
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
|
||||||
import { useMessage } from '/@/hooks/web/useMessage';
|
import { useMessage } from '/@/hooks/web/useMessage';
|
||||||
import { approveApproval, rejectApproval, getApprovalStatus } from '/@/views/approval/flow/approvalHandle.api';
|
import { approveApproval, rejectApproval, cancelApproval, getApprovalStatus } from '/@/views/approval/flow/approvalHandle.api';
|
||||||
import ImApprovalDetailModal from './ImApprovalDetailModal.vue';
|
import ImApprovalDetailModal from './ImApprovalDetailModal.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ImBizRecordMessageContent' });
|
defineOptions({ name: 'ImBizRecordMessageContent' });
|
||||||
@@ -122,8 +142,14 @@
|
|||||||
const rejectOpen = ref(false);
|
const rejectOpen = ref(false);
|
||||||
const rejectReason = ref('');
|
const rejectReason = ref('');
|
||||||
const rejectItem = ref<ImBizRecordItem | null>(null);
|
const rejectItem = ref<ImBizRecordItem | null>(null);
|
||||||
// 本地办理结果:approved / rejected(卡片消息为静态,办理后本地标记)
|
// 本地办理结果:approved / rejected / cancelled(卡片消息为静态,办理后本地标记)
|
||||||
const actionDone = ref<'' | 'approved' | 'rejected'>('');
|
const actionDone = ref<'' | 'approved' | 'rejected' | 'cancelled'>('');
|
||||||
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||||
|
const cancelling = ref(false);
|
||||||
|
const cancelOpen = ref(false);
|
||||||
|
const cancelReason = ref('');
|
||||||
|
const cancelItem = ref<ImBizRecordItem | null>(null);
|
||||||
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||||
|
|
||||||
// 审批实例实时状态(用于旧节点卡片置灰)
|
// 审批实例实时状态(用于旧节点卡片置灰)
|
||||||
interface LiveStatus {
|
interface LiveStatus {
|
||||||
@@ -184,11 +210,38 @@
|
|||||||
return '等待他人处理';
|
return '等待他人处理';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||||
|
// 是否可撤销:审批卡片 且 本人发起(mine) 且 审批中 且 本地未撤销
|
||||||
|
const canCancel = computed(() => {
|
||||||
|
if (!isApprovalCard.value || !props.mine || actionDone.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const s = liveStatus.value;
|
||||||
|
return !!s && s.exists === true && s.status === '0';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发起人视角的结束态提示
|
||||||
|
const mineEndedText = computed(() => {
|
||||||
|
if (!isApprovalCard.value || !props.mine) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (actionDone.value === 'cancelled') return '已撤销';
|
||||||
|
const s = liveStatus.value;
|
||||||
|
if (!s || !s.exists) return '';
|
||||||
|
if (s.status === '1') return '已通过';
|
||||||
|
if (s.status === '2') return '已驳回';
|
||||||
|
if (s.status === '3') return '已撤销';
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||||
|
|
||||||
async function loadLiveStatus() {
|
async function loadLiveStatus() {
|
||||||
const id = singleItem.value?.instanceId;
|
const id = singleItem.value?.instanceId;
|
||||||
if (!isApprovalCard.value || props.mine || !id) {
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人卡片也需加载状态以支持撤销-----
|
||||||
|
if (!isApprovalCard.value || !id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人卡片也需加载状态以支持撤销-----
|
||||||
try {
|
try {
|
||||||
const res: any = await getApprovalStatus(id);
|
const res: any = await getApprovalStatus(id);
|
||||||
liveStatus.value = (res || { exists: false }) as LiveStatus;
|
liveStatus.value = (res || { exists: false }) as LiveStatus;
|
||||||
@@ -202,6 +255,33 @@
|
|||||||
onMounted(loadLiveStatus);
|
onMounted(loadLiveStatus);
|
||||||
watch(() => singleItem.value?.instanceId, loadLiveStatus);
|
watch(() => singleItem.value?.instanceId, loadLiveStatus);
|
||||||
|
|
||||||
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
|
||||||
|
// 撤销/驳回/流转时后端会向处理人推送IM消息,WS到达会触发 onImMessagesUpdated,
|
||||||
|
// 借此实时重新拉取实例状态,使本卡片的「审批/拒绝」按钮即时置灰、不可点击。
|
||||||
|
let unsubscribeMsgUpdated: (() => void) | null = null;
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isApprovalCard.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsubscribeMsgUpdated = onImMessagesUpdated(() => {
|
||||||
|
// 本地已办理的卡片无需再刷新,避免覆盖本地置灰文案
|
||||||
|
if (actionDone.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 已是终态(已通过/已驳回/已撤销/已失效)的卡片无需重复拉取
|
||||||
|
const s = liveStatus.value;
|
||||||
|
if (s && (s.exists === false || (!!s.status && s.status !== '0'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadLiveStatus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
unsubscribeMsgUpdated?.();
|
||||||
|
unsubscribeMsgUpdated = null;
|
||||||
|
});
|
||||||
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】订阅会话消息更新,撤销/流转后处理人卡片按钮实时置灰-----
|
||||||
|
|
||||||
const hasPagePermission = computed(() => hasImBizRecordPagePermission(props.payload.pagePath));
|
const hasPagePermission = computed(() => hasImBizRecordPagePermission(props.payload.pagePath));
|
||||||
// 审批卡片即使无列表页权限也应可办理/查看详情,仅"跳转至列表"受页面权限约束
|
// 审批卡片即使无列表页权限也应可办理/查看详情,仅"跳转至列表"受页面权限约束
|
||||||
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value && !isApprovalCard.value);
|
const showNoPermission = computed(() => !props.mine && !hasPagePermission.value && !isApprovalCard.value);
|
||||||
@@ -231,6 +311,13 @@
|
|||||||
|
|
||||||
async function handleApprove(item: ImBizRecordItem) {
|
async function handleApprove(item: ImBizRecordItem) {
|
||||||
if (!item.instanceId || approving.value) return;
|
if (!item.instanceId || approving.value) return;
|
||||||
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
|
||||||
|
await loadLiveStatus();
|
||||||
|
if (!liveActionable.value) {
|
||||||
|
createMessage.warning(disabledText.value || '该审批已无法办理');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
|
||||||
try {
|
try {
|
||||||
approving.value = true;
|
approving.value = true;
|
||||||
const res: any = await approveApproval({ instanceId: item.instanceId });
|
const res: any = await approveApproval({ instanceId: item.instanceId });
|
||||||
@@ -257,6 +344,14 @@
|
|||||||
createMessage.warning('请填写驳回理由');
|
createMessage.warning('请填写驳回理由');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
|
||||||
|
await loadLiveStatus();
|
||||||
|
if (!liveActionable.value) {
|
||||||
|
createMessage.warning(disabledText.value || '该审批已无法办理');
|
||||||
|
rejectOpen.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】办理前二次校验最新状态,撤销后防误点击-----
|
||||||
try {
|
try {
|
||||||
rejecting.value = true;
|
rejecting.value = true;
|
||||||
await rejectApproval({ instanceId: item.instanceId, reason: rejectReason.value.trim() });
|
await rejectApproval({ instanceId: item.instanceId, reason: rejectReason.value.trim() });
|
||||||
@@ -269,6 +364,30 @@
|
|||||||
rejecting.value = false;
|
rejecting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||||
|
function openCancel(item: ImBizRecordItem) {
|
||||||
|
cancelItem.value = item;
|
||||||
|
cancelReason.value = '';
|
||||||
|
cancelOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmCancel() {
|
||||||
|
const item = cancelItem.value;
|
||||||
|
if (!item?.instanceId || cancelling.value) return;
|
||||||
|
try {
|
||||||
|
cancelling.value = true;
|
||||||
|
await cancelApproval({ instanceId: item.instanceId, reason: cancelReason.value.trim() });
|
||||||
|
createMessage.success('已撤销,单据已恢复到发起前状态');
|
||||||
|
actionDone.value = 'cancelled';
|
||||||
|
cancelOpen.value = false;
|
||||||
|
await loadLiveStatus();
|
||||||
|
emit('handled');
|
||||||
|
} finally {
|
||||||
|
cancelling.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user