Merge remote-tracking branch 'origin/20260519-3.9.2版本-葛昊天分支'

This commit is contained in:
2026-05-29 15:51:29 +08:00
48 changed files with 5603 additions and 261 deletions

View File

@@ -466,3 +466,58 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTes
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/MesXslRubberQuickTestRecord.data.ts
jeecgboot-vue3/src/views/xslmes/mesXslRubberQuickTestRecord/components/MesXslRubberQuickTestRecordModal.vue
jeecgboot-vue3/src/views/mes/material/MesMaterialList.vue
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】我的租户下新增审批流设计,钉钉式可视化拖拽设计(先选单据再设计流程),本期实现设计器 ---
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_111__mes_xsl_approval_flow.sql
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/mapper/MesXslApprovalFlowMapper.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalFlowService.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalFlowServiceImpl.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalFlowController.java
jeecgboot-vue3/src/views/approval/flow/approvalFlow.api.ts
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowList.vue
jeecgboot-vue3/src/views/approval/flow/ApprovalFlowModal.vue
jeecgboot-vue3/src/views/approval/flow/components/flowTypes.ts
jeecgboot-vue3/src/views/approval/flow/components/FlowNode.vue
jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue
jeecgboot-vue3/src/views/approval/flow/components/FlowDesign.vue
jeecgboot-vue3/src/views/approval/flow/components/flow.less
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批运行时:全局悬浮按钮(选单据类型->选单据->发起),本期仅发起(生成审批实例+解析首节点处理人),不办理/不回写业务表 ---
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_112__mes_xsl_approval_instance.sql
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalInstance.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/mapper/MesXslApprovalInstanceMapper.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalInstanceService.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalInstanceServiceImpl.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
jeecgboot-vue3/src/layouts/default/index.vue
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批悬浮按钮仅在配置了审批流的功能页显示:审批流定义增加route_path(功能页路由),前端按当前路由匹配后才显示按钮 ---
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_113__mes_xsl_approval_flow_route.sql
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalFlow.java
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】取消手填功能页路由:publishedList按单据表名自动反查sys_permission菜单url填入routePath,设计表单去掉路由字段 ---
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
jeecgboot-vue3/src/views/approval/flow/approvalFlow.data.ts
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批支持列表多选联动:useListPage自动同步选中行到全局上下文,悬浮按钮发起弹窗自动带入选中单据并批量发起 ---
jeecgboot-vue3/src/components/ApprovalLaunch/useApprovalSelection.ts
jeecgboot-vue3/src/hooks/system/useListPage.ts
jeecgboot-vue3/src/components/ApprovalLaunch/index.vue
jeecgboot-vue3/src/views/approval/flow/launch.api.ts
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】发起审批与IM聊天结合:IM新增系统单聊消息(绕过同部门校验),发起后把审批消息发给当前节点处理人 ---
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/ISysImChatService.java
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/im/service/impl/SysImChatServiceImpl.java
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
-- author:GHT---date:20260529--for: 【QH-MES审批流设计】审批IM消息升级为可跳转业务卡片(biz_record):点击可定位到对应单据,无法定位功能页时退回纯文本 ---
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java

View File

@@ -0,0 +1,27 @@
package org.jeecg.modules.xslmes.approval.callback;
import org.springframework.context.ApplicationEvent;
/**
* 审批动作领域事件。
* 与 {@link IApprovalBizCallback} 等价的另一种接入方式:
* 偏好松耦合的业务可使用 {@code @EventListener} 监听本事件(同步、同事务)。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调
*/
public class ApprovalActionEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private final ApprovalCallbackContext context;
public ApprovalActionEvent(Object source, ApprovalCallbackContext context) {
super(source);
this.context = context;
}
public ApprovalCallbackContext getContext() {
return context;
}
}

View File

