Compare commits
2 Commits
71f9dab1be
...
767214b7db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
767214b7db | ||
|
|
0ff4a201b0 |
@@ -0,0 +1,38 @@
|
||||
package org.jeecg.modules.xslmes.approval.action;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 审批联动业务动作标注。
|
||||
*
|
||||
* <p>把本注解打在业务 Controller 的处理方法上,即声明「该按钮/接口可被审批流程作为回调动作选择」。
|
||||
* 系统启动时会反射扫描所有带本注解的接口方法,取其 {@code @RequestMapping} 真实路径与 HTTP 方法,
|
||||
* 按 {@link #table()} 归类,供审批流设计器的节点「回调接口」下拉选择。</p>
|
||||
*
|
||||
* <p>因为基于 Spring 运行时反射 + 真实映射路径,生产环境天然可用,URL 绝对准确,无需任何源码解析。</p>
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注解
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApprovalBizAction {
|
||||
|
||||
/** 动作展示名(如「批准」「反审核」「启用」),供设计器下拉显示 */
|
||||
String name();
|
||||
|
||||
/** 所属业务表名(如 mes_xsl_rubber_quick_test_std),与审批流绑定的 bizTable 对应 */
|
||||
String table();
|
||||
|
||||
/**
|
||||
* 适用触发时机,可多选;为空表示三种时机均可选用。
|
||||
* 取值:onNodeApprove(本节点通过) / onApprove(最终通过) / onReject(驳回)
|
||||
*/
|
||||
String[] phase() default {};
|
||||
|
||||
/** 排序,越小越靠前 */
|
||||
int order() default 0;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.jeecg.modules.xslmes.approval.action;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 审批联动业务动作注册表。
|
||||
*
|
||||
* <p>容器启动完成后,反射扫描所有带 {@link ApprovalBizAction} 的接口方法,
|
||||
* 取真实映射路径 + HTTP 方法,按业务表归类缓存,供设计器查询选择。</p>
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ApprovalBizActionRegistry {
|
||||
|
||||
@Resource
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/** 业务表 -> 动作列表 */
|
||||
private final Map<String, List<ApprovalBizActionVo>> byTable = new ConcurrentHashMap<>();
|
||||
|
||||
@EventListener(ContextRefreshedEvent.class)
|
||||
public void onContextRefreshed() {
|
||||
try {
|
||||
scan();
|
||||
} catch (Exception e) {
|
||||
log.warn("[审批联动] 扫描 @ApprovalBizAction 失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void scan() {
|
||||
byTable.clear();
|
||||
RequestMappingHandlerMapping mapping;
|
||||
try {
|
||||
mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("[审批联动] 未取到 RequestMappingHandlerMapping,跳过扫描", e);
|
||||
return;
|
||||
}
|
||||
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
|
||||
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
|
||||
HandlerMethod hm = entry.getValue();
|
||||
ApprovalBizAction ann = hm.getMethodAnnotation(ApprovalBizAction.class);
|
||||
if (ann == null || ann.table() == null || ann.table().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
RequestMappingInfo info = entry.getKey();
|
||||
String url = resolveUrl(info);
|
||||
if (url == null || url.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ApprovalBizActionVo vo = new ApprovalBizActionVo();
|
||||
vo.setName(ann.name());
|
||||
vo.setTable(ann.table());
|
||||
vo.setPhase(ann.phase());
|
||||
vo.setOrder(ann.order());
|
||||
vo.setUrl(url);
|
||||
vo.setMethod(resolveMethod(info));
|
||||
vo.setPerms(resolvePerms(hm));
|
||||
byTable.computeIfAbsent(ann.table(), k -> new ArrayList<>()).add(vo);
|
||||
}
|
||||
// 排序
|
||||
for (List<ApprovalBizActionVo> list : byTable.values()) {
|
||||
list.sort(Comparator.comparingInt(ApprovalBizActionVo::getOrder));
|
||||
}
|
||||
int total = byTable.values().stream().mapToInt(List::size).sum();
|
||||
log.info("[审批联动] 已注册业务动作 {} 个,覆盖 {} 张业务表", total, byTable.size());
|
||||
}
|
||||
|
||||
/** 取第一个映射路径(不含 context-path) */
|
||||
private String resolveUrl(RequestMappingInfo info) {
|
||||
Set<String> patterns = info.getPatternValues();
|
||||
if (patterns == null || patterns.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return patterns.iterator().next();
|
||||
}
|
||||
|
||||
/** 取 HTTP 方法,默认 POST */
|
||||
private String resolveMethod(RequestMappingInfo info) {
|
||||
RequestMethodsRequestCondition cond = info.getMethodsCondition();
|
||||
if (cond == null || cond.getMethods().isEmpty()) {
|
||||
return "POST";
|
||||
}
|
||||
return cond.getMethods().iterator().next().name();
|
||||
}
|
||||
|
||||
/** 取接口权限标识(@RequiresPermissions) */
|
||||
private String resolvePerms(HandlerMethod hm) {
|
||||
RequiresPermissions rp = hm.getMethodAnnotation(RequiresPermissions.class);
|
||||
if (rp != null && rp.value().length > 0) {
|
||||
return rp.value()[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 按业务表取可选动作 */
|
||||
public List<ApprovalBizActionVo> getByTable(String table) {
|
||||
if (table == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> list = byTable.get(table);
|
||||
return list == null ? Collections.emptyList() : new ArrayList<>(list);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表+时机自动取业务动作-----------
|
||||
/**
|
||||
* 按业务表 + 时机取动作(如驳回统一执行 onReject 动作,无需在每个流程节点手动配置)。
|
||||
*/
|
||||
public List<ApprovalBizActionVo> getByTableAndPhase(String table, String phase) {
|
||||
if (table == null || phase == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> list = byTable.get(table);
|
||||
if (list == null || list.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ApprovalBizActionVo> result = new ArrayList<>();
|
||||
for (ApprovalBizActionVo vo : list) {
|
||||
String[] phases = vo.getPhase();
|
||||
if (phases == null) {
|
||||
continue;
|
||||
}
|
||||
for (String p : phases) {
|
||||
if (phase.equals(p)) {
|
||||
result.add(vo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表+时机自动取业务动作-----------
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jeecg.modules.xslmes.approval.action;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审批联动业务动作(供设计器节点「回调接口」下拉选择)。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流设计】审批联动业务动作VO
|
||||
*/
|
||||
@Data
|
||||
public class ApprovalBizActionVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 动作名(如「批准」) */
|
||||
private String name;
|
||||
|
||||
/** 接口真实路径(取自 @RequestMapping,不含 context-path),如 /xslmes/xxx/updateStatus */
|
||||
private String url;
|
||||
|
||||
/** HTTP 方法:GET/POST/PUT/DELETE */
|
||||
private String method;
|
||||
|
||||
/** 所属业务表 */
|
||||
private String table;
|
||||
|
||||
/** 适用时机:onNodeApprove/onApprove/onReject,空表示均可 */
|
||||
private String[] phase;
|
||||
|
||||
/** 接口权限标识(取自 @RequiresPermissions,可空) */
|
||||
private String perms;
|
||||
|
||||
/** 排序 */
|
||||
private int order;
|
||||
}
|
||||
@@ -50,6 +50,27 @@ public class ApprovalActionHttpExecutor {
|
||||
* @param phase 时机:onNodeApprove / onApprove / onReject
|
||||
* @param inst 审批实例
|
||||
*/
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】判断节点某阶段是否配置了业务回调-----------
|
||||
/**
|
||||
* 判断节点在指定时机是否配置了回调接口(用于决定是否由业务回调全权负责回退)。
|
||||
*/
|
||||
public boolean hasActions(JSONObject node, String phase) {
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
JSONObject propsObj = node.getJSONObject("props");
|
||||
if (propsObj == null) {
|
||||
return false;
|
||||
}
|
||||
JSONObject callbackActions = propsObj.getJSONObject("callbackActions");
|
||||
if (callbackActions == null) {
|
||||
return false;
|
||||
}
|
||||
JSONArray actions = callbackActions.getJSONArray(phase);
|
||||
return actions != null && !actions.isEmpty();
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】判断节点某阶段是否配置了业务回调-----------
|
||||
|
||||
public void run(JSONObject node, String phase, MesXslApprovalInstance inst) {
|
||||
if (node == null || inst == null) {
|
||||
return;
|
||||
@@ -86,6 +107,25 @@ public class ApprovalActionHttpExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表注解自动调用业务接口-----------
|
||||
/**
|
||||
* 直接按 url+method 调用业务接口(用于驳回统一回退,动作来自 @ApprovalBizAction 注解而非节点配置)。
|
||||
* 无当前处理人登录态时降级跳过。
|
||||
*/
|
||||
public void runByUrl(String method, String url, MesXslApprovalInstance inst) {
|
||||
if (oConvertUtils.isEmpty(url) || inst == null) {
|
||||
return;
|
||||
}
|
||||
String token = currentToken();
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
log.warn("[审批回调] 无当前处理人登录态,跳过驳回业务回退 url={}, bizId={}", url, inst.getBizDataId());
|
||||
return;
|
||||
}
|
||||
String httpMethod = oConvertUtils.getString(method, "POST").toUpperCase();
|
||||
invoke(httpMethod, url, null, inst.getBizDataId(), token);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表注解自动调用业务接口-----------
|
||||
|
||||
private void invoke(String method, String url, JSONObject recordedBody, String bizDataId, String token) {
|
||||
String fullUrl = buildFullUrl(url);
|
||||
HttpMethod httpMethod = HttpMethod.valueOf(method);
|
||||
@@ -95,13 +135,15 @@ public class ApprovalActionHttpExecutor {
|
||||
headers.add(CommonConstant.X_ACCESS_TOKEN, token);
|
||||
headers.add(HttpHeaders.AUTHORIZATION, token);
|
||||
|
||||
// 统一在 url 上附带单据ID的常见参数名,兼容 @RequestParam(id/ids/dataId) 形式的接口
|
||||
// (如密炼PS的 /proofread、/audit、/approve 用的是 @RequestParam ids)
|
||||
String idValue = oConvertUtils.getString(bizDataId, "");
|
||||
String sep = fullUrl.contains("?") ? "&" : "?";
|
||||
fullUrl = fullUrl + sep + "id=" + idValue + "&ids=" + idValue + "&dataId=" + idValue;
|
||||
|
||||
Object bodyToSend = null;
|
||||
if ("GET".equals(method) || "DELETE".equals(method)) {
|
||||
// 查询/删除:单据ID拼到 url
|
||||
String sep = fullUrl.contains("?") ? "&" : "?";
|
||||
fullUrl = fullUrl + sep + "id=" + bizDataId + "&dataId=" + bizDataId;
|
||||
} else {
|
||||
// 写操作:合并录制的 body,并用单据ID覆盖 id
|
||||
if (!("GET".equals(method) || "DELETE".equals(method))) {
|
||||
// 写操作:合并录制的 body,并用单据ID覆盖 id(兼容 @RequestBody 形式)
|
||||
JSONObject body = recordedBody == null ? new JSONObject() : new JSONObject(recordedBody);
|
||||
body.put("id", bizDataId);
|
||||
bodyToSend = body;
|
||||
|
||||
@@ -15,6 +15,8 @@ import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.vo.DictModel;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionVo;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
@@ -47,6 +49,11 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
@Autowired
|
||||
private ISysBaseAPI sysBaseAPI;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表-----
|
||||
@Autowired
|
||||
private ApprovalBizActionRegistry approvalBizActionRegistry;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批联动业务动作注册表-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页字段解析-----
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@@ -258,13 +265,28 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
return Result.OK(data);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按业务表查可选回调动作-----
|
||||
/**
|
||||
* 查询某业务表已标注 @ApprovalBizAction 的可选回调动作,供设计器节点「回调接口」下拉选择。
|
||||
*
|
||||
* @param table 业务表名(审批流绑定的 bizTable)
|
||||
*/
|
||||
@Operation(summary = "审批流设计-业务表可选回调动作")
|
||||
@RequiresPermissions("approval:flow:design")
|
||||
@GetMapping(value = "/bizActions")
|
||||
public Result<List<ApprovalBizActionVo>> bizActions(@RequestParam(name = "table") String table) {
|
||||
return Result.OK(approvalBizActionRegistry.getByTable(table));
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】按业务表查可选回调动作-----
|
||||
|
||||
/**
|
||||
* 根据前端路由反查业务表名。
|
||||
* 约定:jeecg 代码生成的列表组件名为 表名驼峰 + List,sys_permission.component 形如
|
||||
* xslmes/mesXslFormulaSpec/MesXslFormulaSpecList,url 即路由。
|
||||
* 反查:url=routePath -> component -> 末段组件名去掉 List -> 驼峰转下划线得到表名。
|
||||
*/
|
||||
private String resolveTableByRoutePath(String routePath) {
|
||||
/** 根据前端路由反查页面组件路径(sys_permission.component),用于前端按 key 取该页面按钮接口映射 */
|
||||
private String resolveComponentByRoutePath(String routePath) {
|
||||
if (oConvertUtils.isEmpty(routePath)) {
|
||||
return null;
|
||||
}
|
||||
@@ -272,17 +294,20 @@ public class MesXslApprovalFlowController extends JeecgController<MesXslApproval
|
||||
String sql = "SELECT component FROM sys_permission WHERE menu_type IN (0,1) "
|
||||
+ "AND (del_flag = 0 OR del_flag IS NULL) AND url = ? "
|
||||
+ "ORDER BY menu_type DESC LIMIT 1";
|
||||
String component;
|
||||
try {
|
||||
List<String> list = jdbcTemplate.queryForList(sql, String.class, path);
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
component = list.get(0);
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
} catch (Exception e) {
|
||||
log.warn("反查菜单组件失败 routePath={}", routePath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveTableByRoutePath(String routePath) {
|
||||
if (oConvertUtils.isEmpty(routePath)) {
|
||||
return null;
|
||||
}
|
||||
String component = resolveComponentByRoutePath(routePath);
|
||||
if (oConvertUtils.isEmpty(component)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -71,4 +72,40 @@ public class MesXslApprovalHandleController {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.reject(instanceId, reason, user);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
@Operation(summary = "审批办理-撤销(发起人撤回)")
|
||||
@PostMapping("/cancel")
|
||||
public Result<String> cancel(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = (String) body.get("instanceId");
|
||||
String reason = body.get("reason") == null ? null : String.valueOf(body.get("reason"));
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
return Result.error("缺少审批实例ID");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.cancel(instanceId, reason, user);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
@Operation(summary = "审批办理-催办(发起人催处理人)")
|
||||
@PostMapping("/urge")
|
||||
public Result<String> urge(@RequestBody Map<String, Object> body) {
|
||||
String instanceId = (String) body.get("instanceId");
|
||||
if (oConvertUtils.isEmpty(instanceId)) {
|
||||
return Result.error("缺少审批实例ID");
|
||||
}
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return approvalHandleService.urge(instanceId, user);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
|
||||
@Operation(summary = "审批办理-我的待办列表")
|
||||
@GetMapping("/pendingList")
|
||||
public Result<List<Map<String, Object>>> pendingList() {
|
||||
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
return Result.OK(approvalHandleService.pendingList(user));
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表-----
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -165,6 +166,16 @@ public class MesXslApprovalLaunchController {
|
||||
return Result.error("该审批流未发布,无法发起");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】防止同一单据重复发起审批-----
|
||||
long active = approvalInstanceService.count(new LambdaQueryWrapper<MesXslApprovalInstance>()
|
||||
.eq(MesXslApprovalInstance::getBizTable, flow.getBizTable())
|
||||
.eq(MesXslApprovalInstance::getBizDataId, bizDataId)
|
||||
.eq(MesXslApprovalInstance::getStatus, "0"));
|
||||
if (active > 0) {
|
||||
return Result.error("该单据已有审批中的流程,请勿重复发起");
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】防止同一单据重复发起审批-----
|
||||
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
@@ -196,6 +207,7 @@ public class MesXslApprovalLaunchController {
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||
int count = 0;
|
||||
int skipCount = 0;
|
||||
for (Object o : (List<?>) itemsObj) {
|
||||
if (!(o instanceof Map)) {
|
||||
continue;
|
||||
@@ -206,15 +218,29 @@ public class MesXslApprovalLaunchController {
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
continue;
|
||||
}
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
|
||||
long active = approvalInstanceService.count(new LambdaQueryWrapper<MesXslApprovalInstance>()
|
||||
.eq(MesXslApprovalInstance::getBizTable, flow.getBizTable())
|
||||
.eq(MesXslApprovalInstance::getBizDataId, bizDataId)
|
||||
.eq(MesXslApprovalInstance::getStatus, "0"));
|
||||
if (active > 0) {
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
count++;
|
||||
}
|
||||
if (count == 0) {
|
||||
return Result.error("没有有效的单据数据");
|
||||
return Result.error(skipCount > 0 ? "所选单据均已有审批中的流程,无需重复发起" : "没有有效的单据数据");
|
||||
}
|
||||
return Result.OK("已发起 " + count + " 条审批!");
|
||||
String msg = "已发起 " + count + " 条审批!";
|
||||
if (skipCount > 0) {
|
||||
msg += "(" + skipCount + " 条已有审批中流程,已跳过)";
|
||||
}
|
||||
return Result.OK(msg);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,16 @@ public class MesXslApprovalFlow extends JeecgEntity implements Serializable {
|
||||
@Schema(description = "对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)")
|
||||
private String routePath;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
@Schema(description = "业务单据状态字段名(驳回/撤销时回写其发起时原值)")
|
||||
private String statusField;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒配置-----
|
||||
@Schema(description = "超时提醒小时数(0=不提醒),默认24")
|
||||
private Integer timeoutHours;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒配置-----
|
||||
|
||||
@Schema(description = "流程设计JSON(钉钉式节点树)")
|
||||
private String flowConfig;
|
||||
|
||||
|
||||
@@ -46,6 +46,14 @@ public class MesXslApprovalInstance extends JeecgEntity implements Serializable
|
||||
@Schema(description = "业务单据展示标题")
|
||||
private String bizTitle;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
@Schema(description = "业务单据状态字段名(发起时快照)")
|
||||
private String statusField;
|
||||
|
||||
@Schema(description = "发起审批时业务状态原值(驳回/撤销回写)")
|
||||
private String originStatus;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
|
||||
@Schema(description = "当前节点ID")
|
||||
private String currentNodeId;
|
||||
|
||||
@@ -83,6 +91,12 @@ public class MesXslApprovalInstance extends JeecgEntity implements Serializable
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】乐观锁防并发写-----
|
||||
@com.baomidou.mybatisplus.annotation.Version
|
||||
@Schema(description = "乐观锁版本号")
|
||||
private Integer version;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】乐观锁防并发写-----
|
||||
|
||||
@Schema(description = "逻辑删除:0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.jeecg.modules.xslmes.approval.scheduler;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 审批超时提醒定时任务。
|
||||
* 每小时扫描一次所有审批中的实例,对超过各流程配置 timeout_hours 仍未处理的节点
|
||||
* 向当前处理人发送 IM 提醒,同一实例在同一超时周期内只提醒一次。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流完善】超时提醒调度器
|
||||
*/
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒调度器-----
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MesXslApprovalTimeoutScheduler {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalHandleService approvalHandleService;
|
||||
|
||||
/**
|
||||
* 每小时整点执行:扫描超时未处理的审批实例并发送提醒。
|
||||
* 使用 fixedDelay 避免并发执行;应用启动 5 分钟后开始首次执行。
|
||||
*/
|
||||
@Scheduled(initialDelayString = "PT5M", fixedDelayString = "PT1H")
|
||||
public void remindTimeout() {
|
||||
log.info("开始扫描审批超时实例...");
|
||||
try {
|
||||
approvalHandleService.remindTimeoutInstances();
|
||||
} catch (Exception e) {
|
||||
log.warn("审批超时扫描异常", e);
|
||||
}
|
||||
log.info("审批超时实例扫描完成");
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒调度器-----
|
||||
@@ -4,6 +4,7 @@ import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,13 @@ public interface IMesXslApprovalHandleService {
|
||||
*/
|
||||
Result<String> reject(String instanceId, String reason, LoginUser user);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
/**
|
||||
* 撤销:仅发起人本人在审批中可撤回,流程终止并将业务单据恢复到发起时状态。
|
||||
*/
|
||||
Result<String> cancel(String instanceId, String reason, LoginUser user);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
|
||||
/**
|
||||
* 查看单据全部字段 + 审批进度/历史,供 IM 卡片"查看详情"弹窗使用。
|
||||
*/
|
||||
@@ -41,4 +49,25 @@ public interface IMesXslApprovalHandleService {
|
||||
* 返回 status/statusText/currentNodeId/currentNodeName/canApprove。
|
||||
*/
|
||||
Map<String, Object> statusInfo(String instanceId, LoginUser user);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
/**
|
||||
* 催办:发起人向当前处理人发送一次性催办提醒,同一实例每小时最多催一次。
|
||||
*/
|
||||
Result<String> urge(String instanceId, LoginUser user);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办接口-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表查询-----
|
||||
/**
|
||||
* 查询当前用户的待办审批列表(状态为审批中且当前处理人包含该用户)。
|
||||
*/
|
||||
List<Map<String, Object>> pendingList(LoginUser user);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表查询-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒(调度器调用)-----
|
||||
/**
|
||||
* 扫描超时未处理的审批实例并向当前处理人发送提醒(由定时任务调用)。
|
||||
*/
|
||||
void remindTimeoutInstances();
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒(调度器调用)-----
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.im.service.ISysImChatService;
|
||||
import org.jeecg.modules.system.entity.SysUser;
|
||||
import org.jeecg.modules.system.service.ISysUserService;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionRegistry;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizActionVo;
|
||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalActionHttpExecutor;
|
||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
|
||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackDispatcher;
|
||||
@@ -22,13 +24,19 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】通知改为事务提交后发送,解决处理人卡片不实时置灰-----
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】通知改为事务提交后发送,解决处理人卡片不实时置灰-----
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@@ -48,6 +56,12 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
|
||||
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办频率限制(同一实例1h内只催一次)-----
|
||||
/** 记录最近一次催办时间,key=instanceId,value=毫秒时间戳。重启后清空,可接受 */
|
||||
private final Map<String, Long> urgeTimeMap = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private static final long URGE_MIN_INTERVAL_MS = 60 * 60 * 1000L; // 1小时
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办频率限制(同一实例1h内只催一次)-----
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalFlowService flowService;
|
||||
|
||||
@@ -69,6 +83,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
|
||||
@Autowired
|
||||
private ApprovalActionHttpExecutor actionHttpExecutor;
|
||||
|
||||
// 驳回统一回退:按业务表自动发现 @ApprovalBizAction(onReject) 动作,无需在每个流程节点配置
|
||||
@Autowired
|
||||
private ApprovalBizActionRegistry bizActionRegistry;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】审批与业务单据联动回调-----
|
||||
|
||||
// ==================== 发起后进入首节点 ====================
|
||||
@@ -90,7 +108,13 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
log.error("解析流程设计失败 flowId={}", inst.getFlowId(), e);
|
||||
return;
|
||||
}
|
||||
JSONObject firstApprover = findFirstApprover(root);
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起时快照业务状态原值,供驳回/撤销恢复-----
|
||||
snapshotBizStatus(inst, flow);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起时快照业务状态原值,供驳回/撤销恢复-----
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】发起时读取业务记录用于条件分支评估-----
|
||||
Map<String, String> bizRecord = readBizRecord(inst.getBizTable(), inst.getBizDataId());
|
||||
JSONObject firstApprover = findFirstApprover(root, bizRecord);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】发起时读取业务记录用于条件分支评估-----
|
||||
if (firstApprover == null) {
|
||||
// 无审批节点 -> 自动通过
|
||||
inst.setStatus("1");
|
||||
@@ -168,6 +192,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setNodeProgress(progress.toJSONString());
|
||||
inst.setCurrentHandlersText(pendingNames(tasks) + " 待会签");
|
||||
instanceService.updateById(inst);
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】会签模式:通知其他待处理人有人已通过-----
|
||||
notifyCoApproversProgress(inst, flow, tasks, user, progress.getString("nodeName"));
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】会签模式:通知其他待处理人有人已通过-----
|
||||
return Result.OK("已审批,等待其他会签人处理");
|
||||
}
|
||||
|
||||
@@ -234,18 +261,90 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
|
||||
// 驳回 -> 回调业务(可回退业务状态)
|
||||
callbackDispatcher.fireRejected(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, reason));
|
||||
MesXslApprovalFlow rejectFlow = flowService.getById(inst.getFlowId());
|
||||
JSONObject rejectRoot = rejectFlow == null ? null : safeParse(rejectFlow.getFlowConfig());
|
||||
if (rejectRoot != null) {
|
||||
actionHttpExecutor.run(findNodeById(rejectRoot, progress.getString("nodeId")), "onReject", inst);
|
||||
}
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退(按表注解自动执行,全局一处维护)-----
|
||||
revertBizOnReject(inst);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退(按表注解自动执行,全局一处维护)-----
|
||||
|
||||
String handlerName = oConvertUtils.getString(user.getRealname(), user.getUsername());
|
||||
notifyApplicant(inst, user.getUsername(),
|
||||
"您发起的「" + safeTitle(inst) + "」被 " + handlerName + " 驳回。\n驳回理由:" + reason);
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】驳回通知改为事务提交后发送,发起人/会签人卡片实时置灰-----
|
||||
// 通知(发起人 + 同节点其他会签人)延迟到事务提交后发送:
|
||||
// 否则对方收到WS时驳回事务尚未提交,查 getApprovalStatus 仍是「审批中」,卡片按钮不会实时置灰。
|
||||
final String nodeNameSnapshot = progress.getString("nodeName");
|
||||
final String handlerName = oConvertUtils.getString(user.getRealname(), user.getUsername());
|
||||
runAfterCommit(() -> {
|
||||
notifyCoHandlersOnReject(inst, tasks, user, reason, nodeNameSnapshot);
|
||||
notifyApplicant(inst, user.getUsername(),
|
||||
"您发起的「" + safeTitle(inst) + "」被 " + handlerName + " 驳回。\n驳回理由:" + reason);
|
||||
});
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】驳回通知改为事务提交后发送,发起人/会签人卡片实时置灰-----
|
||||
return Result.OK("已驳回");
|
||||
}
|
||||
|
||||
// ==================== 撤销(发起人撤回) ====================
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销并恢复业务单据到发起时状态-----
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<String> cancel(String instanceId, String reason, LoginUser user) {
|
||||
MesXslApprovalInstance inst = instanceService.getById(instanceId);
|
||||
if (inst == null) {
|
||||
return Result.error("审批实例不存在");
|
||||
}
|
||||
if (!"0".equals(inst.getStatus())) {
|
||||
return Result.error("该审批已结束,无法撤销");
|
||||
}
|
||||
// 仅发起人本人可撤销
|
||||
if (user == null || !user.getUsername().equals(inst.getApplyUser())) {
|
||||
return Result.error("只有发起人本人可以撤销该审批");
|
||||
}
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销仅允许单据仍处于发起前(最初)状态时进行-----
|
||||
// 撤销 = 撤回整个审批流,只能在单据还是发起前最初状态时执行;
|
||||
// 一旦下游节点已确认并推进了业务状态(current != 发起时快照originStatus),则不允许撤销。
|
||||
String statusField = inst.getStatusField();
|
||||
if (oConvertUtils.isNotEmpty(statusField) && inst.getOriginStatus() != null) {
|
||||
String currentBizStatus = readBizFieldValue(inst.getBizTable(), inst.getBizDataId(), statusField);
|
||||
if (!Objects.equals(inst.getOriginStatus(), currentBizStatus)) {
|
||||
return Result.error("审批已进入下一环节并被处理,无法撤销");
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销仅允许单据仍处于发起前(最初)状态时进行-----
|
||||
String comment = oConvertUtils.getString(reason, "发起人撤销");
|
||||
appendHistory(inst, inst.getCurrentNodeId(), inst.getCurrentNodeName(), user, "cancel", comment);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销通知改为事务提交后发送,处理人卡片实时置灰-----
|
||||
// 先快照当前处理人(下面会清空)与通知所需信息,通知延迟到事务提交后发送:
|
||||
// 否则处理人收到WS时撤销事务尚未提交,查 getApprovalStatus 仍是「审批中」,卡片按钮不会实时置灰。
|
||||
final String handlersSnapshot = inst.getCurrentHandlers();
|
||||
final String applicantName = oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser());
|
||||
final String bizTitleSnapshot = safeTitle(inst);
|
||||
final String fromUserId = user.getId();
|
||||
final Integer tenantId = inst.getTenantId();
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销通知改为事务提交后发送,处理人卡片实时置灰-----
|
||||
|
||||
inst.setStatus("3");
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("已撤销");
|
||||
instanceService.updateById(inst);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销改为恢复发起前快照,不再走业务「拒绝/驳回」接口-----
|
||||
// 撤销时单据仍处于发起前最初状态,只需把状态字段恢复到发起时快照(幂等),
|
||||
// 不能调用业务 onReject「拒绝」接口——该接口语义是从更高环节往回退,单据已是最初状态时会报「无需拒绝」导致撤销失败。
|
||||
restoreBizStatus(inst);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销改为恢复发起前快照,不再走业务「拒绝/驳回」接口-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销通知改为事务提交后发送,处理人卡片实时置灰-----
|
||||
runAfterCommit(() -> {
|
||||
if (oConvertUtils.isNotEmpty(handlersSnapshot)) {
|
||||
for (String handler : handlersSnapshot.split(",")) {
|
||||
sendOne(fromUserId, handler, tenantId,
|
||||
"您待办的「" + bizTitleSnapshot + "」已被发起人 " + applicantName + " 撤销,无需处理。", "text");
|
||||
}
|
||||
}
|
||||
});
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销通知改为事务提交后发送,处理人卡片实时置灰-----
|
||||
return Result.OK("已撤销");
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销并恢复业务单据到发起时状态-----
|
||||
|
||||
// ==================== 查看详情 ====================
|
||||
|
||||
@Override
|
||||
@@ -326,7 +425,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
instanceService.updateById(inst);
|
||||
// 流程终止(等同驳回) -> 回调业务(可回退业务状态)
|
||||
callbackDispatcher.fireRejected(buildContext(inst, nodeId, nodeName, null, "审批人为空,流程终止"));
|
||||
actionHttpExecutor.run(node, "onReject", inst);
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】流程终止:驳回统一回退(按表注解自动执行)-----
|
||||
revertBizOnReject(inst);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】流程终止:驳回统一回退(按表注解自动执行)-----
|
||||
notifyApplicant(inst, null, "您发起的「" + safeTitle(inst) + "」在「" + nodeName + "」节点无处理人,流程已终止。");
|
||||
return;
|
||||
}
|
||||
@@ -354,6 +455,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
progress.put("nodeName", nodeName);
|
||||
progress.put("mode", mode);
|
||||
progress.put("tasks", tasks);
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】记录进入节点时间,供超时提醒计算-----
|
||||
progress.put("enterTime", now());
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】记录进入节点时间,供超时提醒计算-----
|
||||
|
||||
// 活动处理人:依次审批仅首个,其余全部
|
||||
List<String> activeUsers = new ArrayList<>();
|
||||
@@ -378,10 +482,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
sendApprovalCard(inst, flow, nodeName, activeUsers);
|
||||
}
|
||||
|
||||
/** 当前节点完成后,查找下一审批节点并推进;无则置为通过并通知发起人。 */
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支路由替换原始线性flatten-----
|
||||
/** 当前节点完成后,按条件分支路由查找下一审批节点并推进;无则置为通过并通知发起人。 */
|
||||
private void advanceAfter(MesXslApprovalInstance inst, MesXslApprovalFlow flow, JSONObject root, String currentNodeId, LoginUser actingUser) {
|
||||
List<JSONObject> chain = new ArrayList<>();
|
||||
flatten(root, chain);
|
||||
Map<String, String> bizRecord = readBizRecord(inst.getBizTable(), inst.getBizDataId());
|
||||
List<JSONObject> chain = buildExecSequence(root, bizRecord);
|
||||
int idx = -1;
|
||||
for (int i = 0; i < chain.size(); i++) {
|
||||
if (currentNodeId != null && currentNodeId.equals(chain.get(i).getString("id"))) {
|
||||
@@ -401,6 +506,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
}
|
||||
}
|
||||
if (nextApprover != null) {
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】节点流转时通知发起人进度-----
|
||||
notifyApplicant(inst, actingUser == null ? null : actingUser.getUsername(),
|
||||
"您发起的「" + safeTitle(inst) + "」已进入下一审批节点「" + nextApprover.getString("name") + "」,请关注进展。");
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】节点流转时通知发起人进度-----
|
||||
enterNode(inst, flow, root, nextApprover);
|
||||
} else {
|
||||
inst.setStatus("1");
|
||||
@@ -414,6 +523,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
notifyApplicant(inst, actor, "您发起的「" + safeTitle(inst) + "」审批已全部通过。");
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支路由替换原始线性flatten-----
|
||||
|
||||
// ==================== 处理人解析 ====================
|
||||
|
||||
@@ -573,7 +683,9 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
String content;
|
||||
String msgType;
|
||||
if (oConvertUtils.isNotEmpty(routePath)) {
|
||||
content = buildCardJson(inst, null, false, routePath);
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送卡片不带办理按钮,抄送人无需审核-----
|
||||
content = buildCardJson(inst, null, false, routePath, false);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送卡片不带办理按钮,抄送人无需审核-----
|
||||
msgType = "biz_record";
|
||||
} else {
|
||||
content = String.format("【抄送通知】%s 发起的「%s」抄送给您。",
|
||||
@@ -620,11 +732,22 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
|
||||
/** 构建 biz_record 卡片 JSON(含 instanceId / actionLabel / canApprove,与前端 ImBizRecordPayload 对齐 v=2) */
|
||||
private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath) {
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片-----
|
||||
return buildCardJson(inst, actionLabel, canApprove, routePath, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param approvalCard true=审批卡片(带 instanceId/办理按钮);false=抄送通知卡片(仅展示+定位链接,无办理按钮)
|
||||
*/
|
||||
private String buildCardJson(MesXslApprovalInstance inst, String actionLabel, boolean canApprove, String routePath, boolean approvalCard) {
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】区分审批卡片与抄送通知卡片-----
|
||||
JSONArray fields = new JSONArray();
|
||||
addField(fields, "审批流", inst.getFlowName());
|
||||
addField(fields, "单据", safeTitle(inst));
|
||||
addField(fields, "发起人", oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser()));
|
||||
addField(fields, "当前节点", oConvertUtils.getString(inst.getCurrentNodeName(), "审批"));
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送卡片标注「抄送知会」-----
|
||||
addField(fields, approvalCard ? "当前节点" : "知会", oConvertUtils.getString(inst.getCurrentNodeName(), approvalCard ? "审批" : "抄送"));
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】抄送卡片标注「抄送知会」-----
|
||||
addField(fields, "状态", statusText(inst.getStatus()));
|
||||
|
||||
JSONObject item = new JSONObject();
|
||||
@@ -641,14 +764,18 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
item.put("body", body.toString());
|
||||
String sep = routePath.contains("?") ? "&" : "?";
|
||||
item.put("linkPath", routePath + sep + "imRecordId=" + inst.getBizDataId());
|
||||
// 审批扩展字段
|
||||
item.put("instanceId", inst.getId());
|
||||
item.put("canApprove", canApprove);
|
||||
// 该卡片对应的节点ID(用于前端实时校验是否仍为当前节点,旧节点卡片自动置灰)
|
||||
item.put("nodeId", inst.getCurrentNodeId());
|
||||
if (oConvertUtils.isNotEmpty(actionLabel)) {
|
||||
item.put("actionLabel", actionLabel);
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】仅审批卡片带办理上下文,抄送卡片不可办理-----
|
||||
if (approvalCard) {
|
||||
// 审批扩展字段
|
||||
item.put("instanceId", inst.getId());
|
||||
item.put("canApprove", canApprove);
|
||||
// 该卡片对应的节点ID(用于前端实时校验是否仍为当前节点,旧节点卡片自动置灰)
|
||||
item.put("nodeId", inst.getCurrentNodeId());
|
||||
if (oConvertUtils.isNotEmpty(actionLabel)) {
|
||||
item.put("actionLabel", actionLabel);
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】仅审批卡片带办理上下文,抄送卡片不可办理-----
|
||||
|
||||
JSONArray items = new JSONArray();
|
||||
items.add(item);
|
||||
@@ -668,6 +795,111 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
fields.add(f);
|
||||
}
|
||||
|
||||
// ==================== 业务状态快照/恢复 ====================
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
/**
|
||||
* 发起审批时快照业务单据状态字段原值,写入实例。
|
||||
* 状态字段名来源:审批流配置 status_field 优先,未配置时自动探测单据表是否存在 status 列。
|
||||
*/
|
||||
private void snapshotBizStatus(MesXslApprovalInstance inst, MesXslApprovalFlow flow) {
|
||||
try {
|
||||
String statusField = flow == null ? null : flow.getStatusField();
|
||||
if (oConvertUtils.isEmpty(statusField) && hasColumn(inst.getBizTable(), "status")) {
|
||||
statusField = "status";
|
||||
}
|
||||
if (oConvertUtils.isEmpty(statusField)) {
|
||||
return;
|
||||
}
|
||||
inst.setStatusField(statusField);
|
||||
inst.setOriginStatus(readBizFieldValue(inst.getBizTable(), inst.getBizDataId(), statusField));
|
||||
} catch (Exception e) {
|
||||
log.warn("快照业务状态失败 table={}, id={}", inst.getBizTable(), inst.getBizDataId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回/撤销/终止统一回退入口(全局一处维护,所有流程通用):
|
||||
* 按业务表自动发现标注了 {@code @ApprovalBizAction(phase=onReject)} 的业务接口并调用,
|
||||
* 由业务接口全权负责回退(本单据状态 + 关联单据级联),无需在每个流程节点手动配置「驳回时执行」。
|
||||
* 未标注 onReject 业务动作的单据,退化为通用「恢复到发起时状态」。
|
||||
*/
|
||||
private void revertBizOnReject(MesXslApprovalInstance inst) {
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】单据仍在发起前状态时驳回:跳过业务「拒绝」接口,避免「无需拒绝」报错-----
|
||||
// 若单据当前状态仍等于发起前快照(说明尚未被任何节点审批推进,也没有级联变更),
|
||||
// 则无需调用业务 onReject「拒绝」接口(其语义是从更高环节往回退,单据已是最初状态会报「无需拒绝」),
|
||||
// 直接恢复快照(幂等)即可。常见于在第一个审批节点(如校对)就驳回的场景。
|
||||
if (isBizAtOriginStatus(inst)) {
|
||||
restoreBizStatus(inst);
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】单据仍在发起前状态时驳回:跳过业务「拒绝」接口,避免「无需拒绝」报错-----
|
||||
List<ApprovalBizActionVo> rejectActions = bizActionRegistry.getByTableAndPhase(inst.getBizTable(), "onReject");
|
||||
if (rejectActions != null && !rejectActions.isEmpty()) {
|
||||
for (ApprovalBizActionVo action : rejectActions) {
|
||||
actionHttpExecutor.runByUrl(action.getMethod(), action.getUrl(), inst);
|
||||
}
|
||||
} else {
|
||||
restoreBizStatus(inst);
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】判断业务单据是否仍处于发起前(最初)状态-----
|
||||
/**
|
||||
* 判断业务单据当前状态字段是否仍等于发起审批时的快照原值。
|
||||
* true 表示单据尚未被任何节点推进(仍是最初状态);无快照字段时返回 false(交由正常业务回退处理)。
|
||||
*/
|
||||
private boolean isBizAtOriginStatus(MesXslApprovalInstance inst) {
|
||||
String statusField = inst.getStatusField();
|
||||
if (oConvertUtils.isEmpty(statusField) || inst.getOriginStatus() == null) {
|
||||
return false;
|
||||
}
|
||||
String current = readBizFieldValue(inst.getBizTable(), inst.getBizDataId(), statusField);
|
||||
return Objects.equals(inst.getOriginStatus(), current);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】判断业务单据是否仍处于发起前(最初)状态-----
|
||||
|
||||
/**
|
||||
* 驳回/撤销/终止时,将业务单据状态字段直接回写为发起时原值。
|
||||
* 直接 UPDATE,不触发业务级联同步,确保单据回到“可重新提交”的初始态。
|
||||
*/
|
||||
private void restoreBizStatus(MesXslApprovalInstance inst) {
|
||||
String table = inst.getBizTable();
|
||||
String field = inst.getStatusField();
|
||||
String bizDataId = inst.getBizDataId();
|
||||
String origin = inst.getOriginStatus();
|
||||
if (oConvertUtils.isEmpty(table) || oConvertUtils.isEmpty(field)
|
||||
|| oConvertUtils.isEmpty(bizDataId) || origin == null) {
|
||||
return;
|
||||
}
|
||||
if (!IDENTIFIER.matcher(table).matches() || !IDENTIFIER.matcher(field).matches()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
jdbcTemplate.update("UPDATE " + table + " SET " + field + " = ? WHERE id = ?", origin, bizDataId);
|
||||
log.info("审批结束恢复业务状态 table={}, id={}, {}={}", table, bizDataId, field, origin);
|
||||
} catch (Exception e) {
|
||||
log.warn("恢复业务状态失败 table={}, id={}, field={}", table, bizDataId, field, e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 判断业务表是否存在指定列 */
|
||||
private boolean hasColumn(String table, String column) {
|
||||
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Integer cnt = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(1) FROM information_schema.columns "
|
||||
+ "WHERE table_schema = (SELECT DATABASE()) AND table_name = ? AND column_name = ?",
|
||||
Integer.class, table, column);
|
||||
return cnt != null && cnt > 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回/撤销恢复初始状态-----
|
||||
|
||||
// ==================== 业务表读取 ====================
|
||||
|
||||
/** 读取单据指定字段值 */
|
||||
@@ -768,29 +1000,146 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 流程树遍历 ====================
|
||||
// ==================== 流程树遍历(含条件分支评估) ====================
|
||||
|
||||
/** 沿流程树查找第一个审批节点(遇条件分支取首个分支后续) */
|
||||
private JSONObject findFirstApprover(JSONObject node) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
if ("approver".equals(node.getString("type"))) {
|
||||
return node;
|
||||
}
|
||||
JSONArray branches = node.getJSONArray("conditionNodes");
|
||||
if (branches != null) {
|
||||
for (int i = 0; i < branches.size(); i++) {
|
||||
JSONObject found = findFirstApprover(branches.getJSONObject(i));
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return findFirstApprover(node.getJSONObject("childNode"));
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支评估引擎-----
|
||||
|
||||
/**
|
||||
* 构建考虑条件分支后的执行序列(approver/cc 节点按执行顺序排列)。
|
||||
* 遇到 branch 节点时评估条件,选择唯一匹配的分支路径;无匹配则走最后一个分支(默认分支)。
|
||||
*/
|
||||
private List<JSONObject> buildExecSequence(JSONObject root, Map<String, String> bizRecord) {
|
||||
List<JSONObject> seq = new ArrayList<>();
|
||||
buildExecSeq(root, seq, bizRecord);
|
||||
return seq;
|
||||
}
|
||||
|
||||
/** 按节点ID查找节点(含条件分支与子节点) */
|
||||
private void buildExecSeq(JSONObject node, List<JSONObject> out, Map<String, String> bizRecord) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
String type = node.getString("type");
|
||||
if ("approver".equals(type) || "cc".equals(type)) {
|
||||
out.add(node);
|
||||
}
|
||||
if ("branch".equals(type)) {
|
||||
// 条件分支容器:评估后选唯一路径,之后继续 childNode
|
||||
JSONObject chosen = selectBranch(node, bizRecord);
|
||||
if (chosen != null) {
|
||||
buildExecSeq(chosen.getJSONObject("childNode"), out, bizRecord);
|
||||
}
|
||||
buildExecSeq(node.getJSONObject("childNode"), out, bizRecord);
|
||||
return;
|
||||
}
|
||||
buildExecSeq(node.getJSONObject("childNode"), out, bizRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 branch 节点的 conditionNodes 中选出第一个满足条件的分支;
|
||||
* 无匹配时返回最后一个分支(视为"否则"默认分支)。
|
||||
*/
|
||||
private JSONObject selectBranch(JSONObject branchNode, Map<String, String> bizRecord) {
|
||||
JSONArray condNodes = branchNode.getJSONArray("conditionNodes");
|
||||
if (condNodes == null || condNodes.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
JSONObject defaultBranch = condNodes.getJSONObject(condNodes.size() - 1); // 最后一个作为默认
|
||||
for (int i = 0; i < condNodes.size() - 1; i++) { // 跳过最后一个(默认)
|
||||
JSONObject branch = condNodes.getJSONObject(i);
|
||||
JSONObject props = branch.getJSONObject("props");
|
||||
if (props == null) {
|
||||
continue;
|
||||
}
|
||||
JSONArray conditions = props.getJSONArray("conditions");
|
||||
if (conditions == null || conditions.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
boolean isAnd = "and".equalsIgnoreCase(oConvertUtils.getString(props.getString("conditionType"), "and"));
|
||||
if (evaluateConditions(conditions, bizRecord, isAnd)) {
|
||||
return branch;
|
||||
}
|
||||
}
|
||||
return defaultBranch;
|
||||
}
|
||||
|
||||
private boolean evaluateConditions(JSONArray conditions, Map<String, String> bizRecord, boolean isAnd) {
|
||||
if (conditions == null || conditions.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < conditions.size(); i++) {
|
||||
JSONObject cond = conditions.getJSONObject(i);
|
||||
String field = cond.getString("field");
|
||||
String operator = oConvertUtils.getString(cond.getString("operator"), "eq");
|
||||
String expected = cond.getString("value");
|
||||
String actual = bizRecord == null ? null : bizRecord.get(field);
|
||||
boolean match = evaluateCondition(actual, operator, expected);
|
||||
if (isAnd && !match) {
|
||||
return false;
|
||||
}
|
||||
if (!isAnd && match) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return isAnd; // and:全部通过;or:没有一个通过
|
||||
}
|
||||
|
||||
private boolean evaluateCondition(String actual, String operator, String expected) {
|
||||
switch (operator) {
|
||||
case "empty": return actual == null || actual.trim().isEmpty();
|
||||
case "notEmpty": return actual != null && !actual.trim().isEmpty();
|
||||
case "eq": return Objects.equals(actual, expected);
|
||||
case "ne": return !Objects.equals(actual, expected);
|
||||
case "contains": return actual != null && expected != null && actual.contains(expected);
|
||||
case "gt": case "gte": case "lt": case "lte":
|
||||
try {
|
||||
double a = Double.parseDouble(actual == null ? "" : actual.trim());
|
||||
double e = Double.parseDouble(expected == null ? "" : expected.trim());
|
||||
if ("gt".equals(operator)) return a > e;
|
||||
if ("gte".equals(operator)) return a >= e;
|
||||
if ("lt".equals(operator)) return a < e;
|
||||
return a <= e;
|
||||
} catch (NumberFormatException ex) {
|
||||
return false;
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取业务单据全部字段为 Map,用于条件分支评估 */
|
||||
private Map<String, String> readBizRecord(String table, String bizDataId) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
if (oConvertUtils.isEmpty(table) || oConvertUtils.isEmpty(bizDataId) || !IDENTIFIER.matcher(table).matches()) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
"SELECT * FROM " + table + " WHERE id = ? LIMIT 1", bizDataId);
|
||||
if (rows.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
for (Map.Entry<String, Object> e : rows.get(0).entrySet()) {
|
||||
result.put(e.getKey(), e.getValue() == null ? null : String.valueOf(e.getValue()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("读取业务单据失败(条件分支) table={}, id={}", table, bizDataId, e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 沿执行路径查找第一个审批节点(考虑条件分支) */
|
||||
private JSONObject findFirstApprover(JSONObject root, Map<String, String> bizRecord) {
|
||||
List<JSONObject> seq = buildExecSequence(root, bizRecord);
|
||||
for (JSONObject n : seq) {
|
||||
if ("approver".equals(n.getString("type"))) {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】条件分支评估引擎-----
|
||||
|
||||
/** 按节点ID查找节点(含条件分支与子节点,全树搜索) */
|
||||
private JSONObject findNodeById(JSONObject node, String nodeId) {
|
||||
if (node == null || oConvertUtils.isEmpty(nodeId)) {
|
||||
return null;
|
||||
@@ -810,24 +1159,6 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
return findNodeById(node.getJSONObject("childNode"), nodeId);
|
||||
}
|
||||
|
||||
/** 前序遍历收集审批人/抄送节点(条件分支按顺序展开) */
|
||||
private void flatten(JSONObject node, List<JSONObject> out) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
String type = node.getString("type");
|
||||
if ("approver".equals(type) || "cc".equals(type)) {
|
||||
out.add(node);
|
||||
}
|
||||
JSONArray branches = node.getJSONArray("conditionNodes");
|
||||
if (branches != null) {
|
||||
for (int i = 0; i < branches.size(); i++) {
|
||||
flatten(branches.getJSONObject(i), out);
|
||||
}
|
||||
}
|
||||
flatten(node.getJSONObject("childNode"), out);
|
||||
}
|
||||
|
||||
// ==================== 进度/历史辅助 ====================
|
||||
|
||||
private JSONObject parseProgress(MesXslApprovalInstance inst) {
|
||||
@@ -944,7 +1275,10 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
JSONObject v = new JSONObject();
|
||||
v.put("nodeName", h.getString("nodeName"));
|
||||
v.put("name", h.getString("name"));
|
||||
v.put("actionText", "approve".equals(h.getString("action")) ? "通过" : "驳回");
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】历史动作文案支持撤销-----
|
||||
String act = h.getString("action");
|
||||
v.put("actionText", "approve".equals(act) ? "通过" : ("cancel".equals(act) ? "撤销" : "驳回"));
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】历史动作文案支持撤销-----
|
||||
v.put("comment", h.getString("comment"));
|
||||
v.put("time", h.getString("time"));
|
||||
out.add(v);
|
||||
@@ -994,6 +1328,33 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】事务提交后执行(用于IM通知,避免WS先于事务提交导致前端查到旧状态)-----
|
||||
/**
|
||||
* 在当前事务提交成功后执行任务(如发送IM/WS通知)。
|
||||
* 若当前无活动事务,则立即执行。
|
||||
* 用于解决「通知WS先于业务事务提交到达,前端查到旧状态」的时序问题。
|
||||
*/
|
||||
private void runAfterCommit(Runnable task) {
|
||||
if (task == null) {
|
||||
return;
|
||||
}
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
try {
|
||||
task.run();
|
||||
} catch (Exception e) {
|
||||
log.warn("事务提交后通知任务执行失败", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
task.run();
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】事务提交后执行(用于IM通知,避免WS先于事务提交导致前端查到旧状态)-----
|
||||
|
||||
private List<String> singletonList(String s) {
|
||||
List<String> l = new ArrayList<>();
|
||||
l.add(s);
|
||||
@@ -1020,4 +1381,194 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
private String now() {
|
||||
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
|
||||
}
|
||||
|
||||
// ==================== 催办 ====================
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办:发起人向当前处理人发催办提醒-----
|
||||
@Override
|
||||
public Result<String> urge(String instanceId, LoginUser user) {
|
||||
MesXslApprovalInstance inst = instanceService.getById(instanceId);
|
||||
if (inst == null) {
|
||||
return Result.error("审批实例不存在");
|
||||
}
|
||||
if (!"0".equals(inst.getStatus())) {
|
||||
return Result.error("该审批已结束,无需催办");
|
||||
}
|
||||
if (user == null || !user.getUsername().equals(inst.getApplyUser())) {
|
||||
return Result.error("只有发起人才可以催办");
|
||||
}
|
||||
Long lastUrge = urgeTimeMap.get(instanceId);
|
||||
if (lastUrge != null && System.currentTimeMillis() - lastUrge < URGE_MIN_INTERVAL_MS) {
|
||||
return Result.error("催办过于频繁,请1小时后再试");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(inst.getCurrentHandlers())) {
|
||||
return Result.error("当前无待处理人");
|
||||
}
|
||||
String applicantName = oConvertUtils.getString(inst.getApplyUserName(), inst.getApplyUser());
|
||||
for (String handler : inst.getCurrentHandlers().split(",")) {
|
||||
String uname = handler == null ? "" : handler.trim();
|
||||
if (oConvertUtils.isEmpty(uname)) {
|
||||
continue;
|
||||
}
|
||||
sendOne(user.getId(), uname, inst.getTenantId(),
|
||||
"【催办提醒】" + applicantName + " 催促您处理「" + safeTitle(inst) + "」,请尽快审批。", "text");
|
||||
}
|
||||
urgeTimeMap.put(instanceId, System.currentTimeMillis());
|
||||
return Result.OK("催办已发送");
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】催办:发起人向当前处理人发催办提醒-----
|
||||
|
||||
// ==================== 待办列表 ====================
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表:查询当前用户的待处理审批实例-----
|
||||
@Override
|
||||
public List<Map<String, Object>> pendingList(LoginUser user) {
|
||||
if (user == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
String sql = "SELECT id, flow_name, biz_table_name, biz_title, current_node_name, "
|
||||
+ "apply_user_name, apply_time, current_handlers_text "
|
||||
+ "FROM mes_xsl_approval_instance "
|
||||
+ "WHERE status = '0' AND del_flag = 0 "
|
||||
+ "AND FIND_IN_SET(?, current_handlers) "
|
||||
+ "ORDER BY apply_time DESC LIMIT 200";
|
||||
try {
|
||||
return jdbcTemplate.queryForList(sql, user.getUsername());
|
||||
} catch (Exception e) {
|
||||
log.warn("查询待办列表失败 user={}", user.getUsername(), e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】待办列表:查询当前用户的待处理审批实例-----
|
||||
|
||||
// ==================== 超时提醒(调度器调用) ====================
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒:扫描超时实例并通知处理人-----
|
||||
@Override
|
||||
public void remindTimeoutInstances() {
|
||||
// 查询所有审批中的实例,联查审批流的超时配置
|
||||
String sql = "SELECT i.id, i.flow_id, i.biz_title, i.current_handlers, i.node_progress, "
|
||||
+ "i.tenant_id, i.apply_user, i.apply_user_name, i.flow_name, "
|
||||
+ "COALESCE(f.timeout_hours, 24) AS timeout_hours "
|
||||
+ "FROM mes_xsl_approval_instance i "
|
||||
+ "LEFT JOIN mes_xsl_approval_flow f ON f.id = i.flow_id "
|
||||
+ "WHERE i.status = '0' AND i.del_flag = 0 AND COALESCE(f.timeout_hours, 24) > 0";
|
||||
List<Map<String, Object>> rows;
|
||||
try {
|
||||
rows = jdbcTemplate.queryForList(sql);
|
||||
} catch (Exception e) {
|
||||
log.warn("超时提醒扫描失败", e);
|
||||
return;
|
||||
}
|
||||
for (Map<String, Object> row : rows) {
|
||||
try {
|
||||
processTimeoutRow(row);
|
||||
} catch (Exception e) {
|
||||
log.warn("处理超时实例失败 id={}", row.get("id"), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processTimeoutRow(Map<String, Object> row) {
|
||||
String nodeProgress = (String) row.get("node_progress");
|
||||
if (oConvertUtils.isEmpty(nodeProgress)) {
|
||||
return;
|
||||
}
|
||||
JSONObject progress = safeParse(nodeProgress);
|
||||
if (progress == null) {
|
||||
return;
|
||||
}
|
||||
String enterTimeStr = progress.getString("enterTime");
|
||||
if (oConvertUtils.isEmpty(enterTimeStr)) {
|
||||
return;
|
||||
}
|
||||
Date enterTime;
|
||||
try {
|
||||
enterTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(enterTimeStr);
|
||||
} catch (Exception e) {
|
||||
return;
|
||||
}
|
||||
int timeoutHours = row.get("timeout_hours") == null ? 24 : ((Number) row.get("timeout_hours")).intValue();
|
||||
long elapsedHours = (System.currentTimeMillis() - enterTime.getTime()) / (1000 * 60 * 60);
|
||||
if (elapsedHours < timeoutHours) {
|
||||
return;
|
||||
}
|
||||
// 已超时:通知当前处理人
|
||||
String currentHandlers = (String) row.get("current_handlers");
|
||||
if (oConvertUtils.isEmpty(currentHandlers)) {
|
||||
return;
|
||||
}
|
||||
String instanceId = (String) row.get("id");
|
||||
// 避免重复通知:每个超时周期内只通知一次
|
||||
String reminderKey = instanceId + "_timeout";
|
||||
Long lastRemind = urgeTimeMap.get(reminderKey);
|
||||
if (lastRemind != null && System.currentTimeMillis() - lastRemind < timeoutHours * 3600_000L) {
|
||||
return;
|
||||
}
|
||||
String bizTitle = oConvertUtils.getString((String) row.get("biz_title"), instanceId);
|
||||
String flowName = oConvertUtils.getString((String) row.get("flow_name"), "");
|
||||
String applyUserName = oConvertUtils.getString((String) row.get("apply_user_name"), "");
|
||||
Integer tenantId = row.get("tenant_id") == null ? null : ((Number) row.get("tenant_id")).intValue();
|
||||
String msg = String.format("【超时提醒】您有一条审批待处理已超过%d小时,请尽快处理!\n审批流:%s\n单据:%s\n发起人:%s",
|
||||
elapsedHours, flowName, bizTitle, applyUserName);
|
||||
String applyUser = (String) row.get("apply_user");
|
||||
SysUser fromUser = getUserSafely(applyUser);
|
||||
String fromId = fromUser == null ? null : fromUser.getId();
|
||||
for (String handler : currentHandlers.split(",")) {
|
||||
String uname = handler == null ? "" : handler.trim();
|
||||
if (oConvertUtils.isEmpty(uname)) {
|
||||
continue;
|
||||
}
|
||||
sendOne(fromId, uname, tenantId, msg, "text");
|
||||
}
|
||||
urgeTimeMap.put(reminderKey, System.currentTimeMillis());
|
||||
log.info("审批超时提醒已发送 instanceId={}, elapsedHours={}", instanceId, elapsedHours);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】超时提醒:扫描超时实例并通知处理人-----
|
||||
|
||||
// ==================== 辅助通知方法 ====================
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】全链路通知补全-----
|
||||
/** 会签模式:某人通过后通知其他仍待处理的处理人 */
|
||||
private void notifyCoApproversProgress(MesXslApprovalInstance inst, MesXslApprovalFlow flow,
|
||||
JSONArray tasks, LoginUser actingUser, String nodeName) {
|
||||
String actorName = actingUser == null ? "某人"
|
||||
: oConvertUtils.getString(actingUser.getRealname(), actingUser.getUsername());
|
||||
String msg = "【会签进度】" + actorName + " 已通过「" + safeTitle(inst) + "」的「"
|
||||
+ oConvertUtils.getString(nodeName, "审批") + "」节点,等待您审批。";
|
||||
SysUser from = actingUser == null ? null : getUserSafely(actingUser.getUsername());
|
||||
String fromId = from == null ? null : from.getId();
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
JSONObject t = tasks.getJSONObject(i);
|
||||
if ("pending".equals(t.getString("status"))
|
||||
&& (actingUser == null || !actingUser.getUsername().equals(t.getString("username")))) {
|
||||
sendOne(fromId, t.getString("username"), inst.getTenantId(), msg, "text");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 驳回时通知同节点其他待处理人(无需处理,流程已结束) */
|
||||
private void notifyCoHandlersOnReject(MesXslApprovalInstance inst,
|
||||
JSONArray tasks, LoginUser actingUser, String reason, String nodeName) {
|
||||
if (tasks == null) {
|
||||
return;
|
||||
}
|
||||
String actorName = actingUser == null ? "某人"
|
||||
: oConvertUtils.getString(actingUser.getRealname(), actingUser.getUsername());
|
||||
String msg = "【审批驳回】" + actorName + " 驳回了「" + safeTitle(inst) + "」,流程已终止,无需您继续处理。\n驳回理由:" + reason;
|
||||
SysUser from = actingUser == null ? null : getUserSafely(actingUser.getUsername());
|
||||
String fromId = from == null ? null : from.getId();
|
||||
for (int i = 0; i < tasks.size(); i++) {
|
||||
JSONObject t = tasks.getJSONObject(i);
|
||||
String uname = t.getString("username");
|
||||
if (oConvertUtils.isEmpty(uname)) {
|
||||
continue;
|
||||
}
|
||||
if (actingUser != null && actingUser.getUsername().equals(uname)) {
|
||||
continue;
|
||||
}
|
||||
sendOne(fromId, uname, inst.getTenantId(), msg, "text");
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】全链路通知补全-----
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.xslmes.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* 启用 Spring @Scheduled,用于审批超时提醒等定时任务。
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-05-29 for:【QH-MES审批流完善】启用定时任务支持
|
||||
*/
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】启用定时任务支持-----
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class XslMesSchedulingConfig {
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】启用定时任务支持-----
|
||||
@@ -22,6 +22,7 @@ import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.system.entity.SysDepart;
|
||||
import org.jeecg.modules.system.service.ISysDepartService;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizAction;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMixerPsCompile;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslMixerPsCompileService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -168,6 +169,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
}
|
||||
|
||||
//update-begin---author:jiangxh ---date:20260520 for:【密炼PS编制】校对/审核/批准-----------
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "校对", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 1)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-校对")
|
||||
@Operation(summary = "MES密炼PS编制-校对")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:proofread")
|
||||
@@ -176,6 +180,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return doChangeStatus(ids, "compile", "proofread", "校对");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "审核", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 2)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-审核")
|
||||
@Operation(summary = "MES密炼PS编制-审核")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:audit")
|
||||
@@ -184,6 +191,9 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return doChangeStatus(ids, "proofread", "audit", "审核");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "批准", table = "mes_xsl_mixer_ps_compile", phase = {"onNodeApprove", "onApprove"}, order = 3)
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES密炼PS编制-批准")
|
||||
@Operation(summary = "MES密炼PS编制-批准")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:approve")
|
||||
@@ -201,6 +211,34 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
return Result.OK(actionLabel + "成功!");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
@ApprovalBizAction(name = "拒绝", table = "mes_xsl_mixer_ps_compile", phase = {"onReject"}, order = 4)
|
||||
@AutoLog(value = "MES密炼PS编制-拒绝")
|
||||
@Operation(summary = "MES密炼PS编制-拒绝(一步退回编制并撤销关联操作)")
|
||||
// 拒绝主要由审批流 onReject 回调触发(执行人为当前审批节点处理人),不强制要求其具备密炼PS业务权限码;
|
||||
// 是否允许驳回已由审批节点本身控制,故此处不加 @RequiresPermissions。
|
||||
@PostMapping(value = "/reject")
|
||||
public Result<String> reject(@RequestParam(name = "ids") String ids) {
|
||||
String err = mesXslMixerPsCompileService.rejectBatch(ids, getOperatorName());
|
||||
if (err != null) {
|
||||
return Result.error(err);
|
||||
}
|
||||
return Result.OK("拒绝成功,已退回编制状态!");
|
||||
}
|
||||
|
||||
@AutoLog(value = "MES密炼PS编制-撤回")
|
||||
@Operation(summary = "MES密炼PS编制-撤回(退回上一环节并撤销关联操作)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_mixer_ps_compile:withdraw")
|
||||
@PostMapping(value = "/withdraw")
|
||||
public Result<String> withdraw(@RequestParam(name = "ids") String ids) {
|
||||
String err = mesXslMixerPsCompileService.withdrawBatch(ids, getOperatorName());
|
||||
if (err != null) {
|
||||
return Result.error(err);
|
||||
}
|
||||
return Result.OK("撤回成功!");
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
|
||||
private String getOperatorName() {
|
||||
LoginUser loginUser = null;
|
||||
try {
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizAction;
|
||||
import org.jeecg.modules.mes.material.entity.MesMaterial;
|
||||
import org.jeecg.modules.mes.material.service.IMesMaterialService;
|
||||
import org.jeecg.modules.system.entity.SysDepart;
|
||||
@@ -162,6 +163,9 @@ public class MesXslRubberQuickTestStdController
|
||||
return Result.OK(mesXslRubberQuickTestStdService.selectLinesByStdId(id));
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@ApprovalBizAction(name = "启用/停用", table = "mes_xsl_rubber_quick_test_std", phase = {"onApprove", "onReject"})
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】标注为审批可选回调动作-----
|
||||
@AutoLog(value = "MES胶料快检实验标准-启用/停用")
|
||||
@Operation(summary = "MES胶料快检实验标准-启用/停用(字典 xslmes_rubber_quick_test_std_enable_status:1使用中 0已停用)")
|
||||
@RequiresPermissions("mes:mes_xsl_rubber_quick_test_std:updateStatus")
|
||||
|
||||
@@ -39,6 +39,16 @@ public interface IMesXslFormulaSpecService extends IService<MesXslFormulaSpec> {
|
||||
void syncFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||
//update-end---author:cursor ---date:20260522 for:【配合示方】密炼PS审批联动同步状态与审批人-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
/**
|
||||
* 密炼PS拒绝/撤回时,按发行编号(PS编码)将关联配合示方回退到目标状态,并清空高于目标状态的审批人痕迹。
|
||||
*
|
||||
* @param ps 已回退后的密炼PS编制单
|
||||
* @param mixerPsTargetStatus 密炼PS回退后的目标状态:compile / proofread / audit
|
||||
*/
|
||||
void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A38】配合示方生成混炼示方预览与批量创建-----------
|
||||
/**
|
||||
* 根据配合示方混合段数构建生成混炼示方预览行(示方编号 + 段信息)
|
||||
|
||||
@@ -16,4 +16,22 @@ public interface IMesXslMixerPsCompileService extends IService<MesXslMixerPsComp
|
||||
*/
|
||||
String changeStatusBatch(String ids, String expectedStatus, String targetStatus, String actionLabel, String operatorName);
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】批量流转状态-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
/**
|
||||
* 批量拒绝:将单据一步退回到编制(compile),清空本单据校对/审核/批准全部痕迹,
|
||||
* 并联动回退配合示方、混炼示方、原材料检验标准等关联单据。
|
||||
*
|
||||
* @return 失败原因,null 表示成功
|
||||
*/
|
||||
String rejectBatch(String ids, String operatorName);
|
||||
|
||||
/**
|
||||
* 批量撤回:将单据按当前状态退回上一环节(approve→audit→proofread→compile),
|
||||
* 仅清空被撤回那一步的痕迹,并联动回退关联单据。
|
||||
*
|
||||
* @return 失败原因,null 表示成功
|
||||
*/
|
||||
String withdrawBatch(String ids, String operatorName);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
}
|
||||
|
||||
@@ -46,5 +46,15 @@ public interface IMesXslMixingSpecService extends IService<MesXslMixingSpec> {
|
||||
* @param mixerPsTargetStatus 密炼PS目标状态:proofread / audit / approve
|
||||
*/
|
||||
void syncFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
/**
|
||||
* 密炼PS拒绝/撤回时,按发行编号(PS编码)清空关联混炼示方高于目标状态的审批人痕迹。
|
||||
*
|
||||
* @param ps 已回退后的密炼PS编制单
|
||||
* @param mixerPsTargetStatus 密炼PS回退后的目标状态:compile / proofread / audit
|
||||
*/
|
||||
void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
//update-end---author:cursor ---date:20260526 for:【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人-----------
|
||||
}
|
||||
|
||||
@@ -25,4 +25,11 @@ public interface IMesXslRubberQuickTestStdService extends IService<MesXslRubberQ
|
||||
* 密炼PS(原材料检验标准)批准后,将关联实验标准审核状态置为已批准(仅写入,不随PS反审核回退)
|
||||
*/
|
||||
void markAuditApprovedByPsCompileIds(Collection<String> psCompileIds);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
/**
|
||||
* 密炼PS(原材料检验标准)从已批准回退(拒绝/撤回)时,将关联实验标准审核状态置回草稿(未批准)
|
||||
*/
|
||||
void markAuditDraftByPsCompileIds(Collection<String> psCompileIds);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
}
|
||||
|
||||
@@ -499,6 +499,41 @@ public class MesXslFormulaSpecServiceImpl extends ServiceImpl<MesXslFormulaSpecM
|
||||
}
|
||||
//update-end---author:cursor ---date:20260522 for:【配合示方】密炼PS审批联动同步状态与审批人-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
@Override
|
||||
public void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus) {
|
||||
if (ps == null || oConvertUtils.isEmpty(ps.getPsCode()) || oConvertUtils.isEmpty(mixerPsTargetStatus)) {
|
||||
return;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslFormulaSpec> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(MesXslFormulaSpec::getIssueNumber, ps.getPsCode());
|
||||
switch (mixerPsTargetStatus) {
|
||||
case "compile":
|
||||
// 回退到编制:状态回 compile,清空 校对/审核/批准 三组痕迹
|
||||
wrapper.set(MesXslFormulaSpec::getStatus, "compile")
|
||||
.set(MesXslFormulaSpec::getProofreadBy, null).set(MesXslFormulaSpec::getProofreadTime, null)
|
||||
.set(MesXslFormulaSpec::getAuditBy, null).set(MesXslFormulaSpec::getAuditTime, null)
|
||||
.set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null);
|
||||
break;
|
||||
case "proofread":
|
||||
// 回退到校对:状态回 submit,清空 审核/批准 痕迹(保留校对)
|
||||
wrapper.set(MesXslFormulaSpec::getStatus, "submit")
|
||||
.set(MesXslFormulaSpec::getAuditBy, null).set(MesXslFormulaSpec::getAuditTime, null)
|
||||
.set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null);
|
||||
break;
|
||||
case "audit":
|
||||
// 回退到审核:状态回 review_pass,清空 批准 痕迹(保留校对/审核)
|
||||
wrapper.set(MesXslFormulaSpec::getStatus, "review_pass")
|
||||
.set(MesXslFormulaSpec::getApproveBy, null).set(MesXslFormulaSpec::getApproveTime, null);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
wrapper.set(MesXslFormulaSpec::getUpdateTime, new Date());
|
||||
this.update(wrapper);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退配合示方-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260522 for:【XSLMES-20260522-A38】配合示方生成混炼示方预览与批量创建-----------
|
||||
@Override
|
||||
public MesXslFormulaMixingGeneratePreviewVO buildMixingGeneratePreview(String formulaSpecId) {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.jeecg.modules.xslmes.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
@@ -96,4 +98,93 @@ public class MesXslMixerPsCompileServiceImpl extends ServiceImpl<MesXslMixerPsCo
|
||||
}
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】批量流转状态-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String rejectBatch(String ids, String operatorName) {
|
||||
return doRevertBatch(ids, "拒绝", true, operatorName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String withdrawBatch(String ids, String operatorName) {
|
||||
return doRevertBatch(ids, "撤回", false, operatorName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 逆向回退批处理。
|
||||
*
|
||||
* @param toCompile true=拒绝(一步退回编制),false=撤回(退回上一环节)
|
||||
*/
|
||||
private String doRevertBatch(String ids, String actionLabel, boolean toCompile, String operatorName) {
|
||||
if (oConvertUtils.isEmpty(ids)) {
|
||||
return "请选择要" + actionLabel + "的记录";
|
||||
}
|
||||
for (String rawId : ids.split(",")) {
|
||||
if (oConvertUtils.isEmpty(rawId)) {
|
||||
continue;
|
||||
}
|
||||
String id = rawId.trim();
|
||||
MesXslMixerPsCompile entity = getById(id);
|
||||
if (entity == null) {
|
||||
return "记录不存在或已删除";
|
||||
}
|
||||
String current = entity.getStatus() == null ? "" : entity.getStatus();
|
||||
String prev = prevStatus(current);
|
||||
if (prev == null) {
|
||||
String psCode = oConvertUtils.isEmpty(entity.getPsCode()) ? id : entity.getPsCode();
|
||||
return "PS编码[" + psCode + "]当前为编制状态,无需" + actionLabel;
|
||||
}
|
||||
String targetStatus = toCompile ? "compile" : prev;
|
||||
applyRevert(entity, current, targetStatus);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 执行单条单据的逆向回退:本单据状态+痕迹回退,并联动回退关联单据 */
|
||||
private void applyRevert(MesXslMixerPsCompile entity, String currentStatus, String targetStatus) {
|
||||
boolean leavingApprove = "approve".equals(currentStatus);
|
||||
// 本单据:状态回退 + 清空高于目标状态的痕迹(用 UpdateWrapper 显式 set null,绕过 updateById 对 null 不更新)
|
||||
LambdaUpdateWrapper<MesXslMixerPsCompile> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(MesXslMixerPsCompile::getId, entity.getId())
|
||||
.set(MesXslMixerPsCompile::getStatus, targetStatus);
|
||||
if ("compile".equals(targetStatus)) {
|
||||
wrapper.set(MesXslMixerPsCompile::getProofreadBy, null).set(MesXslMixerPsCompile::getProofreadTime, null)
|
||||
.set(MesXslMixerPsCompile::getAuditBy, null).set(MesXslMixerPsCompile::getAuditTime, null)
|
||||
.set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null);
|
||||
} else if ("proofread".equals(targetStatus)) {
|
||||
wrapper.set(MesXslMixerPsCompile::getAuditBy, null).set(MesXslMixerPsCompile::getAuditTime, null)
|
||||
.set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null);
|
||||
} else if ("audit".equals(targetStatus)) {
|
||||
wrapper.set(MesXslMixerPsCompile::getApproveBy, null).set(MesXslMixerPsCompile::getApproveTime, null);
|
||||
}
|
||||
update(wrapper);
|
||||
// 同步实体内存值,供关联单据联动判断使用
|
||||
entity.setStatus(targetStatus);
|
||||
|
||||
// 联动回退:配合示方(状态+审批人)、混炼示方(审批人)
|
||||
mesXslFormulaSpecService.revertFromMixerPsWorkflow(entity, targetStatus);
|
||||
mesXslMixingSpecService.revertFromMixerPsWorkflow(entity, targetStatus);
|
||||
|
||||
// 离开批准态:原材料检验标准回退为草稿(仅原料检验标准类型)
|
||||
if (leavingApprove && XslMesBizConstants.PS_TYPE_RAW_INSPECT_STD.equals(entity.getPsType())) {
|
||||
mesXslRubberQuickTestStdService.markAuditDraftByPsCompileIds(Collections.singletonList(entity.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
/** 上一环节映射:approve→audit→proofread→compile,compile 无上一环节返回 null */
|
||||
private String prevStatus(String current) {
|
||||
switch (current) {
|
||||
case "approve":
|
||||
return "audit";
|
||||
case "audit":
|
||||
return "proofread";
|
||||
case "proofread":
|
||||
return "compile";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回逆向回退-----------
|
||||
}
|
||||
|
||||
@@ -841,6 +841,38 @@ public class MesXslMixingSpecServiceImpl extends ServiceImpl<MesXslMixingSpecMap
|
||||
}
|
||||
//update-end---author:cursor ---date:20260526 for:【XSLMES-20260526-A61】混炼示方密炼PS审批联动同步审批人-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
@Override
|
||||
public void revertFromMixerPsWorkflow(MesXslMixerPsCompile ps, String mixerPsTargetStatus) {
|
||||
if (ps == null || oConvertUtils.isEmpty(ps.getPsCode()) || oConvertUtils.isEmpty(mixerPsTargetStatus)) {
|
||||
return;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslMixingSpec> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(MesXslMixingSpec::getIssueNumber, ps.getPsCode());
|
||||
switch (mixerPsTargetStatus) {
|
||||
case "compile":
|
||||
// 回退到编制:清空 校对/审核/批准 三组痕迹
|
||||
wrapper.set(MesXslMixingSpec::getProofreadBy, null).set(MesXslMixingSpec::getProofreadTime, null)
|
||||
.set(MesXslMixingSpec::getAuditBy, null).set(MesXslMixingSpec::getAuditTime, null)
|
||||
.set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null);
|
||||
break;
|
||||
case "proofread":
|
||||
// 回退到校对:清空 审核/批准 痕迹(保留校对)
|
||||
wrapper.set(MesXslMixingSpec::getAuditBy, null).set(MesXslMixingSpec::getAuditTime, null)
|
||||
.set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null);
|
||||
break;
|
||||
case "audit":
|
||||
// 回退到审核:清空 批准 痕迹(保留校对/审核)
|
||||
wrapper.set(MesXslMixingSpec::getApproveBy, null).set(MesXslMixingSpec::getApproveTime, null);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
wrapper.set(MesXslMixingSpec::getUpdateTime, new Date());
|
||||
this.update(wrapper);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回联动回退混炼示方-----------
|
||||
|
||||
//update-begin---author:cursor ---date:20260527 for:【配方日志查询】从入参直接构建快照,避免二次查库-----------
|
||||
private MesXslMixingSpecPage buildPageFromInput(
|
||||
MesXslMixingSpec main,
|
||||
|
||||
@@ -156,5 +156,24 @@ public class MesXslRubberQuickTestStdServiceImpl
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260525 for:【MES】原材料检验标准密炼PS批准时关联实验标准置已批准-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
@Override
|
||||
public void markAuditDraftByPsCompileIds(Collection<String> psCompileIds) {
|
||||
if (CollectionUtils.isEmpty(psCompileIds)) {
|
||||
return;
|
||||
}
|
||||
List<String> ids =
|
||||
psCompileIds.stream().filter(id -> oConvertUtils.isNotEmpty(id)).map(String::trim).distinct().collect(Collectors.toList());
|
||||
if (ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
this.lambdaUpdate()
|
||||
.in(MesXslRubberQuickTestStd::getPsCompileId, ids)
|
||||
.and(w -> w.eq(MesXslRubberQuickTestStd::getDelFlag, CommonConstant.DEL_FLAG_0).or().isNull(MesXslRubberQuickTestStd::getDelFlag))
|
||||
.set(MesXslRubberQuickTestStd::getAuditStatus, XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_DRAFT)
|
||||
.update();
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】密炼PS拒绝/撤回时关联实验标准回退为草稿-----------
|
||||
|
||||
//update-end---author:jiangxh ---date:20260525 for:【MES】胶料快检实验标准名称同租户唯一、主子保存-----------
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 【QH-MES审批流设计】驳回/撤销恢复初始状态:审批流配置状态字段名,实例快照发起时业务状态原值
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `mes_xsl_approval_flow`
|
||||
ADD COLUMN `status_field` varchar(64) DEFAULT NULL COMMENT '业务单据状态字段名(驳回/撤销时回写其发起时原值)' AFTER `route_path`;
|
||||
|
||||
ALTER TABLE `mes_xsl_approval_instance`
|
||||
ADD COLUMN `status_field` varchar(64) DEFAULT NULL COMMENT '业务单据状态字段名(发起时快照)' AFTER `biz_title`,
|
||||
ADD COLUMN `origin_status` varchar(64) DEFAULT NULL COMMENT '发起审批时业务状态原值(驳回/撤销回写)' AFTER `status_field`;
|
||||
|
||||
-- 密炼PS编制默认以 status 字段作为可恢复状态字段
|
||||
UPDATE `mes_xsl_approval_flow`
|
||||
SET `status_field` = 'status'
|
||||
WHERE `biz_table` = 'mes_xsl_mixer_ps_compile' AND (`status_field` IS NULL OR `status_field` = '');
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 【QH-MES审批流完善】①乐观锁防并发 ②超时配置 ③催办记录时间
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 审批实例:乐观锁版本号,防止多人同时审批导致进度覆盖写
|
||||
ALTER TABLE `mes_xsl_approval_instance`
|
||||
ADD COLUMN `version` int(11) NOT NULL DEFAULT 0 COMMENT '乐观锁版本号' AFTER `remark`;
|
||||
|
||||
-- 审批流定义:超时提醒小时数(0=不提醒),默认24h
|
||||
ALTER TABLE `mes_xsl_approval_flow`
|
||||
ADD COLUMN `timeout_hours` int(11) NOT NULL DEFAULT 24 COMMENT '超时提醒小时数(0=不提醒)' AFTER `status_field`;
|
||||
@@ -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 { useUserStoreWithOut } from '/@/store/modules/user';
|
||||
import { cloneDeep } from "lodash-es";
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】回调接口录制-----
|
||||
import { notifyFlowApiRecorder } from '/@/utils/flowApiRecorder';
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】回调接口录制-----
|
||||
const globSetting = useGlobSetting();
|
||||
const urlPrefix = globSetting.urlPrefix;
|
||||
const { createMessage, createErrorModal } = useMessage();
|
||||
@@ -93,9 +90,6 @@ const transform: AxiosTransform = {
|
||||
|
||||
// 请求之前处理config
|
||||
beforeRequestHook: (config, options) => {
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制时捕获原始servlet路径(未加前缀/上下文)-----
|
||||
notifyFlowApiRecorder({ url: config.url, method: config.method, data: config.data, params: config.params });
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】录制时捕获原始servlet路径(未加前缀/上下文)-----
|
||||
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
|
||||
|
||||
// http开头的请求url,不加前缀
|
||||
|
||||
@@ -17,6 +17,9 @@ enum Api {
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
designContext = '/xslmes/approvalFlow/designContext',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文-----
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作-----
|
||||
bizActions = '/xslmes/approvalFlow/bizActions',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作-----
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,3 +76,11 @@ export const batchDeleteApprovalFlow = (params, handleSuccess) => {
|
||||
*/
|
||||
export const getApprovalDesignContext = (routePath: string) => defHttp.get({ url: Api.designContext, params: { routePath } });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页设计上下文(解析阶段字段+取/建草稿流程)-----
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作(后端@ApprovalBizAction注解扫描)-----
|
||||
/**
|
||||
* 查询某业务表已标注 @ApprovalBizAction 的可选回调动作,供节点「回调接口」下拉选择。
|
||||
* 返回 [{ name, url, method, table, phase, perms }]
|
||||
*/
|
||||
export const getApprovalBizActions = (table: string) => defHttp.get({ url: Api.bizActions, params: { table } });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】业务表可选回调动作-----
|
||||
|
||||
@@ -10,6 +10,9 @@ enum Api {
|
||||
status = '/xslmes/approvalHandle/status',
|
||||
approve = '/xslmes/approvalHandle/approve',
|
||||
reject = '/xslmes/approvalHandle/reject',
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
cancel = '/xslmes/approvalHandle/cancel',
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
}
|
||||
|
||||
/** 查看单据全部字段 + 审批进度/历史 */
|
||||
@@ -23,3 +26,8 @@ export const approveApproval = (params: { instanceId: string; comment?: string }
|
||||
|
||||
/** 驳回(需填写理由) */
|
||||
export const rejectApproval = (params: { instanceId: string; reason: string }) => defHttp.post({ url: Api.reject, data: params });
|
||||
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
/** 撤销(仅发起人本人,审批中可撤回,业务单据恢复到发起时状态) */
|
||||
export const cancelApproval = (params: { instanceId: string; reason?: string }) => defHttp.post({ url: Api.cancel, data: params });
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人撤销-----
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<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-toolbar">
|
||||
<span class="fd-tb-item">绑定单据:<b>{{ record.bizTableName || record.bizTable }}</b></span>
|
||||
@@ -58,7 +58,6 @@
|
||||
} from './flowTypes';
|
||||
import type { FlowNode as FlowNodeType, NodeType, StageField } from './flowTypes';
|
||||
import { saveApprovalFlowDesign, getApprovalFlowById } from '../approvalFlow.api';
|
||||
import { flowApiRecording } from '/@/utils/flowApiRecorder';
|
||||
|
||||
defineOptions({ name: 'ApprovalFlowDesign' });
|
||||
|
||||
@@ -69,6 +68,9 @@
|
||||
const readonly = ref(false);
|
||||
const record = reactive<any>({ id: '', flowName: '', bizTable: '', bizTableName: '' });
|
||||
const drawerRef = ref();
|
||||
// 当前审批流绑定的业务表,供节点配置按表查可选回调动作
|
||||
const bizTableRef = ref('');
|
||||
provide('approvalBizTable', bizTableRef);
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
const paletteStages = ref<StageField[]>([]);
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】当前页识别到的候选阶段字段----- -->
|
||||
@@ -105,6 +107,7 @@
|
||||
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
|
||||
readonly.value = !!data?.readonly;
|
||||
flowCtx.readonly = readonly.value;
|
||||
bizTableRef.value = data?.record?.bizTable || '';
|
||||
// update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
paletteStages.value = Array.isArray(data?.paletteStages) ? data.paletteStages : [];
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】接收当前页解析出的候选阶段字段----- -->
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@date 2026-05-29 for:【QH-MES审批流设计】新增审批流可视化设计
|
||||
-->
|
||||
<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">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="节点名称">
|
||||
@@ -80,13 +80,17 @@
|
||||
</a-radio-group>
|
||||
</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-alert
|
||||
type="info"
|
||||
show-icon
|
||||
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 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" />
|
||||
<Icon v-if="!readonly" icon="ant-design:minus-circle-outlined" class="fd-cb-del" @click="removeAction(phase.key, i)" />
|
||||
</div>
|
||||
<a-space v-if="!readonly" style="margin-top: 4px">
|
||||
<a-button size="small" @click="recordInto(phase.key)">
|
||||
<Icon icon="ant-design:aim-outlined" />
|
||||
<span>录制接口</span>
|
||||
</a-button>
|
||||
<a-space v-if="!readonly" style="margin-top: 4px" :size="6" wrap>
|
||||
<a-select
|
||||
v-if="pageActionOptions.length"
|
||||
style="width: 240px"
|
||||
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-space>
|
||||
</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>
|
||||
|
||||
<!-- 抄送人 -->
|
||||
@@ -173,20 +190,11 @@
|
||||
</a-space>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<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 { defHttp } from '/@/utils/http/axios';
|
||||
import { ApiSelect } from '/@/components/Form';
|
||||
@@ -194,25 +202,63 @@
|
||||
import { OPERATOR_OPTIONS } from './flowTypes';
|
||||
import type { FlowNode } from './flowTypes';
|
||||
import { useMessage } from '/@/hooks/web/useMessage';
|
||||
import { flowApiRecording, startFlowApiRecord, cancelFlowApiRecord } from '/@/utils/flowApiRecorder';
|
||||
import { getApprovalBizActions } from '../approvalFlow.api';
|
||||
|
||||
const props = defineProps<{ readonly?: boolean }>();
|
||||
const emit = defineEmits(['confirm']);
|
||||
|
||||
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 node = ref<FlowNode | null>(null);
|
||||
const form = ref<any>(null);
|
||||
// 「从页面按钮中选择」下拉的临时选中值(仅作选择器,选完即清空,避免跨节点残留上次的选项)
|
||||
const actionPicker = ref<Record<string, any>>({ onNodeApprove: undefined, onApprove: undefined, onReject: undefined });
|
||||
|
||||
const operatorOptions = OPERATOR_OPTIONS;
|
||||
const readonly = computed(() => !!props.readonly);
|
||||
|
||||
// 回调接口配置:三个触发时机
|
||||
// 回调接口配置:通过类时机需按节点配置;驳回类(onReject)已全局统一(后端按 @ApprovalBizAction 自动执行),无需在此逐节点维护
|
||||
const callbackPhases = [
|
||||
{ key: 'onNodeApprove', label: '本节点通过时执行' },
|
||||
{ key: 'onApprove', label: '流程最终通过时执行' },
|
||||
{ key: 'onReject', label: '驳回时执行' },
|
||||
];
|
||||
const methodOptions = [
|
||||
{ label: 'POST', value: 'POST' },
|
||||
@@ -230,6 +276,8 @@
|
||||
|
||||
function openDrawer(n: FlowNode) {
|
||||
node.value = n;
|
||||
// 切换节点时清空下拉选择器的残留值
|
||||
actionPicker.value = { onNodeApprove: undefined, onApprove: undefined, onReject: undefined };
|
||||
// 编辑副本,确定时回写,避免取消后脏数据
|
||||
form.value = { name: n.name, props: cloneDeep(n.props) };
|
||||
// 审批人节点确保回调接口配置结构存在
|
||||
@@ -252,17 +300,21 @@
|
||||
form.value.props.callbackActions[phaseKey].splice(i, 1);
|
||||
}
|
||||
|
||||
/** 录制:临时隐藏设计器,捕获用户点击业务按钮的请求并回填 */
|
||||
async function recordInto(phaseKey: string) {
|
||||
const cap = await startFlowApiRecord();
|
||||
if (cap) {
|
||||
form.value.props.callbackActions[phaseKey].push({ name: '', method: cap.method, url: cap.url, body: cap.data });
|
||||
createMessage.success(`已录制接口:${cap.method} ${cap.url}`);
|
||||
/** 从「已标注的业务动作」中选择一个接口加入回调(按 url 值查回原始动作) */
|
||||
function addFromButton(phaseKey: string, url: string) {
|
||||
const opt = pageActionOptions.value.find((o) => o.value === url);
|
||||
const raw = opt?.raw;
|
||||
if (!raw || !raw.url) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRecord() {
|
||||
cancelFlowApiRecord();
|
||||
const list = form.value.props.callbackActions[phaseKey];
|
||||
// 选完即清空下拉,使其回到 placeholder(仅作选择器用)
|
||||
actionPicker.value[phaseKey] = undefined;
|
||||
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() {
|
||||
@@ -327,33 +379,3 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</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;
|
||||
|
||||
/* 录制回调接口时,隐藏整个设计器弹窗(含遮罩),让用户能点击业务页面上的真实按钮 */
|
||||
.flow-design-recording-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fd-design {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
</template>
|
||||
<!-- 不可办理(已处理/已流转/非当前处理人):置灰提示 -->
|
||||
<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)">
|
||||
<Icon icon="ant-design:unordered-list-outlined" />
|
||||
<span>跳转至列表</span>
|
||||
@@ -90,17 +97,30 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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 { getImBizRecordFieldValueByLabel, resolveImBizRecordItemFields, resolveImBizRecordListColumnLabels } from './imBizRecordMessage';
|
||||
import { navigateImBizRecordLink } from './imRecordLocate';
|
||||
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 { 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';
|
||||
|
||||
defineOptions({ name: 'ImBizRecordMessageContent' });
|
||||
@@ -122,8 +142,14 @@
|
||||
const rejectOpen = ref(false);
|
||||
const rejectReason = ref('');
|
||||
const rejectItem = ref<ImBizRecordItem | null>(null);
|
||||
// 本地办理结果:approved / rejected(卡片消息为静态,办理后本地标记)
|
||||
const actionDone = ref<'' | 'approved' | 'rejected'>('');
|
||||
// 本地办理结果:approved / rejected / cancelled(卡片消息为静态,办理后本地标记)
|
||||
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 {
|
||||
@@ -184,11 +210,38 @@
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
// update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起人卡片也需加载状态以支持撤销-----
|
||||
try {
|
||||
const res: any = await getApprovalStatus(id);
|
||||
liveStatus.value = (res || { exists: false }) as LiveStatus;
|
||||
@@ -202,6 +255,33 @@
|
||||
onMounted(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 showNoPermission = computed(() => !props.mine && !hasPagePermission.value && !isApprovalCard.value);
|
||||
@@ -231,6 +311,13 @@
|
||||
|
||||
async function handleApprove(item: ImBizRecordItem) {
|
||||
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 {
|
||||
approving.value = true;
|
||||
const res: any = await approveApproval({ instanceId: item.instanceId });
|
||||
@@ -257,6 +344,14 @@
|
||||
createMessage.warning('请填写驳回理由');
|
||||
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 {
|
||||
rejecting.value = true;
|
||||
await rejectApproval({ instanceId: item.instanceId, reason: rejectReason.value.trim() });
|
||||
@@ -269,6 +364,30 @@
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
Reference in New Issue
Block a user