diff --git a/laws-module-system/src/main/java/com/jero/modules/system/entity/SysUser.java b/laws-module-system/src/main/java/com/jero/modules/system/entity/SysUser.java
index 43941f9e..7c9cad96 100644
--- a/laws-module-system/src/main/java/com/jero/modules/system/entity/SysUser.java
+++ b/laws-module-system/src/main/java/com/jero/modules/system/entity/SysUser.java
@@ -258,6 +258,12 @@ public class SysUser implements Serializable {
@TableField(exist = false)
private String groupId;
+ /**
+ * 飞书的id
+ */
+ @ApiModelProperty(value = "飞书的id")
+ private String feishuId;
+
/**
* 拼接真实姓名和工号形成唯一的用户信息可用户查看
* 不能用get开头,否则会造成初始化queryWrapper的bug
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/controller/FeiShuDockingController.java b/laws-modules/src/main/java/com/jero/modules/feishu/controller/FeiShuDockingController.java
new file mode 100644
index 00000000..6c28e09a
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/controller/FeiShuDockingController.java
@@ -0,0 +1,91 @@
+package com.jero.modules.feishu.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.jero.common.api.vo.Result;
+import com.jero.modules.feishu.dto.BotMessageDto;
+import com.jero.modules.feishu.dto.InstanceDto;
+import com.jero.modules.feishu.service.IFeiShuDockingService;
+import com.jero.modules.feishu.vo.definition.ApprovalsDefinitionVo;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+
+/**
+ *
+ * 飞书对接
+ *
+ */
+@Api(tags = "飞书对接")
+@RestController
+@RequestMapping("/feishu")
+@Slf4j
+public class FeiShuDockingController {
+
+ @Resource
+ private IFeiShuDockingService feiShuDockingService;
+
+ /**
+ * 获取token
+ * @return
+ */
+ @GetMapping("/getAccessToken")
+ public String getAccessToken() {
+ return feiShuDockingService.getAccessToken();
+ }
+
+ /**
+ * 创建三方审批定义
+ * @return
+ */
+ @PostMapping("/createApprovalDefinition")
+ public String createApprovalDefinition() {
+ return feiShuDockingService.createApprovalDefinition();
+ }
+
+ /**
+ * 获取指定三方审批定义
+ * @param approvalDefinitionCode
+ * @return
+ */
+ @GetMapping("/getApprovalDefinition")
+ public ApprovalsDefinitionVo getApprovalDefinition(String approvalDefinitionCode) {
+ return feiShuDockingService.getApprovalDefinition(approvalDefinitionCode);
+ }
+
+ /**
+ * 根据手机号获取用户的user_id
+ * @param phones
+ * @return
+ */
+ @PostMapping("/getUserId")
+ public String getUserId(String phones) {
+ return feiShuDockingService.getUserId(phones);
+ }
+
+ /**
+ * 同步三方审批实例
+ * @return
+ */
+ @PostMapping("/synchronizeApprovalInstance")
+ public void synchronizeApprovalInstance(@RequestBody InstanceDto instanceDto) {
+ feiShuDockingService.synchronizeApprovalInstance(instanceDto);
+ }
+
+ /**
+ * 使用Bot发送富文本消息
+ * @return
+ */
+ @PostMapping("/sendHrefMessageByBot")
+ public void sendHrefMessageByBot(@RequestBody BotMessageDto botMessageDto) {
+ feiShuDockingService.sendHrefMessageByBot(botMessageDto);
+ }
+
+ /**
+ * 根据预授权码获取用户信息
+ */
+ @PostMapping("/getUserInfoByCode")
+ public Result getUserInfoByCode(@RequestBody JSONObject codeObject) {
+ return feiShuDockingService.getUserInfoByCode(codeObject);
+ }
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/dto/BotMessageDto.java b/laws-modules/src/main/java/com/jero/modules/feishu/dto/BotMessageDto.java
new file mode 100644
index 00000000..ff3df206
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/dto/BotMessageDto.java
@@ -0,0 +1,27 @@
+package com.jero.modules.feishu.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class BotMessageDto {
+
+ @ApiModelProperty("消息接收者的id,可以为多个,逗号拼接")
+ private String receiveIds;
+
+ @ApiModelProperty("超链接的文字")
+ private String hrefText;
+
+ @ApiModelProperty("超链接前面的普通文字")
+ private String textBeforeHref;
+
+ @ApiModelProperty("超链接后面的普通文字")
+ private String textAfterHref;
+
+ @ApiModelProperty("消息的标题")
+ private String title;
+
+ @ApiModelProperty("超链接的跳转地址")
+ private String hrefUrl;
+
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/dto/InstanceDto.java b/laws-modules/src/main/java/com/jero/modules/feishu/dto/InstanceDto.java
new file mode 100644
index 00000000..c96ddec9
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/dto/InstanceDto.java
@@ -0,0 +1,68 @@
+package com.jero.modules.feishu.dto;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class InstanceDto {
+
+ @ApiModelProperty("发起人的id")
+ private String promoterUserId;
+
+ @ApiModelProperty("审核人的ids,可能为多个,逗号分割")
+ private String reviewedIds;
+
+ @ApiModelProperty("标题")
+ private String title;
+
+ @ApiModelProperty("审核实例的id")
+ private String processInstanceId;
+
+ @ApiModelProperty("审核任务的ids,有几个审核人就对应几个任务id,逗号分割")
+ private String taskIds;
+
+ /**
+ * 审批实例状态
+ * 示例值:"PENDING"
+ * PENDING: 审批中
+ * APPROVED: 审批流程结束,结果为同意
+ * REJECTED: 审批流程结束,结果为拒绝
+ * CANCELED: 审批发起人撤回
+ * DELETED: 审批被删除
+ * HIDDEN: 状态隐藏(不显示状态)
+ * TERMINATED:审批终止
+ */
+ @ApiModelProperty("审核实例的状态")
+ private String instanceStatus;
+
+ @ApiModelProperty("审核实例的开始时间")
+ private Date instanceStartTime;
+
+ /**
+ * 任务状态
+ * 示例值:"PENDING"
+ * 可选值有:
+ * PENDING:待审批
+ * APPROVED:任务同意
+ * REJECTED:任务拒绝
+ * TRANSFERRED:任务转交
+ * DONE:任务通过但审批人未操作;审批人看不到这个任务, 若想要看到, 可以通过抄送该人.
+ */
+ @ApiModelProperty("审核任务的状态,有几个审核人对应几个审核任务的状态,逗号分割")
+ private String taskStatus;
+
+ @ApiModelProperty("发起人的PC跳转地址")
+ private String promoterPcLink;
+
+ @ApiModelProperty("发起人的移动端跳转地址")
+ private String promoterMobileLink;
+
+ @ApiModelProperty("审核人的PC端跳转地址,有几个审核人对应几条链接,逗号分割")
+ private String reviewPcLink;
+
+ @ApiModelProperty("审核人的移动端跳转地址,有几个审核人对应几条链接,逗号分割")
+ private String reviewMobileLink;
+
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/service/IFeiShuDockingService.java b/laws-modules/src/main/java/com/jero/modules/feishu/service/IFeiShuDockingService.java
new file mode 100644
index 00000000..39d37bcb
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/service/IFeiShuDockingService.java
@@ -0,0 +1,54 @@
+package com.jero.modules.feishu.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.jero.common.api.vo.Result;
+import com.jero.modules.feishu.dto.BotMessageDto;
+import com.jero.modules.feishu.dto.InstanceDto;
+import com.jero.modules.feishu.vo.definition.ApprovalsDefinitionVo;
+
+public interface IFeiShuDockingService {
+
+ /**
+ * 获取飞书应用的token
+ * @return
+ */
+ String getAccessToken();
+
+ /**
+ * 创建三方审批的定义
+ * @return
+ */
+ String createApprovalDefinition();
+
+ /**
+ * 查看指定三方审批定义
+ * @return
+ */
+ ApprovalsDefinitionVo getApprovalDefinition(String approvalDefinitionCode);
+
+ /**
+ * 获取用户id
+ * @return
+ */
+ String getUserId(String phones);
+
+ /**
+ * 同步三方审批实例
+ * @return
+ */
+ void synchronizeApprovalInstance(InstanceDto instanceDto);
+
+ /**
+ * 使用机器人发送消息
+ * @param botMessageDto
+ * @return
+ */
+ void sendHrefMessageByBot(BotMessageDto botMessageDto);
+
+ /**
+ * 根据预授权码获取用户信息
+ * @param codeObject
+ * @return
+ */
+ Result getUserInfoByCode(JSONObject codeObject);
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/service/impl/FeiShuDockingServiceImpl.java b/laws-modules/src/main/java/com/jero/modules/feishu/service/impl/FeiShuDockingServiceImpl.java
new file mode 100644
index 00000000..dd7b5e9f
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/service/impl/FeiShuDockingServiceImpl.java
@@ -0,0 +1,603 @@
+package com.jero.modules.feishu.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.jero.common.api.CommonAPI;
+import com.jero.common.api.vo.Result;
+import com.jero.common.constant.CommonConstant;
+import com.jero.common.exception.JeroBootException;
+import com.jero.common.system.util.JwtUtil;
+import com.jero.common.system.vo.SysDictItemCore;
+import com.jero.common.util.RedisUtil;
+import com.jero.common.util.oConvertUtils;
+import com.jero.modules.feishu.dto.BotMessageDto;
+import com.jero.modules.feishu.dto.InstanceDto;
+import com.jero.modules.feishu.service.IFeiShuDockingService;
+import com.jero.modules.feishu.vo.bot.BotMessageVo;
+import com.jero.modules.feishu.vo.definition.*;
+import com.jero.modules.feishu.vo.instance.ApprovalsInstanceVo;
+import com.jero.modules.feishu.vo.instance.InstanceLink;
+import com.jero.modules.feishu.vo.instance.InstanceTask;
+import com.jero.modules.system.entity.*;
+import com.jero.modules.system.service.*;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class FeiShuDockingServiceImpl implements IFeiShuDockingService {
+
+ @Resource
+ private RestTemplate restTemplate;
+ @Resource
+ private Environment env;
+ @Resource
+ private ISysUserService sysUserService;
+ @Autowired
+ private RedisUtil redisUtil;
+ @Autowired
+ private ISysDepartService sysDepartService;
+ @Autowired
+ private ISysTenantService sysTenantService;
+ @Autowired
+ private ISysDictService sysDictService;
+ @Resource
+ private ISysUserRoleService sysUserRoleService;
+ @Resource
+ private ISysRoleService sysRoleService;
+ @Resource
+ private CommonAPI commonAPI;
+ @Value("${feishu.app_id}")
+ private String appId;
+ @Value("${feishu.app_secret}")
+ private String appSecret;
+ @Value("${feishu.managersIds}")
+ private String managersIds;
+ @Value("${feishu.approvalName}")
+ private String approvalName;
+
+ private static final String USER_INFO = "userInfo";
+
+ @Override
+ public String getAccessToken() {
+ String url = env.getProperty("feishu.get_token");
+ assert url != null;
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("app_id", appId);
+ jsonObject.put("app_secret", appSecret);
+ String res = restTemplate.postForObject(url, jsonObject, String.class);
+ JSONObject result = JSON.parseObject(res);
+ String code = result.getString("code");
+ if ("0".equals(code)) {
+ return result.getString("tenant_access_token");
+ } else {
+ throw new JeroBootException("获取飞书token失败");
+ }
+ }
+
+ @Override
+ public String createApprovalDefinition() {
+ String url = env.getProperty("feishu.create_approval_definition");
+ assert url != null;
+ // 获取token
+ String accessToken = getAccessToken();
+ // 构建对象
+ ApprovalsDefinitionVo approvalsDefinitionVo = buildDefinitionVo();
+ // 设置请求头
+ HttpHeaders headers = createHeaders(accessToken);
+ // 构建包含查询参数的 URL
+ UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
+ .queryParam("department_id_type", "open_department_id")
+ .queryParam("user_id_type", "user_id");
+ // 创建 HttpEntity
+ HttpEntity entity = new HttpEntity<>(approvalsDefinitionVo, headers);
+ // 发送请求
+ ResponseEntity response = restTemplate.exchange(builder.toUriString(), HttpMethod.POST, entity, String.class);
+ // 处理响应
+ JSONObject jsonResponse = JSON.parseObject(response.getBody());
+ if ("0".equals((String.valueOf(jsonResponse.get("code"))))) {
+ JSONObject data = (JSONObject) jsonResponse.get("data");
+ return data.getString("approval_code");
+ } else {
+ log.error(jsonResponse.get("msg").toString());
+ throw new JeroBootException("创建三方审批定义失败");
+ }
+ }
+
+ // 构建创建审批定义的入参对象
+ public ApprovalsDefinitionVo buildDefinitionVo() {
+ ApprovalsDefinitionVo approvalsDefinitionVo = new ApprovalsDefinitionVo();
+ // 构建审批定义vo
+ approvalsDefinitionVo.setApproval_name("@i18n@1");
+ approvalsDefinitionVo.setApproval_code("permission_test3");
+ approvalsDefinitionVo.setGroup_code("work_group");
+ approvalsDefinitionVo.setGroup_name("@i18n@2");
+ // 构建三方审批相关对象.
+ DefinitionExternal definitionExternal = new DefinitionExternal();
+ definitionExternal.setCreate_link_pc("");
+ definitionExternal.setCreate_link_mobile("");
+ definitionExternal.setSupport_pc(true);
+ definitionExternal.setSupport_mobile(true);
+ definitionExternal.setSupport_batch_read(true);
+ definitionExternal.setEnable_mark_readed(true);
+ // 构建可见人列表对象
+ DefinitionViewers definitionViewers = new DefinitionViewers();
+ definitionViewers.setViewer_type("NONE");
+ List viewers = new ArrayList<>();
+ viewers.add(definitionViewers);
+ // 构建语言文案
+ List texts = new ArrayList<>();
+ texts.add(new DefinitionTexts("@i18n@1",approvalName));
+ texts.add(new DefinitionTexts("@i18n@2",approvalName));
+ texts.add(new DefinitionTexts("@i18n@3","飞书"));
+ // 构建国际化文案对象
+ DefinitionI18nResources definitionI18nResources = new DefinitionI18nResources();
+ definitionI18nResources.setLocale("zh-CN");
+ definitionI18nResources.setIs_default(true);
+ definitionI18nResources.setTexts(texts);
+ List i18n_resources = new ArrayList<>();
+ i18n_resources.add(definitionI18nResources);
+ approvalsDefinitionVo.setExternal(definitionExternal);
+ approvalsDefinitionVo.setViewers(viewers);
+ approvalsDefinitionVo.setI18n_resources(i18n_resources);
+ List mangers = Arrays.asList(managersIds.split(","));
+ String[] mangersString = mangers.toArray(new String[0]);
+ approvalsDefinitionVo.setManagers(mangersString);
+ return approvalsDefinitionVo;
+ }
+
+ @Override
+ public ApprovalsDefinitionVo getApprovalDefinition(String approvalDefinitionCode) {
+ String url = env.getProperty("feishu.get_approval_definition");
+ assert url != null;
+ // 获取token
+ String accessToken = getAccessToken();
+ // 拼接URL
+ String fullUrl = url + "/" + approvalDefinitionCode;
+ // 设置请求头
+ HttpHeaders headers = createHeaders(accessToken);
+ // 创建 HttpEntity
+ HttpEntity entity = new HttpEntity<>(headers);
+ // 发送请求
+ ResponseEntity response = restTemplate.exchange(fullUrl, HttpMethod.GET, entity, String.class);
+ JSONObject jsonResponse = JSON.parseObject(response.getBody());
+ if ("0".equals((String.valueOf(jsonResponse.get("code"))))) {
+ JSONObject dataJson = jsonResponse.getJSONObject("data");
+ return JSON.toJavaObject(dataJson, ApprovalsDefinitionVo.class);
+ } else {
+ log.error(jsonResponse.get("msg").toString());
+ throw new JeroBootException("查看三方审批定义失败");
+ }
+ }
+
+ @Override
+ public String getUserId(String phones) {
+ try {
+ String url = env.getProperty("feishu.get_user_id");
+ assert url != null;
+ // 获取token
+ String accessToken = getAccessToken();
+ // 构建请求对象
+ JSONObject jsonObject = new JSONObject();
+ List phonesList = Arrays.asList(phones.split(","));
+ String[] phonesString = phonesList.toArray(new String[0]);
+ jsonObject.put("mobiles", phonesString);
+ // 设置请求头
+ HttpHeaders headers = createHeaders(accessToken);
+ // 构建包含查询参数的 URL
+ UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url)
+ .queryParam("user_id_type", "user_id");
+ // 创建 HttpEntity
+ HttpEntity entity = new HttpEntity<>(jsonObject, headers);
+ // 发送请求
+ ResponseEntity response = restTemplate.exchange(builder.toUriString(), HttpMethod.POST, entity, String.class);
+ // 处理响应
+ JSONObject jsonResponse = JSON.parseObject(response.getBody());
+ if ("0".equals((String.valueOf(jsonResponse.get("code"))))) {
+ JSONObject data = jsonResponse.getJSONObject("data");
+ JSONArray userList = data.getJSONArray("user_list");
+ // 将 JSONArray 转换为 List
+ List userListObjects = userList.toJavaList(JSONObject.class);
+ return userListObjects.get(0).getString("user_id");
+ } else {
+ log.error("获取飞书用户id失败" + jsonResponse.get("msg").toString());
+ return "";
+ }
+ } catch (Exception e) {
+ log.error(e.getMessage());
+ }
+ return "";
+ }
+
+ /**
+ * 同步三方审批实例
+ *
+ * @return
+ */
+ @Override
+ public void synchronizeApprovalInstance(InstanceDto instanceDto) {
+ String url = env.getProperty("feishu.synchronize_approval_instance");
+ assert url != null;
+ try {
+ // 获取token
+ String accessToken = getAccessToken();
+ // 构建对象
+ ApprovalsInstanceVo approvalsInstanceVo = buildApprovalsInstanceVo(instanceDto);
+ // 设置请求头
+ HttpHeaders headers = createHeaders(accessToken);
+ // 创建 HttpEntity
+ HttpEntity entity = new HttpEntity<>(approvalsInstanceVo, headers);
+ restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+ }catch (Exception e) {
+ log.error("同步三方审批实例异常" + e.getMessage());
+ }
+ }
+
+ private ApprovalsInstanceVo buildApprovalsInstanceVo(InstanceDto instanceDto) {
+ // 转换userId为飞书Id
+ // 发起人的id
+ String promoterUserId = instanceDto.getPromoterUserId();
+ if (StringUtils.isNoneBlank(promoterUserId)) {
+ SysUser promoterUser = sysUserService.getById(promoterUserId);
+ promoterUserId = promoterUser.getFeishuId();
+ }
+ // 审核人的id(可能为多个),查出对应的飞书id
+ String reviewedIds = instanceDto.getReviewedIds();
+ List newReviewedIds = new ArrayList<>();
+ if (StringUtils.isNotBlank(reviewedIds)) {
+ List reviewedIdList = Arrays.asList(reviewedIds.split(","));
+ for (String reviewedId : reviewedIdList) {
+ SysUser reviewUser = sysUserService.getById(reviewedId);
+ if (!Objects.isNull(reviewUser) && StringUtils.isNotBlank(reviewUser.getFeishuId())) {
+ newReviewedIds.add(reviewUser.getFeishuId());
+ }
+ }
+ }
+ // 任务id
+ String taskIds = instanceDto.getTaskIds();
+ List taskIdList = new ArrayList<>();
+ if (StringUtils.isNotBlank(taskIds)) {
+ taskIdList = Arrays.asList(instanceDto.getTaskIds().split(","));
+ }
+ // 任务状态
+ String taskStatus = instanceDto.getTaskStatus();
+ List taskStatusList = new ArrayList<>();
+ if (StringUtils.isNotBlank(taskIds)) {
+ taskStatusList = Arrays.asList(taskStatus.split(","));
+ }
+
+ // 替换跳转的地址为单点登录的地址
+ String url = env.getProperty("feishu.sign_login_in");
+ assert url != null;
+ // 审核人对应的 pc 跳转地址
+ String reviewPcLink = instanceDto.getReviewPcLink();
+ List reviewPcLinkList = new ArrayList<>();
+ if (StringUtils.isNotBlank(reviewPcLink)) {
+ reviewPcLinkList = Arrays.asList(reviewPcLink.split(","));
+ reviewPcLinkList = reviewPcLinkList.stream()
+ .map(link -> url.replace("{REDIRECT_URI}", link))
+ .collect(Collectors.toList());
+ }
+ // 审核人对应的 mobile 跳转地址
+ String reviewMobileLink = instanceDto.getReviewMobileLink();
+ List reviewMobileLinkList = new ArrayList<>();
+ if (StringUtils.isNotBlank(reviewMobileLink)) {
+ reviewMobileLinkList = Arrays.asList(reviewMobileLink.split(","));
+ reviewMobileLinkList = reviewMobileLinkList.stream()
+ .map(link -> url.replace("{REDIRECT_URI}", link))
+ .collect(Collectors.toList());
+ }
+ // 发起人对应的pc的跳转地址
+ String promoterPcLink = instanceDto.getPromoterPcLink();
+ if (StringUtils.isNotBlank(promoterPcLink)) {
+ promoterPcLink = url.replace("{REDIRECT_URI}", promoterPcLink);
+ }
+ // 发起人移动端的跳转地址
+ String promoterMobileLink = instanceDto.getPromoterMobileLink();
+ if (StringUtils.isNotBlank(promoterMobileLink)) {
+ promoterMobileLink = url.replace("{REDIRECT_URI}", promoterMobileLink);
+ }
+ // 审批定义的code
+ String approvalDefinition = createApprovalDefinition();
+ // 发起人相关的信息
+ ApprovalsInstanceVo approvalsInstanceVo = new ApprovalsInstanceVo();
+ approvalsInstanceVo.setApproval_code(approvalDefinition);
+ approvalsInstanceVo.setStatus(instanceDto.getInstanceStatus());
+ approvalsInstanceVo.setInstance_id(instanceDto.getProcessInstanceId());
+ InstanceLink instanceLink = new InstanceLink();
+ instanceLink.setPc_link(promoterPcLink);
+ instanceLink.setMobile_link(promoterMobileLink);
+ approvalsInstanceVo.setLinks(instanceLink);
+ approvalsInstanceVo.setTitle("@i18n@1");
+ approvalsInstanceVo.setUser_id(promoterUserId); // 审批发起人的user_id
+ approvalsInstanceVo.setStart_time(String.valueOf(instanceDto.getInstanceStartTime().getTime()));
+ approvalsInstanceVo.setEnd_time("0");
+ approvalsInstanceVo.setUpdate_time(String.valueOf(System.currentTimeMillis()));
+ approvalsInstanceVo.setUpdate_mode("UPDATE");
+ approvalsInstanceVo.setDisplay_method("BROWSER");
+ List instanceTasks = new ArrayList<>();
+ for (int i = 0; i < newReviewedIds.size(); i++) {
+ // 审核人相关的信息
+ InstanceTask instanceTask = new InstanceTask();
+ instanceTask.setUser_id(newReviewedIds.get(i)); // 审批人的id
+ if (!taskIdList.isEmpty()) {
+ instanceTask.setTask_id(taskIdList.get(i));
+ }
+ InstanceLink taskLink = new InstanceLink();
+ if (!reviewPcLinkList.isEmpty()) {
+ taskLink.setPc_link(reviewPcLinkList.get(i));
+ }
+ if (!reviewMobileLinkList.isEmpty()) {
+ taskLink.setMobile_link(reviewMobileLinkList.get(i));
+ }
+ instanceTask.setLinks(taskLink);
+ if (!taskStatusList.isEmpty()) {
+ instanceTask.setStatus(taskStatusList.get(i));
+ }
+ instanceTask.setTitle("@i18n@1");
+ instanceTask.setCreate_time(String.valueOf(System.currentTimeMillis()));
+ instanceTask.setEnd_time(String.valueOf(System.currentTimeMillis()));
+ instanceTask.setUpdate_time(String.valueOf(System.currentTimeMillis()));
+ instanceTask.setDisplay_method("BROWSER");
+ instanceTasks.add(instanceTask);
+ }
+ approvalsInstanceVo.setTask_list(instanceTasks);
+ // 国际化文案
+ DefinitionI18nResources definitionI18nResources = new DefinitionI18nResources();
+ definitionI18nResources.setLocale("zh-CN");
+ definitionI18nResources.setIs_default(true);
+ List texts = new ArrayList<>();
+ texts.add(new DefinitionTexts("@i18n@1",instanceDto.getTitle()));
+ definitionI18nResources.setTexts(texts);
+ List i18n_resources = new ArrayList<>();
+ i18n_resources.add(definitionI18nResources);
+ approvalsInstanceVo.setI18n_resources(i18n_resources);
+ return approvalsInstanceVo;
+ }
+
+ private HttpHeaders createHeaders(String accessToken) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Authorization", "Bearer " + accessToken);
+ headers.set("Content-Type", "application/json");
+ return headers;
+ }
+
+ @Override
+ public void sendHrefMessageByBot(BotMessageDto botMessageDto) {
+ String url = env.getProperty("feishu.bot_send_message");
+ String signLoginUrl = env.getProperty("feishu.sign_login_in");
+ assert url != null;
+ assert signLoginUrl != null;
+ try {
+ // 获取token
+ String accessToken = getAccessToken();
+ // 构建参数
+ List newPushRange = new ArrayList<>();
+ if (StringUtils.isNotBlank(botMessageDto.getReceiveIds())) {
+ List pushRangeList = Arrays.asList(botMessageDto.getReceiveIds().split(","));
+ for (String id : pushRangeList) {
+ SysUser reviewUser = sysUserService.getById(id);
+ if (!Objects.isNull(reviewUser) && StringUtils.isNotBlank(reviewUser.getFeishuId())) {
+ newPushRange.add(reviewUser.getFeishuId());
+ }
+ }
+ }
+ if (newPushRange.isEmpty()) {
+ log.error("接收消息的飞书用户均不存在,消息发送终止");
+ return;
+ }
+ // 将url替换为单点登录的地址
+ String hrefUrl = botMessageDto.getHrefUrl();
+ if (StringUtils.isNotBlank(hrefUrl)) {
+ hrefUrl = signLoginUrl.replace("{REDIRECT_URI}", hrefUrl);
+ }
+ // 构建消息内容部分
+ JSONArray contentArray = new JSONArray();
+ // 构建普通文本前缀
+ if (StringUtils.isNotBlank(botMessageDto.getTextBeforeHref())) {
+ JSONObject textBeforeHrefObject = new JSONObject();
+ textBeforeHrefObject.put("tag", "text");
+ textBeforeHrefObject.put("text", botMessageDto.getTextBeforeHref());
+ contentArray.add(textBeforeHrefObject);
+ }
+ // 构建超链接文本
+ if (StringUtils.isNotBlank(botMessageDto.getHrefText()) && StringUtils.isNotBlank(hrefUrl)) {
+ JSONObject hrefContentObject = new JSONObject();
+ hrefContentObject.put("tag", "a");
+ hrefContentObject.put("href", hrefUrl);
+ hrefContentObject.put("text", botMessageDto.getHrefText());
+ contentArray.add(hrefContentObject);
+ }
+ // 构建普通文本后缀
+ if (StringUtils.isNotBlank(botMessageDto.getTextAfterHref())) {
+ JSONObject textAfterHrefObject = new JSONObject();
+ textAfterHrefObject.put("tag", "text");
+ textAfterHrefObject.put("text", botMessageDto.getTextAfterHref());
+ contentArray.add(textAfterHrefObject);
+ }
+
+ List contentList = new ArrayList<>();
+ contentList.add(contentArray);
+ JSONObject zhCnObject = new JSONObject();
+ zhCnObject.put("title", botMessageDto.getTitle());
+ zhCnObject.put("content", contentList);
+ JSONObject finalObject = new JSONObject();
+ finalObject.put("zh_cn", zhCnObject);
+ String content = finalObject.toJSONString();
+ BotMessageVo botMessageVo = new BotMessageVo();
+ botMessageVo.setMsg_type("post");
+ botMessageVo.setContent(content);
+
+ // 设置请求头
+ HttpHeaders headers = createHeaders(accessToken);
+ // 构建包含查询参数的 URL
+ UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url).queryParam("receive_id_type", "user_id");
+ // 创建 HttpEntity
+ for (String pushId : newPushRange) {
+ String uuid = UUID.randomUUID().toString().replace("-", "");
+ botMessageVo.setReceive_id(pushId);
+ botMessageVo.setUuid(uuid);
+ HttpEntity entity = new HttpEntity<>(botMessageVo, headers);
+ restTemplate.exchange(builder.toUriString(), HttpMethod.POST, entity, String.class);
+ }
+ } catch (Exception e) {
+ log.error("飞书发送消息异常" + e.getMessage());
+ }
+ }
+
+
+ /**
+ * 根据预授权码获取用户信息
+ *
+ * @param codeObject
+ * @return
+ */
+ @Override
+ public Result getUserInfoByCode(JSONObject codeObject) {
+ String url = env.getProperty("feishu.get_userinfo_by_code");
+ assert url != null;
+ String code = codeObject.getString("code");
+ // 获取token
+ String accessToken = getAccessToken();
+ // 设置请求头
+ HttpHeaders headers = createHeaders(accessToken);
+ // 创建 HttpEntity
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("grant_type","authorization_code");
+ jsonObject.put("code",code);
+ HttpEntity entity = new HttpEntity<>(jsonObject, headers);
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
+ // 处理响应
+ Result result = new Result<>();
+ JSONObject jsonResponse = JSON.parseObject(response.getBody());
+ if ("0".equals((String.valueOf(jsonResponse.get("code"))))) {
+ JSONObject dataJson = jsonResponse.getJSONObject("data");
+ String feishuId = dataJson.getString("user_id");
+ // 从sys_user表查询对应的用户信息
+ LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
+ lambdaQueryWrapper.eq(SysUser::getFeishuId,feishuId);
+ SysUser suer = sysUserService.getOne(lambdaQueryWrapper);
+ //用户登录信息
+ userInfo(suer, result);
+ } else {
+ log.error("获取用户信息失败");
+ }
+ return result;
+ }
+
+ private Result userInfo(SysUser sysUser, Result result) {
+ String multiDepart ="multi_depart";
+ String syspassword = sysUser.getPassword();
+ String username = sysUser.getUsername();
+ //获取用户角色信息
+ String userId = sysUser.getId();
+ if (StringUtils.isNotBlank(userId)){
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(SysUserRole::getUserId, userId);
+ List sysUserRoles = sysUserRoleService.list(wrapper);
+ if (!sysUserRoles.isEmpty()){
+ List collect = sysUserRoles.stream().map(SysUserRole::getRoleId).distinct().collect(Collectors.toList());
+ List roles = sysRoleService.listByIds(collect);
+ String code = roles.stream().map(SysRole::getRoleCode).collect(Collectors.joining(","));
+ sysUser.setRoleCode(code);
+ }
+ }
+ // 获取用户部门信息
+ JSONObject obj = new JSONObject();
+ List departs = sysDepartService.queryUserDeparts(sysUser.getId());
+ obj.put("departs", departs);
+ if (departs == null || departs.isEmpty()) {
+ obj.put(multiDepart, 0);
+ } else if (departs.size() == 1) {
+ sysUserService.updateUserDepart(username, departs.get(0).getOrgCode());
+ obj.put(multiDepart, 1);
+ } else {
+ //查询当前是否有登录部门
+ SysUser sysUserById = sysUserService.getById(sysUser.getId());
+ if(oConvertUtils.isEmpty(sysUserById.getOrgCode())){
+ sysUserService.updateUserDepart(username, departs.get(0).getOrgCode());
+ }
+ obj.put(multiDepart, 2);
+ }
+
+ if (CollectionUtils.isNotEmpty(departs)) {
+ List departIdList = departs.stream().map(SysDepart::getId).collect(Collectors.toList());
+ sysUser.setDepartIds(StringUtils.join(departIdList, ","));
+ }
+
+ String tenantIds = sysUser.getRelTenantIds();
+ if (oConvertUtils.isNotEmpty(tenantIds)) {
+ List tenantIdList = Arrays.asList(tenantIds.split(","));
+ // 该方法仅查询有效的租户,如果返回0个就说明所有的租户均无效。
+ List tenantList = sysTenantService.queryEffectiveTenant(tenantIdList);
+ if (tenantList.isEmpty()) {
+ return Result.error("与该用户关联的租户均已被冻结,无法登录!");
+ } else {
+ obj.put("tenantList", tenantList);
+ }
+ }
+ // 生成token
+ String token = JwtUtil.sign(username, syspassword);
+ // 设置token缓存有效时间
+ redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
+ redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
+ String departIds = sysUser.getDepartIds();
+ if (StringUtils.isNotBlank(departIds)) {
+ String departIds_dicText = translateDictValue("id", "depart_name", "sys_depart", departIds, null);
+ sysUser.setDepartIds_dicText(departIds_dicText);
+ }
+ obj.put("token", token);
+ obj.put(USER_INFO, sysUser);
+ obj.put("sysAllDictItems", sysDictService.queryAllDictItems());
+ result.setResult(obj);
+ result.setMessage("登录成功");
+ return result;
+ }
+
+ private String translateDictValue(String code, String text, String table, String key, List listDict) {
+ if (oConvertUtils.isEmpty(key)) {
+ return null;
+ }
+ StringBuilder textValue = new StringBuilder();
+ String[] keys = key.split(",");
+ for (String k : keys) {
+ String tmpValue = null;
+ if (k.trim().length() == 0) {
+ continue; //跳过循环
+ }
+ if (!org.springframework.util.StringUtils.isEmpty(table)) {
+ tmpValue = commonAPI.queryTableDictTextByKey(table, text, code, k.trim());
+ } else {
+ List listNew = listDict.stream()
+ .filter(o -> Objects.equals(o.getDictCode(), code) && Objects.equals(o.getItemValue(), k.trim())).collect(Collectors.toList());
+ if (!org.springframework.util.CollectionUtils.isEmpty(listNew)) {
+ tmpValue = listNew.get(0).getItemText();
+ }
+ }
+
+ if (tmpValue != null) {
+ if (!"".contentEquals(textValue)) {
+ textValue.append(",");
+ }
+ textValue.append(tmpValue);
+ }
+ }
+ return textValue.toString();
+ }
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/bot/BotMessageVo.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/bot/BotMessageVo.java
new file mode 100644
index 00000000..5178f3d0
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/bot/BotMessageVo.java
@@ -0,0 +1,35 @@
+package com.jero.modules.feishu.vo.bot;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class BotMessageVo {
+
+ /**
+ * 消息接收者的id
+ */
+ @ApiModelProperty("消息接收者的ID")
+ private String receive_id;
+
+ /**
+ * 消息类型 包括:text、post、image、file、audio、media、
+ * sticker、interactive、share_chat、share_user等
+ */
+ @ApiModelProperty("消息类型")
+ private String msg_type;
+
+ /**
+ * 消息内容,JSON结构序列化后的字符串
+ */
+ @ApiModelProperty("消息内容")
+ private String content;
+
+ /**
+ * 由开发者生成的唯一字符串序列,用于发送消息请求去重;
+ * 持有相同uuid的请求1小时内至多成功发送一条消息
+ */
+ @ApiModelProperty("UUID")
+ private String uuid;
+
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/ApprovalsDefinitionVo.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/ApprovalsDefinitionVo.java
new file mode 100644
index 00000000..2caccb4d
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/ApprovalsDefinitionVo.java
@@ -0,0 +1,80 @@
+package com.jero.modules.feishu.vo.definition;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class ApprovalsDefinitionVo {
+
+ /**
+ *
+ * 审批名称的国际化文案 Key,以 @i18n@ 开头,长度不得少于 9 个字符
+ * 示例值: @i18n@1
+ */
+ @ApiModelProperty("审批名称的国际化文案")
+ private String approval_name;
+
+ /**
+ *
+ * 审批定义 code,用户自定义,定义的唯一标识,如果不存在该 code,则创建,否则更新
+ * 示例值: permission_test
+ */
+ @ApiModelProperty("审批定义code")
+ private String approval_code;
+
+ /**
+ *
+ * 审批定义所属审批分组,用户自定义; 如果group_code当前不存在,则会新建审批分组;
+ * 如果group_code已经存在,则会使用group_name更新审批分组名称
+ * 示例值: work_group
+ */
+ @ApiModelProperty("审批定义所属审批分组")
+ private String group_code;
+
+ /**
+ * 分组名称,值的格式是 i18n key,文案放在 i18n_resource;
+ * 如果是 group_code 当前不存在,则该 group_name 必填,否则,
+ * 如果填写了则会更新分组名称,不填则不更新分组名称;
+ * 审批发起页 审批定义的分组名称来自该字段
+ * 示例值: @i18n@2
+ */
+ @ApiModelProperty("审批定义所属审批分组")
+ private String group_name;
+
+ /**
+ * 审批定义的说明,值的格式是 i18n key,文案放在 i18n_resource;
+ * 审批发起页 审批定义的说明内容来自该字段
+ * 示例值: @i18n@2
+ */
+ @ApiModelProperty("审批定义的说明")
+ private String description;
+
+ /**
+ * 三方审批相关
+ */
+ @ApiModelProperty("三方审批相关")
+ private DefinitionExternal external;
+
+ /**
+ * 可见人列表(最大支持长度200),可通知配置多个可见人,
+ * 只有在配置的范围内用户可以在审批发起页看到该审批,
+ * 默认不传,则是任何人不可见
+ */
+ @ApiModelProperty("可见人列表")
+ private List viewers;
+
+ /**
+ * 国际化文案
+ */
+ @ApiModelProperty("国际化文案")
+ private List i18n_resources;
+
+ /**
+ * 根据user_id_type填写流程管理员id列表(最大支持长度200)
+ * 示例值:["19a294c2"]
+ */
+ @ApiModelProperty("流程管理员id列表")
+ private String[] managers;
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionExternal.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionExternal.java
new file mode 100644
index 00000000..499c6e24
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionExternal.java
@@ -0,0 +1,108 @@
+package com.jero.modules.feishu.vo.definition;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 三方审批相关
+ */
+@Data
+public class DefinitionExternal {
+
+ /**
+ *
+ * 列表中用于提示审批来自哪里,i18n key,
+ * 注意不需要“来自”前缀,审批中心会拼上前缀
+ * 示例值:"@i18n@3"
+ */
+ @ApiModelProperty("列表中用于提示审批来自哪里")
+ private String biz_name;
+
+ /**
+ * 审批定义业务类别
+ * 示例值:"permission"
+ */
+ @ApiModelProperty("审批定义业务类别")
+ private String biz_type;
+
+ /**
+ * 移动端发起链接,如果设置了该链接,则会在移动端审批发起页展示该审批,
+ * 用户点击后会跳转到该链接进行发起;
+ * 如果不填,则在mobile端不显示该审批
+ */
+ @ApiModelProperty("移动端发起链接")
+ private String create_link_mobile;
+
+ /**
+ * PC端发起链接,如果设置了该链接,则会在PC端审批发起页展示该审批,
+ * 用户点击后会跳转到该链接进行发起;
+ * 如果不填,则在PC端不显示该审批;
+ */
+ @ApiModelProperty("PC端发起链接")
+ private String create_link_pc;
+
+ /**
+ * 审批实例、审批任务、审批抄送是否要在PC端展示,
+ * 如果为 true,则PC端列表会展示该定义下的实例信息,否则,不展示
+ */
+ @ApiModelProperty("审批实例、审批任务、审批抄送是否要在PC端展示")
+ private boolean support_pc;
+
+ /**
+ * 审批实例、审批任务、审批抄送是否要在移动端展示,
+ * 如果为 true,则PC端列表会展示该定义下的实例信息,否则,不展示
+ */
+ @ApiModelProperty("审批实例、审批任务、审批抄送是否要在移动端展示")
+ private boolean support_mobile;
+
+ /**
+ * 是否支持批量已读
+ */
+ @ApiModelProperty("是否支持批量已读")
+ private boolean support_batch_read;
+
+ /**
+ * 是否支持批量已读(该字段无效)
+ */
+ @ApiModelProperty("是否支持标注可读(该字段无效)")
+ private boolean enable_mark_readed;
+
+ /**
+ * 是否支持快速操作
+ */
+ @ApiModelProperty("是否支持快速操作")
+ private boolean enable_quick_operate;
+
+ /**
+ * 三方系统的操作回调 url,【待审批】列表的任务审批人点同意或拒绝操作后,审批中心调用该地址通知三方系统
+ */
+ @ApiModelProperty("三方系统的操作回调 url")
+ private String action_callback_url;
+
+ /**
+ * 回调时带的 token, 用于业务系统验证请求来自审批,
+ */
+ @ApiModelProperty("回调时带的token")
+ private String action_callback_token;
+
+ /**
+ * 请求参数加密密钥,如果配置了该参数,则会对请求参数进行加密,业务需要对请求进行解密
+ */
+ @ApiModelProperty("请求参数加密密钥")
+ private String action_callback_key;
+
+ /**
+ * 是否支持批量审批
+ */
+ @ApiModelProperty("是否支持批量审批")
+ private boolean allow_batch_operate;
+
+ /**
+ * 审批流程数据是否不纳入效率统计
+ */
+ @ApiModelProperty("审批流程数据是否不纳入效率统计")
+ private boolean exclude_efficiency_statistics;
+
+
+
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionI18nResources.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionI18nResources.java
new file mode 100644
index 00000000..4e51a958
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionI18nResources.java
@@ -0,0 +1,61 @@
+package com.jero.modules.feishu.vo.definition;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 国际化文案
+ */
+@Data
+public class DefinitionI18nResources {
+
+ /**
+ * 语言可选值有: zh-CN:中文 en-US:英文 ja-JP:日文
+ * 示例值:"zh-CN"
+ * zh-CN:中文
+ * en-US:英文
+ * ja-JP:日文
+ */
+ @ApiModelProperty("语言可选值有: zh-CN:中文 en-US:英文 ja-JP:日文")
+ private String locale;
+
+ /**
+ * 是否默认语言,默认语言需要包含所有key,非默认语言如果key不存在会使用默认语言代替
+ */
+ @ApiModelProperty("是否默认语言,默认语言需要包含所有key,非默认语言如果key不存在会使用默认语言代替")
+ @JSONField(name = "is_default")
+ private boolean is_default;
+
+ /**
+ * 示例值: "{ @i18n@1: 权限申请 }"
+ */
+ @ApiModelProperty("{ @i18n@1: 权限申请 }")
+ private List texts;
+
+ public String getLocale() {
+ return locale;
+ }
+
+ public void setLocale(String locale) {
+ this.locale = locale;
+ }
+
+ public boolean isIs_default() {
+ return is_default;
+ }
+
+ public void setIs_default(boolean is_default) {
+ this.is_default = is_default;
+ }
+
+ public List getTexts() {
+ return texts;
+ }
+
+ public void setTexts(List texts) {
+ this.texts = texts;
+ }
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionTexts.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionTexts.java
new file mode 100644
index 00000000..213393bb
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionTexts.java
@@ -0,0 +1,30 @@
+package com.jero.modules.feishu.vo.definition;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class DefinitionTexts {
+
+ public DefinitionTexts(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public DefinitionTexts() {
+ }
+
+ /**
+ 文案key
+ 示例值:"@i18n@1"
+ */
+ @ApiModelProperty("文案key")
+ private String key;
+
+ /**
+ 文案
+ 示例值:"people"
+ */
+ @ApiModelProperty("文案")
+ private String value;
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionViewers.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionViewers.java
new file mode 100644
index 00000000..a8803199
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/definition/DefinitionViewers.java
@@ -0,0 +1,36 @@
+package com.jero.modules.feishu.vo.definition;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 可见人列表
+ */
+@Data
+public class DefinitionViewers {
+
+ /**
+ 可见人类型 示例值:"USER"
+ * 可选值有:
+ TENANT:租户内可见
+ DEPARTMENT:指定部门
+ USER:指定用户
+ NONE:任何人都不可见
+ */
+ @ApiModelProperty("可见人类型 TENANT:租户内可见 DEPARTMENT:指定部门 USER:指定用户 NONE:任何人都不可见")
+ private String viewer_type;
+
+ /**
+ * 当 viewer_type 是 USER,根据user_id_type填写用户id
+ * 示例值:"19a294c2"
+ */
+ @ApiModelProperty("当 viewer_type 是 USER,根据user_id_type填写用户id")
+ private String viewer_user_id;
+
+ /**
+ * 当 viewer_type 为DEPARTMENT,根据department_id_type填写部门id
+ * 示例值:"od-ac9d697abfa990b715dcc33d58a62a9d"
+ */
+ @ApiModelProperty("当 viewer_type 为DEPARTMENT,根据department_id_type填写部门id")
+ private String viewer_department_id;
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/ApprovalsInstanceVo.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/ApprovalsInstanceVo.java
new file mode 100644
index 00000000..8a5fa8d9
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/ApprovalsInstanceVo.java
@@ -0,0 +1,185 @@
+package com.jero.modules.feishu.vo.instance;
+
+import com.jero.modules.feishu.vo.definition.DefinitionI18nResources;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class ApprovalsInstanceVo {
+
+ /**
+ * 审批定义 code, 创建审批定义返回的值,表示该实例属于哪个流程;
+ * 该字段会影响到列表中该实例的标题,标题取自对应定义的 name 字段
+ * 示例值:"81D31358-93AF-92D6-7425-01A5D67C4E71"
+ */
+ @ApiModelProperty("审批定义 code")
+ private String approval_code;
+
+ /**
+ * 审批实例状态
+ * 示例值:"PENDING"
+ * PENDING: 审批中
+ * APPROVED: 审批流程结束,结果为同意
+ * REJECTED: 审批流程结束,结果为拒绝
+ * CANCELED: 审批发起人撤回
+ * DELETED: 审批被删除
+ * HIDDEN: 状态隐藏(不显示状态)
+ * TERMINATED:审批终止
+ */
+ @ApiModelProperty("审批实例状态")
+ private String status;
+
+ /**
+ * 审批实例扩展 JSON。单据编号通过传business_key字段来实现。
+ * 以下示例值未转义,使用时请注意转义。
+ * 你可查看请求体示例中转义后的 extra 示例值
+ * 示例值:"{"xxx":"xxx","business_key":"xxx"}"
+ */
+ @ApiModelProperty("审批实例扩展JSON")
+ private String extra;
+
+ /**
+ *
+ * 审批实例唯一标识,用户自定义,需确保证租户和应用下都唯一
+ * 示例值:"24492654"
+ */
+ @ApiModelProperty("审批实例唯一标识")
+ private String instance_id;
+
+ /**
+ * 审批实例链接集合 ,
+ * 用于【已发起】列表的跳转,跳转回三方系统
+ */
+ @ApiModelProperty("审批实例链接集合")
+ private InstanceLink links;
+
+ /**
+ * 审批展示名称,如果填写了该字段,则审批列表中的审批名称使用该字段,
+ * 如果不填该字段,则审批名称使用审批定义的名称
+ * 示例值:"@i18n@1"
+ */
+ @ApiModelProperty("审批展示名称")
+ private String title;
+
+ /**
+ * 用户提交审批时填写的表单数据,用于所有审批列表中展示。可传多个值,
+ * 但审批中心pc展示前2个,移动端展示前3个,长度不超过2048字符
+ * 示例值:[{ "name": "@i18n@2", "value": "@i18n@3" }]
+ */
+ @ApiModelProperty("用户提交审批时填写的表单数据")
+ private List form;
+
+ /**
+ *
+ * 审批发起人 user_id,发起人可在【已发起】列表中看到所有已发起的审批;
+ * 在【待审批】,【已审批】【抄送我】列表中,该字段展示审批是谁发起的。
+ * 示例值:"a987sf9s"
+ */
+ @ApiModelProperty("审批发起人 user_id")
+ private String user_id;
+
+ /**
+ * 审批发起人 用户名,如果发起人不是真实的用户(例如是某个部门),
+ * 没有 user_id,则可以使用该字段传名称
+ * 示例值:"@i18n@9"
+ */
+ @ApiModelProperty("审批发起人 用户名")
+ private String user_name;
+
+ /**
+ * 审批发起人 open id
+ * 示例值:"ou_be73cbc0ee35eb6ca54e9e7cc14998c1"
+ */
+ @ApiModelProperty("审批发起人 open id")
+ private String open_id;
+
+ /**
+ * 发起人部门,用于列表中展示发起人所属部门。不传则不展示。
+ * 如果用户没加入任何部门,传 "",将展示租户名称传 department_name 展示部门名称
+ * 示例值:"od-8ec33278bc2"
+ */
+ @ApiModelProperty("发起人部门")
+ private String department_id;
+
+ /**
+ * 审批发起人 部门,如果发起人不是真实的用户(例如是某个部门),
+ * 没有 department_id,则可以使用该字段传名称
+ * 示例值:"@i18n@10"
+ */
+ @ApiModelProperty("审批发起人 部门")
+ private String department_name;
+
+ /**
+ * 审批发起时间,Unix毫秒时间戳
+ * 示例值:"1556468012678"
+ */
+ @ApiModelProperty("审批发起时间 Unix毫秒时间戳")
+ private String start_time;
+
+ /**
+ * 审批实例结束时间,Unix毫秒时间戳
+ * 示例值:"1556468012678"
+ */
+ @ApiModelProperty("审批实例结束时间 Unix毫秒时间戳")
+ private String end_time;
+
+ /**
+ * 审批实例最近更新时间;用于推送数据版本控制如果 update_mode 值为 UPDATE,
+ * 则只有传过来的 update_time 有变化时(变大),才会更新审批中心中的审批实例信息。
+ * 使用该字段主要用来避免并发时老的数据更新了新的数据
+ * 示例值:"1556468012678"
+ */
+ @ApiModelProperty("审批实例最近更新时间 Unix毫秒时间戳")
+ private String update_time;
+
+ /**
+ * 列表页打开审批实例的方式
+ * 示例值:"BROWSER"
+ * BROWSER:跳转系统默认浏览器打开
+ * SIDEBAR:飞书中侧边抽屉打开
+ * NORMAL:飞书内嵌页面打开
+ * TRUSTEESHIP:以托管打开
+ */
+ @ApiModelProperty("列表页打开审批实例的方式")
+ private String display_method;
+
+ /**
+ * 更新方式, 当 update_mode=REPLACE时,每次都以当前推送的数据为最终数据,会删掉审批中心中多余的任务、抄送数据(不在这次推送的数据中);
+ * 当 update_mode=UPDATE时,则不会删除审批中心的数据,而只是进行新增和更新实例、任务数据
+ * 示例值:"UPDATE"
+ * 可选值有:
+ * REPLACE:全量替换,默认值
+ * UPDATE:增量更新
+ */
+ @ApiModelProperty("列表页打开审批实例的方式")
+ private String update_mode;
+
+ /**
+ * 抄送列表
+ */
+ @ApiModelProperty("任务列表")
+ private List task_list;
+
+ /**
+ * 国际化文案
+ */
+ @ApiModelProperty("国际化文案")
+ private List i18n_resources;
+
+ /**
+ * 单据托管认证token,托管回调会附带此token,帮助业务方认证
+ * 示例值:"788981c886b1c28ac29d1e68efd60683d6d90dfce80938ee9453e2a5f3e9e306"
+ */
+ @ApiModelProperty("单据托管认证token")
+ private String trusteeship_url_token;
+
+ /**
+ * 用户的类型,会影响请求参数用户标识域的选择,包括加签操作回传的目标用户, 目前仅支持 "user_id"
+ * 示例值:"user_id"
+ */
+ @ApiModelProperty("用户的类型")
+ private String trusteeship_user_id_type;
+
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceForm.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceForm.java
new file mode 100644
index 00000000..14cf52bc
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceForm.java
@@ -0,0 +1,22 @@
+package com.jero.modules.feishu.vo.instance;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+public class InstanceForm {
+
+ /**
+ * 表单字段名称
+ * 示例值:"@i18n@2"
+ */
+ @ApiModelProperty("表单字段名称")
+ private String name;
+
+ /**
+ * 表单值
+ * 示例值:"@i18n@3"
+ */
+ @ApiModelProperty("表单值")
+ private String value;
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceLink.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceLink.java
new file mode 100644
index 00000000..3ebbaa90
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceLink.java
@@ -0,0 +1,23 @@
+package com.jero.modules.feishu.vo.instance;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+
+@Data
+public class InstanceLink {
+
+ /**
+ * pc端的跳转链接,当用户使用飞书 pc 端时,
+ * 使用该字段进行跳转,托管的链接保持不变。
+ */
+ @ApiModelProperty("pc端的跳转链接")
+ private String pc_link;
+
+ /**
+ * 移动端的跳转链接,当用户使用飞书 pc 端时,
+ * 使用该字段进行跳转,托管的链接保持不变。
+ */
+ @ApiModelProperty("移动端的跳转链接")
+ private String mobile_link;
+}
diff --git a/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceTask.java b/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceTask.java
new file mode 100644
index 00000000..571f0018
--- /dev/null
+++ b/laws-modules/src/main/java/com/jero/modules/feishu/vo/instance/InstanceTask.java
@@ -0,0 +1,153 @@
+package com.jero.modules.feishu.vo.instance;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 任务列表
+ */
+@Data
+public class InstanceTask {
+
+ /**
+ * 审批实例内的唯一标识,用于更新审批任务时定位数据
+ */
+ @ApiModelProperty("审批实例内的唯一标识")
+ private String task_id;
+
+ /**
+ * 审批人 user_id,和 open_id 二者至少填一个。
+ * 该任务会出现在审批人的【待审批】或【已审批】列表中
+ */
+ @ApiModelProperty("审批人 user_id")
+ private String user_id;
+
+ /**
+ * 审批人 open_id,和 user_id 二者至少填一个
+ * 示例值:"ou_be73cbc0ee35eb6ca54e9e7cc14998c1"
+ */
+ @ApiModelProperty("审批人 open_id")
+ private String open_id;
+
+ /**
+ * 审批任务名称
+ * 示例值:"i18n1"
+ */
+ @ApiModelProperty("审批任务名称")
+ private String title;
+
+ /**
+ * 【待审批】或【已审批】中使用的跳转链接,
+ * 用于跳转回三方系统pc_link 和 mobile_link 必须填一个,
+ * 填写的是哪一端的链接,即会跳转到该链接,不受平台影响
+ */
+ @ApiModelProperty("跳转链接")
+ private InstanceLink links;
+
+ /**
+ * 任务状态
+ * 示例值:"PENDING"
+ * 可选值有:
+ * PENDING:待审批
+ * APPROVED:任务同意
+ * REJECTED:任务拒绝
+ * TRANSFERRED:任务转交
+ * DONE:任务通过但审批人未操作;审批人看不到这个任务, 若想要看到, 可以通过抄送该人.
+ */
+ @ApiModelProperty("任务状态")
+ private String status;
+
+ /**
+ * 扩展 json,任务结束原因需传complete_reason字段。枚举值与对应说明:
+ * approved:同意
+ * rejected:拒绝
+ * node_auto_reject:(因逻辑判断产生的)自动拒绝
+ * specific_rollback:退回(包括退回到发起人、退回到中间任一审批人)
+ * add:并加签(添加新审批人,和我一起审批)
+ * add_pre:前加签(添加新审批人,在我之前审批)
+ * add_post:后加签(添加新审批人,在我之后审批)
+ * delete_assignee:减签
+ * forward: 手动转交
+ * forward_resign:离职自动转交
+ * recall:撤销(撤回单据,单据失效)
+ * delete :删除审批单
+ * admin_forward:管理员在后台操作转交
+ * system_forward:系统自动转交
+ * auto_skip:自动通过
+ * manual_skip:手动跳过
+ * submit_again:重新提交任务
+ * restart:重新启动流程
+ * others:其他(作为兜底)
+ * 示例值:"{"xxx":"xxx","complete_reason":"approved"}"
+ */
+ @ApiModelProperty("扩展 json")
+ private String extra;
+
+ /**
+ * 任务创建时间,Unix 毫秒时间戳
+ * 示例值:"1556468012678"
+ */
+ @ApiModelProperty("任务创建时间,Unix 毫秒时间戳")
+ private String create_time;
+
+ /**
+ * 任务完成时间:未结束的审批为 0,Unix 毫秒时间戳
+ * 示例值:"1556468012678"
+ */
+ @ApiModelProperty("任务完成时间:未结束的审批为 0,Unix 毫秒时间戳")
+ private String end_time;
+
+ /**
+ * task最近更新时间,用于推送数据版本控制; 更新策略同 instance 中的 update_time
+ * 示例值:"1556468012678"
+ */
+ @ApiModelProperty("任务完成时间:未结束的审批为 0,Unix 毫秒时间戳")
+ private String update_time;
+
+ /**
+ * 操作上下文,当用户操作时,
+ * 回调请求中带上该参数,用于传递该任务的上下文数据
+ * 示例值:"123456"
+ */
+ @ApiModelProperty("操作上下文")
+ private String action_context;
+
+ /**
+ * 列表页打开审批任务的方式
+ * 示例值:"BROWSER"
+ * 可选值有:
+ * BROWSER:跳转系统默认浏览器打开
+ * SIDEBAR:飞书中侧边抽屉打开
+ * NORMAL:飞书内嵌页面打开
+ * TRUSTEESHIP:以托管模式打开
+ */
+ @ApiModelProperty("列表页打开审批任务的方式")
+ private String display_method;
+
+ /**
+ * 三方任务支持不纳入效率统计。false:纳入效率统计。true:不纳入效率统计
+ * 示例值:false
+ * 默认值:false
+ */
+ @ApiModelProperty("三方任务支持不纳入效率统计")
+ private boolean exclude_statistics;
+
+ /**
+ * 节点id:必须同时满足
+ * 一个流程内,每个节点id唯一。如一个流程下「直属上级」、「隔级上级」等每个节点的Node_id均不一样
+ * 同一个流程定义内,不同审批实例中的相同节点,Node_id要保持不变。例如张三和李四分别发起了请假申请,这2个审批实例中的「直属上级」节点的node_id应该保持一致
+ * 示例值:"node"
+ */
+ @ApiModelProperty("节点id")
+ private String node_id;
+
+ /**
+ * 节点名称,如「财务审批」「法务审批」,支持中英日三种语言。示例:i18n@name。需要在i18n_resources中传该名称对应的国际化文案
+ * 示例值:"i18n@name"
+ */
+ @ApiModelProperty("节点名称")
+ private String node_name;
+
+}
diff --git a/laws-single-startup/src/main/resources/application-dev.yml b/laws-single-startup/src/main/resources/application-dev.yml
index b129a2e5..cfd9eca7 100644
--- a/laws-single-startup/src/main/resources/application-dev.yml
+++ b/laws-single-startup/src/main/resources/application-dev.yml
@@ -557,3 +557,44 @@ sys_role:
# 超级管理员
super_admin: superAdmin
+# 飞书对接:
+feishu:
+ # 重定向地址
+ redirect:
+ # 动态消息:
+ news_feed: http://localhost:3344/standardRegulationLibrary/DynamicMessageList
+ # 法规导入流程
+ regulation_import_process: http://localhost:3344/standardRegulationLibrary/standardMasterPlan
+ # 市场清单发布流程
+ market_publishing_process: http://localhost:3344/standardRegulationLibrary/marketRegulationsList/marketReg
+ # 节点催办
+ node_urging: http://localhost:3344/workCenter/processCenter/processCenter
+ # 资料中心
+ data_center: http://localhost:3344/standardRegulationLibrary/ProfileCenterList
+
+ # app的id
+ app_id : cli_a61204ce741ed00b
+ # app秘钥
+ app_secret: lblKYMV4im4nP4YkBdgefKLPEdGBwaXA
+ # 审批定义的名称:
+ approvalName: 奇瑞法规智库
+ # 审批定义的流程管理员的user_id:
+ managersIds: aacb77d4
+ # 基础地址:
+ base_url: https://open.feishu.cn/open-apis
+ # 获取token的地址
+ get_token: ${feishu.base_url}/auth/v3/tenant_access_token/internal
+ # 获取用户id
+ get_user_id: ${feishu.base_url}/contact/v3/users/batch_get_id
+ # 创建三方审批定义
+ create_approval_definition: ${feishu.base_url}/approval/v4/external_approvals
+ # 查看指定三方审批定义
+ get_approval_definition: ${feishu.base_url}/approval/v4/external_approvals
+ # 同步三方审批实例
+ synchronize_approval_instance: ${feishu.base_url}/approval/v4/external_instances
+ # 使用Bot发送消息
+ bot_send_message: ${feishu.base_url}/im/v1/messages
+ # 飞书根据预登录code获取用户信息
+ get_userinfo_by_code: ${feishu.base_url}/authen/v1/access_token
+ # 飞书单点登录地址
+ sign_login_in: ${feishu.base_url}/authen/v1/index?redirect_uri={REDIRECT_URI}&app_id=${feishu.app_id}&state=feishu