@@ -0,0 +1,169 @@
package org.jeecg.modules.xslmes.approval.callback;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
/**
* 审批节点「回调接口」HTTP 执行器。
*
* 审批到对应时机时读取节点配置中录制好的业务接口url+method
* 以「当前审批处理人」的登录态(透传当前请求的 X-Access-Token内部调用该接口
* 自动带上单据ID覆盖 id 参数),从而真实执行业务页面按钮背后的逻辑。
*
* 限制:自动流转 / 无人值守节点(无登录态请求上下文)时降级跳过并记录日志。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】节点回调接口内部调用执行
*/
@Slf4j
@Component
public class ApprovalActionHttpExecutor {
@Value("${server.port:8080}")
private int serverPort;
@Value("${server.servlet.context-path:}")
private String contextPath;
private final RestTemplate restTemplate = new RestTemplate();
/**
* 执行某节点在指定时机配置的所有回调接口。
*
* @param node 流程节点 JSON含 props.callbackActions
* @param phase 时机onNodeApprove / onApprove / onReject
* @param inst 审批实例
*/
public void run(JSONObject node, String phase, MesXslApprovalInstance inst) {
if (node == null || inst == null) {
return;
}
JSONObject propsObj = node.getJSONObject("props");
if (propsObj == null) {
return;
}
JSONObject callbackActions = propsObj.getJSONObject("callbackActions");
if (callbackActions == null) {
return;
}
JSONArray actions = callbackActions.getJSONArray(phase);
if (actions == null || actions.isEmpty()) {
return;
}
String token = currentToken();
for (int i = 0; i < actions.size(); i++) {
JSONObject action = actions.getJSONObject(i);
if (action == null) {
continue;
}
String url = action.getString("url");
if (oConvertUtils.isEmpty(url)) {
continue;
}
if (oConvertUtils.isEmpty(token)) {
// 无登录态(自动流转/无人值守) -> 降级跳过
log.warn("[审批回调] 无当前处理人登录态,跳过接口调用 phase={}, url={}, bizId={}", phase, url, inst.getBizDataId());
continue;
}
String method = oConvertUtils.getString(action.getString("method"), "POST").toUpperCase();
invoke(method, url, action.getJSONObject("body"), inst.getBizDataId(), token);
}
}
private void invoke(String method, String url, JSONObject recordedBody, String bizDataId, String token) {
String fullUrl = buildFullUrl(url);
HttpMethod httpMethod = HttpMethod.valueOf(method);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(CommonConstant.X_ACCESS_TOKEN, token);
headers.add(HttpHeaders.AUTHORIZATION, token);
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
JSONObject body = recordedBody == null ? new JSONObject() : new JSONObject(recordedBody);
body.put("id", bizDataId);
bodyToSend = body;
}
try {
HttpEntity<Object> entity = new HttpEntity<>(bodyToSend, headers);
ResponseEntity<String> resp = restTemplate.exchange(fullUrl, httpMethod, entity, String.class);
String respBody = resp.getBody();
if (!resp.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("回调接口返回非2xx" + resp.getStatusCode());
}
// Jeecg 统一返回 {success:false,...} 视为业务失败
if (oConvertUtils.isNotEmpty(respBody)) {
try {
JSONObject r = JSONObject.parseObject(respBody);
if (r != null && r.containsKey("success") && Boolean.FALSE.equals(r.getBoolean("success"))) {
throw new RuntimeException("回调接口业务失败:" + oConvertUtils.getString(r.getString("message"), "未知错误"));
}
} catch (RuntimeException re) {
throw re;
} catch (Exception ignore) {
// 非JSON响应不强校验
}
}
log.info("[审批回调] 已调用业务接口成功 {} {} bizId={}", method, fullUrl, bizDataId);
} catch (RuntimeException e) {
log.error("[审批回调] 调用业务接口失败 {} {} bizId={}", method, fullUrl, bizDataId, e);
// 抛出以回滚整个审批动作,保证审批与业务一致
throw e;
}
}
/** 构建内部调用绝对地址http://127.0.0.1:port + context-path + url */
private String buildFullUrl(String url) {
String ctx = oConvertUtils.getString(contextPath, "");
if (oConvertUtils.isNotEmpty(ctx) && !ctx.startsWith("/")) {
ctx = "/" + ctx;
}
String path = url.startsWith("/") ? url : "/" + url;
// 录制到的路径已含 context-path 时不重复拼接
if (oConvertUtils.isNotEmpty(ctx) && (path.equals(ctx) || path.startsWith(ctx + "/"))) {
ctx = "";
}
return "http://127.0.0.1:" + serverPort + ctx + path;
}
/** 取当前请求的登录 token处理人身份 */
private String currentToken() {
try {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) {
return null;
}
HttpServletRequest request = attrs.getRequest();
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
if (oConvertUtils.isEmpty(token)) {
token = request.getHeader(HttpHeaders.AUTHORIZATION);
}
return token;
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,80 @@
package org.jeecg.modules.xslmes.approval.callback;
import lombok.Data;
import lombok.experimental.Accessors;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
import java.io.Serializable;
/**
* 审批回调上下文。
* 由审批引擎在「节点通过 / 最终通过 / 驳回」时构建并传给业务回调,
* 业务模块据此调用自身已有的审核/回写接口,实现审批与业务功能的统一联动。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调
*/
@Data
@Accessors(chain = true)
public class ApprovalCallbackContext implements Serializable {
private static final long serialVersionUID = 1L;
/** 回调动作类型 */
public enum Action {
/** 单个审批节点通过(中间态,每个节点都会触发一次) */
NODE_APPROVED,
/** 整个流程最终通过 */
APPROVED,
/** 被驳回(任一节点驳回即终止) */
REJECTED
}
/** 回调动作 */
private Action action;
/** 审批实例ID */
private String instanceId;
/** 审批流定义ID */
private String flowId;
/** 审批流名称 */
private String flowName;
/** 业务单据表名 */
private String bizTable;
/** 业务单据中文名 */
private String bizTableName;
/** 业务单据记录ID业务表主键 */
private String bizDataId;
/** 业务单据展示标题 */
private String bizTitle;
/** 当前/刚处理的节点ID */
private String nodeId;
/** 当前/刚处理的节点名称 */
private String nodeName;
/** 操作人 username系统自动处理时为 null/system */
private String operatorUsername;
/** 操作人姓名 */
private String operatorName;
/** 审批意见 / 驳回理由 */
private String comment;
/** 发起人 username */
private String applyUser;
/** 是否为流程最终结束APPROVED/REJECTED 时为 true */
private boolean finalResult;
/** 完整审批实例(供业务读取租户、发起信息等) */
private transient MesXslApprovalInstance instance;
}

View File

@@ -0,0 +1,115 @@
package org.jeecg.modules.xslmes.approval.callback;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 审批业务回调分发器。
* 统一在审批引擎流转关键点调用:
* <ol>
* <li>按业务表名路由到对应的 {@link IApprovalBizCallback} 实现(强类型,业务直接调用自身接口);</li>
* <li>同时发布 {@link ApprovalActionEvent} 供 {@code @EventListener} 松耦合监听。</li>
* </ol>
* 回调与审批状态变更同事务执行,回调异常将向上抛出以回滚整个审批动作。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调
*/
@Slf4j
@Component
public class ApprovalCallbackDispatcher {
/** 监听所有业务表的通配符 */
private static final String ANY_TABLE = "*";
private final ObjectProvider<List<IApprovalBizCallback>> callbacksProvider;
private final ApplicationEventPublisher eventPublisher;
public ApprovalCallbackDispatcher(ObjectProvider<List<IApprovalBizCallback>> callbacksProvider,
ApplicationEventPublisher eventPublisher) {
this.callbacksProvider = callbacksProvider;
this.eventPublisher = eventPublisher;
}
/** 节点通过(中间态) */
public void fireNodeApproved(ApprovalCallbackContext ctx) {
ctx.setAction(ApprovalCallbackContext.Action.NODE_APPROVED);
ctx.setFinalResult(false);
dispatch(ctx);
}
/** 流程最终通过 */
public void fireApproved(ApprovalCallbackContext ctx) {
ctx.setAction(ApprovalCallbackContext.Action.APPROVED);
ctx.setFinalResult(true);
dispatch(ctx);
}
/** 驳回 */
public void fireRejected(ApprovalCallbackContext ctx) {
ctx.setAction(ApprovalCallbackContext.Action.REJECTED);
ctx.setFinalResult(true);
dispatch(ctx);
}
private void dispatch(ApprovalCallbackContext ctx) {
if (ctx == null || oConvertUtils.isEmpty(ctx.getBizTable())) {
return;
}
// 1) 强类型回调:按表路由 + 通配
for (IApprovalBizCallback cb : matchedCallbacks(ctx.getBizTable())) {
invoke(cb, ctx);
}
// 2) 领域事件:松耦合监听(同步、同事务)
try {
eventPublisher.publishEvent(new ApprovalActionEvent(this, ctx));
} catch (RuntimeException e) {
log.error("审批领域事件处理失败 table={}, bizId={}, action={}", ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
throw e;
}
}
private List<IApprovalBizCallback> matchedCallbacks(String bizTable) {
List<IApprovalBizCallback> all = callbacksProvider.getIfAvailable();
List<IApprovalBizCallback> matched = new ArrayList<>();
if (all == null) {
return matched;
}
for (IApprovalBizCallback cb : all) {
String support = cb.supportTable();
if (ANY_TABLE.equals(support) || (support != null && support.equalsIgnoreCase(bizTable))) {
matched.add(cb);
}
}
return matched;
}
private void invoke(IApprovalBizCallback cb, ApprovalCallbackContext ctx) {
try {
switch (ctx.getAction()) {
case NODE_APPROVED:
cb.onNodeApproved(ctx);
break;
case APPROVED:
cb.onApproved(ctx);
break;
case REJECTED:
cb.onRejected(ctx);
break;
default:
break;
}
} catch (RuntimeException e) {
log.error("审批业务回调执行失败 callback={}, table={}, bizId={}, action={}",
cb.getClass().getSimpleName(), ctx.getBizTable(), ctx.getBizDataId(), ctx.getAction(), e);
// 抛出以回滚整个审批动作,保证审批与业务数据一致
throw e;
}
}
}

View File

@@ -0,0 +1,64 @@
package org.jeecg.modules.xslmes.approval.callback;
/**
* 业务审批回调扩展点SPI
*
* 各业务模块按需实现本接口并声明 {@link #supportTable()}(绑定的业务表名),
* 审批引擎会在审批流转的关键节点自动回调对应实现,业务模块在回调里
* 调用自己「已有的」审核/回写接口(如更新审核状态、扣减库存、生成下游单据等),
* 从而把审批流程与业务单据的功能统一串联起来。
*
* <p>事务说明:回调与审批状态变更处于同一事务内,
* 若回调抛出异常,整个审批动作回滚(审批失败),保证审批与业务数据一致。</p>
*
* <p>使用方式:实现类标注为 Spring Bean@Component / @Service即可被自动收集。</p>
*
* <pre>{@code
* @Component
* public class RubberStdApprovalCallback implements IApprovalBizCallback {
* @Override public String supportTable() { return "mes_xsl_rubber_quick_test_std"; }
* @Override public void onApproved(ApprovalCallbackContext ctx) {
* // 调用业务自身已有接口完成回写
* stdService.lambdaUpdate()
* .eq(MesXslRubberQuickTestStd::getId, ctx.getBizDataId())
* .set(MesXslRubberQuickTestStd::getAuditStatus, "1").update();
* }
* }
* }</pre>
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调
*/
public interface IApprovalBizCallback {
/**
* 绑定的业务表名(与审批流定义 bizTable 一致)。
* 返回 "*" 表示监听所有业务表。
*/
String supportTable();
/**
* 单个审批节点通过(中间态)。每经过一个审批节点通过都会触发一次。
* 适合更新中间状态(如「审核中」「已校对」等)。
* 默认空实现,业务按需重写。
*/
default void onNodeApproved(ApprovalCallbackContext ctx) {
// 默认不处理
}
/**
* 整个审批流程最终通过。适合执行终态业务(如置为「已批准」、生效、扣库存等)。
* 默认空实现,业务按需重写。
*/
default void onApproved(ApprovalCallbackContext ctx) {
// 默认不处理
}
/**
* 审批被驳回(流程终止)。适合回退业务状态(如置回「草稿」、释放占用等)。
* 默认空实现,业务按需重写。
*/
default void onRejected(ApprovalCallbackContext ctx) {
// 默认不处理
}
}

View File

@@ -0,0 +1,59 @@
package org.jeecg.modules.xslmes.approval.callback.impl;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackContext;
import org.jeecg.modules.xslmes.approval.callback.IApprovalBizCallback;
import org.jeecg.modules.xslmes.common.XslMesBizConstants;
import org.jeecg.modules.xslmes.entity.MesXslRubberQuickTestStd;
import org.jeecg.modules.xslmes.service.IMesXslRubberQuickTestStdService;
import org.springframework.stereotype.Component;
/**
* 胶料快检实验标准 审批回调示例。
* 演示如何把审批流转结果联动到业务单据已有功能:
* 审批最终通过 -> 审核状态置「已批准」;驳回 -> 回退「草稿」。
*
* 业务在回调里直接调用自身的 service此处用 lambdaUpdate 更新审核状态字段),
* 与原有「批准/反审核」逻辑保持统一。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批与业务单据联动回调-示例
*/
@Slf4j
@Component
public class RubberQuickTestStdApprovalCallback implements IApprovalBizCallback {
private final IMesXslRubberQuickTestStdService stdService;
public RubberQuickTestStdApprovalCallback(IMesXslRubberQuickTestStdService stdService) {
this.stdService = stdService;
}
@Override
public String supportTable() {
return "mes_xsl_rubber_quick_test_std";
}
@Override
public void onApproved(ApprovalCallbackContext ctx) {
updateAuditStatus(ctx.getBizDataId(), XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_APPROVED);
log.info("[审批联动] 实验标准 {} 审批通过,审核状态置为已批准", ctx.getBizDataId());
}
@Override
public void onRejected(ApprovalCallbackContext ctx) {
updateAuditStatus(ctx.getBizDataId(), XslMesBizConstants.RUBBER_QUICK_TEST_STD_AUDIT_DRAFT);
log.info("[审批联动] 实验标准 {} 被驳回,审核状态回退为草稿", ctx.getBizDataId());
}
private void updateAuditStatus(String bizDataId, String auditStatus) {
if (oConvertUtils.isEmpty(bizDataId)) {
return;
}
stdService.lambdaUpdate()
.eq(MesXslRubberQuickTestStd::getId, bizDataId)
.set(MesXslRubberQuickTestStd::getAuditStatus, auditStatus)
.update();
}
}

View File

@@ -0,0 +1,433 @@
package org.jeecg.modules.xslmes.approval.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.AutoLog;
import org.jeecg.common.system.api.ISysBaseAPI;
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.entity.MesXslApprovalFlow;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* MES 审批流设计
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】新增审批流可视化设计
*/
@Tag(name = "MES审批流设计")
@RestController
@RequestMapping("/xslmes/approvalFlow")
@Slf4j
public class MesXslApprovalFlowController extends JeecgController<MesXslApprovalFlow, IMesXslApprovalFlowService> {
@Autowired
private IMesXslApprovalFlowService mesXslApprovalFlowService;
@Autowired
private ISysBaseAPI sysBaseAPI;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】当前页字段解析-----
@Autowired
private JdbcTemplate jdbcTemplate;
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
/**
* 审批阶段关键字配置有序key=阶段标识name=阶段中文nodeType=对应节点类型keywords=列注释匹配关键字。
* 解析顺序即默认流程顺序:校对 -> 审核 -> 审批 -> 分发 -> 抄送。
*/
private static final String[][] STAGE_DEFS = new String[][]{
{"proofread", "校对", "approver", "校对"},
{"review", "审核", "approver", "审核|审查"},
{"approve", "审批", "approver", "审批|批准|核准"},
{"distribute", "分发", "approver", "分发|发放"},
{"cc", "抄送", "cc", "抄送"},
};
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】当前页字段解析-----
/**
* 根据所选单据表名翻译字典,回填单据中文名
*/
private void fillBizTableName(MesXslApprovalFlow flow) {
if (oConvertUtils.isEmpty(flow.getBizTable())) {
return;
}
List<DictModel> items = sysBaseAPI.getDictItems("mes_xsl_approval_biz_doc");
if (items != null) {
for (DictModel item : items) {
if (flow.getBizTable().equals(item.getValue())) {
flow.setBizTableName(item.getText());
break;
}
}
}
}
@Operation(summary = "审批流设计-分页列表查询")
@RequiresPermissions("approval:flow:list")
@GetMapping(value = "/list")
public Result<IPage<MesXslApprovalFlow>> queryPageList(
MesXslApprovalFlow mesXslApprovalFlow,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<MesXslApprovalFlow> queryWrapper = QueryGenerator.initQueryWrapper(mesXslApprovalFlow, req.getParameterMap());
// 列表查询不返回大字段 flowConfig避免传输冗余
queryWrapper.select(MesXslApprovalFlow.class, info -> !"flow_config".equals(info.getColumn()));
queryWrapper.orderByDesc("create_time");
Page<MesXslApprovalFlow> page = new Page<>(pageNo, pageSize);
IPage<MesXslApprovalFlow> pageList = mesXslApprovalFlowService.page(page, queryWrapper);
return Result.OK(pageList);
}
@AutoLog(value = "审批流设计-添加")
@Operation(summary = "审批流设计-添加")
@RequiresPermissions("approval:flow:add")
@PostMapping(value = "/add")
public Result<String> add(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getFlowName())) {
return Result.error("审批流名称不能为空");
}
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getBizTable())) {
return Result.error("请先选择绑定单据");
}
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getStatus())) {
mesXslApprovalFlow.setStatus("0");
}
fillBizTableName(mesXslApprovalFlow);
mesXslApprovalFlowService.save(mesXslApprovalFlow);
return Result.OK("添加成功!");
}
@AutoLog(value = "审批流设计-编辑")
@Operation(summary = "审批流设计-编辑")
@RequiresPermissions("approval:flow:edit")
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
public Result<String> edit(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getId())) {
return Result.error("主键不能为空");
}
fillBizTableName(mesXslApprovalFlow);
mesXslApprovalFlowService.updateById(mesXslApprovalFlow);
return Result.OK("编辑成功!");
}
@AutoLog(value = "审批流设计-保存流程设计")
@Operation(summary = "审批流设计-保存流程设计")
@RequiresPermissions("approval:flow:design")
@PostMapping(value = "/saveDesign")
public Result<String> saveDesign(@RequestBody MesXslApprovalFlow mesXslApprovalFlow) {
if (oConvertUtils.isEmpty(mesXslApprovalFlow.getId())) {
return Result.error("主键不能为空");
}
MesXslApprovalFlow update = new MesXslApprovalFlow();
update.setId(mesXslApprovalFlow.getId());
update.setFlowConfig(mesXslApprovalFlow.getFlowConfig());
// 设计保存时若仍为草稿则置为已发布
if (oConvertUtils.isNotEmpty(mesXslApprovalFlow.getStatus())) {
update.setStatus(mesXslApprovalFlow.getStatus());
}
mesXslApprovalFlowService.updateById(update);
return Result.OK("流程设计已保存!");
}
@AutoLog(value = "审批流设计-发布/停用")
@Operation(summary = "审批流设计-发布/停用")
@RequiresPermissions("approval:flow:design")
@PostMapping(value = "/updateStatus")
public Result<String> updateStatus(
@RequestParam(name = "id") String id,
@RequestParam(name = "status") String status) {
if (!"0".equals(status) && !"1".equals(status) && !"2".equals(status)) {
return Result.error("状态参数非法");
}
boolean ok = mesXslApprovalFlowService.lambdaUpdate()
.eq(MesXslApprovalFlow::getId, id)
.set(MesXslApprovalFlow::getStatus, status)
.update();
return ok ? Result.OK("操作成功") : Result.error("操作失败");
}
@AutoLog(value = "审批流设计-删除")
@Operation(summary = "审批流设计-删除")
@RequiresPermissions("approval:flow:delete")
@DeleteMapping(value = "/delete")
public Result<String> delete(@RequestParam(name = "id") String id) {
mesXslApprovalFlowService.removeById(id);
return Result.OK("删除成功!");
}
@AutoLog(value = "审批流设计-批量删除")
@Operation(summary = "审批流设计-批量删除")
@RequiresPermissions("approval:flow:delete")
@DeleteMapping(value = "/deleteBatch")
public Result<String> deleteBatch(@RequestParam(name = "ids") String ids) {
mesXslApprovalFlowService.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
@Operation(summary = "审批流设计-通过id查询")
@GetMapping(value = "/queryById")
public Result<MesXslApprovalFlow> queryById(@RequestParam(name = "id") String id) {
MesXslApprovalFlow entity = mesXslApprovalFlowService.getById(id);
if (entity == null) {
return Result.error("未找到对应数据");
}
return Result.OK(entity);
}
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】全局审批流程设计悬浮按钮-当前页字段解析-----
/**
* 设计上下文:供全局"审批流程设计"悬浮按钮调用。
* 1) 根据当前功能页路由反查绑定的业务表;
* 2) 解析该表的字段,识别"校对/审核/审批/分发/抄送"等阶段字段(不存在不报错,存在即解析);
* 3) 取/建该业务表的草稿审批流,返回流程记录(含id)供直接进入设计器。
*
* @param routePath 当前功能页前端路由(如 /xslmes/mesXslFormulaSpec/MesXslFormulaSpecList)
*/
@Operation(summary = "审批流设计-当前页设计上下文")
@RequiresPermissions("approval:flow:design")
@GetMapping(value = "/designContext")
public Result<Map<String, Object>> designContext(@RequestParam(name = "routePath") String routePath) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("routePath", routePath);
data.put("bizTable", null);
data.put("bizTableName", null);
data.put("stages", new ArrayList<>());
data.put("flow", null);
// 1) 路由 -> 业务表名(无法反查时不报错,返回空上下文,前端提示)
String table = resolveTableByRoutePath(routePath);
if (oConvertUtils.isEmpty(table) || !tableExists(table)) {
return Result.OK(data);
}
data.put("bizTable", table);
// 2) 业务表中文名:优先字典,其次表注释
String bizTableName = resolveBizTableName(table);
data.put("bizTableName", bizTableName);
// 3) 解析阶段字段
data.put("stages", parseStageFields(table));
// 4) 取/建草稿审批流
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
MesXslApprovalFlow flow = findFlowByTable(table, tenantId);
if (flow == null) {
flow = new MesXslApprovalFlow();
flow.setFlowName((oConvertUtils.isNotEmpty(bizTableName) ? bizTableName : table) + "审批流");
flow.setBizTable(table);
flow.setBizTableName(bizTableName);
flow.setRoutePath(routePath);
flow.setStatus("0");
if (tenantId != null) {
flow.setTenantId(tenantId);
}
mesXslApprovalFlowService.save(flow);
} else if (oConvertUtils.isEmpty(flow.getRoutePath())) {
// 历史数据未记录路由,回填一次便于后续发起按钮匹配
mesXslApprovalFlowService.lambdaUpdate()
.eq(MesXslApprovalFlow::getId, flow.getId())
.set(MesXslApprovalFlow::getRoutePath, routePath)
.update();
flow.setRoutePath(routePath);
}
data.put("flow", flow);
return Result.OK(data);
}
/**
* 根据前端路由反查业务表名。
* 约定jeecg 代码生成的列表组件名为 表名驼峰 + Listsys_permission.component 形如
* xslmes/mesXslFormulaSpec/MesXslFormulaSpecListurl 即路由。
* 反查url=routePath -> component -> 末段组件名去掉 List -> 驼峰转下划线得到表名。
*/
private String resolveTableByRoutePath(String routePath) {
if (oConvertUtils.isEmpty(routePath)) {
return null;
}
String path = routePath.trim().replaceAll("/+$", "");
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);
} catch (Exception e) {
log.warn("反查菜单组件失败 routePath={}", routePath, e);
return null;
}
if (oConvertUtils.isEmpty(component)) {
return null;
}
// 取组件路径末段xslmes/mesXslFormulaSpec/MesXslFormulaSpecList -> MesXslFormulaSpecList
String comp = component.contains("/") ? component.substring(component.lastIndexOf('/') + 1) : component;
if (comp.endsWith("List")) {
comp = comp.substring(0, comp.length() - "List".length());
}
return camelToUnderline(comp);
}
/** 驼峰转下划线小写MesXslFormulaSpec -> mes_xsl_formula_spec */
private String camelToUnderline(String camel) {
if (oConvertUtils.isEmpty(camel)) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < camel.length(); i++) {
char c = camel.charAt(i);
if (Character.isUpperCase(c)) {
if (i > 0) {
sb.append('_');
}
sb.append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
}
/** 校验表是否存在于当前库 */
private boolean tableExists(String table) {
if (!IDENTIFIER.matcher(table).matches()) {
return false;
}
try {
Integer cnt = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) AND table_name = ?",
Integer.class, table);
return cnt != null && cnt > 0;
} catch (Exception e) {
log.warn("校验表存在失败 table={}", table, e);
return false;
}
}
/** 业务表中文名:优先字典 mes_xsl_approval_biz_doc其次表注释 */
private String resolveBizTableName(String table) {
List<DictModel> items = sysBaseAPI.getDictItems("mes_xsl_approval_biz_doc");
if (items != null) {
for (DictModel item : items) {
if (table.equals(item.getValue())) {
return item.getText();
}
}
}
try {
List<String> comments = jdbcTemplate.queryForList(
"SELECT table_comment FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) AND table_name = ?",
String.class, table);
if (!comments.isEmpty() && oConvertUtils.isNotEmpty(comments.get(0))) {
return comments.get(0);
}
} catch (Exception e) {
log.warn("查询表注释失败 table={}", table, e);
}
return null;
}
/**
* 解析表字段,识别审批阶段字段。每个阶段最多取一个字段(优先列注释含"人/员"的人员字段)。
* 返回有序列表:[{stageKey, stageName, nodeType, field, fieldComment}]
*/
private List<Map<String, Object>> parseStageFields(String table) {
List<Map<String, Object>> stages = new ArrayList<>();
List<Map<String, Object>> columns;
try {
columns = jdbcTemplate.queryForList(
"SELECT column_name AS name, column_comment AS comment FROM information_schema.columns "
+ "WHERE table_schema = (SELECT DATABASE()) AND table_name = ? ORDER BY ordinal_position",
table);
} catch (Exception e) {
log.warn("查询表字段失败 table={}", table, e);
return stages;
}
for (String[] def : STAGE_DEFS) {
String stageKey = def[0];
String stageName = def[1];
String nodeType = def[2];
String[] keywords = def[3].split("\\|");
Map<String, Object> hit = matchStageColumn(columns, keywords);
if (hit != null) {
Map<String, Object> stage = new LinkedHashMap<>();
stage.put("stageKey", stageKey);
stage.put("stageName", stageName);
stage.put("nodeType", nodeType);
stage.put("field", hit.get("name"));
stage.put("fieldComment", hit.get("comment"));
stages.add(stage);
}
}
return stages;
}
/** 在列集合中按关键字匹配阶段字段,优先返回注释含"人/员"的人员字段 */
private Map<String, Object> matchStageColumn(List<Map<String, Object>> columns, String[] keywords) {
Map<String, Object> firstMatch = null;
for (Map<String, Object> col : columns) {
String comment = col.get("comment") == null ? "" : String.valueOf(col.get("comment"));
if (oConvertUtils.isEmpty(comment)) {
continue;
}
boolean matched = false;
for (String kw : keywords) {
if (comment.contains(kw)) {
matched = true;
break;
}
}
if (!matched) {
continue;
}
if (firstMatch == null) {
firstMatch = col;
}
// 人员字段优先(如"校对人""审核员"
if (comment.contains("") || comment.contains("")) {
return col;
}
}
return firstMatch;
}
/** 按业务表+租户查找审批流(取最近一条) */
private MesXslApprovalFlow findFlowByTable(String table, Integer tenantId) {
QueryWrapper<MesXslApprovalFlow> qw = new QueryWrapper<>();
qw.eq("biz_table", table);
if (tenantId != null) {
qw.eq("tenant_id", tenantId);
}
qw.orderByDesc("create_time");
qw.last("LIMIT 1");
return mesXslApprovalFlowService.getOne(qw, false);
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】全局审批流程设计悬浮按钮-当前页字段解析-----
}

