新增钉钉审批模板配置功能,包括相关实体、控制器、服务及接口的实现,支持审批模板的增删改查及从钉钉同步模板,增强了系统的审批流管理能力。

This commit is contained in:
geht
2026-06-04 11:38:08 +08:00
parent 1c5cede957
commit 4785c55e52
23 changed files with 4755 additions and 11 deletions

View File

@@ -1,14 +1,26 @@
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;
@@ -22,6 +34,7 @@ import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.system.entity.SysDepart;
import org.jeecg.modules.system.service.ISysDepartService;
import org.jeecg.modules.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;
@@ -44,6 +57,11 @@ public class MesXslMixerPsCompileController extends JeecgController<MesXslMixerP
@Autowired
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编制-分页列表查询")
@GetMapping(value = "/list")
public Result<IPage<MesXslMixerPsCompile>> queryPageList(
@@ -283,6 +301,122 @@ 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) {

View File

@@ -0,0 +1,43 @@
package org.jeecg.modules.xslmes.dingtalk.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 钉钉审批表单控件
* 对应官方 SDKFormComponent
*
* 支持的 componentType
* TextField 单行文本
* TextareaField 多行文本
* NumberField 数字输入
* DDSelectField 单选
* DDMultiSelectField 多选
* DDDateField 日期
* DDDateRangeField 时间区间
* DDPhotoField 图片
* DDAttachment 附件
* DepartmentField 部门
* InnerContactField 联系人
* TextNote 说明文字
* MoneyField 金额
* PhoneField 电话
* AddressField 省市区
* StarRatingField 评分
* TableField 明细(含子控件 children
*/
@Data
@Accessors(chain = true)
public class DingFormComponent {
/** 控件类型 */
private String componentType;
/** 控件属性 */
private DingFormComponentProps props;
/** 子控件列表,仅 TableField 使用 */
private List<DingFormComponent> children;
}

View File

@@ -0,0 +1,99 @@
package org.jeecg.modules.xslmes.dingtalk.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 钉钉审批表单控件属性
* 对应官方 SDKFormComponentProps
*/
@Data
@Accessors(chain = true)
public class DingFormComponentProps {
/** 控件唯一标识,建议格式:{componentType}-{bizKey} */
private String componentId;
/** 控件标题(钉钉表单中显示的字段名) */
private String label;
/** 占位提示文字 */
private String placeholder;
/** 是否必填 */
private Boolean required;
/** 日期格式DDDateField/DDDateRangeField 使用,如 "yyyy-MM-dd" */
private String format;
/** 单位NumberField/DDDateField/DDDateRangeField 使用 */
private String unit;
/** 说明内容TextNote 使用 */
private String content;
/** 说明文字超链接TextNote 使用 */
private String link;
/** 是否参与打印("0"=否TextNote 使用 */
private String print;
/** 单选/多选选项列表DDSelectField/DDMultiSelectField 使用 */
private List<SelectOption> options;
/** 业务别名DDSelectField 使用(如 "staff_type" */
private String bizAlias;
/** 金额大写显示("0"=否 "1"=是MoneyField 使用 */
private String upper;
/** 电话模式("phone"PhoneField 使用 */
private String mode;
/** 联系人选择模式("1"=多选InnerContactField 使用 */
private String choice;
/** 部门是否多选DepartmentField 使用 */
private Boolean multiple;
/** 省市区精度("city"=市级 "district"=区级AddressField 使用 */
private String addressModel;
/** 评分最大值StarRatingField 使用 */
private Integer limit;
/** 明细视图模式("table"/"list"TableField 使用 */
private String tableViewMode;
/** 明细打印方向true=纵向 false=横向TableField 使用 */
private Boolean verticalPrint;
/** 明细汇总字段TableField 使用 */
private List<StatField> statField;
/** 可关联的审批单列表RelateField 使用 */
private List<AvailableTemplate> availableTemplates;
@Data
@Accessors(chain = true)
public static class SelectOption {
private String key;
private String value;
}
@Data
@Accessors(chain = true)
public static class StatField {
private String componentId;
private String label;
}
@Data
@Accessors(chain = true)
public static class AvailableTemplate {
private String name;
private String processCode;
}
}

View File

@@ -0,0 +1,25 @@
package org.jeecg.modules.xslmes.dingtalk.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 创建钉钉审批模板请求体
* POST https://api.dingtalk.com/v1.0/workflow/forms
* 对应官方 SDKFormCreateRequest
*/
@Data
@Accessors(chain = true)
public class DingFormCreateRequest {
/** 模板名称 */
private String name;
/** 模板描述 */
private String description;
/** 表单控件列表 */
private List<DingFormComponent> formComponents;
}

View File

@@ -0,0 +1,28 @@
package org.jeecg.modules.xslmes.dingtalk.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
* 更新钉钉审批模板请求体
* PUT https://api.dingtalk.com/v1.0/workflow/forms
* 对应官方 SDKFormUpdateRequest
*/
@Data
@Accessors(chain = true)
public class DingFormUpdateRequest {
/** 要更新的模板 processCode */
private String processCode;
/** 模板名称 */
private String name;
/** 模板描述 */
private String description;
/** 表单控件列表 */
private List<DingFormComponent> formComponents;
}

View File

@@ -0,0 +1,73 @@
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.aspect.annotation.Dict;
import org.jeecg.common.system.base.entity.JeecgEntity;
import org.jeecgframework.poi.excel.annotation.Excel;
import java.io.Serializable;
/**
* 钉钉审批模板配置
*
* @author GHT
* @date 2026-06-03 for【MESToDing审批配置】钉钉审批模板配置
*/
@Data
@TableName("mes_xsl_ding_process_tpl")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description = "钉钉审批模板配置")
public class MesXslDingProcessTpl extends JeecgEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Excel(name = "模板名称", width = 20)
@Schema(description = "模板名称")
private String tplName;
@Excel(name = "钉钉processCode", width = 35)
@Schema(description = "钉钉processCode")
private String processCode;
@Excel(name = "业务类型标识", width = 20)
@Schema(description = "业务类型标识(供审批流关联使用)")
private String bizType;
@Excel(name = "表单字段映射", width = 30)
@Schema(description = "表单字段映射JSON(钉钉字段名→MES字段名)")
private String formFields;
@Excel(name = "状态", width = 10, dicCode = "mes_ding_tpl_status")
@Dict(dicCode = "mes_ding_tpl_status")
@Schema(description = "状态:0停用 1启用")
private String status;
@Excel(name = "排序", width = 10)
@Schema(description = "排序")
private Integer sortNo;
@Excel(name = "备注", width = 30)
@Schema(description = "备注")
private String remark;
//update-begin---author:GHT ---date:2026-06-04 for【MESToDing审批配置】绑定MES审批流审批人来源-----------
@Schema(description = "绑定的MES审批流ID(用于发起审批时解析审批人)")
private String flowId;
//update-end---author:GHT ---date:2026-06-04 for【MESToDing审批配置】绑定MES审批流审批人来源-----------
@Schema(description = "逻辑删除:0正常 1已删除")
@TableLogic
private Integer delFlag;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "所属部门编码")
private String sysOrgCode;
}

