新增钉钉 Stream SDK 依赖,支持无 HTTP 上下文的后台线程显式传入 token 进行审批回调。同时,完善了 MES 审批台账功能,新增审批记录同步、批量发起审批时的门禁与台账写入逻辑,增强了系统的审批流管理能力。
This commit is contained in:
@@ -677,4 +677,34 @@ jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockReason/MesXslRubberSmallLoc
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockLog/MesXslRubberSmallLockLog.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockLog/MesXslRubberSmallLockLog.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockLog/MesXslRubberSmallLockLogList.vue
|
||||
jeecgboot-vue3/src/views/xslmes/mesXslRubberSmallLockLog/components/MesXslRubberSmallLockLogModal.vue
|
||||
|
||||
-- author:GHT---date:20260604--for: 【QH-MES审批台账】跨 MES/钉钉 统一审批门禁台账 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_125__mes_xsl_approval_record.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/constant/ApprovalRecordConstants.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/entity/MesXslApprovalRecord.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/mapper/MesXslApprovalRecordMapper.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalRecordService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalRecordServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/IMesXslApprovalGateService.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalGateServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/vo/ApprovalGateVo.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalGateController.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalLaunchController.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/service/impl/MesXslApprovalHandleServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/controller/MesXslDingProcessTplController.java
|
||||
jeecgboot-vue3/src/views/approval/gate/approvalGate.api.ts
|
||||
jeecgboot-vue3/src/components/DingTplLaunch/DingBindLaunchModal.vue
|
||||
jeecgboot-vue3/src/components/DingTplLaunch/index.vue
|
||||
|
||||
-- author:GHT---date:20260604--for: 【QH-MES审批台账】MESToDing审批配置下增加审批台账菜单与列表页 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_126__mes_xsl_approval_record_menu.sql
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalRecordController.java
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecord.api.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecord.data.ts
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecordList.vue
|
||||
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/components/MesXslApprovalRecordDetailModal.vue
|
||||
|
||||
-- author:GHT---date:20260604--for: 【钉钉Stream回调】Stream模式接收钉钉审批结果并回写MES审批台账 -----
|
||||
jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/service/impl/ThirdAppDingtalkServiceImpl.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingBpmsEventProcessor.java
|
||||
jeecg-boot/jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingTalkStreamClient.java
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
<!-- 钉钉 Stream SDK: 官方 Stream 模式事件订阅,无需公网回调地址 GHT 20260604 -->
|
||||
<dependency>
|
||||
<groupId>com.dingtalk.open</groupId>
|
||||
<artifactId>dingtalk-stream</artifactId>
|
||||
<version>1.3.12</version>
|
||||
</dependency>
|
||||
<!-- 复用打印模板模块:打印机枚举、业务绑定、PDF 提交队列 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
|
||||
@@ -75,6 +75,19 @@ public class ApprovalActionHttpExecutor {
|
||||
if (node == null || inst == null) {
|
||||
return;
|
||||
}
|
||||
String token = currentToken();
|
||||
run(node, phase, inst.getBizDataId(), token);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】钉钉后台线程无HTTP上下文,支持显式传入token-----
|
||||
/**
|
||||
* 以显式 token 执行节点回调配置的接口(供无 HTTP 上下文的后台线程使用,如钉钉 Stream 回调)。
|
||||
* 当 token 为空时降级跳过(与原有行为一致)。
|
||||
*/
|
||||
public void run(JSONObject node, String phase, String bizDataId, String token) {
|
||||
if (node == null || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return;
|
||||
}
|
||||
JSONObject propsObj = node.getJSONObject("props");
|
||||
if (propsObj == null) {
|
||||
return;
|
||||
@@ -87,7 +100,6 @@ public class ApprovalActionHttpExecutor {
|
||||
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) {
|
||||
@@ -98,14 +110,14 @@ public class ApprovalActionHttpExecutor {
|
||||
continue;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(token)) {
|
||||
// 无登录态(自动流转/无人值守) -> 降级跳过
|
||||
log.warn("[审批回调] 无当前处理人登录态,跳过接口调用 phase={}, url={}, bizId={}", phase, url, inst.getBizDataId());
|
||||
log.warn("[审批回调] 无登录态,跳过接口调用 phase={}, url={}, bizId={}", phase, url, bizDataId);
|
||||
continue;
|
||||
}
|
||||
String method = oConvertUtils.getString(action.getString("method"), "POST").toUpperCase();
|
||||
invoke(method, url, action.getJSONObject("body"), inst.getBizDataId(), token);
|
||||
invoke(method, url, action.getJSONObject("body"), bizDataId, token);
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】钉钉后台线程无HTTP上下文,支持显式传入token-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】驳回统一回退:按表注解自动调用业务接口-----------
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.jeecg.modules.xslmes.approval.constant;
|
||||
|
||||
/**
|
||||
* 审批台账常量
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
public final class ApprovalRecordConstants {
|
||||
|
||||
private ApprovalRecordConstants() {
|
||||
}
|
||||
|
||||
/** MES 审批通道 */
|
||||
public static final String CHANNEL_MES = "MES";
|
||||
|
||||
/** 钉钉审批通道 */
|
||||
public static final String CHANNEL_DINGTALK = "DINGTALK";
|
||||
|
||||
/** 流转中 */
|
||||
public static final String STATUS_RUNNING = "0";
|
||||
|
||||
/** 审批通过 */
|
||||
public static final String STATUS_APPROVED = "1";
|
||||
|
||||
/** 审批拒绝 */
|
||||
public static final String STATUS_REJECTED = "2";
|
||||
|
||||
/** 已撤销 */
|
||||
public static final String STATUS_CANCELLED = "3";
|
||||
|
||||
/** 发起失败 */
|
||||
public static final String STATUS_LAUNCH_FAILED = "4";
|
||||
}
|
||||
@@ -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.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.vo.ApprovalGateVo;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批门禁 API
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Tag(name = "MES审批门禁")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/approvalGate")
|
||||
@Slf4j
|
||||
public class MesXslApprovalGateController {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
|
||||
@Operation(summary = "检查是否允许发起审批")
|
||||
@GetMapping("/canLaunch")
|
||||
public Result<ApprovalGateVo> canLaunch(
|
||||
@RequestParam("bizTable") String bizTable,
|
||||
@RequestParam("bizDataId") String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return Result.error("bizTable 与 bizDataId 不能为空");
|
||||
}
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
ApprovalGateVo vo = approvalGateService.checkCanLaunch(bizTable.trim(), bizDataId.trim(), tenantId);
|
||||
return Result.OK(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "批量检查是否允许发起审批")
|
||||
@PostMapping("/canLaunchBatch")
|
||||
public Result<List<ApprovalGateVo>> canLaunchBatch(@RequestBody CanLaunchBatchRequest req) {
|
||||
if (req == null || oConvertUtils.isEmpty(req.getBizTable()) || req.getBizDataIds() == null || req.getBizDataIds().isEmpty()) {
|
||||
return Result.error("bizTable 与 bizDataIds 不能为空");
|
||||
}
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
List<ApprovalGateVo> list = approvalGateService.checkCanLaunchBatch(req.getBizTable().trim(), req.getBizDataIds(), tenantId);
|
||||
return Result.OK(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询业务单据审批台账历史")
|
||||
@GetMapping("/history")
|
||||
public Result<List<MesXslApprovalRecord>> history(
|
||||
@RequestParam("bizTable") String bizTable,
|
||||
@RequestParam("bizDataId") String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return Result.error("bizTable 与 bizDataId 不能为空");
|
||||
}
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(null);
|
||||
return Result.OK(approvalGateService.listHistory(bizTable.trim(), bizDataId.trim(), tenantId));
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CanLaunchBatchRequest {
|
||||
private String bizTable;
|
||||
private List<String> bizDataIds;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.jeecg.modules.xslmes.approval.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -12,8 +11,10 @@ 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.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
||||
import org.jeecg.modules.xslmes.approval.vo.ApprovalGateVo;
|
||||
import org.jeecg.modules.xslmes.common.MesXslTenantUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -52,6 +53,11 @@ public class MesXslApprovalLaunchController {
|
||||
private IMesXslApprovalHandleService approvalHandleService;
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起改用流转引擎进入首节点-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】发起审批统一门禁与台账写入-----
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】发起审批统一门禁与台账写入-----
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@@ -166,22 +172,22 @@ public class MesXslApprovalLaunchController {
|
||||
return Result.error("该审批流未发布,无法发起");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】防止同一单据重复发起审批-----
|
||||
long active = approvalInstanceService.count(new LambdaQueryWrapper<MesXslApprovalInstance>()
|
||||
.eq(MesXslApprovalInstance::getBizTable, flow.getBizTable())
|
||||
.eq(MesXslApprovalInstance::getBizDataId, bizDataId)
|
||||
.eq(MesXslApprovalInstance::getStatus, "0"));
|
||||
if (active > 0) {
|
||||
return Result.error("该单据已有审批中的流程,请勿重复发起");
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】防止同一单据重复发起审批-----
|
||||
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(flow.getTenantId());
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】发起审批统一门禁与台账写入-----
|
||||
try {
|
||||
approvalGateService.assertCanLaunch(flow.getBizTable(), bizDataId, tenantId);
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalGateService.createRunningRecord(
|
||||
approvalGateService.buildMesDraft(flow.getBizTable(), flow.getBizTableName(), bizDataId, bizTitle,
|
||||
flow.getId(), flow.getFlowName(), inst.getId(), loginUser, tenantId));
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】发起后由引擎进入首节点(解析处理人/建进度/发可办理卡片)-----
|
||||
syncApprovalRecordAfterLaunch(inst.getId());
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】发起审批统一门禁与台账写入-----
|
||||
return Result.OK("发起成功!");
|
||||
}
|
||||
|
||||
@@ -218,23 +224,25 @@ public class MesXslApprovalLaunchController {
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
continue;
|
||||
}
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
|
||||
long active = approvalInstanceService.count(new LambdaQueryWrapper<MesXslApprovalInstance>()
|
||||
.eq(MesXslApprovalInstance::getBizTable, flow.getBizTable())
|
||||
.eq(MesXslApprovalInstance::getBizDataId, bizDataId)
|
||||
.eq(MesXslApprovalInstance::getStatus, "0"));
|
||||
if (active > 0) {
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】批量发起统一门禁与台账写入-----
|
||||
Integer tenantId = MesXslTenantUtils.resolveTenantId(flow.getTenantId());
|
||||
ApprovalGateVo gate = approvalGateService.checkCanLaunch(flow.getBizTable(), bizDataId, tenantId);
|
||||
if (gate.getAllowed() == null || !gate.getAllowed()) {
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】批量发起防重:跳过已有审批中实例的单据-----
|
||||
MesXslApprovalInstance inst = buildInstance(flow, bizDataId, bizTitle, loginUser);
|
||||
approvalInstanceService.save(inst);
|
||||
approvalGateService.createRunningRecord(
|
||||
approvalGateService.buildMesDraft(flow.getBizTable(), flow.getBizTableName(), bizDataId, bizTitle,
|
||||
flow.getId(), flow.getFlowName(), inst.getId(), loginUser, tenantId));
|
||||
approvalHandleService.enterFirstNode(inst, loginUser);
|
||||
syncApprovalRecordAfterLaunch(inst.getId());
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】批量发起统一门禁与台账写入-----
|
||||
count++;
|
||||
}
|
||||
if (count == 0) {
|
||||
return Result.error(skipCount > 0 ? "所选单据均已有审批中的流程,无需重复发起" : "没有有效的单据数据");
|
||||
return Result.error(skipCount > 0 ? "所选单据均不允许发起审批(可能已有流转中或已通过流程)" : "没有有效的单据数据");
|
||||
}
|
||||
String msg = "已发起 " + count + " 条审批!";
|
||||
if (skipCount > 0) {
|
||||
@@ -265,4 +273,14 @@ public class MesXslApprovalLaunchController {
|
||||
// 处理人解析、节点进度初始化、卡片发送统一由 IMesXslApprovalHandleService.enterFirstNode 完成
|
||||
return inst;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】发起后同步台账终态(如无节点自动通过)-----
|
||||
private void syncApprovalRecordAfterLaunch(String instanceId) {
|
||||
MesXslApprovalInstance latest = approvalInstanceService.getById(instanceId);
|
||||
if (latest == null || "0".equals(latest.getStatus())) {
|
||||
return;
|
||||
}
|
||||
approvalGateService.finishByMesInstance(instanceId, latest.getStatus(), latest.getCurrentHandlersText());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】发起后同步台账终态(如无节点自动通过)-----
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
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.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* MES 审批台账(只读查询)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】台账列表菜单
|
||||
*/
|
||||
@Tag(name = "MES审批台账")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/mesXslApprovalRecord")
|
||||
@Slf4j
|
||||
public class MesXslApprovalRecordController extends JeecgController<MesXslApprovalRecord, IMesXslApprovalRecordService> {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalRecordService mesXslApprovalRecordService;
|
||||
|
||||
@Operation(summary = "MES审批台账-分页列表查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_record:list")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<MesXslApprovalRecord>> queryPageList(
|
||||
MesXslApprovalRecord model,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<MesXslApprovalRecord> queryWrapper = QueryGenerator.initQueryWrapper(model, req.getParameterMap());
|
||||
queryWrapper.orderByDesc("apply_time").orderByDesc("create_time");
|
||||
Page<MesXslApprovalRecord> page = new Page<>(pageNo, pageSize);
|
||||
IPage<MesXslApprovalRecord> pageList = mesXslApprovalRecordService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
@Operation(summary = "MES审批台账-通过id查询")
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_record:list")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<MesXslApprovalRecord> queryById(@RequestParam(name = "id") String id) {
|
||||
MesXslApprovalRecord entity = mesXslApprovalRecordService.getById(id);
|
||||
if (entity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(entity);
|
||||
}
|
||||
|
||||
@RequiresPermissions("xslmes:mes_xsl_approval_record:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, MesXslApprovalRecord model) {
|
||||
return super.exportXls(request, model, MesXslApprovalRecord.class, "MES审批台账");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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 审批台账(跨 MES/钉钉 统一门禁)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("mes_xsl_approval_record")
|
||||
@Schema(description = "MES审批台账")
|
||||
public class MesXslApprovalRecord extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "业务单据表名")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "业务单据中文名")
|
||||
private String bizTableName;
|
||||
|
||||
@Schema(description = "业务编码(菜单permission.id)")
|
||||
private String bizCode;
|
||||
|
||||
@Schema(description = "业务单据记录ID")
|
||||
private String bizDataId;
|
||||
|
||||
@Schema(description = "业务单据展示标题")
|
||||
private String bizTitle;
|
||||
|
||||
@Schema(description = "审批通道 MES/DINGTALK")
|
||||
@Dict(dicCode = "mes_xsl_approval_channel")
|
||||
private String channel;
|
||||
|
||||
@Schema(description = "外部实例ID(MES实例ID或钉钉instanceId)")
|
||||
private String externalInstanceId;
|
||||
|
||||
@Schema(description = "MES审批流ID")
|
||||
private String flowId;
|
||||
|
||||
@Schema(description = "MES审批流名称")
|
||||
private String flowName;
|
||||
|
||||
@Schema(description = "钉钉审批模板ID")
|
||||
private String templateId;
|
||||
|
||||
@Schema(description = "钉钉审批模板名称")
|
||||
private String templateName;
|
||||
|
||||
@Schema(description = "同一业务单据第几次发起")
|
||||
private Integer launchNo;
|
||||
|
||||
@Schema(description = "状态 0流转中 1通过 2拒绝 3撤销 4发起失败")
|
||||
@Dict(dicCode = "mes_xsl_approval_record_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 = "办结时间")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date finishTime;
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】与 MesXslApprovalInstance 对齐,复用 isBizAtOriginStatus 逻辑-----
|
||||
@Schema(description = "业务单据状态字段名(发起时快照,驳回回写依据)")
|
||||
private String statusField;
|
||||
|
||||
@Schema(description = "发起审批时业务状态原值(驳回回写依据)")
|
||||
private String originStatus;
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】与 MesXslApprovalInstance 对齐,复用 isBizAtOriginStatus 逻辑-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁字段,记录已处理的节点回调数-----
|
||||
@Schema(description = "钉钉回调已处理节点数(幂等去重,勿用于业务逻辑)")
|
||||
private Integer processedOpCount;
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁字段,记录已处理的节点回调数-----
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "逻辑删除 0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
|
||||
/**
|
||||
* MES 审批台账 Mapper
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
public interface MesXslApprovalRecordMapper extends BaseMapper<MesXslApprovalRecord> {
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.jeecg.modules.xslmes.approval.service;
|
||||
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.vo.ApprovalGateVo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 审批门禁服务:统一判断能否发起审批,并维护审批台账
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
public interface IMesXslApprovalGateService {
|
||||
|
||||
/**
|
||||
* 检查是否允许对指定业务单据发起审批(跨 MES/钉钉通道)
|
||||
*/
|
||||
ApprovalGateVo checkCanLaunch(String bizTable, String bizDataId, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 批量检查是否允许发起审批
|
||||
*/
|
||||
List<ApprovalGateVo> checkCanLaunchBatch(String bizTable, List<String> bizDataIds, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 校验允许发起,不允许则抛出 IllegalStateException(供 Controller 转 Result.error)
|
||||
*/
|
||||
void assertCanLaunch(String bizTable, String bizDataId, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 发起审批时写入台账(状态=流转中)
|
||||
*/
|
||||
MesXslApprovalRecord createRunningRecord(MesXslApprovalRecord draft);
|
||||
|
||||
/**
|
||||
* MES 审批实例办结时同步台账
|
||||
*/
|
||||
void finishByMesInstance(String mesInstanceId, String status, String remark);
|
||||
|
||||
/**
|
||||
* 按外部实例 ID 办结台账(预留钉钉回调等场景)。
|
||||
* 返回 true 表示本次调用实际更新了台账(状态从流转中变为终态);
|
||||
* 返回 false 表示台账已是终态,本次为重复调用。
|
||||
*/
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:返回是否实际更新,供调用方判断是否跳过后续业务回调-----
|
||||
boolean finishByExternalInstance(String channel, String externalInstanceId, String status, String remark);
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:返回是否实际更新,供调用方判断是否跳过后续业务回调-----
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁标记节点已处理-----
|
||||
/**
|
||||
* 用乐观锁尝试将台账的 processed_op_count 推进到 nodeIndex+1。
|
||||
* 返回 true 表示成功(本次是第一个处理该节点的线程);
|
||||
* 返回 false 表示已被其他线程/事件处理过,调用方应跳过。
|
||||
*/
|
||||
boolean tryMarkNodeProcessed(String recordId, int nodeIndex);
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁标记节点已处理-----
|
||||
|
||||
/**
|
||||
* 查询业务单据审批历史(按发起时间倒序)
|
||||
*/
|
||||
List<MesXslApprovalRecord> listHistory(String bizTable, String bizDataId, Integer tenantId);
|
||||
|
||||
/**
|
||||
* 构建 MES 通道台账草稿
|
||||
*/
|
||||
MesXslApprovalRecord buildMesDraft(String bizTable, String bizTableName, String bizDataId, String bizTitle,
|
||||
String flowId, String flowName, String mesInstanceId, LoginUser loginUser,
|
||||
Integer tenantId);
|
||||
|
||||
/**
|
||||
* 构建钉钉通道台账草稿
|
||||
*/
|
||||
MesXslApprovalRecord buildDingDraft(String bizTable, String bizTableName, String bizCode, String bizDataId,
|
||||
String bizTitle, String flowId, String flowName, String templateId,
|
||||
String templateName, String dingInstanceId, LoginUser loginUser,
|
||||
Integer tenantId);
|
||||
}
|
||||
@@ -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.MesXslApprovalRecord;
|
||||
|
||||
/**
|
||||
* MES 审批台账 Service
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
public interface IMesXslApprovalRecordService extends IService<MesXslApprovalRecord> {
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package org.jeecg.modules.xslmes.approval.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.xslmes.approval.constant.ApprovalRecordConstants;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.jeecg.modules.xslmes.approval.vo.ApprovalGateVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批门禁服务实现
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MesXslApprovalGateServiceImpl implements IMesXslApprovalGateService {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalRecordService recordService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Override
|
||||
public ApprovalGateVo checkCanLaunch(String bizTable, String bizDataId, Integer tenantId) {
|
||||
ApprovalGateVo vo = new ApprovalGateVo()
|
||||
.setBizTable(bizTable)
|
||||
.setBizDataId(bizDataId);
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return vo.setAllowed(false).setReason("业务单据信息不完整,无法判断审批状态");
|
||||
}
|
||||
MesXslApprovalRecord latest = findLatest(bizTable, bizDataId, tenantId);
|
||||
if (latest == null) {
|
||||
return vo.setAllowed(true).setReason("尚未发起过审批,允许发起");
|
||||
}
|
||||
fillLatestInfo(vo, latest);
|
||||
if (ApprovalRecordConstants.STATUS_RUNNING.equals(latest.getStatus())) {
|
||||
String channelText = channelText(latest.getChannel());
|
||||
return vo.setAllowed(false).setReason("该单据已有" + channelText + "流程流转中,请勿重复发起");
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_APPROVED.equals(latest.getStatus())) {
|
||||
return vo.setAllowed(false).setReason("该单据已审批通过,无需重复发起");
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_REJECTED.equals(latest.getStatus())) {
|
||||
return vo.setAllowed(true).setReason("上次审批已被拒绝,允许重新发起");
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_CANCELLED.equals(latest.getStatus())) {
|
||||
return vo.setAllowed(true).setReason("上次审批已撤销,允许重新发起");
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_LAUNCH_FAILED.equals(latest.getStatus())) {
|
||||
return vo.setAllowed(true).setReason("上次发起失败,允许重新发起");
|
||||
}
|
||||
return vo.setAllowed(true).setReason("允许发起审批");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApprovalGateVo> checkCanLaunchBatch(String bizTable, List<String> bizDataIds, Integer tenantId) {
|
||||
List<ApprovalGateVo> list = new ArrayList<>();
|
||||
if (bizDataIds == null || bizDataIds.isEmpty()) {
|
||||
return list;
|
||||
}
|
||||
for (String bizDataId : bizDataIds) {
|
||||
if (oConvertUtils.isEmpty(bizDataId)) {
|
||||
continue;
|
||||
}
|
||||
list.add(checkCanLaunch(bizTable, bizDataId, tenantId));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertCanLaunch(String bizTable, String bizDataId, Integer tenantId) {
|
||||
ApprovalGateVo gate = checkCanLaunch(bizTable, bizDataId, tenantId);
|
||||
if (gate.getAllowed() == null || !gate.getAllowed()) {
|
||||
throw new IllegalStateException(oConvertUtils.getString(gate.getReason(), "当前不允许发起审批"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public MesXslApprovalRecord createRunningRecord(MesXslApprovalRecord draft) {
|
||||
if (draft == null) {
|
||||
throw new IllegalArgumentException("台账数据不能为空");
|
||||
}
|
||||
assertCanLaunch(draft.getBizTable(), draft.getBizDataId(), draft.getTenantId());
|
||||
Date now = new Date();
|
||||
if (draft.getApplyTime() == null) {
|
||||
draft.setApplyTime(now);
|
||||
}
|
||||
draft.setStatus(ApprovalRecordConstants.STATUS_RUNNING);
|
||||
draft.setLaunchNo(nextLaunchNo(draft.getBizTable(), draft.getBizDataId(), draft.getTenantId()));
|
||||
recordService.save(draft);
|
||||
return draft;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void finishByMesInstance(String mesInstanceId, String status, String remark) {
|
||||
finishByExternalInstance(ApprovalRecordConstants.CHANNEL_MES, mesInstanceId, status, remark);
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:返回实际影响行数,0表示台账已是终态-----
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean finishByExternalInstance(String channel, String externalInstanceId, String status, String remark) {
|
||||
if (oConvertUtils.isEmpty(externalInstanceId) || oConvertUtils.isEmpty(status)) {
|
||||
return false;
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_RUNNING.equals(status)) {
|
||||
return false;
|
||||
}
|
||||
LambdaUpdateWrapper<MesXslApprovalRecord> uw = new LambdaUpdateWrapper<MesXslApprovalRecord>()
|
||||
.eq(MesXslApprovalRecord::getExternalInstanceId, externalInstanceId)
|
||||
.eq(MesXslApprovalRecord::getStatus, ApprovalRecordConstants.STATUS_RUNNING);
|
||||
if (oConvertUtils.isNotEmpty(channel)) {
|
||||
uw.eq(MesXslApprovalRecord::getChannel, channel);
|
||||
}
|
||||
uw.set(MesXslApprovalRecord::getStatus, status)
|
||||
.set(MesXslApprovalRecord::getFinishTime, new Date());
|
||||
if (oConvertUtils.isNotEmpty(remark)) {
|
||||
uw.set(MesXslApprovalRecord::getRemark, remark);
|
||||
}
|
||||
// getBaseMapper().update() 返回实际影响行数,0 = 台账已是终态(重复事件)
|
||||
int affected = recordService.getBaseMapper().update(null, uw);
|
||||
return affected > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryMarkNodeProcessed(String recordId, int nodeIndex) {
|
||||
if (oConvertUtils.isEmpty(recordId)) return false;
|
||||
// 乐观锁:processed_op_count < nodeIndex+1 时才推进,保证同一节点只被处理一次
|
||||
// 两个并发线程同时到达时,MySQL 行锁保证只有一个 UPDATE 成功
|
||||
int affected = jdbcTemplate.update(
|
||||
"UPDATE mes_xsl_approval_record SET processed_op_count = ? " +
|
||||
"WHERE id = ? AND processed_op_count < ? AND del_flag = 0",
|
||||
nodeIndex + 1, recordId, nodeIndex + 1);
|
||||
return affected > 0;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:返回实际影响行数,0表示台账已是终态-----
|
||||
|
||||
@Override
|
||||
public List<MesXslApprovalRecord> listHistory(String bizTable, String bizDataId, Integer tenantId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) {
|
||||
return List.of();
|
||||
}
|
||||
LambdaQueryWrapper<MesXslApprovalRecord> qw = baseBizQuery(bizTable, bizDataId, tenantId);
|
||||
qw.orderByDesc(MesXslApprovalRecord::getApplyTime).orderByDesc(MesXslApprovalRecord::getCreateTime);
|
||||
return recordService.list(qw);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MesXslApprovalRecord buildMesDraft(String bizTable, String bizTableName, String bizDataId, String bizTitle,
|
||||
String flowId, String flowName, String mesInstanceId, LoginUser loginUser,
|
||||
Integer tenantId) {
|
||||
MesXslApprovalRecord record = new MesXslApprovalRecord();
|
||||
record.setBizTable(bizTable);
|
||||
record.setBizTableName(bizTableName);
|
||||
record.setBizDataId(bizDataId);
|
||||
record.setBizTitle(oConvertUtils.isNotEmpty(bizTitle) ? bizTitle : bizDataId);
|
||||
record.setChannel(ApprovalRecordConstants.CHANNEL_MES);
|
||||
record.setExternalInstanceId(mesInstanceId);
|
||||
record.setFlowId(flowId);
|
||||
record.setFlowName(flowName);
|
||||
record.setTenantId(tenantId);
|
||||
fillApplyUser(record, loginUser);
|
||||
return record;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MesXslApprovalRecord buildDingDraft(String bizTable, String bizTableName, String bizCode, String bizDataId,
|
||||
String bizTitle, String flowId, String flowName, String templateId,
|
||||
String templateName, String dingInstanceId, LoginUser loginUser,
|
||||
Integer tenantId) {
|
||||
MesXslApprovalRecord record = new MesXslApprovalRecord();
|
||||
record.setBizTable(bizTable);
|
||||
record.setBizTableName(bizTableName);
|
||||
record.setBizCode(bizCode);
|
||||
record.setBizDataId(bizDataId);
|
||||
record.setBizTitle(oConvertUtils.isNotEmpty(bizTitle) ? bizTitle : bizDataId);
|
||||
record.setChannel(ApprovalRecordConstants.CHANNEL_DINGTALK);
|
||||
record.setExternalInstanceId(dingInstanceId);
|
||||
record.setFlowId(flowId);
|
||||
record.setFlowName(flowName);
|
||||
record.setTemplateId(templateId);
|
||||
record.setTemplateName(templateName);
|
||||
record.setTenantId(tenantId);
|
||||
fillApplyUser(record, loginUser);
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】快照业务状态原值,复用 isBizAtOriginStatus 逻辑-----
|
||||
snapshotOriginStatus(record, bizTable, bizDataId);
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】快照业务状态原值,复用 isBizAtOriginStatus 逻辑-----
|
||||
return record;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】发起时快照业务状态,驳回时供共用判断-----
|
||||
/**
|
||||
* 探测业务表是否存在 status 字段,若存在则读取当前值快照到台账。
|
||||
* 与 MesXslApprovalHandleServiceImpl.snapshotBizStatus() 逻辑对齐,
|
||||
* 使驳回时的 isBizAtOriginStatus 判断可跨 MES/钉钉两通道复用。
|
||||
*/
|
||||
private void snapshotOriginStatus(MesXslApprovalRecord record, String bizTable, String bizDataId) {
|
||||
if (oConvertUtils.isEmpty(bizTable) || oConvertUtils.isEmpty(bizDataId)) return;
|
||||
if (!bizTable.matches("^[A-Za-z0-9_]+$")) return;
|
||||
try {
|
||||
Integer cnt = jdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(1) FROM information_schema.columns " +
|
||||
"WHERE table_schema=(SELECT DATABASE()) AND table_name=? AND column_name='status'",
|
||||
Integer.class, bizTable);
|
||||
if (cnt == null || cnt == 0) return;
|
||||
java.util.List<String> vals = jdbcTemplate.queryForList(
|
||||
"SELECT status FROM " + bizTable + " WHERE id=? LIMIT 1", String.class, bizDataId);
|
||||
if (!vals.isEmpty()) {
|
||||
record.setStatusField("status");
|
||||
record.setOriginStatus(vals.get(0));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[ApprovalGate] 快照业务状态失败 table={} id={}: {}", bizTable, bizDataId, e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】发起时快照业务状态,驳回时供共用判断-----
|
||||
|
||||
private MesXslApprovalRecord findLatest(String bizTable, String bizDataId, Integer tenantId) {
|
||||
LambdaQueryWrapper<MesXslApprovalRecord> qw = baseBizQuery(bizTable, bizDataId, tenantId);
|
||||
qw.orderByDesc(MesXslApprovalRecord::getApplyTime).orderByDesc(MesXslApprovalRecord::getCreateTime).last("LIMIT 1");
|
||||
return recordService.getOne(qw, false);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<MesXslApprovalRecord> baseBizQuery(String bizTable, String bizDataId, Integer tenantId) {
|
||||
LambdaQueryWrapper<MesXslApprovalRecord> qw = new LambdaQueryWrapper<>();
|
||||
qw.eq(MesXslApprovalRecord::getBizTable, bizTable)
|
||||
.eq(MesXslApprovalRecord::getBizDataId, bizDataId);
|
||||
if (tenantId != null) {
|
||||
qw.eq(MesXslApprovalRecord::getTenantId, tenantId);
|
||||
}
|
||||
return qw;
|
||||
}
|
||||
|
||||
private int nextLaunchNo(String bizTable, String bizDataId, Integer tenantId) {
|
||||
LambdaQueryWrapper<MesXslApprovalRecord> qw = baseBizQuery(bizTable, bizDataId, tenantId);
|
||||
long count = recordService.count(qw);
|
||||
return (int) count + 1;
|
||||
}
|
||||
|
||||
private void fillLatestInfo(ApprovalGateVo vo, MesXslApprovalRecord latest) {
|
||||
vo.setLatestRecordId(latest.getId())
|
||||
.setLatestStatus(latest.getStatus())
|
||||
.setLatestChannel(latest.getChannel())
|
||||
.setLatestChannelText(channelText(latest.getChannel()))
|
||||
.setLatestStatusText(statusText(latest.getStatus()));
|
||||
}
|
||||
|
||||
private void fillApplyUser(MesXslApprovalRecord record, LoginUser loginUser) {
|
||||
if (loginUser == null) {
|
||||
return;
|
||||
}
|
||||
record.setApplyUser(loginUser.getUsername());
|
||||
record.setApplyUserName(loginUser.getRealname());
|
||||
record.setSysOrgCode(loginUser.getOrgCode());
|
||||
}
|
||||
|
||||
private String channelText(String channel) {
|
||||
if (ApprovalRecordConstants.CHANNEL_DINGTALK.equals(channel)) {
|
||||
return "钉钉审批";
|
||||
}
|
||||
if (ApprovalRecordConstants.CHANNEL_MES.equals(channel)) {
|
||||
return "MES审批";
|
||||
}
|
||||
return oConvertUtils.getString(channel, "审批");
|
||||
}
|
||||
|
||||
private String statusText(String status) {
|
||||
if (ApprovalRecordConstants.STATUS_RUNNING.equals(status)) {
|
||||
return "流转中";
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_APPROVED.equals(status)) {
|
||||
return "审批通过";
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_REJECTED.equals(status)) {
|
||||
return "审批拒绝";
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_CANCELLED.equals(status)) {
|
||||
return "已撤销";
|
||||
}
|
||||
if (ApprovalRecordConstants.STATUS_LAUNCH_FAILED.equals(status)) {
|
||||
return "发起失败";
|
||||
}
|
||||
return oConvertUtils.getString(status, "");
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import org.jeecg.modules.xslmes.approval.callback.ApprovalCallbackDispatcher;
|
||||
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.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalHandleService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalInstanceService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -68,6 +69,11 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
@Autowired
|
||||
private IMesXslApprovalInstanceService instanceService;
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】MES审批办结同步台账-----
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】MES审批办结同步台账-----
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@@ -121,6 +127,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentNodeName("无审批节点");
|
||||
inst.setCurrentHandlersText("无审批节点,自动通过");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, "无审批节点,自动通过");
|
||||
// 无审批节点直接最终通过 -> 回调业务
|
||||
callbackDispatcher.fireApproved(buildContext(inst, inst.getCurrentNodeId(), "无审批节点", applyUser, "无审批节点,自动通过"));
|
||||
notifyApplicant(inst, applyUser == null ? null : applyUser.getUsername(),
|
||||
@@ -209,6 +216,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("审批通过");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, comment);
|
||||
// 无后续流程,直接最终通过
|
||||
callbackDispatcher.fireApproved(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, comment));
|
||||
return Result.OK("审批通过");
|
||||
@@ -258,6 +266,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlersText("已驳回");
|
||||
inst.setNodeProgress(progress.toJSONString());
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, reason);
|
||||
|
||||
// 驳回 -> 回调业务(可回退业务状态)
|
||||
callbackDispatcher.fireRejected(buildContext(inst, progress.getString("nodeId"), progress.getString("nodeName"), user, reason));
|
||||
@@ -324,6 +333,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("已撤销");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, comment);
|
||||
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】撤销改为恢复发起前快照,不再走业务「拒绝/驳回」接口-----
|
||||
// 撤销时单据仍处于发起前最初状态,只需把状态字段恢复到发起时快照(幂等),
|
||||
@@ -423,6 +433,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("审批人为空,流程终止");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, "审批人为空,流程终止");
|
||||
// 流程终止(等同驳回) -> 回调业务(可回退业务状态)
|
||||
callbackDispatcher.fireRejected(buildContext(inst, nodeId, nodeName, null, "审批人为空,流程终止"));
|
||||
//update-begin---author:GHT ---date:2026-05-29 for:【QH-MES审批流设计】流程终止:驳回统一回退(按表注解自动执行)-----
|
||||
@@ -516,6 +527,7 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
inst.setCurrentHandlers(null);
|
||||
inst.setCurrentHandlersText("审批通过");
|
||||
instanceService.updateById(inst);
|
||||
syncApprovalRecord(inst, null);
|
||||
// 流程最终通过 -> 回调业务(终态)
|
||||
callbackDispatcher.fireApproved(buildContext(inst, currentNodeId, inst.getCurrentNodeName(), actingUser, null));
|
||||
actionHttpExecutor.run(findNodeById(root, currentNodeId), "onApprove", inst);
|
||||
@@ -1571,4 +1583,13 @@ public class MesXslApprovalHandleServiceImpl implements IMesXslApprovalHandleSer
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-05-29 for:【QH-MES审批流完善】全链路通知补全-----
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】MES审批办结同步台账-----
|
||||
private void syncApprovalRecord(MesXslApprovalInstance inst, String remark) {
|
||||
if (inst == null || oConvertUtils.isEmpty(inst.getId()) || "0".equals(inst.getStatus())) {
|
||||
return;
|
||||
}
|
||||
approvalGateService.finishByMesInstance(inst.getId(), inst.getStatus(), remark);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】MES审批办结同步台账-----
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jeecg.modules.xslmes.approval.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.mapper.MesXslApprovalRecordMapper;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* MES 审批台账 ServiceImpl
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Service
|
||||
public class MesXslApprovalRecordServiceImpl extends ServiceImpl<MesXslApprovalRecordMapper, MesXslApprovalRecord>
|
||||
implements IMesXslApprovalRecordService {
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.jeecg.modules.xslmes.approval.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 审批门禁检查结果
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【QH-MES审批台账】跨通道审批门禁
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "审批门禁检查结果")
|
||||
public class ApprovalGateVo implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "是否允许发起审批")
|
||||
private Boolean allowed;
|
||||
|
||||
@Schema(description = "不允许时的原因说明")
|
||||
private String reason;
|
||||
|
||||
@Schema(description = "业务单据表名")
|
||||
private String bizTable;
|
||||
|
||||
@Schema(description = "业务单据记录ID")
|
||||
private String bizDataId;
|
||||
|
||||
@Schema(description = "最新台账记录ID")
|
||||
private String latestRecordId;
|
||||
|
||||
@Schema(description = "最新台账状态")
|
||||
private String latestStatus;
|
||||
|
||||
@Schema(description = "最新台账通道 MES/DINGTALK")
|
||||
private String latestChannel;
|
||||
|
||||
@Schema(description = "最新台账通道展示名")
|
||||
private String latestChannelText;
|
||||
|
||||
@Schema(description = "最新台账状态展示名")
|
||||
private String latestStatusText;
|
||||
}
|
||||
@@ -1,26 +1,14 @@
|
||||
package org.jeecg.modules.xslmes.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.jeecg.dingtalk.api.core.response.Response;
|
||||
import com.jeecg.dingtalk.api.user.JdtUserAPI;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
@@ -34,7 +22,6 @@ import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.system.entity.SysDepart;
|
||||
import org.jeecg.modules.system.service.ISysDepartService;
|
||||
import org.jeecg.modules.system.service.impl.ThirdAppDingtalkServiceImpl;
|
||||
import org.jeecg.modules.xslmes.approval.action.ApprovalBizAction;
|
||||
import org.jeecg.modules.xslmes.entity.MesXslMixerPsCompile;
|
||||
import org.jeecg.modules.xslmes.service.IMesXslMixerPsCompileService;
|
||||
@@ -58,8 +45,6 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
private ISysDepartService sysDepartService;
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】注入钉钉服务-----
|
||||
@Autowired
|
||||
private ThirdAppDingtalkServiceImpl dingtalkService;
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】注入钉钉服务-----
|
||||
|
||||
@Operation(summary = "MES密炼PS编制-分页列表查询")
|
||||
@@ -301,122 +286,6 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
|
||||
}
|
||||
//update-end---author:jiangxh ---date:20260520 for:【密炼PS编制】保存前校验与冗余回填-----------
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】测试接口-----
|
||||
@Operation(summary = "钉钉审批测试:用当前登录人手机号获取钉钉userId,并尝试PROC-GENERIC发起审批")
|
||||
@PostMapping(value = "/ddApprovalTest")
|
||||
public Result<Map<String, Object>> ddApprovalTest() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
|
||||
// ① 取当前登录用户
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
String username = loginUser.getUsername();
|
||||
String realname = oConvertUtils.isNotEmpty(loginUser.getRealname()) ? loginUser.getRealname() : username;
|
||||
String phone = loginUser.getPhone();
|
||||
result.put("step1_user", realname + "(" + username + ")");
|
||||
result.put("step1_phone", oConvertUtils.isNotEmpty(phone) ? phone : "【未配置手机号】");
|
||||
|
||||
if (oConvertUtils.isEmpty(phone)) {
|
||||
result.put("结论", "当前账号未配置手机号,无法查询钉钉userId");
|
||||
return Result.OK(result);
|
||||
}
|
||||
|
||||
// ② 获取 AccessToken
|
||||
String accessToken;
|
||||
try {
|
||||
accessToken = dingtalkService.getAccessToken();
|
||||
} catch (Exception e) {
|
||||
result.put("step2_accessToken", "获取失败: " + e.getMessage());
|
||||
return Result.OK(result);
|
||||
}
|
||||
if (oConvertUtils.isEmpty(accessToken)) {
|
||||
result.put("step2_accessToken", "获取失败,请在[系统配置-第三方应用]中检查钉钉配置");
|
||||
return Result.OK(result);
|
||||
}
|
||||
result.put("step2_accessToken", accessToken.substring(0, Math.min(10, accessToken.length())) + "...[已截断]");
|
||||
|
||||
// ③ 手机号查钉钉 userId
|
||||
Response<String> userIdResp = JdtUserAPI.getUseridByMobile(phone, accessToken);
|
||||
result.put("step3_getUseridByMobile_errcode", userIdResp.getErrcode());
|
||||
result.put("step3_getUseridByMobile_errmsg", userIdResp.getErrmsg());
|
||||
result.put("step3_dingtalkUserId", userIdResp.isSuccess() ? userIdResp.getResult() : "查询失败");
|
||||
|
||||
if (!userIdResp.isSuccess() || oConvertUtils.isEmpty(userIdResp.getResult())) {
|
||||
result.put("结论", "手机号未在钉钉通讯录中找到对应用户,请确认该手机号已在企业钉钉中注册");
|
||||
return Result.OK(result);
|
||||
}
|
||||
String dtUserId = userIdResp.getResult();
|
||||
|
||||
// ④ 用真实模板 processCode 发起审批实例
|
||||
try {
|
||||
String processCode = "PROC-71957EB4-6F64-4AD7-AA1C-3CD7E797687B"; // MES测试审批流
|
||||
String url = "https://api.dingtalk.com/v1.0/workflow/processInstances";
|
||||
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
|
||||
|
||||
// 表单字段(对应钉钉模板中的控件标题)
|
||||
JSONArray formValues = new JSONArray();
|
||||
for (String[] kv : new String[][]{
|
||||
{"PS编码", "TEST-" + System.currentTimeMillis() % 10000},
|
||||
{"类型", "MES测试"},
|
||||
{"发行日期", new SimpleDateFormat("yyyy-MM-dd").format(new Date())},
|
||||
{"发送部门", "技术部"},
|
||||
{"标题", "MES钉钉审批测试 " + now},
|
||||
}) {
|
||||
JSONObject f = new JSONObject();
|
||||
f.put("name", kv[0]);
|
||||
f.put("value", kv[1]);
|
||||
formValues.add(f);
|
||||
}
|
||||
|
||||
// 审批人:用当前登录人自己做测试审批人(and会签,只有1人)
|
||||
JSONArray approvers = new JSONArray();
|
||||
JSONObject approverNode = new JSONObject();
|
||||
approverNode.put("actionType", "AND");
|
||||
JSONArray approverIds = new JSONArray();
|
||||
approverIds.add(dtUserId);
|
||||
approverNode.put("userIds", approverIds);
|
||||
approvers.add(approverNode);
|
||||
|
||||
JSONObject reqBody = new JSONObject();
|
||||
reqBody.put("processCode", processCode);
|
||||
reqBody.put("originatorUserId", dtUserId);
|
||||
reqBody.put("deptId", -1);
|
||||
reqBody.put("approvers", approvers);
|
||||
reqBody.put("formComponentValues", formValues);
|
||||
|
||||
result.put("step4_请求体", reqBody);
|
||||
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
HttpRequest httpReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("x-acs-dingtalk-access-token", accessToken)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(reqBody.toJSONString()))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
String respBody = httpClient.send(httpReq, HttpResponse.BodyHandlers.ofString()).body();
|
||||
|
||||
JSONObject ddResp = JSONObject.parseObject(respBody);
|
||||
result.put("step4_钉钉响应", ddResp);
|
||||
if (ddResp.containsKey("instanceId") || ddResp.containsKey("processInstanceId")) {
|
||||
String iid = ddResp.containsKey("instanceId")
|
||||
? ddResp.getString("instanceId")
|
||||
: ddResp.getString("processInstanceId");
|
||||
result.put("结论", "✅ 审批实例创建成功!instanceId=" + iid + ",请到钉钉「待我审批」查看");
|
||||
} else {
|
||||
result.put("结论", "❌ 创建失败,code=" + ddResp.getString("code") + " msg=" + ddResp.getString("message"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("钉钉审批实例创建测试异常", e);
|
||||
result.put("step4_钉钉响应", "HTTP请求异常: " + e.getMessage());
|
||||
result.put("结论", "❌ 请求钉钉接口失败");
|
||||
}
|
||||
|
||||
return Result.OK(result);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【钉钉PROC-GENERIC可行性验证】测试接口-----
|
||||
|
||||
//update-begin---author:jiangxh ---date:20260520 for:【密炼PS编制】仅编制状态允许删除-----------
|
||||
private String assertCompileStatusForDelete(MesXslMixerPsCompile entity) {
|
||||
if (entity == null) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.dto.DingFormComponent;
|
||||
import org.jeecg.modules.xslmes.dingtalk.dto.DingFormComponentProps;
|
||||
import org.jeecg.modules.xslmes.dingtalk.dto.DingFormCreateRequest;
|
||||
@@ -75,6 +76,9 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】绑定MES审批流(审批人来源)-----------
|
||||
|
||||
@Operation(summary = "钉钉审批模板配置-分页列表查询")
|
||||
@@ -98,10 +102,40 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_process_tpl:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody MesXslDingProcessTpl mesXslDingProcessTpl) {
|
||||
if (oConvertUtils.isEmpty(mesXslDingProcessTpl.getProcessCode())) {
|
||||
mesXslDingProcessTpl.setProcessCode("");
|
||||
}
|
||||
mesXslDingProcessTplService.save(mesXslDingProcessTpl);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】新增审批模板(本地草稿,processCode 由后续创建钉钉模板回填)-----
|
||||
@AutoLog(value = "钉钉审批模板配置-新增审批模板草稿")
|
||||
@Operation(summary = "钉钉审批模板配置-新增审批模板(返回含 id 的记录,供打开表单设计器)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_process_tpl:add")
|
||||
@PostMapping(value = "/addNewTemplate")
|
||||
public Result<MesXslDingProcessTpl> addNewTemplate(@RequestBody MesXslDingProcessTpl body) {
|
||||
if (body == null || oConvertUtils.isEmpty(body.getTplName())) {
|
||||
return Result.error("模板名称不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(body.getStatus())) {
|
||||
body.setStatus("1");
|
||||
}
|
||||
if (body.getSortNo() == null) {
|
||||
body.setSortNo(0);
|
||||
}
|
||||
if (oConvertUtils.isEmpty(body.getFormFields())) {
|
||||
body.setFormFields("[]");
|
||||
}
|
||||
// process_code 列 NOT NULL,草稿阶段用空串占位,创建钉钉模板后回填真实 processCode
|
||||
if (oConvertUtils.isEmpty(body.getProcessCode())) {
|
||||
body.setProcessCode("");
|
||||
}
|
||||
mesXslDingProcessTplService.save(body);
|
||||
return Result.OK("审批模板已创建,请继续设计表单并推送到钉钉", body);
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】新增审批模板(本地草稿,processCode 由后续创建钉钉模板回填)-----
|
||||
|
||||
@AutoLog(value = "钉钉审批模板配置-编辑")
|
||||
@Operation(summary = "钉钉审批模板配置-编辑")
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_process_tpl:edit")
|
||||
@@ -206,22 +240,37 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
JSONObject result = ddResp.getJSONObject("result");
|
||||
JSONArray processList = result == null ? null : result.getJSONArray("process_list");
|
||||
|
||||
// 查询本地已存在的 processCode,标记哪些已导入
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】同步列表:已导入判定含 processCode 与同名本地草稿-----
|
||||
Set<String> existCodes = new HashSet<>();
|
||||
mesXslDingProcessTplService.list().forEach(t -> existCodes.add(t.getProcessCode()));
|
||||
Map<String, String> draftIdByTplName = new HashMap<>();
|
||||
for (MesXslDingProcessTpl t : mesXslDingProcessTplService.list()) {
|
||||
if (oConvertUtils.isNotEmpty(t.getProcessCode())) {
|
||||
existCodes.add(t.getProcessCode());
|
||||
} else if (oConvertUtils.isNotEmpty(t.getTplName())) {
|
||||
draftIdByTplName.put(t.getTplName().trim(), t.getId());
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
if (processList != null) {
|
||||
for (int i = 0; i < processList.size(); i++) {
|
||||
JSONObject item = processList.getJSONObject(i);
|
||||
String code = item.getString("process_code");
|
||||
String name = oConvertUtils.getString(item.getString("name"), "").trim();
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put("processCode", item.getString("process_code"));
|
||||
row.put("name", item.getString("name"));
|
||||
row.put("processCode", code);
|
||||
row.put("name", name);
|
||||
row.put("description", item.getString("description"));
|
||||
row.put("imported", existCodes.contains(item.getString("process_code")));
|
||||
boolean imported = oConvertUtils.isNotEmpty(code) && existCodes.contains(code);
|
||||
String localDraftId = (!imported && oConvertUtils.isNotEmpty(name))
|
||||
? draftIdByTplName.get(name) : null;
|
||||
row.put("imported", imported || localDraftId != null);
|
||||
row.put("localDraftId", localDraftId);
|
||||
row.put("linkDraft", localDraftId != null && !imported);
|
||||
list.add(row);
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】同步列表:已导入判定含 processCode 与同名本地草稿-----
|
||||
return Result.OK(list);
|
||||
} catch (Exception e) {
|
||||
log.error("从钉钉同步审批模板失败", e);
|
||||
@@ -235,28 +284,62 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
if (items == null || items.isEmpty()) {
|
||||
return Result.error("请选择要导入的模板");
|
||||
}
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】批量导入:优先回填同名本地草稿,避免重复记录-----
|
||||
Set<String> existCodes = new HashSet<>();
|
||||
mesXslDingProcessTplService.list().forEach(t -> existCodes.add(t.getProcessCode()));
|
||||
for (MesXslDingProcessTpl t : mesXslDingProcessTplService.list()) {
|
||||
if (oConvertUtils.isNotEmpty(t.getProcessCode())) {
|
||||
existCodes.add(t.getProcessCode());
|
||||
}
|
||||
}
|
||||
|
||||
List<MesXslDingProcessTpl> toSave = new ArrayList<>();
|
||||
int linkedCount = 0;
|
||||
for (Map<String, Object> item : items) {
|
||||
String code = String.valueOf(item.getOrDefault("processCode", ""));
|
||||
String name = String.valueOf(item.getOrDefault("name", ""));
|
||||
String code = String.valueOf(item.getOrDefault("processCode", "")).trim();
|
||||
String name = String.valueOf(item.getOrDefault("name", "")).trim();
|
||||
String localDraftId = item.get("localDraftId") != null ? String.valueOf(item.get("localDraftId")) : null;
|
||||
if (oConvertUtils.isEmpty(code) || existCodes.contains(code)) {
|
||||
continue;
|
||||
}
|
||||
MesXslDingProcessTpl draft = null;
|
||||
if (oConvertUtils.isNotEmpty(localDraftId)) {
|
||||
draft = mesXslDingProcessTplService.getById(localDraftId);
|
||||
}
|
||||
if (draft == null && oConvertUtils.isNotEmpty(name)) {
|
||||
draft = findDraftWithoutProcessCode(name);
|
||||
}
|
||||
if (draft != null && oConvertUtils.isEmpty(draft.getProcessCode())) {
|
||||
MesXslDingProcessTpl update = new MesXslDingProcessTpl();
|
||||
update.setId(draft.getId());
|
||||
update.setProcessCode(code);
|
||||
if (oConvertUtils.isNotEmpty(name)) {
|
||||
update.setTplName(name);
|
||||
}
|
||||
mesXslDingProcessTplService.updateById(update);
|
||||
existCodes.add(code);
|
||||
linkedCount++;
|
||||
continue;
|
||||
}
|
||||
MesXslDingProcessTpl tpl = new MesXslDingProcessTpl();
|
||||
tpl.setProcessCode(code);
|
||||
tpl.setTplName(name);
|
||||
tpl.setStatus("1");
|
||||
tpl.setSortNo(0);
|
||||
toSave.add(tpl);
|
||||
existCodes.add(code);
|
||||
}
|
||||
if (toSave.isEmpty()) {
|
||||
if (toSave.isEmpty() && linkedCount == 0) {
|
||||
return Result.OK("所选模板均已存在,无需重复导入");
|
||||
}
|
||||
mesXslDingProcessTplService.saveBatch(toSave);
|
||||
return Result.OK("成功导入 " + toSave.size() + " 个模板");
|
||||
if (!toSave.isEmpty()) {
|
||||
mesXslDingProcessTplService.saveBatch(toSave);
|
||||
}
|
||||
String msg = "成功导入 " + toSave.size() + " 个模板";
|
||||
if (linkedCount > 0) {
|
||||
msg += ",回填本地草稿 " + linkedCount + " 条";
|
||||
}
|
||||
return Result.OK(msg);
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【MESToDing审批配置】批量导入:优先回填同名本地草稿,避免重复记录-----
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】从钉钉同步审批模板列表-----
|
||||
|
||||
@@ -435,6 +518,7 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】创建/更新钉钉审批模板-----
|
||||
@Operation(summary = "钉钉审批模板配置-创建钉钉审批模板(POST不带processCode=新建)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_process_tpl:edit")
|
||||
@PostMapping(value = "/createDingTemplate")
|
||||
public Result<Map<String, Object>> createDingTemplate(@RequestBody Map<String, Object> body) {
|
||||
String id = String.valueOf(body.getOrDefault("id", ""));
|
||||
@@ -464,13 +548,16 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
return Result.error("钉钉返回错误: " + resp.getString("message") + " (code=" + resp.getString("code") + ")");
|
||||
}
|
||||
|
||||
String processCode = resp.getString("processCode");
|
||||
if (oConvertUtils.isNotEmpty(processCode)) {
|
||||
MesXslDingProcessTpl update = new MesXslDingProcessTpl();
|
||||
update.setId(tpl.getId());
|
||||
update.setProcessCode(processCode);
|
||||
mesXslDingProcessTplService.updateById(update);
|
||||
// 钉钉 v1.0 接口 processCode 在 result 节点内,非响应根字段
|
||||
String processCode = extractProcessCodeFromDingFormResp(resp);
|
||||
if (oConvertUtils.isEmpty(processCode)) {
|
||||
log.warn("【钉钉创建模板】响应中未解析到 processCode,原始响应: {}", respBody);
|
||||
return Result.error("钉钉模板已提交,但未返回 processCode,请从钉钉同步回填或查看后台日志");
|
||||
}
|
||||
MesXslDingProcessTpl update = new MesXslDingProcessTpl();
|
||||
update.setId(tpl.getId());
|
||||
update.setProcessCode(processCode);
|
||||
mesXslDingProcessTplService.updateById(update);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("processCode", processCode);
|
||||
@@ -483,6 +570,7 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
}
|
||||
|
||||
@Operation(summary = "钉钉审批模板配置-更新钉钉审批模板(POST带processCode=更新已有)")
|
||||
@RequiresPermissions("xslmes:mes_xsl_ding_process_tpl:edit")
|
||||
@PostMapping(value = "/updateDingTemplate")
|
||||
public Result<Map<String, Object>> updateDingTemplate(@RequestBody Map<String, Object> body) {
|
||||
String id = String.valueOf(body.getOrDefault("id", ""));
|
||||
@@ -742,6 +830,34 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
return "TextField";
|
||||
}
|
||||
|
||||
/** 从「创建/更新审批表单」响应中解析 processCode(兼容根节点与 result 节点) */
|
||||
private String extractProcessCodeFromDingFormResp(JSONObject resp) {
|
||||
if (resp == null) {
|
||||
return null;
|
||||
}
|
||||
String code = resp.getString("processCode");
|
||||
if (oConvertUtils.isNotEmpty(code)) {
|
||||
return code;
|
||||
}
|
||||
JSONObject result = resp.getJSONObject("result");
|
||||
if (result != null && oConvertUtils.isNotEmpty(result.getString("processCode"))) {
|
||||
return result.getString("processCode");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 查找尚未绑定 processCode 的本地草稿(按模板名称) */
|
||||
private MesXslDingProcessTpl findDraftWithoutProcessCode(String tplName) {
|
||||
if (oConvertUtils.isEmpty(tplName)) {
|
||||
return null;
|
||||
}
|
||||
QueryWrapper<MesXslDingProcessTpl> qw = new QueryWrapper<>();
|
||||
qw.eq("tpl_name", tplName.trim());
|
||||
qw.and(w -> w.isNull("process_code").or().eq("process_code", ""));
|
||||
qw.last("LIMIT 1");
|
||||
return mesXslDingProcessTplService.getOne(qw, false);
|
||||
}
|
||||
|
||||
/** 统一 HTTP 调用钉钉 v1.0 接口 */
|
||||
private String callDingApi(String method, String url, String accessToken, String jsonBody) throws Exception {
|
||||
HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
|
||||
@@ -1093,6 +1209,20 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
// ⑤ 查询发起人在钉钉的所属部门 ID
|
||||
long originatorDeptId = resolveUserDeptId(originatorDtUserId, accessToken);
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】钉钉发起前统一门禁校验-----
|
||||
String bizTable = firstNonEmpty(str(body.get("bizTable")), approvalFlow.getBizTable());
|
||||
String bizDataId = str(body.get("bizDataId"));
|
||||
String bizTitle = str(body.get("bizTitle"));
|
||||
String bizCode = str(body.get("bizCode"));
|
||||
if (oConvertUtils.isNotEmpty(bizTable) && oConvertUtils.isNotEmpty(bizDataId)) {
|
||||
try {
|
||||
approvalGateService.assertCanLaunch(bizTable, bizDataId, tenantId);
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】钉钉发起前统一门禁校验-----
|
||||
|
||||
// ⑥ 组装钉钉发起审批请求体
|
||||
JSONObject reqBody = new JSONObject();
|
||||
reqBody.put("processCode", tpl.getProcessCode());
|
||||
@@ -1124,8 +1254,18 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
return Result.error("钉钉返回错误: " + resp.getString("message") + " (code=" + resp.getString("code") + ")");
|
||||
}
|
||||
|
||||
String dingInstanceId = resp.getString("instanceId");
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】钉钉发起成功后写入审批台账-----
|
||||
if (oConvertUtils.isNotEmpty(bizTable) && oConvertUtils.isNotEmpty(bizDataId)) {
|
||||
approvalGateService.createRunningRecord(
|
||||
approvalGateService.buildDingDraft(bizTable, approvalFlow.getBizTableName(), bizCode, bizDataId,
|
||||
bizTitle, flowId, approvalFlow.getFlowName(), tpl.getId(), tpl.getTplName(),
|
||||
dingInstanceId, loginUser, tenantId));
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】钉钉发起成功后写入审批台账-----
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("instanceId", resp.getString("instanceId"));
|
||||
result.put("instanceId", dingInstanceId);
|
||||
result.put("tplName", tpl.getTplName());
|
||||
return Result.OK("审批发起成功!审批人将在钉钉「待我审批」中收到任务", result);
|
||||
} catch (Exception e) {
|
||||
@@ -1556,5 +1696,18 @@ public class MesXslDingProcessTplController extends JeecgController<MesXslDingPr
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【QH-MES审批台账】钉钉发起参数解析辅助-----
|
||||
private String str(Object value) {
|
||||
return value == null ? "" : String.valueOf(value).trim();
|
||||
}
|
||||
|
||||
private String firstNonEmpty(String first, String second) {
|
||||
if (oConvertUtils.isNotEmpty(first)) {
|
||||
return first.trim();
|
||||
}
|
||||
return oConvertUtils.isNotEmpty(second) ? second.trim() : "";
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【QH-MES审批台账】钉钉发起参数解析辅助-----
|
||||
//update-end---author:GHT ---date:2026-06-03 for:【MESToDing审批配置】手动填表发起钉钉审批-----------
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.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.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.modules.print.catalog.IPrintBizEntityFieldCatalogProvider;
|
||||
import org.jeecg.modules.print.service.IPrintBizPermEntityService;
|
||||
import org.jeecg.modules.print.util.PrintBizDetailPropertyScanner;
|
||||
import org.jeecg.modules.print.util.PrintBizEntityFieldIntrospector;
|
||||
import org.jeecg.modules.print.vo.PrintBizDetailSlotVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizFieldItemVO;
|
||||
import org.jeecg.modules.print.vo.PrintBizTypeVO;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingTplBind;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingTplBindService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingProcessTplService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板绑定:将菜单业务实体字段与审批模板表单控件可视化绑定
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【MESToDing审批配置】审批模板绑定
|
||||
*/
|
||||
@Tag(name = "钉钉审批模板绑定")
|
||||
@RestController
|
||||
@RequestMapping("/xslmes/dingTplBind")
|
||||
@Slf4j
|
||||
public class MesXslDingTplBindController {
|
||||
|
||||
@Autowired private IMesXslDingTplBindService bindService;
|
||||
@Autowired private IMesXslDingProcessTplService tplService;
|
||||
@Autowired private IPrintBizPermEntityService printBizPermEntityService;
|
||||
@Autowired private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired(required = false)
|
||||
private IPrintBizEntityFieldCatalogProvider fieldCatalogProvider;
|
||||
|
||||
// ═══════════════════════ 菜单树 ═══════════════════════
|
||||
|
||||
/**
|
||||
* 查询 sys_permission 一二级菜单树(menu_type=0/1,最多两层),供左侧选择业务菜单。
|
||||
* 不限租户(全量展示,与打印绑定白名单选树逻辑一致)。
|
||||
*/
|
||||
@Operation(summary = "查询一二级菜单树(供左侧树选择)")
|
||||
@GetMapping("/menuTree")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:list")
|
||||
public Result<List<MenuNodeVO>> menuTree() {
|
||||
String sql = "SELECT id, parent_id, name, sort_no, menu_type, icon " +
|
||||
"FROM sys_permission " +
|
||||
"WHERE del_flag = 0 AND status = '1' AND menu_type IN (0, 1) " +
|
||||
"ORDER BY sort_no ASC, create_time ASC";
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
|
||||
|
||||
Map<String, MenuNodeVO> nodeMap = new LinkedHashMap<>();
|
||||
for (Map<String, Object> row : rows) {
|
||||
MenuNodeVO n = new MenuNodeVO();
|
||||
n.setId(str(row.get("id")));
|
||||
n.setName(str(row.get("name")));
|
||||
n.setParentId(str(row.get("parent_id")));
|
||||
n.setMenuType(row.get("menu_type") != null ? ((Number) row.get("menu_type")).intValue() : 0);
|
||||
n.setIcon(str(row.get("icon")));
|
||||
n.setChildren(new ArrayList<>());
|
||||
nodeMap.put(n.getId(), n);
|
||||
}
|
||||
|
||||
// 构建两层树:根节点(parentId 为空)及其直接子节点
|
||||
List<MenuNodeVO> roots = new ArrayList<>();
|
||||
Set<String> rootIds = new HashSet<>();
|
||||
for (MenuNodeVO n : nodeMap.values()) {
|
||||
if (StringUtils.isBlank(n.getParentId()) || !nodeMap.containsKey(n.getParentId())) {
|
||||
roots.add(n);
|
||||
rootIds.add(n.getId());
|
||||
}
|
||||
}
|
||||
for (MenuNodeVO n : nodeMap.values()) {
|
||||
if (rootIds.contains(n.getParentId())) {
|
||||
nodeMap.get(n.getParentId()).getChildren().add(n);
|
||||
}
|
||||
}
|
||||
// 过滤掉没有子节点的根节点(纯目录空节点无意义展示)
|
||||
roots.removeIf(r -> r.getChildren().isEmpty() && r.getMenuType() == 0);
|
||||
return Result.OK(roots);
|
||||
}
|
||||
|
||||
// ═══════════════════════ 绑定总览列表 ═══════════════════════
|
||||
|
||||
@Operation(summary = "查询所有绑定记录(按创建时间倒序)")
|
||||
@GetMapping("/list")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:list")
|
||||
public Result<List<MesXslDingTplBind>> list() {
|
||||
List<MesXslDingTplBind> rows = bindService.list(
|
||||
new QueryWrapper<MesXslDingTplBind>()
|
||||
.orderByDesc("create_time"));
|
||||
return Result.OK(rows);
|
||||
}
|
||||
|
||||
// ═══════════════════════ 模板列表 ═══════════════════════
|
||||
|
||||
@Operation(summary = "可用钉钉审批模板列表(状态启用)")
|
||||
@GetMapping("/tplList")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:list")
|
||||
public Result<List<MesXslDingProcessTpl>> tplList() {
|
||||
List<MesXslDingProcessTpl> list = tplService.list(
|
||||
new QueryWrapper<MesXslDingProcessTpl>()
|
||||
.eq("status", "1")
|
||||
.eq("del_flag", 0)
|
||||
.orderByAsc("sort_no")
|
||||
.orderByDesc("create_time"));
|
||||
return Result.OK(list);
|
||||
}
|
||||
|
||||
// ═══════════════════════ 业务字段 ═══════════════════════
|
||||
|
||||
@Operation(summary = "按业务编码获取主实体可用字段")
|
||||
@GetMapping("/bizFields")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:list")
|
||||
public Result<List<PrintBizFieldItemVO>> bizFields(@RequestParam(name = "bizCode") String bizCode) {
|
||||
if (StringUtils.isBlank(bizCode)) {
|
||||
return Result.error("bizCode 不能为空");
|
||||
}
|
||||
String code = bizCode.trim();
|
||||
if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(code)) {
|
||||
return Result.OK(fieldCatalogProvider.listMainFields(code));
|
||||
}
|
||||
PrintBizTypeVO vo = printBizPermEntityService.resolveBizTypeVo(code);
|
||||
if (vo == null || StringUtils.isBlank(vo.getDescription())) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
Class<?> cls = PrintBizEntityFieldIntrospector.tryLoadClass(vo.getDescription().trim());
|
||||
if (cls == null) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
return Result.OK(PrintBizEntityFieldIntrospector.listFields(cls));
|
||||
}
|
||||
|
||||
@Operation(summary = "主实体上的明细槽位(供 TableField 绑定明细集合)")
|
||||
@GetMapping("/detailSlots")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:list")
|
||||
public Result<List<PrintBizDetailSlotVO>> detailSlots(@RequestParam(name = "bizCode") String bizCode) {
|
||||
if (StringUtils.isBlank(bizCode)) {
|
||||
return Result.error("bizCode 不能为空");
|
||||
}
|
||||
String code = bizCode.trim();
|
||||
if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(code)) {
|
||||
return Result.OK(fieldCatalogProvider.listDetailSlots(code));
|
||||
}
|
||||
PrintBizTypeVO vo = printBizPermEntityService.resolveBizTypeVo(code);
|
||||
if (vo == null || StringUtils.isBlank(vo.getDescription())) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
Class<?> cls = PrintBizEntityFieldIntrospector.tryLoadClass(vo.getDescription().trim());
|
||||
if (cls == null) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
return Result.OK(PrintBizDetailPropertyScanner.listSlots(cls));
|
||||
}
|
||||
|
||||
@Operation(summary = "指定明细槽位的字段列表(字段路径带属性名前缀)")
|
||||
@GetMapping("/detailFields")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:list")
|
||||
public Result<List<PrintBizFieldItemVO>> detailFields(
|
||||
@RequestParam(name = "bizCode") String bizCode,
|
||||
@RequestParam(name = "detailProperty") String detailProperty,
|
||||
@RequestParam(name = "slotKind", defaultValue = "LIST") String slotKind) {
|
||||
if (StringUtils.isAnyBlank(bizCode, detailProperty)) {
|
||||
return Result.error("bizCode 与 detailProperty 不能为空");
|
||||
}
|
||||
String code = bizCode.trim();
|
||||
if (fieldCatalogProvider != null && fieldCatalogProvider.hasCatalogForBiz(code)) {
|
||||
return Result.OK(fieldCatalogProvider.listPrefixedDetailFields(code, detailProperty.trim(), slotKind.trim()));
|
||||
}
|
||||
PrintBizTypeVO vo = printBizPermEntityService.resolveBizTypeVo(code);
|
||||
if (vo == null || StringUtils.isBlank(vo.getDescription())) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
Class<?> cls = PrintBizEntityFieldIntrospector.tryLoadClass(vo.getDescription().trim());
|
||||
if (cls == null) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
return Result.OK(PrintBizDetailPropertyScanner.listPrefixedDetailFields(cls, detailProperty.trim(), slotKind.trim()));
|
||||
}
|
||||
|
||||
// ═══════════════════════ 按路由检测绑定(全局悬浮按钮使用) ═══════════════════════
|
||||
|
||||
/**
|
||||
* 通过前端路由路径查找对应的审批模板绑定。
|
||||
* 全局悬浮按钮在每次路由跳转时调用,若返回非 null 则显示"发起钉钉审批"按钮。
|
||||
*/
|
||||
@Operation(summary = "按前端路由路径检测是否有钉钉绑定(null=无)")
|
||||
@GetMapping("/bindingByRoute")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:list")
|
||||
public Result<MesXslDingTplBind> bindingByRoute(@RequestParam(name = "routePath") String routePath) {
|
||||
if (StringUtils.isBlank(routePath)) {
|
||||
return Result.OK(null);
|
||||
}
|
||||
String path = routePath.trim();
|
||||
String sql = "SELECT id FROM sys_permission WHERE url = ? AND del_flag = 0 LIMIT 1";
|
||||
List<String> ids;
|
||||
try {
|
||||
ids = jdbcTemplate.queryForList(sql, String.class, path);
|
||||
} catch (Exception e) {
|
||||
return Result.OK(null);
|
||||
}
|
||||
if (ids.isEmpty()) {
|
||||
return Result.OK(null);
|
||||
}
|
||||
MesXslDingTplBind bind = bindService.getByBizCode(ids.get(0));
|
||||
return Result.OK(bind);
|
||||
}
|
||||
|
||||
// ═══════════════════════ 绑定 CRUD ═══════════════════════
|
||||
|
||||
@Operation(summary = "按业务编码查询已保存的绑定配置")
|
||||
@GetMapping("/getByBizCode")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:list")
|
||||
public Result<MesXslDingTplBind> getByBizCode(@RequestParam(name = "bizCode") String bizCode) {
|
||||
if (StringUtils.isBlank(bizCode)) {
|
||||
return Result.error("bizCode 不能为空");
|
||||
}
|
||||
MesXslDingTplBind row = bindService.getByBizCode(bizCode.trim());
|
||||
return row != null ? Result.OK(row) : Result.OK(null);
|
||||
}
|
||||
|
||||
@AutoLog(value = "钉钉审批模板绑定-保存")
|
||||
@Operation(summary = "保存绑定配置(新增或更新)")
|
||||
@PostMapping("/save")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:save")
|
||||
public Result<String> save(@RequestBody SaveBindRequest req) {
|
||||
if (req == null || StringUtils.isBlank(req.getBizCode())) {
|
||||
return Result.error("业务编码不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(req.getTemplateId())) {
|
||||
return Result.error("请选择钉钉审批模板");
|
||||
}
|
||||
MesXslDingProcessTpl tpl = tplService.getById(req.getTemplateId());
|
||||
if (tpl == null) {
|
||||
return Result.error("选择的钉钉审批模板不存在");
|
||||
}
|
||||
MesXslDingTplBind existing = bindService.getByBizCode(req.getBizCode().trim());
|
||||
if (existing != null) {
|
||||
// 更新已有绑定
|
||||
existing.setTemplateId(req.getTemplateId());
|
||||
existing.setTemplateName(tpl.getTplName());
|
||||
if (StringUtils.isNotBlank(req.getBizName())) {
|
||||
existing.setBizName(req.getBizName());
|
||||
}
|
||||
existing.setFieldMappingJson(normalizeJson(req.getFieldMappingJson()));
|
||||
bindService.updateById(existing);
|
||||
return Result.OK("更新成功");
|
||||
}
|
||||
// 新增
|
||||
MesXslDingTplBind bind = new MesXslDingTplBind();
|
||||
bind.setBizCode(req.getBizCode().trim());
|
||||
bind.setBizName(req.getBizName());
|
||||
bind.setTemplateId(req.getTemplateId());
|
||||
bind.setTemplateName(tpl.getTplName());
|
||||
bind.setFieldMappingJson(normalizeJson(req.getFieldMappingJson()));
|
||||
bindService.save(bind);
|
||||
return Result.OK("保存成功");
|
||||
}
|
||||
|
||||
@AutoLog(value = "钉钉审批模板绑定-删除")
|
||||
@Operation(summary = "删除绑定配置")
|
||||
@DeleteMapping("/delete")
|
||||
@RequiresPermissions("xslmes:mesXslDingTplBind:delete")
|
||||
public Result<String> delete(@RequestParam(name = "id") String id) {
|
||||
bindService.removeById(id);
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
|
||||
// ═══════════════════════ 辅助方法 ═══════════════════════
|
||||
|
||||
private static String str(Object o) {
|
||||
return o == null ? null : o.toString();
|
||||
}
|
||||
|
||||
/** 确保 mapping JSON 为数组字符串,避免空或非法格式入库 */
|
||||
private static String normalizeJson(String raw) {
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
return "[]";
|
||||
}
|
||||
String trimmed = raw.trim();
|
||||
if (trimmed.startsWith("[")) {
|
||||
return trimmed;
|
||||
}
|
||||
return "[]";
|
||||
}
|
||||
|
||||
// ═══════════════════════ 内部 VO ═══════════════════════
|
||||
|
||||
@Data
|
||||
public static class MenuNodeVO {
|
||||
private String id;
|
||||
private String name;
|
||||
private String parentId;
|
||||
private Integer menuType;
|
||||
private String icon;
|
||||
private List<MenuNodeVO> children;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SaveBindRequest {
|
||||
private String bizCode;
|
||||
private String bizName;
|
||||
private String templateId;
|
||||
private String fieldMappingJson;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.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.system.base.entity.JeecgEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板绑定(菜单业务实体 ↔ 钉钉审批模板表单字段)
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【MESToDing审批配置】审批模板绑定
|
||||
*/
|
||||
@Data
|
||||
@TableName("mes_xsl_ding_tpl_bind")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "钉钉审批模板绑定")
|
||||
public class MesXslDingTplBind extends JeecgEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "业务编码(sys_permission.id,与打印绑定 biz_code 含义一致)")
|
||||
private String bizCode;
|
||||
|
||||
@Schema(description = "业务名称(菜单名)")
|
||||
private String bizName;
|
||||
|
||||
@Schema(description = "钉钉审批模板ID(mes_xsl_ding_process_tpl.id)")
|
||||
private String templateId;
|
||||
|
||||
@Schema(description = "钉钉审批模板名称")
|
||||
private String templateName;
|
||||
|
||||
@Schema(description = "字段绑定JSON:[{componentId,componentLabel,componentName,parentId,bizField}]")
|
||||
private String fieldMappingJson;
|
||||
|
||||
@Schema(description = "逻辑删除:0正常 1已删除")
|
||||
@TableLogic
|
||||
private Integer delFlag;
|
||||
|
||||
@Schema(description = "租户ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "所属部门编码")
|
||||
private String sysOrgCode;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingTplBind;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板绑定 Mapper
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04
|
||||
*/
|
||||
public interface MesXslDingTplBindMapper extends BaseMapper<MesXslDingTplBind> {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.xslmes.dingtalk.mapper.MesXslDingTplBindMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingTplBind;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板绑定 Service
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04
|
||||
*/
|
||||
public interface IMesXslDingTplBindService extends IService<MesXslDingTplBind> {
|
||||
|
||||
/** 按业务编码查询绑定记录(未删除的第一条) */
|
||||
MesXslDingTplBind getByBizCode(String bizCode);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingTplBind;
|
||||
import org.jeecg.modules.xslmes.dingtalk.mapper.MesXslDingTplBindMapper;
|
||||
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingTplBindService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 钉钉审批模板绑定 ServiceImpl
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04
|
||||
*/
|
||||
@Service
|
||||
public class MesXslDingTplBindServiceImpl extends ServiceImpl<MesXslDingTplBindMapper, MesXslDingTplBind>
|
||||
implements IMesXslDingTplBindService {
|
||||
|
||||
@Override
|
||||
public MesXslDingTplBind getByBizCode(String bizCode) {
|
||||
return getOne(new QueryWrapper<MesXslDingTplBind>().eq("biz_code", bizCode).last("LIMIT 1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package org.jeecg.modules.xslmes.dingtalk.stream;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.ApprovalCallbackDispatcher;
|
||||
import org.jeecg.modules.xslmes.approval.callback.ApprovalActionHttpExecutor;
|
||||
import org.jeecg.modules.xslmes.approval.constant.ApprovalRecordConstants;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalFlow;
|
||||
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalFlowService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalGateService;
|
||||
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 钉钉审批事件处理器。
|
||||
* <p>
|
||||
* 收到 Stream 事件后,主动调用 {@link DingTalkWorkflowService#getProcessInstance} 拉取完整实例数据,
|
||||
* 通过 {@code operationRecords} 精准得知"哪个节点由谁操作",从而:
|
||||
* <ul>
|
||||
* <li>用审批人自己的 JWT Token 调用业务接口(proofread_by/audit_by 写入真实操作人);</li>
|
||||
* <li>以 operationRecord 的顺序索引映射到 MES 流程节点,执行节点配置的 callbackActions;</li>
|
||||
* <li>同时触发 {@link org.jeecg.modules.xslmes.approval.callback.IApprovalBizCallback} Bean 回调。</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author GHT
|
||||
* @date 2026-06-04 for:【钉钉Stream回调】基于operationRecords精准映射节点与操作人
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DingBpmsEventProcessor {
|
||||
|
||||
@Autowired
|
||||
private IMesXslApprovalGateService approvalGateService;
|
||||
@Autowired
|
||||
private IMesXslApprovalRecordService approvalRecordService;
|
||||
@Autowired
|
||||
private IMesXslApprovalFlowService approvalFlowService;
|
||||
@Autowired
|
||||
private ApprovalCallbackDispatcher callbackDispatcher;
|
||||
@Autowired
|
||||
private ApprovalActionHttpExecutor actionHttpExecutor;
|
||||
@Autowired
|
||||
private DingTalkWorkflowService workflowService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
// ==================== bpms_instance_change ====================
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】拉取实例详情后精准执行节点回调-----
|
||||
public void onInstanceChange(JSONObject data) {
|
||||
if (data == null) return;
|
||||
String processInstanceId = data.getString("processInstanceId");
|
||||
String type = data.getString("type");
|
||||
String result = data.getString("result");
|
||||
|
||||
log.info("[DingBpms] bpms_instance_change instanceId={} type={} result={}",
|
||||
processInstanceId, type, result);
|
||||
|
||||
if (oConvertUtils.isEmpty(processInstanceId) || "start".equals(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ① 映射终态
|
||||
String status;
|
||||
String remark;
|
||||
if ("finish".equals(type)) {
|
||||
if ("agree".equals(result)) {
|
||||
status = ApprovalRecordConstants.STATUS_APPROVED;
|
||||
remark = "钉钉审批通过";
|
||||
} else if ("refuse".equals(result)) {
|
||||
status = ApprovalRecordConstants.STATUS_REJECTED;
|
||||
remark = "钉钉审批拒绝";
|
||||
} else {
|
||||
log.info("[DingBpms] 审批转交 result={},不处理 instanceId={}", result, processInstanceId);
|
||||
return;
|
||||
}
|
||||
} else if ("terminate".equals(type)) {
|
||||
status = ApprovalRecordConstants.STATUS_CANCELLED;
|
||||
remark = "钉钉审批已终止";
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:finishByExternalInstance条件为status=RUNNING,0行更新即终态已处理-----
|
||||
// ② 更新台账(乐观条件:WHERE status=RUNNING;返回 false 表示已是终态,本次为重复事件,直接跳过)
|
||||
try {
|
||||
boolean updated = approvalGateService.finishByExternalInstance(
|
||||
ApprovalRecordConstants.CHANNEL_DINGTALK, processInstanceId, status, remark);
|
||||
if (!updated) {
|
||||
log.info("[DingBpms] instanceId={} 台账已是终态,跳过重复的终态事件", processInstanceId);
|
||||
return;
|
||||
}
|
||||
log.info("[DingBpms] 台账已更新 instanceId={} -> status={}", processInstanceId, status);
|
||||
} catch (Exception e) {
|
||||
log.error("[DingBpms] 台账更新失败 instanceId={}: {}", processInstanceId, e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:finishByExternalInstance条件为status=RUNNING,0行更新即终态已处理-----
|
||||
|
||||
if (ApprovalRecordConstants.STATUS_CANCELLED.equals(status)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ③ 拉取完整审批实例
|
||||
MesXslApprovalRecord record = findRecord(processInstanceId);
|
||||
if (record == null || oConvertUtils.isEmpty(record.getBizTable())) {
|
||||
return;
|
||||
}
|
||||
JSONObject instance = workflowService.getProcessInstance(processInstanceId);
|
||||
List<JSONObject> taskOps = workflowService.getTaskOperations(instance);
|
||||
List<JSONObject> mesNodes = loadApproverNodes(record.getFlowId());
|
||||
|
||||
ApprovalCallbackContext ctx = buildContext(record, remark);
|
||||
|
||||
if (ApprovalRecordConstants.STATUS_APPROVED.equals(status)) {
|
||||
// 最终通过:执行最后一个节点的 onApprove(用最后审批人的 token)
|
||||
if (!mesNodes.isEmpty() && !taskOps.isEmpty()) {
|
||||
JSONObject lastOp = taskOps.get(taskOps.size() - 1);
|
||||
String lastDtUserId = lastOp.getString("userId");
|
||||
String token = workflowService.generateTokenByDtUserId(lastDtUserId);
|
||||
JSONObject lastNode = mesNodes.get(mesNodes.size() - 1);
|
||||
actionHttpExecutor.run(lastNode, "onApprove", record.getBizDataId(), token);
|
||||
}
|
||||
callbackDispatcher.fireApproved(ctx);
|
||||
|
||||
} else {
|
||||
// 驳回:复用与 MES 内部审批相同的 isBizAtOriginStatus 逻辑
|
||||
// 台账已在发起时快照了 originStatus;若单据状态仍为原值,说明未被推进,跳过 onReject
|
||||
JSONObject refuseOp = findRefuseOp(taskOps);
|
||||
if (refuseOp != null) {
|
||||
int refuseIndex = taskOps.indexOf(refuseOp);
|
||||
boolean bizAtOrigin = isBizAtOriginStatus(record);
|
||||
if (!bizAtOrigin && refuseIndex < mesNodes.size()) {
|
||||
// 单据已被前面节点推进,需要调 onReject 回退
|
||||
String dtUserId = refuseOp.getString("userId");
|
||||
String token = workflowService.generateTokenByDtUserId(dtUserId);
|
||||
try {
|
||||
actionHttpExecutor.run(mesNodes.get(refuseIndex), "onReject",
|
||||
record.getBizDataId(), token);
|
||||
} catch (Exception e) {
|
||||
log.error("[DingBpms] onReject HTTP 回调失败: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
log.info("[DingBpms] 单据仍处于发起前原始状态,跳过 onReject 回调 instanceId={}",
|
||||
processInstanceId);
|
||||
}
|
||||
}
|
||||
callbackDispatcher.fireRejected(ctx);
|
||||
}
|
||||
|
||||
log.info("[DingBpms] 终态回调完成 bizTable={} bizDataId={} status={}",
|
||||
record.getBizTable(), record.getBizDataId(), status);
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】拉取实例详情后精准执行节点回调-----
|
||||
|
||||
// ==================== bpms_task_change ====================
|
||||
|
||||
//update-begin---author:GHT ---date:20260604 for:【钉钉Stream回调】节点通过时按operationRecords索引执行onNodeApprove-----
|
||||
/**
|
||||
* 节点任务变更:每个审批人操作时触发。
|
||||
* <p>
|
||||
* 节点通过时:
|
||||
* <ol>
|
||||
* <li>拉取审批实例详情,从 operationRecords 得到"已完成任务列表";</li>
|
||||
* <li>最后一条完成操作的索引 = 本次节点在 MES 流程中的位置;</li>
|
||||
* <li>用该操作人的 Token 执行对应 MES 节点的 onNodeApprove 回调接口;</li>
|
||||
* <li>触发 IApprovalBizCallback.onNodeApproved。</li>
|
||||
* </ol>
|
||||
*/
|
||||
public void onTaskChange(JSONObject data) {
|
||||
if (data == null) return;
|
||||
String processInstanceId = data.getString("processInstanceId");
|
||||
String type = data.getString("type");
|
||||
String result = data.getString("result");
|
||||
String actionerDtUserId = data.getString("actionerUserId");
|
||||
|
||||
log.info("[DingBpms] bpms_task_change instanceId={} type={} result={} actionerUserId={}",
|
||||
processInstanceId, type, result, actionerDtUserId);
|
||||
|
||||
// 只处理节点"完成-通过"
|
||||
if (!"finish".equals(type) || !"agree".equals(result)) {
|
||||
// 拒绝终态由 bpms_instance_change 统一处理,此处不重复触发
|
||||
return;
|
||||
}
|
||||
MesXslApprovalRecord record = findRecord(processInstanceId);
|
||||
if (record == null || oConvertUtils.isEmpty(record.getBizTable())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 拉取实例详情
|
||||
JSONObject instance = workflowService.getProcessInstance(processInstanceId);
|
||||
if (instance == null) {
|
||||
log.warn("[DingBpms] 获取审批实例详情失败 instanceId={},跳过节点回调", processInstanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
List<JSONObject> taskOps = workflowService.getTaskOperations(instance);
|
||||
List<JSONObject> mesNodes = loadApproverNodes(record.getFlowId());
|
||||
|
||||
if (taskOps.isEmpty() || mesNodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 刚完成的是最后一条操作(index = taskOps.size()-1)
|
||||
int nodeIndex = taskOps.size() - 1;
|
||||
if (nodeIndex >= mesNodes.size()) {
|
||||
log.debug("[DingBpms] 节点索引 {} 超出 MES 节点数 {},跳过", nodeIndex, mesNodes.size());
|
||||
return;
|
||||
}
|
||||
|
||||
//update-begin---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁推进processed_op_count,并发安全且重启不丢-----
|
||||
// tryMarkNodeProcessed:UPDATE ... SET processed_op_count=nodeIndex+1 WHERE processed_op_count<nodeIndex+1
|
||||
// MySQL 行锁保证原子性:并发两个事件只有一个成功,另一个返回 false
|
||||
boolean claimed = approvalGateService.tryMarkNodeProcessed(record.getId(), nodeIndex);
|
||||
if (!claimed) {
|
||||
log.info("[DingBpms] 节点{} 回调已执行,跳过重复事件 instanceId={}", nodeIndex + 1, processInstanceId);
|
||||
return;
|
||||
}
|
||||
//update-end---author:GHT ---date:2026-06-04 for:【20260604】钉钉回调幂等去重:DB乐观锁推进processed_op_count,并发安全且重启不丢-----
|
||||
|
||||
// 操作人:优先用 operationRecords 里的 userId,兜底用事件里的 actionerUserId
|
||||
JSONObject lastOp = taskOps.get(nodeIndex);
|
||||
String dtUserId = oConvertUtils.isNotEmpty(lastOp.getString("userId"))
|
||||
? lastOp.getString("userId") : actionerDtUserId;
|
||||
String actioner = oConvertUtils.isNotEmpty(lastOp.getString("showName"))
|
||||
? lastOp.getString("showName") : "审批人";
|
||||
|
||||
String token = workflowService.generateTokenByDtUserId(dtUserId);
|
||||
JSONObject node = mesNodes.get(nodeIndex);
|
||||
|
||||
// 执行该节点的 onNodeApprove HTTP 回调
|
||||
actionHttpExecutor.run(node, "onNodeApprove", record.getBizDataId(), token);
|
||||
|
||||
// 触发 IApprovalBizCallback.onNodeApproved
|
||||
try {
|
||||
ApprovalCallbackContext ctx = buildContext(record, "钉钉节点审批通过(" + actioner + ")")
|
||||
.setOperatorName(actioner);
|
||||
callbackDispatcher.fireNodeApproved(ctx);
|
||||
} catch (Exception e) {
|
||||
log.error("[DingBpms] 节点 Bean 回调失败 instanceId={}: {}", processInstanceId, e.getMessage(), e);
|
||||
}
|
||||
|
||||
log.info("[DingBpms] 节点{}/{} 回调完成 actioner={} bizTable={} bizDataId={}",
|
||||
nodeIndex + 1, mesNodes.size(), actioner, record.getBizTable(), record.getBizDataId());
|
||||
}
|
||||
//update-end---author:GHT ---date:20260604 for:【钉钉Stream回调】节点通过时按operationRecords索引执行onNodeApprove-----
|
||||
|
||||
// ==================== 内部辅助 ====================
|
||||
|
||||
private MesXslApprovalRecord findRecord(String processInstanceId) {
|
||||
if (oConvertUtils.isEmpty(processInstanceId)) return null;
|
||||
List<MesXslApprovalRecord> list = approvalRecordService.list(
|
||||
new LambdaQueryWrapper<MesXslApprovalRecord>()
|
||||
.eq(MesXslApprovalRecord::getExternalInstanceId, processInstanceId)
|
||||
.eq(MesXslApprovalRecord::getChannel, ApprovalRecordConstants.CHANNEL_DINGTALK)
|
||||
.orderByDesc(MesXslApprovalRecord::getCreateTime)
|
||||
.last("LIMIT 1"));
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
}
|
||||
|
||||
private List<JSONObject> loadApproverNodes(String flowId) {
|
||||
List<JSONObject> result = new ArrayList<>();
|
||||
if (oConvertUtils.isEmpty(flowId)) return result;
|
||||
try {
|
||||
MesXslApprovalFlow flow = approvalFlowService.getById(flowId);
|
||||
if (flow == null || oConvertUtils.isEmpty(flow.getFlowConfig())) return result;
|
||||
collectApproverNodes(JSONObject.parseObject(flow.getFlowConfig()), result);
|
||||
} catch (Exception e) {
|
||||
log.warn("[DingBpms] 加载流程节点失败 flowId={}: {}", flowId, e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void collectApproverNodes(JSONObject node, List<JSONObject> out) {
|
||||
if (node == null) return;
|
||||
if ("approver".equals(node.getString("type"))) {
|
||||
out.add(node);
|
||||
}
|
||||
JSONArray branches = node.getJSONArray("conditionNodes");
|
||||
if (branches != null && !branches.isEmpty()) {
|
||||
Object first = branches.get(0);
|
||||
if (first instanceof JSONObject) {
|
||||
collectApproverNodes(((JSONObject) first).getJSONObject("childNode"), out);
|
||||
}
|
||||
}
|
||||
collectApproverNodes(node.getJSONObject("childNode"), out);
|
||||
}
|
||||
|
||||
/** 找第一条 result=REFUSE 的操作记录 */
|
||||
private JSONObject findRefuseOp(List<JSONObject> taskOps) {
|
||||
for (JSONObject op : taskOps) {
|
||||
String r = op.getString("result");
|
||||
if ("REFUSE".equalsIgnoreCase(r) || "refuse".equalsIgnoreCase(r)) {
|
||||
return op;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断业务单据是否仍处于发起审批前的原始状态。
|
||||
* 与 MesXslApprovalHandleServiceImpl.isBizAtOriginStatus(MesXslApprovalInstance) 逻辑完全一致,
|
||||
* 此处基于 MesXslApprovalRecord(钉钉通道)实现,使驳回逻辑在两通道间真正复用。
|
||||
*/
|
||||
private boolean isBizAtOriginStatus(MesXslApprovalRecord record) {
|
||||
String statusField = record.getStatusField();
|
||||
String originStatus = record.getOriginStatus();
|
||||
if (oConvertUtils.isEmpty(statusField) || originStatus == null) {
|
||||
// 无快照信息(旧数据兼容):退化为 false,走 onReject 调用
|
||||
return false;
|
||||
}
|
||||
if (!record.getBizTable().matches("^[A-Za-z0-9_]+$")
|
||||
|| !statusField.matches("^[A-Za-z0-9_]+$")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
List<String> 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("钉钉审批");
|
||||
}
|
||||
}
|
||||
@@ -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)。
|
||||
* <p>
|
||||
* 无需注册公网回调地址:应用主动建立长连接,钉钉通过该通道推送事件(如审批结果),
|
||||
* 官方 SDK 内部自动维护重连与心跳。
|
||||
* <p>
|
||||
* 启动时机:{@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回调】处理事件并回写审批台账-----
|
||||
}
|
||||
@@ -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),
|
||||
* 按时间顺序排列,每条记录对应一个审批节点的操作(通过/拒绝)。
|
||||
* <p>
|
||||
* 返回的列表索引与 MES 流程节点列表索引一一对应:第0条 = 第1个节点,第1条 = 第2个节点,以此类推。
|
||||
*/
|
||||
public List<JSONObject> getTaskOperations(JSONObject instanceResult) {
|
||||
List<JSONObject> 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。
|
||||
* <p>
|
||||
* 查询链:sys_third_account(third_user_id=dtUserId) → sys_user_id → sys_user.username/password
|
||||
* <p>
|
||||
* 这样回调业务接口时,接口内部通过 {@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<String> 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)。
|
||||
* <p>
|
||||
* JeecgBoot Shiro 在 {@code jwtTokenRefresh()} 中验证 token 时,
|
||||
* 必须从 Redis {@code "prefix_user_token:" + token} 取到缓存值,
|
||||
* 否则报 "Token失效,请重新登录"。新生成的 token 需要手动写入 Redis 才能通过校验。
|
||||
* <p>
|
||||
* 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-----
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user