View File

@@ -0,0 +1,74 @@
package org.jeecg.modules.xslmes.approval.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* MES 审批办理(运行时-流转)。
* 供 IM 审批卡片按钮调用:查看详情 / 审批通过 / 驳回。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批办理/流转
*/
@Tag(name = "MES审批办理")
@RestController
@RequestMapping("/xslmes/approvalHandle")
@Slf4j
public class MesXslApprovalHandleController {
@Autowired
private IMesXslApprovalHandleService approvalHandleService;
@Operation(summary = "审批办理-单据详情")
@GetMapping("/detail")
public Result<Map<String, Object>> detail(@RequestParam("instanceId") String instanceId) {
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
Map<String, Object> data = approvalHandleService.detail(instanceId, user);
if (data == null || data.isEmpty()) {
return Result.error("审批实例不存在");
}
return Result.OK(data);
}
@Operation(summary = "审批办理-实时状态(卡片置灰判断)")
@GetMapping("/status")
public Result<Map<String, Object>> status(@RequestParam("instanceId") String instanceId) {
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
Map<String, Object> data = approvalHandleService.statusInfo(instanceId, user);
return Result.OK(data);
}
@Operation(summary = "审批办理-通过")
@PostMapping("/approve")
public Result<String> approve(@RequestBody Map<String, Object> body) {
String instanceId = (String) body.get("instanceId");
String comment = body.get("comment") == null ? null : String.valueOf(body.get("comment"));
if (oConvertUtils.isEmpty(instanceId)) {
return Result.error("缺少审批实例ID");
}
LoginUser user = (LoginUser) SecurityUtils.getSubject().getPrincipal();
return approvalHandleService.approve(instanceId, comment, user);
}
@Operation(summary = "审批办理-驳回")
@PostMapping("/reject")
public Result<String> reject(@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.reject(instanceId, reason, user);
}
}