View File

@@ -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.MesXslDingProcessTpl;
/**
* 钉钉审批模板配置 Mapper
*
* @author GHT
* @date 2026-06-03
*/
public interface MesXslDingProcessTplMapper extends BaseMapper<MesXslDingProcessTpl> {
}

View File

@@ -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.MesXslDingProcessTplMapper">
</mapper>

View File

@@ -0,0 +1,13 @@
package org.jeecg.modules.xslmes.dingtalk.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl;
/**
* 钉钉审批模板配置 Service 接口
*
* @author GHT
* @date 2026-06-03
*/
public interface IMesXslDingProcessTplService extends IService<MesXslDingProcessTpl> {
}

View File

@@ -0,0 +1,18 @@
package org.jeecg.modules.xslmes.dingtalk.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.jeecg.modules.xslmes.dingtalk.entity.MesXslDingProcessTpl;
import org.jeecg.modules.xslmes.dingtalk.mapper.MesXslDingProcessTplMapper;
import org.jeecg.modules.xslmes.dingtalk.service.IMesXslDingProcessTplService;
import org.springframework.stereotype.Service;
/**
* 钉钉审批模板配置 ServiceImpl
*
* @author GHT
* @date 2026-06-03
*/
@Service
public class MesXslDingProcessTplServiceImpl extends ServiceImpl<MesXslDingProcessTplMapper, MesXslDingProcessTpl>
implements IMesXslDingProcessTplService {
}