vals = jdbcTemplate.queryForList(
+ "SELECT " + statusField + " FROM " + record.getBizTable()
+ + " WHERE id=? LIMIT 1", String.class, record.getBizDataId());
+ return !vals.isEmpty() && java.util.Objects.equals(originStatus, vals.get(0));
+ } catch (Exception e) {
+ log.warn("[DingBpms] 读取业务状态失败 table={}: {}", record.getBizTable(), e.getMessage());
+ return false;
+ }
+ }
+
+ private ApprovalCallbackContext buildContext(MesXslApprovalRecord record, String comment) {
+ return new ApprovalCallbackContext()
+ .setInstanceId(record.getId())
+ .setFlowId(record.getFlowId())
+ .setFlowName(record.getFlowName())
+ .setBizTable(record.getBizTable())
+ .setBizTableName(record.getBizTableName())
+ .setBizDataId(record.getBizDataId())
+ .setBizTitle(record.getBizTitle())
+ .setApplyUser(record.getApplyUser())
+ .setComment(comment)
+ .setOperatorUsername("dingtalk")
+ .setOperatorName("钉钉审批");
+ }
+}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java
new file mode 100644
index 0000000..f0859c7
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java
@@ -0,0 +1,151 @@
+package org.jeecg.modules.xslmes.dingtalk.stream;
+
+import com.dingtalk.open.app.api.GenericEventListener;
+import com.dingtalk.open.app.api.OpenDingTalkStreamClientBuilder;
+import com.dingtalk.open.app.api.message.GenericOpenDingTalkEvent;
+import com.dingtalk.open.app.api.security.AuthClientCredential;
+import com.dingtalk.open.app.stream.protocol.event.EventAckStatus;
+import lombok.extern.slf4j.Slf4j;
+import org.jeecg.common.util.oConvertUtils;
+import org.jeecg.modules.system.service.impl.ThirdAppDingtalkServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.SmartLifecycle;
+import org.springframework.stereotype.Component;
+
+/**
+ * 钉钉 Stream 模式事件接收客户端(基于官方 SDK com.dingtalk.open:dingtalk-stream)。
+ *
+ * 无需注册公网回调地址:应用主动建立长连接,钉钉通过该通道推送事件(如审批结果),
+ * 官方 SDK 内部自动维护重连与心跳。
+ *
+ * 启动时机:{@link SmartLifecycle}(phase=MAX-100)确保 Spring 上下文完全就绪后再建连。
+ *
+ * @author GHT
+ * @date 2026-06-04 for:【钉钉Stream回调】基于官方SDK的Stream客户端
+ */
+@Slf4j
+@Component
+public class DingTalkStreamClient implements SmartLifecycle {
+
+ @Autowired
+ private ThirdAppDingtalkServiceImpl dingtalkService;
+
+ @Autowired
+ private DingBpmsEventProcessor bpmsEventProcessor;
+
+ private volatile boolean running = false;
+
+ // ==================== SmartLifecycle ====================
+
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】应用启动后自动建立Stream连接-----
+ @Override
+ public int getPhase() {
+ // 最后启动,确保 DB / Spring Bean 全部就绪
+ return Integer.MAX_VALUE - 100;
+ }
+
+ @Override
+ public void start() {
+ running = true;
+ // 在后台线程初始化,避免阻塞 Spring 上下文刷新
+ Thread t = new Thread(this::initSdkClient, "ding-stream");
+ t.setDaemon(true);
+ t.start();
+ }
+
+ @Override
+ public void stop() {
+ running = false;
+ // SDK 内部使用 daemon 线程,JVM 退出时自动终止
+ log.info("[DingStream] 钉钉 Stream 客户端已停止");
+ }
+
+ @Override
+ public boolean isRunning() {
+ return running;
+ }
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】应用启动后自动建立Stream连接-----
+
+ // ==================== SDK 初始化(官方写法)====================
+
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】使用官方SDK建立Stream长连接-----
+ private void initSdkClient() {
+ try {
+ String[] creds = dingtalkService.getDingAppCredentials();
+ if (creds == null || oConvertUtils.isEmpty(creds[0]) || oConvertUtils.isEmpty(creds[1])) {
+ log.warn("[DingStream] 钉钉 AppKey/AppSecret 未配置,Stream 连接未启动。"
+ + "请在【系统配置-第三方应用】中完成钉钉应用配置后重启服务。");
+ return;
+ }
+
+ log.info("[DingStream] 正在建立钉钉 Stream 连接,AppKey={}", creds[0]);
+
+ // 官方写法:build().start() 链式调用,SDK 内部管理长连接与重连
+ OpenDingTalkStreamClientBuilder
+ .custom()
+ .credential(new AuthClientCredential(creds[0], creds[1]))
+ .registerAllEventListener(new GenericEventListener() {
+ @Override
+ public EventAckStatus onEvent(GenericOpenDingTalkEvent event) {
+ return handleEvent(event);
+ }
+ })
+ .build()
+ .start();
+
+ log.info("[DingStream] 钉钉 Stream 客户端已启动,等待审批事件推送");
+
+ } catch (Exception e) {
+ log.error("[DingStream] SDK 启动失败,请检查钉钉配置: {}", e.getMessage(), e);
+ }
+ }
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】使用官方SDK建立Stream长连接-----
+
+ // ==================== 事件处理 ====================
+
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】处理事件并回写审批台账-----
+ private EventAckStatus handleEvent(GenericOpenDingTalkEvent event) {
+ try {
+ String eventType = event.getEventType();
+ log.debug("[DingStream] 收到事件 eventId={} eventType={} bornTime={}",
+ event.getEventId(), eventType, event.getEventBornTime());
+
+ if (!"bpms_instance_change".equals(eventType) && !"bpms_task_change".equals(eventType)) {
+ return EventAckStatus.SUCCESS;
+ }
+
+ // getData() 返回 fastjson2 JSONObject
+ com.alibaba.fastjson2.JSONObject data = toJsonObject(event.getData());
+ if (data == null) {
+ log.warn("[DingStream] 事件 data 为空,eventType={}", eventType);
+ return EventAckStatus.SUCCESS;
+ }
+
+ if ("bpms_instance_change".equals(eventType)) {
+ bpmsEventProcessor.onInstanceChange(data);
+ } else {
+ bpmsEventProcessor.onTaskChange(data);
+ }
+
+ return EventAckStatus.SUCCESS;
+
+ } catch (Exception e) {
+ log.error("[DingStream] 事件处理异常 eventType={}: {}", event.getEventType(), e.getMessage(), e);
+ // LATER:通知钉钉稍后重推,避免丢失事件
+ return EventAckStatus.LATER;
+ }
+ }
+
+ private static com.alibaba.fastjson2.JSONObject toJsonObject(Object raw) {
+ if (raw == null) return null;
+ if (raw instanceof com.alibaba.fastjson2.JSONObject) {
+ return (com.alibaba.fastjson2.JSONObject) raw;
+ }
+ try {
+ return com.alibaba.fastjson2.JSONObject.parseObject(String.valueOf(raw));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】处理事件并回写审批台账-----
+}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkWorkflowService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkWorkflowService.java
new file mode 100644
index 0000000..377ad60
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkWorkflowService.java
@@ -0,0 +1,204 @@
+package org.jeecg.modules.xslmes.dingtalk.stream;
+
+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.system.util.JwtUtil;
+import org.jeecg.common.util.RedisUtil;
+import org.jeecg.common.util.oConvertUtils;
+import org.jeecg.modules.system.entity.SysUser;
+import org.jeecg.modules.system.service.ISysUserService;
+import org.jeecg.modules.system.service.impl.ThirdAppDingtalkServiceImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 钉钉审批工作流 API 封装。
+ * 提供「获取审批实例详情」和「审批人身份 Token 生成」两个核心能力,
+ * 供 {@link DingBpmsEventProcessor} 在收到事件后主动查询完整实例数据。
+ *
+ * @author GHT
+ * @date 2026-06-04 for:【钉钉Stream回调】主动拉取审批实例详情,精准映射节点与操作人
+ */
+@Slf4j
+@Service
+public class DingTalkWorkflowService {
+
+ private static final String PROCESS_INSTANCE_URL =
+ "https://api.dingtalk.com/v1.0/workflow/processInstances";
+
+ @Autowired
+ private ThirdAppDingtalkServiceImpl dingtalkService;
+
+ @Autowired
+ private ISysUserService sysUserService;
+
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ private RedisUtil redisUtil;
+
+ // ==================== 审批实例详情 ====================
+
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】拉取钉钉审批实例详情-----
+ /**
+ * 调用 GET /v1.0/workflow/processInstances 获取审批实例完整数据。
+ *
+ * @param processInstanceId 钉钉审批实例 ID
+ * @return result 节点(含 status/result/operationRecords/tasks/formComponentValues 等),失败返回 null
+ */
+ public JSONObject getProcessInstance(String processInstanceId) {
+ if (oConvertUtils.isEmpty(processInstanceId)) {
+ return null;
+ }
+ // 后台线程无 TenantContext,必须用绕过租户校验的专用方法
+ String accessToken = dingtalkService.getAccessTokenForBackground();
+ if (oConvertUtils.isEmpty(accessToken)) {
+ log.warn("[DingWorkflow] AccessToken 获取失败,无法查询审批实例 {}", processInstanceId);
+ return null;
+ }
+ try {
+ HttpClient client = HttpClient.newBuilder()
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+ HttpRequest req = HttpRequest.newBuilder()
+ .uri(URI.create(PROCESS_INSTANCE_URL + "?processInstanceId=" + processInstanceId))
+ .header("x-acs-dingtalk-access-token", accessToken)
+ .GET()
+ .timeout(Duration.ofSeconds(10))
+ .build();
+ String body = client.send(req, HttpResponse.BodyHandlers.ofString()).body();
+ JSONObject resp = JSONObject.parseObject(body);
+ if (resp.containsKey("code")) {
+ log.warn("[DingWorkflow] 查询审批实例失败 instanceId={} code={} msg={}",
+ processInstanceId, resp.getString("code"), resp.getString("message"));
+ return null;
+ }
+ return resp.getJSONObject("result");
+ } catch (Exception e) {
+ log.error("[DingWorkflow] 调用钉钉审批实例详情接口异常 instanceId={}: {}",
+ processInstanceId, e.getMessage(), e);
+ return null;
+ }
+ }
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】拉取钉钉审批实例详情-----
+
+ // ==================== operationRecords 解析 ====================
+
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】从operationRecords提取节点操作序列-----
+ /**
+ * 从审批实例的 operationRecords 中提取所有"正常任务执行"记录(type=EXECUTE_TASK_NORMAL),
+ * 按时间顺序排列,每条记录对应一个审批节点的操作(通过/拒绝)。
+ *
+ * 返回的列表索引与 MES 流程节点列表索引一一对应:第0条 = 第1个节点,第1条 = 第2个节点,以此类推。
+ */
+ public List getTaskOperations(JSONObject instanceResult) {
+ List ops = new ArrayList<>();
+ if (instanceResult == null) {
+ return ops;
+ }
+ JSONArray records = instanceResult.getJSONArray("operationRecords");
+ if (records == null || records.isEmpty()) {
+ return ops;
+ }
+ for (int i = 0; i < records.size(); i++) {
+ JSONObject rec = records.getJSONObject(i);
+ if (rec == null) continue;
+ String type = rec.getString("type");
+ // 只取正常节点执行(过滤掉发起、转交、评论等操作)
+ if ("EXECUTE_TASK_NORMAL".equals(type) || "EXECUTE_TASK_AGENT".equals(type)) {
+ ops.add(rec);
+ }
+ }
+ return ops;
+ }
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】从operationRecords提取节点操作序列-----
+
+ // ==================== 操作人身份 Token ====================
+
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】按钉钉userId生成操作人JWT Token-----
+ /**
+ * 将钉钉 userId 映射到 MES 系统用户,并生成该用户的 JWT Token。
+ *
+ * 查询链:sys_third_account(third_user_id=dtUserId) → sys_user_id → sys_user.username/password
+ *
+ * 这样回调业务接口时,接口内部通过 {@code SecurityUtils.getSubject().getPrincipal()}
+ * 拿到的就是真实审批人,而非 admin,保证 proofread_by/audit_by/approve_by 字段写入正确。
+ *
+ * @param dtUserId 钉钉 userId(来自 operationRecord.userId)
+ * @return JWT token;找不到绑定或发生异常时返回 null(调用方降级用 admin token)
+ */
+ public String generateTokenByDtUserId(String dtUserId) {
+ if (oConvertUtils.isEmpty(dtUserId)) {
+ return null;
+ }
+ try {
+ // ① 钉钉userId → MES sys_user_id
+ List userIds = jdbcTemplate.queryForList(
+ "SELECT sys_user_id FROM sys_third_account " +
+ "WHERE third_type='dingtalk' AND third_user_id=? AND (del_flag=0 OR del_flag IS NULL) LIMIT 1",
+ String.class, dtUserId);
+ if (userIds.isEmpty() || oConvertUtils.isEmpty(userIds.get(0))) {
+ log.debug("[DingWorkflow] 钉钉用户 {} 未绑定 MES 账号,降级使用 admin token", dtUserId);
+ return generateAdminToken();
+ }
+ // ② sys_user_id → username + password
+ SysUser user = sysUserService.getById(userIds.get(0));
+ if (user == null || oConvertUtils.isEmpty(user.getPassword())) {
+ return generateAdminToken();
+ }
+ return signAndCache(user.getUsername(), user.getPassword());
+ } catch (Exception e) {
+ log.warn("[DingWorkflow] 生成用户 token 失败 dtUserId={}: {}", dtUserId, e.getMessage());
+ return generateAdminToken();
+ }
+ }
+
+ /** 生成 admin 系统 token,用于钉钉用户未绑定 MES 账号时的兜底 */
+ public String generateAdminToken() {
+ try {
+ SysUser admin = sysUserService.getUserByName("admin");
+ if (admin == null || oConvertUtils.isEmpty(admin.getPassword())) {
+ log.warn("[DingWorkflow] admin 用户不存在,无法生成系统 token");
+ return null;
+ }
+ return signAndCache(admin.getUsername(), admin.getPassword());
+ } catch (Exception e) {
+ log.warn("[DingWorkflow] 生成 admin token 失败: {}", e.getMessage());
+ return null;
+ }
+ }
+
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】生成token后写入Redis,通过Shiro校验-----
+ /**
+ * 生成 JWT Token 并写入 Redis(key=PREFIX_USER_TOKEN+token,value=token)。
+ *
+ * JeecgBoot Shiro 在 {@code jwtTokenRefresh()} 中验证 token 时,
+ * 必须从 Redis {@code "prefix_user_token:" + token} 取到缓存值,
+ * 否则报 "Token失效,请重新登录"。新生成的 token 需要手动写入 Redis 才能通过校验。
+ *
+ * TTL 设置为 JwtUtil.EXPIRE_TIME / 1000(秒),与正常登录保持一致。
+ */
+ private String signAndCache(String username, String password) {
+ String token = JwtUtil.sign(username, password);
+ if (oConvertUtils.isNotEmpty(token)) {
+ // 写入 Redis,让 Shiro jwtTokenRefresh 能查到
+ long ttlSeconds = JwtUtil.EXPIRE_TIME / 1000;
+ redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token, ttlSeconds);
+ }
+ return token;
+ }
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】生成token后写入Redis,通过Shiro校验-----
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】按钉钉userId生成操作人JWT Token-----
+}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesAlarmController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesAlarmController.java
index b81e577..56fc6b2 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesAlarmController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesAlarmController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesAutoXlLogController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesAutoXlLogController.java
index d78d068..5d915dc 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesAutoXlLogController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesAutoXlLogController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesBinToMaterController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesBinToMaterController.java
index e9db809..82db6ee 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesBinToMaterController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesBinToMaterController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesCheckScaleLogController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesCheckScaleLogController.java
index dd7f630..51a706c 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesCheckScaleLogController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesCheckScaleLogController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixActController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixActController.java
index 2c5a525..a060eb6 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixActController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixActController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixAlarmController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixAlarmController.java
index 616d6a6..3a4ad72 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixAlarmController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixAlarmController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixBatchController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixBatchController.java
index 8cf0996..50cfda4 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixBatchController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixBatchController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixConController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixConController.java
index e41d2d2..120a342 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixConController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixConController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixCurveController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixCurveController.java
index 5342b86..f8cc9bb 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixCurveController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixCurveController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixExePlanController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixExePlanController.java
index c5e91c8..9af3d48 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixExePlanController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixExePlanController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixStepController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixStepController.java
index 9a6f731..2848110 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixStepController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixStepController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixWeightController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixWeightController.java
index 04e4e86..4bc63db 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixWeightController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/McsToMesMixWeightController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsMaterialController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsMaterialController.java
index c6928fd..32c956f 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsMaterialController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsMaterialController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsMixPlanController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsMixPlanController.java
index 93e8a68..ead15ad 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsMixPlanController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsMixPlanController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeController.java
index 351ef48..884961d 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeMixStepController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeMixStepController.java
index 83062ae..b8963f9 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeMixStepController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeMixStepController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeWeightController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeWeightController.java
index 6b98591..a60172e 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeWeightController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/mcs/controller/MesToMcsRecipeWeightController.java
@@ -1,4 +1,4 @@
-package org.jeecg.modules.xslmes.mcs.controller;
+package org.jeecg.modules.xslmes.mcs.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.jeecg.common.api.vo.Result;
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysThirdAppConfig.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysThirdAppConfig.java
index dcc1ae0..2e24cb0 100644
--- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysThirdAppConfig.java
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/entity/SysThirdAppConfig.java
@@ -66,6 +66,12 @@ public class SysThirdAppConfig {
@Schema(description = "是否启用(0-否,1-是)")
private Integer status;
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】Stream事件推送主配置标识-----
+ /**Stream事件推送主配置(0-否,1-是)*/
+ @Schema(description = "Stream事件推送主配置(0-否,1-是),同一 thirdType 中只有一条记录为1")
+ private Integer streamEnabled;
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】Stream事件推送主配置标识-----
+
/**创建日期*/
@Excel(name = "创建日期", width = 20, format = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java
index 7a4295a..c4f3df6 100644
--- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java
@@ -1196,6 +1196,70 @@ public class ThirdAppDingtalkServiceImpl implements IThirdAppService {
return configMapper.getThirdConfigByThirdType(tenantId,MessageTypeEnum.DD.getType());
}
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】获取钉钉应用凭证(Stream模式专用)-----
+ /**
+ * 获取钉钉应用凭证 [clientId, clientSecret],供 Stream 长连接使用。
+ *
+ * 优先级:
+ * ① stream_enabled=1 的记录(管理员在「钉钉集成」页面显式指定的 Stream 主配置)
+ * ② 兜底:AppKey 长度 > 10 的第一条有效记录(迁移期或未配置时保持可用)
+ *
+ * @return [appKey, appSecret];未配置时返回 null
+ */
+ public String[] getDingAppCredentials() {
+ java.util.List all = configMapper.selectList(
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper()
+ .eq(SysThirdAppConfig::getThirdType, THIRD_TYPE)
+ .isNotNull(SysThirdAppConfig::getClientId)
+ .isNotNull(SysThirdAppConfig::getClientSecret)
+ // stream_enabled=1 的排最前面
+ .orderByDesc(SysThirdAppConfig::getStreamEnabled)
+ .orderByDesc(SysThirdAppConfig::getTenantId));
+
+ for (SysThirdAppConfig c : all) {
+ String appKey = c.getClientId();
+ String appSecret = c.getClientSecret();
+ if (oConvertUtils.isNotEmpty(appKey) && appKey.length() > 10
+ && oConvertUtils.isNotEmpty(appSecret) && appSecret.length() > 10) {
+ return new String[]{appKey, appSecret};
+ }
+ }
+ return null;
+ }
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】获取钉钉应用凭证(Stream模式专用)-----
+
+ //update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】后台线程专用AccessToken(绕过租户检查)-----
+ /**
+ * 后台线程专用:获取钉钉 AccessToken,不依赖 TenantContext。
+ *
+ * 原 {@link #getAccessToken()} 内部调 {@code tenantIzExist(0)},
+ * 后台线程无租户上下文时 tenantId 默认 0,若 tenant=0 不存在直接抛异常。
+ * 本方法复用 {@link #getDingAppCredentials()} 的安全查询,绕过租户校验。
+ *
+ * @return AccessToken 字符串;未配置或失败时返回 null
+ */
+ public String getAccessTokenForBackground() {
+ String[] creds = getDingAppCredentials();
+ if (creds == null || oConvertUtils.isEmpty(creds[0]) || oConvertUtils.isEmpty(creds[1])) {
+ log.warn("[DingBg] 未找到有效钉钉配置,无法获取 AccessToken");
+ return null;
+ }
+ try {
+ // JdtBaseAPI 已在文件顶部 import com.jeecg.dingtalk.api.base.JdtBaseAPI
+ // AccessToken 已在文件顶部 import com.jeecg.dingtalk.api.core.vo.AccessToken
+ AccessToken token = JdtBaseAPI.getAccessToken(creds[0], creds[1]);
+ if (token != null && oConvertUtils.isNotEmpty(token.getAccessToken())) {
+ return token.getAccessToken();
+ }
+ log.warn("[DingBg] getAccessToken 返回空");
+ return null;
+ } catch (Exception e) {
+ log.warn("[DingBg] 获取 AccessToken 失败: {}", e.getMessage());
+ return null;
+ }
+ }
+ //update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】后台线程专用AccessToken(绕过租户检查)-----
+
/**
* 获取钉钉accessToken
* @param config
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
index 843eb86..0cde415 100644
--- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
@@ -126,6 +126,8 @@ spring:
web-stat-filter:
enabled: true
dynamic:
+ # 非主数据源懒加载,避免花生壳/SQL Server 未就绪时拖慢或阻断启动
+ lazy: true
druid:
# 连接池的配置信息
# 初始化大小,最小,最大
@@ -165,9 +167,18 @@ spring:
#update-begin---author:geh ---date:2026-06-02 for:【MES上辅机】新增 SQL Server 中间表数据源(MES_ShareDB)-----------
sqlserver_mcs:
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
- url: jdbc:sqlserver://1lo04860wn636.vicp.fun:31601;DatabaseName=MES_ShareDB;encrypt=false;trustServerCertificate=true;SelectMethod=cursor;
+ # loginTimeout/connectTimeout:花生壳映射不稳定时延长 TCP/登录等待(单位:秒 / 毫秒)
+ url: jdbc:sqlserver://1lo04860wn636.vicp.fun:31601;DatabaseName=MES_ShareDB;encrypt=false;trustServerCertificate=true;SelectMethod=cursor;loginTimeout=120;connectTimeout=120000;
username: sa
password: 123456
+ druid:
+ initial-size: 0
+ min-idle: 0
+ max-wait: 120000
+ connect-timeout: 120000
+ connection-error-retry-attempts: 10
+ time-between-connect-error-millis: 3000
+ break-after-acquire-failure: false
#update-end---author:geh ---date:2026-06-02 for:【MES上辅机】新增 SQL Server 中间表数据源(MES_ShareDB)-----------
# # shardingjdbc数据源
# sharding-db:
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_100__mes_xsl_approval_record_processed_op_count.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_100__mes_xsl_approval_record_processed_op_count.sql
new file mode 100644
index 0000000..855e6bd
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_100__mes_xsl_approval_record_processed_op_count.sql
@@ -0,0 +1,5 @@
+-- 【20260604】钉钉回调幂等去重:台账新增 processed_op_count 字段
+-- 用于 bpms_task_change 节点回调的 DB 乐观锁去重,记录已处理的节点回调数
+-- 默认 0,每处理一个节点后 +1;并发事件通过 WHERE processed_op_count < ? 条件竞争
+ALTER TABLE mes_xsl_approval_record
+ ADD COLUMN processed_op_count INT NOT NULL DEFAULT 0 COMMENT '钉钉回调已处理节点数(幂等去重)';
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_122__mes_xsl_ding_process_tpl.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_122__mes_xsl_ding_process_tpl.sql
new file mode 100644
index 0000000..801f674
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_122__mes_xsl_ding_process_tpl.sql
@@ -0,0 +1,74 @@
+-- ============================================================
+-- MESToDing审批配置 - 钉钉审批模板配置
+-- author: GHT date: 2026-06-03
+-- ============================================================
+
+-- ========== 建表 DDL ==========
+CREATE TABLE IF NOT EXISTS `mes_xsl_ding_process_tpl` (
+ `id` varchar(32) NOT NULL COMMENT '主键',
+ `tpl_name` varchar(100) NOT NULL COMMENT '模板名称',
+ `process_code` varchar(100) NOT NULL COMMENT '钉钉processCode',
+ `biz_type` varchar(50) DEFAULT NULL COMMENT '业务类型标识(供审批流关联使用)',
+ `form_fields` text DEFAULT NULL COMMENT '表单字段映射JSON(钉钉字段名→MES字段名)',
+ `status` char(1) NOT NULL DEFAULT '1' COMMENT '状态:0停用 1启用',
+ `sort_no` int NOT NULL DEFAULT 0 COMMENT '排序',
+ `remark` varchar(500) 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 '更新时间',
+ `del_flag` int NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常 1已删除',
+ `tenant_id` int NOT NULL DEFAULT 0 COMMENT '租户ID',
+ `sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门编码',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES-钉钉审批模板配置';
+
+-- ========== 字典 mes_ding_tpl_status ==========
+INSERT INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `type`, `tenant_id`)
+VALUES (REPLACE(UUID(),'-',''), '钉钉审批模板状态', 'mes_ding_tpl_status', '钉钉审批模板启用/停用状态', 0, 'admin', NOW(), NULL, NULL, 0, 0);
+
+INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`)
+SELECT REPLACE(UUID(),'-',''), id, '启用', '1', NULL, 1, 1, 'admin', NOW(), NULL, NULL FROM `sys_dict` WHERE `dict_code` = 'mes_ding_tpl_status' LIMIT 1;
+
+INSERT INTO `sys_dict_item` (`id`, `dict_id`, `item_text`, `item_value`, `description`, `sort_order`, `status`, `create_by`, `create_time`, `update_by`, `update_time`)
+SELECT REPLACE(UUID(),'-',''), id, '停用', '0', NULL, 2, 1, 'admin', NOW(), NULL, NULL FROM `sys_dict` WHERE `dict_code` = 'mes_ding_tpl_status' LIMIT 1;
+
+-- ========== 菜单权限 ==========
+-- 注意:该页面对应的前台目录为 views/xslmes/dingtalk/mesXslDingProcessTpl 文件夹下
+
+-- 父菜单:MESToDing审批配置(目录级,is_leaf=0)
+INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
+VALUES ('178046026420801', NULL, 'MESToDing审批配置', '/mestoding', 'layouts/RouteView', NULL, NULL, 0, NULL, '1', 99.00, 0, 'ant-design:dingtalk-outlined', 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0);
+
+-- 子菜单:钉钉审批模板配置(is_leaf=0,有按钮子级)
+INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
+VALUES ('178046026420802', '178046026420801', '钉钉审批模板配置', '/xslmes/mesXslDingProcessTplList', 'xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList', NULL, NULL, 0, NULL, '1', 1.00, 0, NULL, 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0);
+
+-- 按钮权限(parent_id = 178046026420802,is_leaf=1)
+INSERT 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 ('178046026420803', '178046026420802', '添加钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT 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 ('178046026420804', '178046026420802', '编辑钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT 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 ('178046026420805', '178046026420802', '删除钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT 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 ('178046026420806', '178046026420802', '批量删除钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT 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 ('178046026420807', '178046026420802', '导出excel_钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:exportXls', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT 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 ('178046026420808', '178046026420802', '导入excel_钉钉审批模板配置', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_ding_process_tpl:importExcel', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-03 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+-- ========== admin 角色授权(role_id = f6817f48af4fb3af11b9e8bf182f618b)==========
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420801', NULL, '2026-06-03 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420802', NULL, '2026-06-03 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420803', NULL, '2026-06-03 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420804', NULL, '2026-06-03 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420805', NULL, '2026-06-03 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420806', NULL, '2026-06-03 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420807', NULL, '2026-06-03 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420808', NULL, '2026-06-03 00:00:00', '127.0.0.1');
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_123__mes_xsl_ding_process_tpl_flow_id.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_123__mes_xsl_ding_process_tpl_flow_id.sql
new file mode 100644
index 0000000..118f846
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_123__mes_xsl_ding_process_tpl_flow_id.sql
@@ -0,0 +1,6 @@
+-- ============================================================
+-- 钉钉审批模板配置 - 新增绑定MES审批流字段
+-- author: GHT date: 2026-06-04
+-- ============================================================
+ALTER TABLE `mes_xsl_ding_process_tpl`
+ ADD COLUMN `flow_id` varchar(32) DEFAULT NULL COMMENT '绑定的MES审批流ID(用于发起审批时解析审批人)';
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_124__mes_xsl_ding_tpl_bind.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_124__mes_xsl_ding_tpl_bind.sql
new file mode 100644
index 0000000..678dbe1
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_124__mes_xsl_ding_tpl_bind.sql
@@ -0,0 +1,43 @@
+-- ============================================================
+-- MESToDing审批配置 - 审批模板绑定
+-- author: GHT date: 2026-06-04
+-- ============================================================
+
+CREATE TABLE IF NOT EXISTS `mes_xsl_ding_tpl_bind` (
+ `id` varchar(32) NOT NULL COMMENT '主键',
+ `biz_code` varchar(64) NOT NULL COMMENT '业务编码(sys_permission.id)',
+ `biz_name` varchar(200) DEFAULT NULL COMMENT '业务名称(菜单名)',
+ `template_id` varchar(32) NOT NULL COMMENT '钉钉审批模板ID',
+ `template_name` varchar(200) DEFAULT NULL COMMENT '钉钉审批模板名称',
+ `field_mapping_json` longtext DEFAULT NULL COMMENT '字段绑定JSON:[{componentId,componentLabel,componentName,parentId,bizField}]',
+ `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 '更新时间',
+ `del_flag` int NOT NULL DEFAULT 0 COMMENT '逻辑删除:0正常 1已删除',
+ `tenant_id` int NOT NULL DEFAULT 0 COMMENT '租户ID',
+ `sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门编码',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_ding_tpl_bind_biz_code` (`biz_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES-钉钉审批模板绑定';
+
+-- ========== 菜单权限 ==========
+-- 子菜单:审批模板绑定(挂在 MESToDing审批配置 父菜单 178046026420801 下)
+INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
+VALUES ('178046026420810', '178046026420801', '审批模板绑定', '/xslmes/dingTplBindList', 'xslmes/dingtalk/dingTplBind/index', NULL, NULL, 0, NULL, '1', 2.00, 0, 'ant-design:link-outlined', 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '2026-06-04 00:00:00', NULL, NULL, 0);
+
+-- 按钮权限
+INSERT 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 ('178046026420811', '178046026420810', '查询审批模板绑定', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mesXslDingTplBind:list', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-04 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT 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 ('178046026420812', '178046026420810', '保存审批模板绑定', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mesXslDingTplBind:save', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-04 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT 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 ('178046026420813', '178046026420810', '删除审批模板绑定', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mesXslDingTplBind:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-04 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+-- ========== admin 角色授权 ==========
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420810', NULL, '2026-06-04 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420811', NULL, '2026-06-04 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420812', NULL, '2026-06-04 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420813', NULL, '2026-06-04 00:00:00', '127.0.0.1');
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_125__mes_xsl_approval_record.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_125__mes_xsl_approval_record.sql
new file mode 100644
index 0000000..307e45a
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_125__mes_xsl_approval_record.sql
@@ -0,0 +1,56 @@
+-- 【QH-MES审批台账】跨 MES/钉钉 统一审批门禁台账
+SET NAMES utf8mb4;
+
+CREATE TABLE IF NOT EXISTS `mes_xsl_approval_record` (
+ `id` varchar(32) NOT NULL COMMENT '主键',
+ `biz_table` varchar(100) NOT NULL COMMENT '业务单据表名',
+ `biz_table_name` varchar(200) DEFAULT NULL COMMENT '业务单据中文名',
+ `biz_code` varchar(64) DEFAULT NULL COMMENT '业务编码(菜单permission.id,钉钉绑定用)',
+ `biz_data_id` varchar(64) NOT NULL COMMENT '业务单据记录ID',
+ `biz_title` varchar(300) DEFAULT NULL COMMENT '业务单据展示标题',
+ `channel` varchar(20) NOT NULL COMMENT '审批通道 MES/DINGTALK',
+ `external_instance_id` varchar(128) DEFAULT NULL COMMENT '外部实例ID(MES实例ID或钉钉instanceId)',
+ `flow_id` varchar(32) DEFAULT NULL COMMENT 'MES审批流ID',
+ `flow_name` varchar(100) DEFAULT NULL COMMENT 'MES审批流名称',
+ `template_id` varchar(32) DEFAULT NULL COMMENT '钉钉审批模板ID',
+ `template_name` varchar(200) DEFAULT NULL COMMENT '钉钉审批模板名称',
+ `launch_no` int DEFAULT '1' COMMENT '同一业务单据第几次发起',
+ `status` varchar(2) DEFAULT '0' COMMENT '状态 0流转中 1通过 2拒绝 3撤销 4发起失败',
+ `apply_user` varchar(50) DEFAULT NULL COMMENT '发起人username',
+ `apply_user_name` varchar(100) DEFAULT NULL COMMENT '发起人姓名',
+ `apply_time` datetime DEFAULT NULL COMMENT '发起时间',
+ `finish_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_rec_biz` (`tenant_id`, `biz_table`, `biz_data_id`, `apply_time`),
+ KEY `idx_appr_rec_ext` (`channel`, `external_instance_id`),
+ KEY `idx_appr_rec_status` (`tenant_id`, `biz_table`, `biz_data_id`, `status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MES审批台账(跨通道门禁)';
+
+-- 审批通道字典
+INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
+VALUES ('1995000000000000350', '审批通道', 'mes_xsl_approval_channel', '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
+('1995000000000000351', '1995000000000000350', 'MES审批', 'MES', 'MES审批', 1, 1, 'admin', NOW()),
+('1995000000000000352', '1995000000000000350', '钉钉审批', 'DINGTALK', '钉钉审批', 2, 1, 'admin', NOW());
+
+-- 台账状态字典(在实例状态基础上增加「发起失败」)
+INSERT IGNORE INTO `sys_dict` (`id`, `dict_name`, `dict_code`, `description`, `del_flag`, `create_by`, `create_time`, `type`, `tenant_id`)
+VALUES ('1995000000000000353', '审批台账状态', 'mes_xsl_approval_record_status', '审批台账状态', 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
+('1995000000000000354', '1995000000000000353', '流转中', '0', '流转中', 1, 1, 'admin', NOW()),
+('1995000000000000355', '1995000000000000353', '审批通过', '1', '审批通过', 2, 1, 'admin', NOW()),
+('1995000000000000356', '1995000000000000353', '审批拒绝', '2', '审批拒绝', 3, 1, 'admin', NOW()),
+('1995000000000000357', '1995000000000000353', '已撤销', '3', '已撤销', 4, 1, 'admin', NOW()),
+('1995000000000000358', '1995000000000000353', '发起失败', '4', '发起失败', 5, 1, 'admin', NOW());
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_126__mes_xsl_approval_record_menu.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_126__mes_xsl_approval_record_menu.sql
new file mode 100644
index 0000000..f89eca2
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_126__mes_xsl_approval_record_menu.sql
@@ -0,0 +1,18 @@
+-- ============================================================
+-- MESToDing审批配置 - 审批台账菜单
+-- author: GHT date: 2026-06-04
+-- ============================================================
+
+-- 子菜单:审批台账(挂在 MESToDing审批配置 父菜单 178046026420801 下)
+INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external)
+VALUES ('178046026420820', '178046026420801', '审批台账', '/xslmes/mesXslApprovalRecordList', 'xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecordList', NULL, NULL, 0, NULL, '1', 3.00, 0, 'ant-design:audit-outlined', 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '2026-06-04 00:00:00', NULL, NULL, 0);
+
+INSERT 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 ('178046026420821', '178046026420820', '查询审批台账', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_approval_record:list', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-04 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT 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 ('178046026420822', '178046026420820', '导出审批台账', NULL, NULL, 0, NULL, NULL, 2, 'xslmes:mes_xsl_approval_record:exportXls', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2026-06-04 00:00:00', NULL, NULL, 0, 0, '1', 0);
+
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420820', NULL, '2026-06-04 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420821', NULL, '2026-06-04 00:00:00', '127.0.0.1');
+INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, operate_date, operate_ip) VALUES (REPLACE(UUID(),'-',''), 'f6817f48af4fb3af11b9e8bf182f618b', '178046026420822', NULL, '2026-06-04 00:00:00', '127.0.0.1');
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_127__sys_third_app_config_stream_enabled.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_127__sys_third_app_config_stream_enabled.sql
new file mode 100644
index 0000000..e3f1efb
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_127__sys_third_app_config_stream_enabled.sql
@@ -0,0 +1,4 @@
+-- GHT 20260604 【钉钉Stream回调】sys_third_app_config 新增 stream_enabled 字段
+-- 用于在第三方应用配置页面指定哪条钉钉配置作为 Stream 事件推送的主连接
+ALTER TABLE sys_third_app_config
+ ADD COLUMN stream_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Stream事件推送主配置(0-否,1-是)';
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_128__mes_xsl_approval_record_origin_status.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_128__mes_xsl_approval_record_origin_status.sql
new file mode 100644
index 0000000..64e2b17
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_128__mes_xsl_approval_record_origin_status.sql
@@ -0,0 +1,5 @@
+-- GHT 20260604 【钉钉Stream回调】mes_xsl_approval_record 新增 origin_status/status_field 字段
+-- 与 mes_xsl_approval_instance 对齐,用于驳回时共用 isBizAtOriginStatus 逻辑
+ALTER TABLE mes_xsl_approval_record
+ ADD COLUMN status_field VARCHAR(64) NULL COMMENT '业务状态字段名(发起时快照)',
+ ADD COLUMN origin_status VARCHAR(64) NULL COMMENT '发起审批时业务状态原值(驳回回写依据)';
diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_129__mes_xsl_ding_process_tpl_process_code_default.sql b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_129__mes_xsl_ding_process_tpl_process_code_default.sql
new file mode 100644
index 0000000..ab86972
--- /dev/null
+++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_129__mes_xsl_ding_process_tpl_process_code_default.sql
@@ -0,0 +1,4 @@
+-- process_code 允许草稿阶段为空(创建钉钉模板前),默认空串
+-- author: GHT date: 2026-06-04
+ALTER TABLE `mes_xsl_ding_process_tpl`
+ MODIFY COLUMN `process_code` varchar(100) NOT NULL DEFAULT '' COMMENT '钉钉processCode,未推送钉钉前为空';
diff --git a/jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue b/jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue
new file mode 100644
index 0000000..ab0bd35
--- /dev/null
+++ b/jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue
@@ -0,0 +1,807 @@
+
+
+
+
+
+
审批流程
+
+
+
🔗
+
请在右侧「审批流配置」
页签中选择审批流
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ node.nodeType==='cc'?'抄送':'审批' }}
+
+ {{ modeLabel(node.multiMode) }}
+
+
{{ node.nodeName }}
+
+
+ {{ u.realname }}
+ ·
+
+
+
⚠ 有未解析成员,请补充手机号
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 审批流配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 已选数据
+ {{ allRows.length }}
+
+
+
+
{{ idx + 1 }}
+
{{ getRowLabel(row, idx) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jeecgboot-vue3/src/components/DingTplLaunch/index.vue b/jeecgboot-vue3/src/components/DingTplLaunch/index.vue
new file mode 100644
index 0000000..acd1d7f
--- /dev/null
+++ b/jeecgboot-vue3/src/components/DingTplLaunch/index.vue
@@ -0,0 +1,257 @@
+
+
+
+
+
+
+ 钉钉审批
+ {{ selectedRows.length }}
+
+
+
+
+
+
+
+
+
+
diff --git a/jeecgboot-vue3/src/components/DingTplLaunch/useDraggablePosition.ts b/jeecgboot-vue3/src/components/DingTplLaunch/useDraggablePosition.ts
new file mode 100644
index 0000000..d6ae55a
--- /dev/null
+++ b/jeecgboot-vue3/src/components/DingTplLaunch/useDraggablePosition.ts
@@ -0,0 +1,157 @@
+import { reactive, ref, type Ref } from 'vue';
+
+const STORAGE_KEY = 'mes_ding_tpl_launch_pos';
+
+export interface FloatPosition {
+ left: number;
+ top: number;
+}
+
+function readAllPositions(): Record {
+ try {
+ return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') || {};
+ } catch {
+ return {};
+ }
+}
+
+/** 查询条件区域(BasicTable 搜索表单)右侧的默认坐标 */
+export function calcDefaultPosition(btnWidth: number, btnHeight: number): FloatPosition {
+ const formEl =
+ document.querySelector('.jeecg-basic-table-form-container .ant-form') ||
+ document.querySelector('.jeecg-basic-table-form-container');
+ if (!formEl) {
+ return {
+ left: Math.max(8, window.innerWidth - btnWidth - 24),
+ top: 100,
+ };
+ }
+ const rect = formEl.getBoundingClientRect();
+ return {
+ left: Math.max(8, rect.right - btnWidth - 12),
+ top: Math.max(8, rect.top + (rect.height - btnHeight) / 2),
+ };
+}
+
+/**
+ * 可拖拽悬浮按钮位置:默认对齐查询区右侧,拖拽后按路由持久化。
+ */
+export function useDraggablePosition(routePath: Ref) {
+ const pos = reactive({ left: 0, top: 0 });
+ const isDragging = ref(false);
+ let moved = false;
+ let pointerId: number | null = null;
+ let startX = 0;
+ let startY = 0;
+ let startLeft = 0;
+ let startTop = 0;
+ let btnWidth = 120;
+ let btnHeight = 36;
+
+ function setButtonSize(width: number, height: number) {
+ btnWidth = width;
+ btnHeight = height;
+ }
+
+ function loadPosition(path: string): boolean {
+ const saved = readAllPositions()[path];
+ if (saved && typeof saved.left === 'number' && typeof saved.top === 'number') {
+ pos.left = saved.left;
+ pos.top = saved.top;
+ return true;
+ }
+ return false;
+ }
+
+ function savePosition(path: string) {
+ const all = readAllPositions();
+ all[path] = { left: pos.left, top: pos.top };
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
+ }
+
+ function applyDefaultPosition() {
+ const p = calcDefaultPosition(btnWidth, btnHeight);
+ pos.left = p.left;
+ pos.top = p.top;
+ }
+
+ function initPosition(path: string, preferDefault = false) {
+ if (!path) return;
+ if (!preferDefault && loadPosition(path)) return;
+ applyDefaultPosition();
+ }
+
+ function clampPosition() {
+ const maxLeft = window.innerWidth - btnWidth - 8;
+ const maxTop = window.innerHeight - btnHeight - 8;
+ pos.left = Math.min(Math.max(8, pos.left), maxLeft);
+ pos.top = Math.min(Math.max(8, pos.top), maxTop);
+ }
+
+ function onPointerMove(e: PointerEvent) {
+ if (pointerId !== e.pointerId) return;
+ const dx = e.clientX - startX;
+ const dy = e.clientY - startY;
+ if (!moved && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
+ moved = true;
+ isDragging.value = true;
+ }
+ if (!moved) return;
+ pos.left = startLeft + dx;
+ pos.top = startTop + dy;
+ clampPosition();
+ }
+
+ function endDrag(path: string) {
+ document.removeEventListener('pointermove', onPointerMove);
+ document.removeEventListener('pointerup', onPointerUp);
+ document.removeEventListener('pointercancel', onPointerUp);
+ if (moved && path) {
+ savePosition(path);
+ }
+ pointerId = null;
+ setTimeout(() => {
+ isDragging.value = false;
+ moved = false;
+ }, 0);
+ }
+
+ function onPointerUp(e: PointerEvent) {
+ if (pointerId !== e.pointerId) return;
+ endDrag(routePath.value);
+ }
+
+ function onPointerDown(e: PointerEvent, el: HTMLElement | null) {
+ if (e.button !== 0) return;
+ if (el) {
+ const rect = el.getBoundingClientRect();
+ setButtonSize(rect.width, rect.height);
+ }
+ moved = false;
+ isDragging.value = false;
+ pointerId = e.pointerId;
+ startX = e.clientX;
+ startY = e.clientY;
+ startLeft = pos.left;
+ startTop = pos.top;
+ (e.currentTarget as HTMLElement)?.setPointerCapture?.(e.pointerId);
+ document.addEventListener('pointermove', onPointerMove);
+ document.addEventListener('pointerup', onPointerUp);
+ document.addEventListener('pointercancel', onPointerUp);
+ }
+
+ function wasDragged() {
+ return moved;
+ }
+
+ return {
+ pos,
+ isDragging,
+ setButtonSize,
+ initPosition,
+ applyDefaultPosition,
+ onPointerDown,
+ wasDragged,
+ clampPosition,
+ };
+}
diff --git a/jeecgboot-vue3/src/layouts/default/index.vue b/jeecgboot-vue3/src/layouts/default/index.vue
index de43a56..db3f467 100644
--- a/jeecgboot-vue3/src/layouts/default/index.vue
+++ b/jeecgboot-vue3/src/layouts/default/index.vue
@@ -13,6 +13,9 @@
+
+
+
@@ -48,6 +51,9 @@
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-----
ApprovalDesignFloat: createAsyncComponent(() => import('/@/components/ApprovalDesign/index.vue')),
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】全局审批流程设计悬浮按钮-----
+ //update-begin---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】全局钉钉审批模板绑定发起按钮-----
+ DingTplLaunchFloat: createAsyncComponent(() => import('/@/components/DingTplLaunch/index.vue')),
+ //update-end---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】全局钉钉审批模板绑定发起按钮-----
LayoutHeader,
LayoutContent,
LayoutSideBar,
diff --git a/jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue b/jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue
index 26480c1..b66a1d8 100644
--- a/jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue
+++ b/jeecgboot-vue3/src/views/approval/flow/components/NodeConfigDrawer.vue
@@ -53,7 +53,10 @@
-
+
+
+ 单人审批只能指定一位,已自动保留第一位
+
@@ -65,12 +68,16 @@
第3级主管
-
-
- 会签(需全部同意)
+
+
+ 单人审批
+ 会签(全部同意)
或签(一人同意)
依次审批
+
+ 单人审批:仅允许指定一位审批人,对应钉钉 actionType = NONE
+
@@ -193,7 +200,7 @@
diff --git a/jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/components/MesXslApprovalRecordDetailModal.vue b/jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/components/MesXslApprovalRecordDetailModal.vue
new file mode 100644
index 0000000..5d6ec89
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/components/MesXslApprovalRecordDetailModal.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/dingTplBind.api.ts b/jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/dingTplBind.api.ts
new file mode 100644
index 0000000..acf2e53
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/dingTplBind.api.ts
@@ -0,0 +1,38 @@
+import { defHttp } from '/@/utils/http/axios';
+
+const BASE = '/xslmes/dingTplBind';
+
+export const getMenuTree = () => defHttp.get({ url: `${BASE}/menuTree` });
+
+export const getTplList = () => defHttp.get({ url: `${BASE}/tplList` });
+
+export const getBizFields = (bizCode: string) =>
+ defHttp.get({ url: `${BASE}/bizFields`, params: { bizCode } });
+
+export const getDetailSlots = (bizCode: string) =>
+ defHttp.get({ url: `${BASE}/detailSlots`, params: { bizCode } });
+
+export const getDetailFields = (bizCode: string, detailProperty: string, slotKind = 'LIST') =>
+ defHttp.get({ url: `${BASE}/detailFields`, params: { bizCode, detailProperty, slotKind } });
+
+export const getBindList = () => defHttp.get({ url: `${BASE}/list` });
+
+export const getBindingByRoute = (routePath: string) =>
+ defHttp.get({ url: `${BASE}/bindingByRoute`, params: { routePath } }, { errorMessageMode: 'none' });
+
+export const getByBizCode = (bizCode: string) =>
+ defHttp.get({ url: `${BASE}/getByBizCode`, params: { bizCode } });
+
+export const saveBind = (data: {
+ bizCode: string;
+ bizName?: string;
+ templateId: string;
+ fieldMappingJson: string;
+}) => defHttp.post({ url: `${BASE}/save`, data });
+
+export const deleteBind = (id: string) =>
+ defHttp.delete({ url: `${BASE}/delete`, params: { id } });
+
+/** 复用现有接口:拉取钉钉模板表单字段(含 dingFields) */
+export const getTemplateDetail = (id: string) =>
+ defHttp.get({ url: '/xslmes/mesXslDingProcessTpl/getTemplateDetail', params: { id } });
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/index.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/index.vue
new file mode 100644
index 0000000..6367ac8
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/dingTplBind/index.vue
@@ -0,0 +1,1132 @@
+
+
+
+
+
+
+
+ 无可用菜单
+
+
+
+
+ {{ node2.name }}
+ ●
+
+
+
+
+
+
+
+
+
+
+
+
+ 绑定列表
+
+
+
+
+
+
+
+
+
+
+
+
+ 当前菜单:
+ {{ selectedBizName }}
+
+ ← 请从左侧选择功能菜单
+
+
+
+
+
+
+ 自动匹配
+
+
+ 删除绑定
+
+
+ 保存绑定
+
+
+
+
+
+
+
+
+
+
+
🔗
+
从左侧选择一个功能菜单,再选择钉钉审批模板,将模板字段与实体字段进行绑定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
主表字段绑定
+
+
+
+
+ {{ record.componentName }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 明细表字段绑定
+
+ ({{ tf.componentLabel }})
+
+
+
+
+
+
对应业务明细集合:
+
onDetailSlotChange(tf, v as string | undefined)"
+ />
+ 确定后系统会反射该集合的子字段供列绑定
+
+
+
+
+
+
+
+ {{ record.componentName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseFieldCount(record.fieldMappingJson) }} 个字段
+
+
+
+ 编辑
+
+
+ 解除绑定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts
new file mode 100644
index 0000000..be4ffcd
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.api.ts
@@ -0,0 +1,83 @@
+import { defHttp } from '/@/utils/http/axios';
+import { useMessage } from '/@/hooks/web/useMessage';
+
+const { createConfirm } = useMessage();
+
+enum Api {
+ list = '/xslmes/mesXslDingProcessTpl/list',
+ save = '/xslmes/mesXslDingProcessTpl/add',
+ edit = '/xslmes/mesXslDingProcessTpl/edit',
+ deleteOne = '/xslmes/mesXslDingProcessTpl/delete',
+ deleteBatch = '/xslmes/mesXslDingProcessTpl/deleteBatch',
+ importExcel = '/xslmes/mesXslDingProcessTpl/importExcel',
+ exportXls = '/xslmes/mesXslDingProcessTpl/exportXls',
+ syncFromDingtalk = '/xslmes/mesXslDingProcessTpl/syncFromDingtalk',
+ batchImport = '/xslmes/mesXslDingProcessTpl/batchImport',
+ getTemplateDetail = '/xslmes/mesXslDingProcessTpl/getTemplateDetail',
+ saveFieldMapping = '/xslmes/mesXslDingProcessTpl/saveFieldMapping',
+ addNewTemplate = '/xslmes/mesXslDingProcessTpl/addNewTemplate',
+ createDingTemplate = '/xslmes/mesXslDingProcessTpl/createDingTemplate',
+ updateDingTemplate = '/xslmes/mesXslDingProcessTpl/updateDingTemplate',
+ launchApproval = '/xslmes/mesXslDingProcessTpl/launchApproval',
+ bindFlow = '/xslmes/mesXslDingProcessTpl/bindFlow',
+ approvalFlowList = '/xslmes/approvalFlow/list',
+ previewFlowApprovers = '/xslmes/mesXslDingProcessTpl/previewFlowApprovers',
+}
+
+export const getExportUrl = Api.exportXls;
+export const getImportUrl = Api.importExcel;
+
+export const list = (params) => defHttp.get({ url: Api.list, params });
+
+export const deleteOne = (params, handleSuccess) =>
+ defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => handleSuccess());
+
+export const batchDelete = (params, handleSuccess) => {
+ createConfirm({
+ iconType: 'warning',
+ title: '确认删除',
+ content: '是否删除选中数据',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: () =>
+ defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => handleSuccess()),
+ });
+};
+
+export const saveOrUpdate = (params, isUpdate) =>
+ defHttp.post({ url: isUpdate ? Api.edit : Api.save, params }, { successMessageMode: 'none' });
+
+/** 新增审批模板草稿(返回含 id 的完整记录) */
+export const addNewTemplate = (params) =>
+ defHttp.post({ url: Api.addNewTemplate, params }, { successMessageMode: 'none' });
+
+export const syncFromDingtalk = () => defHttp.get({ url: Api.syncFromDingtalk }, { successMessageMode: 'none' });
+
+export const batchImport = (params) => defHttp.post({ url: Api.batchImport, params }, { successMessageMode: 'none' });
+
+export const getTemplateDetail = (id: string) =>
+ defHttp.get({ url: Api.getTemplateDetail, params: { id } }, { successMessageMode: 'none' });
+
+export const queryById = (id: string) =>
+ defHttp.get({ url: '/xslmes/mesXslDingProcessTpl/queryById', params: { id } }, { successMessageMode: 'none' });
+
+export const saveFieldMapping = (params) =>
+ defHttp.post({ url: Api.saveFieldMapping, params }, { successMessageMode: 'none' });
+
+export const createDingTemplate = (params) =>
+ defHttp.post({ url: Api.createDingTemplate, params }, { successMessageMode: 'none' });
+
+export const updateDingTemplate = (params) =>
+ defHttp.post({ url: Api.updateDingTemplate, params }, { successMessageMode: 'none' });
+
+export const launchApproval = (params) =>
+ defHttp.post({ url: Api.launchApproval, params }, { successMessageMode: 'none' });
+
+export const bindApprovalFlow = (params: { id: string; flowId: string }) =>
+ defHttp.post({ url: Api.bindFlow, params }, { successMessageMode: 'none' });
+
+export const getApprovalFlowList = (params?) =>
+ defHttp.get({ url: Api.approvalFlowList, params }, { successMessageMode: 'none' });
+
+export const previewFlowApprovers = (flowId: string) =>
+ defHttp.get({ url: Api.previewFlowApprovers, params: { flowId } }, { successMessageMode: 'none' });
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.data.ts b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.data.ts
new file mode 100644
index 0000000..852ee59
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTpl.data.ts
@@ -0,0 +1,103 @@
+import { BasicColumn, FormSchema } from '/@/components/Table';
+
+export const columns: BasicColumn[] = [
+ { title: '模板名称', align: 'center', dataIndex: 'tplName', width: 180 },
+ { title: '钉钉processCode', align: 'center', dataIndex: 'processCode', width: 280 },
+ { title: '业务类型标识', align: 'center', dataIndex: 'bizType', width: 140 },
+ { title: '状态', align: 'center', dataIndex: 'status_dictText', width: 90 },
+ { title: '排序', align: 'center', dataIndex: 'sortNo', width: 80 },
+ { title: '备注', align: 'center', dataIndex: 'remark', width: 200 },
+ { title: '创建时间', align: 'center', dataIndex: 'createTime', width: 160 },
+];
+
+export const searchFormSchema: FormSchema[] = [
+ {
+ label: '模板名称',
+ field: 'tplName',
+ component: 'JInput',
+ colProps: { span: 6 },
+ },
+ {
+ label: '钉钉processCode',
+ field: 'processCode',
+ component: 'JInput',
+ colProps: { span: 6 },
+ },
+ {
+ label: '业务类型标识',
+ field: 'bizType',
+ component: 'JInput',
+ colProps: { span: 6 },
+ },
+ {
+ label: '状态',
+ field: 'status',
+ component: 'JDictSelectTag',
+ componentProps: { dictCode: 'mes_ding_tpl_status' },
+ colProps: { span: 6 },
+ },
+];
+
+export const formSchema: FormSchema[] = [
+ { label: '', field: 'id', component: 'Input', show: false },
+ {
+ label: '模板名称',
+ field: 'tplName',
+ component: 'Input',
+ componentProps: { placeholder: '请输入模板名称' },
+ dynamicRules: () => [{ required: true, message: '请输入模板名称!' }],
+ },
+ {
+ label: 'processCode',
+ field: 'processCode',
+ component: 'Input',
+ componentProps: {
+ placeholder: '钉钉返回的 processCode;「新增审批模板」流程中可留空,设计器创建钉钉模板后自动回填',
+ },
+ },
+ {
+ label: '业务类型标识',
+ field: 'bizType',
+ component: 'Input',
+ componentProps: { placeholder: '供审批流关联使用,如 mixer_ps、formula_spec' },
+ },
+ {
+ label: '表单字段映射',
+ field: 'formFields',
+ component: 'InputTextArea',
+ componentProps: {
+ placeholder: '{"PS编码":"psCode","类型":"type"} —— 钉钉模板字段名→MES字段名',
+ rows: 4,
+ },
+ },
+ {
+ label: '状态',
+ field: 'status',
+ component: 'JDictSelectTag',
+ componentProps: {
+ dictCode: 'mes_ding_tpl_status',
+ placeholder: '请选择状态',
+ getPopupContainer: () => document.body,
+ },
+ },
+ {
+ label: '排序',
+ field: 'sortNo',
+ component: 'InputNumber',
+ componentProps: { placeholder: '请输入排序值', style: 'width:100%' },
+ },
+ {
+ label: '备注',
+ field: 'remark',
+ component: 'InputTextArea',
+ componentProps: { placeholder: '请输入备注', rows: 2 },
+ },
+];
+
+export const superQuerySchema = {
+ tplName: { title: '模板名称', order: 0, view: 'text', type: 'string' },
+ processCode: { title: 'processCode', order: 1, view: 'text', type: 'string' },
+ bizType: { title: '业务类型标识', order: 2, view: 'text', type: 'string' },
+ status: { title: '状态', order: 3, view: 'list', type: 'string', dictCode: 'mes_ding_tpl_status' },
+ sortNo: { title: '排序', order: 4, view: 'number', type: 'number' },
+};
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue
new file mode 100644
index 0000000..7c50711
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/MesXslDingProcessTplList.vue
@@ -0,0 +1,385 @@
+
+
+
+
+
+
+ 新增审批模板
+
+
+
+ 快速录入
+
+
+ 导出
+
+
+ 导入
+
+
+
+ 从钉钉同步
+
+
+
+
+
+
+
+ 删除
+
+
+
+
+ 批量操作
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ schemaData.tplName }}
+ {{ schemaData.bizType || '—' }}
+
+ {{ schemaData.processCode }}
+ 未创建
+
+
+
+
+
+ 钉钉表单字段
+
{{ schemaData.dingFields.length }} 个
+
+
+
+
+ 未从钉钉获取到字段(模板可能无 processCode 或字段为空)
+
+
+
+ {{ JSON.stringify(schemaData, null, 2) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 待回填本地
+ {{ record.imported ? '已导入' : '未导入' }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingApprovalLaunchModal.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingApprovalLaunchModal.vue
new file mode 100644
index 0000000..de40b72
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingApprovalLaunchModal.vue
@@ -0,0 +1,776 @@
+
+
+
+
+
+
审批流程
+
+
+
🔗
+
请先在「审批流配置」
页签中选择审批流
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ node.nodeType === 'cc' ? '抄送' : '审批' }}
+
+ {{ modeLabel(node.multiMode) }}
+
+
{{ node.nodeName }}
+
+
+
+ {{ u.realname }}
+
+ ·
+
+
+
+ ⚠ 有未解析成员,请补充手机号
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 审批流配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplCreateModal.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplCreateModal.vue
new file mode 100644
index 0000000..b12fdf0
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplCreateModal.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 创建成功后打开表单设计器
+
+
+
+
+
+
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue
new file mode 100644
index 0000000..c89dc21
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/DingTplDesigner.vue
@@ -0,0 +1,1208 @@
+
+
+
+
+
+
+
+
+
{{ group.title }}
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
{{ tplData?.tplName || '表单预览' }}
+
已创建
+
未创建
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 子控件
+
{{ selectedComp.children?.length || 0 }} 个
+
+
+
+ + 添加子控件
+
+ addTableChild(key as string)">
+ {{ t.label }}
+
+
+
+
+
+
+
+
+
+ 关联的审批模板
+
+
+ + 添加关联模板
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 请点击画布中的控件
查看和编辑属性
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/MesXslDingProcessTplModal.vue b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/MesXslDingProcessTplModal.vue
new file mode 100644
index 0000000..22e0fa5
--- /dev/null
+++ b/jeecgboot-vue3/src/views/xslmes/dingtalk/mesXslDingProcessTpl/components/MesXslDingProcessTplModal.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+