View File

@@ -0,0 +1,242 @@
package org.jeecg.modules.xslmes.approval.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* MES 审批发起(运行时-本期仅发起)
* 供全局"发起审批"悬浮按钮调用:选单据类型 -> 选单据记录 -> 发起。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】发起审批运行时
*/
@Tag(name = "MES审批发起")
@RestController
@RequestMapping("/xslmes/approvalLaunch")
@Slf4j
public class MesXslApprovalLaunchController {
/** 合法标识符(表名/字段名)白名单校验,防 SQL 注入 */
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z0-9_]+$");
@Autowired
private IMesXslApprovalFlowService approvalFlowService;
@Autowired
private IMesXslApprovalInstanceService approvalInstanceService;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起改用流转引擎进入首节点-----
@Autowired
private IMesXslApprovalHandleService approvalHandleService;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起改用流转引擎进入首节点-----
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 已发布审批流列表(按租户隔离),即"可发起的单据类型"。
* 同时按"功能模块(单据表)"自动反查其菜单路由填入 routePath供前端控制悬浮按钮仅在该功能页显示无需手工配置。
*/
@Operation(summary = "发起审批-已发布审批流列表")
@GetMapping("/publishedList")
public Result<List<MesXslApprovalFlow>> publishedList() {
QueryWrapper<MesXslApprovalFlow> qw = new QueryWrapper<>();
qw.eq("status", "1");
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
if (tenantId != null) {
qw.eq("tenant_id", tenantId);
}
qw.orderByDesc("create_time");
List<MesXslApprovalFlow> list = approvalFlowService.list(qw);
// 未手工指定 route_path 时,按单据表名自动反查菜单路由
for (MesXslApprovalFlow flow : list) {
if (oConvertUtils.isEmpty(flow.getRoutePath())) {
flow.setRoutePath(resolveRoutePathByTable(flow.getBizTable()));
}
}
return Result.OK(list);
}
/**
* 根据单据表名反查对应功能菜单的前端路由。
* 约定jeecg 代码生成的列表组件名为 表名驼峰 + List如 mes_xsl_formula_spec -> MesXslFormulaSpecList
* 对应 sys_permission.component 形如 xslmes/mesXslFormulaSpec/MesXslFormulaSpecList取其 url 即路由。
*/
private String resolveRoutePathByTable(String table) {
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
return null;
}
StringBuilder comp = new StringBuilder();
for (String p : table.split("_")) {
if (oConvertUtils.isEmpty(p)) {
continue;
}
comp.append(Character.toUpperCase(p.charAt(0))).append(p.substring(1));
}
comp.append("List");
String sql = "SELECT url FROM sys_permission WHERE menu_type IN (0,1) "
+ "AND (del_flag = 0 OR del_flag IS NULL) "
+ "AND (component LIKE ? OR component LIKE ?) "
+ "ORDER BY menu_type DESC LIMIT 1";
try {
List<String> urls = jdbcTemplate.queryForList(sql, String.class, "%/" + comp, "%" + comp);
return urls.isEmpty() ? null : urls.get(0);
} catch (Exception e) {
log.warn("反查菜单路由失败 table={}", table, e);
return null;
}
}
/**
* 根据审批流绑定的单据表查询业务单据记录id + 标题),供发起时选择
*/
@Operation(summary = "发起审批-业务单据记录列表")
@GetMapping("/bizRecords")
public Result<List<Map<String, Object>>> bizRecords(
@RequestParam("flowId") String flowId,
@RequestParam(value = "keyword", required = false) String keyword) {
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
if (flow == null) {
return Result.error("审批流不存在");
}
String table = flow.getBizTable();
String titleField = flow.getTitleField();
if (oConvertUtils.isEmpty(table) || !IDENTIFIER.matcher(table).matches()) {
return Result.error("单据表名非法");
}
boolean hasTitle = oConvertUtils.isNotEmpty(titleField) && IDENTIFIER.matcher(titleField).matches();
StringBuilder sql = new StringBuilder("SELECT id, ");
sql.append(hasTitle ? titleField : "id").append(" AS title FROM ").append(table);
List<Object> args = new ArrayList<>();
if (hasTitle && oConvertUtils.isNotEmpty(keyword)) {
sql.append(" WHERE ").append(titleField).append(" LIKE CONCAT('%', ?, '%')");
args.add(keyword);
}
// 主键一定存在,按 id 倒序近似最新;限制条数防全表
sql.append(" ORDER BY id DESC LIMIT 100");
try {
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), args.toArray());
return Result.OK(list);
} catch (Exception e) {
log.error("查询业务单据失败 table={}, field={}", table, titleField, e);
return Result.error("查询业务单据失败:" + e.getMessage());
}
}
/**
* 发起审批:根据审批流定义创建审批实例,解析首个审批节点处理人
*/
@Operation(summary = "发起审批-发起")
@PostMapping("/launch")
public Result<String> launch(@RequestBody Map<String, Object> body) {
String flowId = (String) body.get("flowId");
String bizDataId = (String) body.get("bizDataId");
String bizTitle = (String) body.get("bizTitle");
if (oConvertUtils.isEmpty(flowId) || oConvertUtils.isEmpty(bizDataId)) {
return Result.error("请选择审批流和单据");
}
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
if (flow == null) {
return Result.error("审批流不存在");
}
if (!"1".equals(flow.getStatus())) {
return Result.error("该审批流未发布,无法发起");
}
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
approvalInstanceService.save(inst);
approvalHandleService.enterFirstNode(inst, loginUser);
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
return Result.OK("发起成功!");
}
/**
* 批量发起审批:用于列表多选后一次性发起
*/
@Operation(summary = "发起审批-批量发起")
@PostMapping("/launchBatch")
public Result<String> launchBatch(@RequestBody Map<String, Object> body) {
String flowId = (String) body.get("flowId");
Object itemsObj = body.get("items");
if (oConvertUtils.isEmpty(flowId) || !(itemsObj instanceof List) || ((List<?>) itemsObj).isEmpty()) {
return Result.error("请选择审批流和单据");
}
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
if (flow == null) {
return Result.error("审批流不存在");
}
if (!"1".equals(flow.getStatus())) {
return Result.error("该审批流未发布,无法发起");
}
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
int count = 0;
for (Object o : (List<?>) itemsObj) {
if (!(o instanceof Map)) {
continue;
}
Map<?, ?> item = (Map<?, ?>) o;
String bizDataId = item.get("bizDataId") == null ? null : String.valueOf(item.get("bizDataId"));
String bizTitle = item.get("bizTitle") == null ? null : String.valueOf(item.get("bizTitle"));
if (oConvertUtils.isEmpty(bizDataId)) {
continue;
}
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
approvalInstanceService.save(inst);
approvalHandleService.enterFirstNode(inst, loginUser);
count++;
}
if (count == 0) {
return Result.error("没有有效的单据数据");
}
return Result.OK("已发起 " + count + " 条审批!");
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】批量发起逐条进入首节点(支持按单据字段解析处理人/会签)-----
}
/**
* 构建一条审批实例(基础字段;处理人解析/卡片由流转引擎完成)
*/
private MesXslApprovalInstance buildInstance(MesXslApprovalFlow flow, String bizDataId, String bizTitle, LoginUser loginUser) {
MesXslApprovalInstance inst = new MesXslApprovalInstance();
inst.setFlowId(flow.getId());
inst.setFlowName(flow.getFlowName());
inst.setBizTable(flow.getBizTable());
inst.setBizTableName(flow.getBizTableName());
inst.setBizDataId(bizDataId);
inst.setBizTitle(oConvertUtils.isNotEmpty(bizTitle) ? bizTitle : bizDataId);
inst.setStatus("0");
if (loginUser != null) {
inst.setApplyUser(loginUser.getUsername());
inst.setApplyUserName(loginUser.getRealname());
}
inst.setApplyTime(new Date());
inst.setTenantId(MesXslTenantUtils.resolveTenantId(flow.getTenantId()));
// 处理人解析、节点进度初始化、卡片发送统一由 IMesXslApprovalHandleService.enterFirstNode 完成
return inst;
}
}

View File

@@ -0,0 +1,68 @@
package org.jeecg.modules.xslmes.approval.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.system.base.entity.JeecgEntity;
import java.io.Serializable;
/**
* MES 审批流定义
* 钉钉式可视化审批流设计,绑定 MES 业务单据。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】新增审批流可视化设计
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("mes_xsl_approval_flow")
@Schema(description = "MES审批流定义")
public class MesXslApprovalFlow extends JeecgEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "审批流名称")
private String flowName;
@Schema(description = "绑定单据表名")
@Dict(dicCode = "mes_xsl_approval_biz_doc")
private String bizTable;
@Schema(description = "绑定单据中文名(冗余展示)")
private String bizTableName;
@Schema(description = "单据标题字段名(发起选单据时展示)")
private String titleField;
@Schema(description = "对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)")
private String routePath;
@Schema(description = "流程设计JSON(钉钉式节点树)")
private String flowConfig;
@Schema(description = "状态0草稿 1已发布 2已停用")
@Dict(dicCode = "mes_xsl_approval_flow_status")
private String status;
@Schema(description = "排序")
private Double sortNo;
@Schema(description = "备注")
private String remark;
@Schema(description = "逻辑删除0正常 1已删除")
@TableLogic
private Integer delFlag;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "所属部门编码")
private String sysOrgCode;
}

View File

@@ -0,0 +1,95 @@
package org.jeecg.modules.xslmes.approval.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecg.common.system.base.entity.JeecgEntity;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* MES 审批实例(本期仅记录发起,不含办理)
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】发起审批运行时
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("mes_xsl_approval_instance")
@Schema(description = "MES审批实例")
public class MesXslApprovalInstance extends JeecgEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "审批流定义ID")
private String flowId;
@Schema(description = "审批流名称")
private String flowName;
@Schema(description = "业务单据表名")
private String bizTable;
@Schema(description = "业务单据中文名")
private String bizTableName;
@Schema(description = "业务单据记录ID")
private String bizDataId;
@Schema(description = "业务单据展示标题")
private String bizTitle;
@Schema(description = "当前节点ID")
private String currentNodeId;
@Schema(description = "当前节点名称")
private String currentNodeName;
@Schema(description = "当前处理人(username逗号分隔)")
private String currentHandlers;
@Schema(description = "当前处理人展示文本")
private String currentHandlersText;
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】审批办理/流转(会签/或签/依次)进度与历史-----
@Schema(description = "当前节点处理进度JSON(nodeId/mode/tasks)")
private String nodeProgress;
@Schema(description = "审批历史JSON数组")
private String history;
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】审批办理/流转(会签/或签/依次)进度与历史-----
@Schema(description = "状态0审批中 1已通过 2已驳回 3已撤销")
@Dict(dicCode = "mes_xsl_approval_instance_status")
private String status;
@Schema(description = "发起人username")
private String applyUser;
@Schema(description = "发起人姓名")
private String applyUserName;
@Schema(description = "发起时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date applyTime;
@Schema(description = "备注")
private String remark;
@Schema(description = "逻辑删除0正常 1已删除")
@TableLogic
private Integer delFlag;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "所属部门编码")
private String sysOrgCode;
}

View File

@@ -0,0 +1,15 @@
package org.jeecg.modules.xslmes.approval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
/**
* MES 审批流定义 Mapper
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】新增审批流可视化设计
*/
@Mapper
public interface MesXslApprovalFlowMapper extends BaseMapper<MesXslApprovalFlow> {
}

View File

@@ -0,0 +1,15 @@
package org.jeecg.modules.xslmes.approval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
/**
* MES 审批实例 Mapper
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】发起审批运行时
*/
@Mapper
public interface MesXslApprovalInstanceMapper extends BaseMapper<MesXslApprovalInstance> {
}

View File

@@ -0,0 +1,13 @@
package org.jeecg.modules.xslmes.approval.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
/**
* MES 审批流定义 Service
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】新增审批流可视化设计
*/
public interface IMesXslApprovalFlowService extends IService<MesXslApprovalFlow> {
}

View File

@@ -0,0 +1,44 @@
package org.jeecg.modules.xslmes.approval.service;
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.Map;
/**
* MES 审批办理/流转引擎。
* 负责:发起后进入首节点、审批通过/驳回的流转推进(支持会签/或签/依次)、查看单据全字段详情。
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】审批办理/流转
*/
public interface IMesXslApprovalHandleService {
/**
* 发起后进入首个审批节点:解析处理人、初始化节点进度、发送审批卡片。
* 实例需已先行 save带 id
*/
void enterFirstNode(MesXslApprovalInstance inst, LoginUser applyUser);
/**
* 审批通过:标记当前处理人任务完成,按节点 multiMode 判断是否流转到下一节点。
*/
Result<String> approve(String instanceId, String comment, LoginUser user);
/**
* 驳回:任一处理人驳回即终止流程,通知发起人。
*/
Result<String> reject(String instanceId, String reason, LoginUser user);
/**
* 查看单据全部字段 + 审批进度/历史,供 IM 卡片"查看详情"弹窗使用。
*/
Map<String, Object> detail(String instanceId, LoginUser user);
/**
* 轻量状态查询:供 IM 卡片实时判断是否仍可办理(旧节点卡片置灰)。
* 返回 status/statusText/currentNodeId/currentNodeName/canApprove。
*/
Map<String, Object> statusInfo(String instanceId, LoginUser user);
}

View File

@@ -0,0 +1,13 @@
package org.jeecg.modules.xslmes.approval.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
/**
* MES 审批实例 Service
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】发起审批运行时
*/
public interface IMesXslApprovalInstanceService extends IService<MesXslApprovalInstance> {
}

View File

@@ -0,0 +1,17 @@
package org.jeecg.modules.xslmes.approval.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
import org.jeecg.modules.xslmes.approval.mapper.MesXslApprovalFlowMapper;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
import org.springframework.stereotype.Service;
/**
* MES 审批流定义 ServiceImpl
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】新增审批流可视化设计
*/
@Service
public class MesXslApprovalFlowServiceImpl extends ServiceImpl<MesXslApprovalFlowMapper, MesXslApprovalFlow> implements IMesXslApprovalFlowService {
}

View File

@@ -0,0 +1,17 @@
package org.jeecg.modules.xslmes.approval.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalInstance;
import org.jeecg.modules.xslmes.approval.mapper.MesXslApprovalInstanceMapper;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
import org.springframework.stereotype.Service;
/**
* MES 审批实例 ServiceImpl
*
* @author GHT
* @date 2026-05-29 for【QH-MES审批流设计】发起审批运行时
*/
@Service
public class MesXslApprovalInstanceServiceImpl extends ServiceImpl<MesXslApprovalInstanceMapper, MesXslApprovalInstance> implements IMesXslApprovalInstanceService {
}

View File

@@ -48,6 +48,21 @@ public interface ISysImChatService {
SysImMessageVO sendMessage(String userId, Integer tenantId, SysImSendMessageDTO dto);
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)-----
/**
* 系统消息:以 fromUser 身份给 toUser 发送单聊消息。
* 与 sendMessage 区别:自动获取/创建单聊会话,且不做"同部门/同租户聊天"校验,专供审批通知等系统场景。
*
* @param fromUserId 发送人用户ID一般为业务发起人
* @param toUserId 接收人用户ID
* @param tenantId 租户ID
* @param content 消息内容
* @param msgType 消息类型 text/biz_record 等,空则按 text
* @return 消息VO发送失败返回 null
*/
SysImMessageVO sendSystemSingleMessage(String fromUserId, String toUserId, Integer tenantId, String content, String msgType);
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】系统单聊消息(绕过同部门校验,供审批等系统通知场景)-----
void markRead(String userId, String conversationId);

View File

@@ -473,6 +473,57 @@ public class SysImChatServiceImpl implements ISysImChatService {
}
//update-end---author:cursor ---date:20260528 for【IM聊天-OA】发送消息-----------
//update-begin---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】系统单聊消息(绕过同部门校验)-----
@Override
@Transactional(rollbackFor = Exception.class)
public SysImMessageVO sendSystemSingleMessage(String fromUserId, String toUserId, Integer tenantId, String content, String msgType) {
if (oConvertUtils.isEmpty(fromUserId) || oConvertUtils.isEmpty(toUserId) || oConvertUtils.isEmpty(content)) {
return null;
}
// 不给自己发送
if (fromUserId.equals(toUserId)) {
return null;
}
Integer tenant = tenantId == null ? 0 : tenantId;
Date now = new Date();
// 获取或创建单聊会话(系统通知场景,不做同部门校验)
String pairKey = buildPairKey(fromUserId, toUserId);
SysImConversation conversation = conversationMapper.selectOne(new LambdaQueryWrapper<SysImConversation>()
.eq(SysImConversation::getTenantId, tenant)
.eq(SysImConversation::getUserPairKey, pairKey));
if (conversation == null) {
conversation = new SysImConversation();
conversation.setConvType(CONV_TYPE_SINGLE);
conversation.setUserPairKey(pairKey);
conversation.setTenantId(tenant);
conversation.setCreateBy(fromUserId);
conversation.setCreateTime(now);
conversation.setUpdateTime(now);
conversationMapper.insert(conversation);
createMember(conversation.getId(), fromUserId, now);
createMember(conversation.getId(), toUserId, now);
}
// 写入消息
SysImMessage message = new SysImMessage();
message.setConversationId(conversation.getId());
message.setSenderId(fromUserId);
message.setContent(content.trim());
message.setMsgType(oConvertUtils.isEmpty(msgType) ? MSG_TYPE_TEXT : msgType);
message.setTenantId(tenant);
message.setCreateTime(now);
messageMapper.insert(message);
// 更新会话摘要与未读、推送
conversation.setLastContent(truncate(resolveLastContent(message.getMsgType(), message.getContent()), 200));
conversation.setLastTime(now);
conversation.setUpdateTime(now);
conversationMapper.updateById(conversation);
memberMapper.incrementUnreadExceptSender(conversation.getId(), fromUserId);
SysImMessageVO messageVo = toMessageVo(message, fromUserId);
pushChatMessage(conversation.getId(), fromUserId, messageVo, conversation.getConvType());
return messageVo;
}
//update-end---author:GHT ---date:2026-05-29 for【QH-MES审批流设计】系统单聊消息(绕过同部门校验)-----
//update-begin---author:cursor ---date:20260528 for【IM聊天-OA】标记已读-----------
@Override
@Transactional(rollbackFor = Exception.class)

View File

@@ -0,0 +1,97 @@
-- QH-MES审批流设计审批流定义表 + 可审批单据字典 + 我的租户菜单与授权
SET NAMES utf8mb4;
-- 1) 审批流定义表
CREATE TABLE IF NOT EXISTS `mes_xsl_approval_flow` (
`id` varchar(32) NOT NULL COMMENT '主键',
`flow_name` varchar(100) NOT NULL COMMENT '审批流名称',
`biz_table` varchar(100) NOT NULL COMMENT '绑定单据表名',
`biz_table_name` varchar(200) DEFAULT NULL COMMENT '绑定单据中文名(冗余展示)',
`flow_config` longtext COMMENT '流程设计JSON(钉钉式节点树)',
`status` varchar(1) DEFAULT '0' COMMENT '状态 0草稿 1已发布 2已停用',
`sort_no` double DEFAULT '0' COMMENT '排序',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`del_flag` int DEFAULT '0' COMMENT '逻辑删除 0正常 1已删除',
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门编码',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_appr_flow_tenant_biz` (`tenant_id`, `biz_table`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES审批流定义表';
-- 2) 可审批单据字典(单据来源:MES现有业务表) item_value=表名 item_text=单据中文名
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
VALUES ('1995000000000000310', '可审批单据', 'mes_xsl_approval_biz_doc', 'MES审批流可绑定的业务单据', 0, 'admin', NOW(), 0, 0);
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES
('1995000000000000311', '1995000000000000310', '配合示方', 'mes_xsl_formula_spec', '配合示方主表', 1, 1, 'admin', NOW()),
('1995000000000000312', '1995000000000000310', '混炼示方', 'mes_xsl_mixing_spec', '混炼示方主表', 2, 1, 'admin', NOW()),
('1995000000000000313', '1995000000000000310', '密炼PS编制', 'mes_xsl_mixer_ps_compile', '密炼PS编制', 3, 1, 'admin', NOW()),
('1995000000000000314', '1995000000000000310', '胶料快检标准', 'mes_xsl_rubber_quick_test_std', '胶料快检实验标准', 4, 1, 'admin', NOW()),
('1995000000000000315', '1995000000000000310', '原料入场记录', 'mes_xsl_raw_material_entry', '原料入场记录', 5, 1, 'admin', NOW());
-- 2.1) 审批流状态字典
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
VALUES ('1995000000000000320', '审批流状态', 'mes_xsl_approval_flow_status', 'MES审批流定义状态', 0, 'admin', NOW(), 0, 0);
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES
('1995000000000000321', '1995000000000000320', '草稿', '0', '草稿', 1, 1, 'admin', NOW()),
('1995000000000000322', '1995000000000000320', '已发布', '1', '已发布', 2, 1, 'admin', NOW()),
('1995000000000000323', '1995000000000000320', '已停用', '2', '已停用', 3, 1, 'admin', NOW());
-- 3) 我的租户 -> 审批流设计 菜单(parent_id=我的租户 1674708136602542082)
INSERT IGNORE INTO `sys_permission` (
`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`,
`menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`,
`hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`,
`del_flag`, `rule_flag`, `status`, `internal_or_external`
) VALUES (
'1995000000000000301', '1674708136602542082', '审批流设计', '/approval/ApprovalFlowList',
'approval/flow/ApprovalFlowList', 1, 'ApprovalFlowList', NULL,
1, NULL, '0', 4.00, 0, 'ant-design:partition-outlined', 0, 1,
0, 0, '租户审批流可视化设计', 'admin', NOW(), 'admin', NOW(),
0, 0, '1', 0
);
-- 按钮权限
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('1995000000000000302', '1995000000000000301', '查询', 2, 'approval:flow:list', '1', 1.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('1995000000000000303', '1995000000000000301', '新增', 2, 'approval:flow:add', '1', 2.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('1995000000000000304', '1995000000000000301', '编辑', 2, 'approval:flow:edit', '1', 3.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('1995000000000000305', '1995000000000000301', '删除', 2, 'approval:flow:delete', '1', 4.00, 0, 1, 0, '1', 0, 'admin', NOW());
INSERT IGNORE INTO `sys_permission` (`id`, `parent_id`, `name`, `menu_type`, `perms`, `perms_type`, `sort_no`, `is_route`, `is_leaf`, `hidden`, `status`, `del_flag`, `create_by`, `create_time`)
VALUES ('1995000000000000306', '1995000000000000301', '设计/发布', 2, 'approval:flow:design', '1', 5.00, 0, 1, 0, '1', 0, 'admin', NOW());
-- 4) 授权给超级管理员 admin
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
FROM `sys_role` r
CROSS JOIN `sys_permission` p
WHERE r.`role_code` = 'admin'
AND p.`id` IN ('1995000000000000301','1995000000000000302','1995000000000000303','1995000000000000304','1995000000000000305','1995000000000000306')
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);
-- 5) 授权给租户管理员 zuhuadmin(挂在"我的租户"下必须授权,否则租户管理员看不到)
INSERT INTO `sys_role_permission` (`id`, `role_id`, `permission_id`, `data_rule_ids`, `operate_date`, `operate_ip`)
SELECT REPLACE(UUID(), '-', ''), r.id, p.id, NULL, NOW(), '127.0.0.1'
FROM `sys_role` r
CROSS JOIN `sys_permission` p
WHERE r.`role_code` = 'zuhuadmin'
AND p.`id` IN ('1995000000000000301','1995000000000000302','1995000000000000303','1995000000000000304','1995000000000000305','1995000000000000306')
AND NOT EXISTS (
SELECT 1 FROM `sys_role_permission` rp WHERE rp.`role_id` = r.`id` AND rp.`permission_id` = p.`id`
);

View File

@@ -0,0 +1,47 @@
-- QH-MES审批流设计审批实例表 + 审批流定义增加单据标题字段
SET NAMES utf8mb4;
-- 1) 审批流定义表增加"单据标题字段名"发起时用于展示具体单据
ALTER TABLE `mes_xsl_approval_flow`
ADD COLUMN `title_field` varchar(100) DEFAULT NULL COMMENT '单据标题字段名(发起选单据时展示)' AFTER `biz_table_name`;
-- 2) 审批实例表(本期仅发起记录实例与当前处理人)
CREATE TABLE IF NOT EXISTS `mes_xsl_approval_instance` (
`id` varchar(32) NOT NULL COMMENT '主键',
`flow_id` varchar(32) NOT NULL COMMENT '审批流定义ID',
`flow_name` varchar(100) DEFAULT NULL COMMENT '审批流名称',
`biz_table` varchar(100) DEFAULT NULL COMMENT '业务单据表名',
`biz_table_name` varchar(200) DEFAULT NULL COMMENT '业务单据中文名',
`biz_data_id` varchar(64) DEFAULT NULL COMMENT '业务单据记录ID',
`biz_title` varchar(300) DEFAULT NULL COMMENT '业务单据展示标题',
`current_node_id` varchar(64) DEFAULT NULL COMMENT '当前节点ID',
`current_node_name` varchar(100) DEFAULT NULL COMMENT '当前节点名称',
`current_handlers` varchar(1000) DEFAULT NULL COMMENT '当前处理人(username逗号分隔)',
`current_handlers_text` varchar(1000) DEFAULT NULL COMMENT '当前处理人展示文本',
`status` varchar(2) DEFAULT '0' COMMENT '状态 0审批中 1已通过 2已驳回 3已撤销',
`apply_user` varchar(50) DEFAULT NULL COMMENT '发起人username',
`apply_user_name` varchar(100) DEFAULT NULL COMMENT '发起人姓名',
`apply_time` datetime DEFAULT NULL COMMENT '发起时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`del_flag` int DEFAULT '0' COMMENT '逻辑删除 0正常 1已删除',
`tenant_id` int DEFAULT NULL COMMENT '租户ID',
`sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门编码',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_appr_inst_tenant` (`tenant_id`, `flow_id`),
KEY `idx_appr_inst_apply` (`apply_user`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES审批实例表';
-- 3) 审批实例状态字典
INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
VALUES ('1995000000000000330', '审批实例状态', 'mes_xsl_approval_instance_status', 'MES审批实例状态', 0, 'admin', NOW(), 0, 0);
INSERT IGNORE INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`)
VALUES
('1995000000000000331', '1995000000000000330', '审批中', '0', '审批中', 1, 1, 'admin', NOW()),
('1995000000000000332', '1995000000000000330', '已通过', '1', '已通过', 2, 1, 'admin', NOW()),
('1995000000000000333', '1995000000000000330', '已驳回', '2', '已驳回', 3, 1, 'admin', NOW()),
('1995000000000000334', '1995000000000000330', '已撤销', '3', '已撤销', 4, 1, 'admin', NOW());

View File

@@ -0,0 +1,5 @@
-- QH-MES审批流设计审批流定义增加"功能页面路由"用于控制发起审批悬浮按钮仅在对应功能页显示
SET NAMES utf8mb4;
ALTER TABLE `mes_xsl_approval_flow`
ADD COLUMN `route_path` varchar(255) DEFAULT NULL COMMENT '对应功能页面前端路由(发起审批悬浮按钮仅在该页显示)' AFTER `title_field`;

View File

@@ -0,0 +1,6 @@
-- QH-MES审批流设计审批办理/流转实例表增加当前节点处理进度与历史(支持会签/或签/依次)
SET NAMES utf8mb4;
ALTER TABLE `mes_xsl_approval_instance`
ADD COLUMN `node_progress` longtext COMMENT '当前节点处理进度JSON(nodeId/mode/tasks)' AFTER `current_handlers_text`,
ADD COLUMN `history` longtext COMMENT '审批历史JSON数组' AFTER `node_progress`;