| | |
| | | |
| | | import com.alibaba.fastjson2.JSONArray; |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; |
| | | import com.dingtalk.api.DefaultDingTalkClient; |
| | | import com.dingtalk.api.DingTalkClient; |
| | | import com.dingtalk.api.request.OapiGettokenRequest; |
| | | import com.dingtalk.api.request.OapiMessageCorpconversationAsyncsendV2Request; |
| | | import com.dingtalk.api.request.OapiUserListidRequest; |
| | | import com.dingtalk.api.request.OapiV2UserGetbymobileRequest; |
| | | import com.dingtalk.api.response.OapiGettokenResponse; |
| | | import com.dingtalk.api.response.OapiMessageCorpconversationAsyncsendV2Response; |
| | | import com.dingtalk.api.response.OapiUserListidResponse; |
| | | import com.dingtalk.api.response.OapiV2UserGetbymobileResponse; |
| | | import com.dingtalk.api.request.*; |
| | | import com.dingtalk.api.response.*; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.ruoyi.common.core.domain.entity.SysUser; |
| | | import com.ruoyi.common.exception.base.BaseException; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.common.utils.http.HttpUtils; |
| | | import com.ruoyi.project.domain.vo.DingDetpInfoVo; |
| | | import com.ruoyi.project.domain.vo.DingTalkReqVo; |
| | | import com.ruoyi.project.domain.vo.DingUserInfoVo; |
| | | import com.ruoyi.project.service.DingTalkService; |
| | | import com.ruoyi.system.mapper.SysUserMapper; |
| | | import com.taobao.api.ApiException; |
| | | import lombok.extern.log4j.Log4j; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.collections4.CollectionUtils; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.text.SimpleDateFormat; |
| | | import java.util.ArrayList; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | |
| | | * @date 2023-05-23 |
| | | */ |
| | | @Service |
| | | @Log4j |
| | | @Slf4j |
| | | public class DingTalkServiceImpl implements DingTalkService { |
| | | |
| | | |
| | |
| | | @Value("${dingAppSecret}") |
| | | private String dingAppSecret; |
| | | |
| | | @Value("${agentId}") |
| | | private Long agentId; |
| | | |
| | | /** |
| | | * access_token 缓存。 |
| | | * 钉钉 token 有效期 7200 秒,这里按 6000 秒保守刷新; |
| | | * 原实现每次发消息都调一次 /gettoken,钉钉对取 token 有频次限制,审核高峰期容易整体失败。 |
| | | */ |
| | | private volatile String cachedAccessToken; |
| | | /** access_token 过期时间(epoch millis) */ |
| | | private volatile long accessTokenExpireAt; |
| | | private static final long ACCESS_TOKEN_TTL_MILLIS = 6000L * 1000L; |
| | | |
| | | /** |
| | | * 钉钉接口超时(毫秒)。 |
| | | * SDK 未显式设置超时时会无限等待,钉钉网络不通将占满容器线程、拖死审批等调用方, |
| | | * 因此这里对所有出站请求统一设置连接/读取超时。 |
| | | */ |
| | | private static final int CONNECT_TIMEOUT_MS = 3000; |
| | | private static final int READ_TIMEOUT_MS = 5000; |
| | | |
| | | @Autowired |
| | | private SysUserMapper sysUserMapper; |
| | | |
| | | /** |
| | | * 创建带超时的钉钉客户端 |
| | | */ |
| | | private DefaultDingTalkClient newClient(String url) { |
| | | DefaultDingTalkClient client = new DefaultDingTalkClient(url); |
| | | client.setConnectTimeout(CONNECT_TIMEOUT_MS); |
| | | client.setReadTimeout(READ_TIMEOUT_MS); |
| | | return client; |
| | | } |
| | | |
| | | /** |
| | | * 发送钉钉通知(对外唯一入口)。 |
| | | * |
| | | * <p><b>本方法保证不向外抛任何异常</b>:钉钉属于旁路通知,任何失败都只记日志并返回 false, |
| | | * 绝不能影响审批、上报等业务主流程。 |
| | | */ |
| | | @Override |
| | | public Boolean sendNotification(DingTalkReqVo dingTalkReqVo) { |
| | | String body = null; |
| | | try { |
| | | return doSendNotification(dingTalkReqVo); |
| | | } catch (Throwable t) { |
| | | log.error("钉钉通知发送发生未预期异常,已忽略(不影响业务主流程)", t); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private Boolean doSendNotification(DingTalkReqVo dingTalkReqVo) { |
| | | List<String> userIdlist = new ArrayList(); |
| | | Boolean result = false; |
| | | log.info("发送钉钉通知"); |
| | | if (dingTalkReqVo == null) { |
| | | log.warn("钉钉通知入参为空,跳过发送"); |
| | | return false; |
| | | } |
| | | log.info("发送钉钉通知,title={}", dingTalkReqVo.getTitle()); |
| | | |
| | | String accessToken = null; |
| | | try { |
| | | accessToken = getAccessToken(); |
| | | } catch (ApiException e) { |
| | | e.printStackTrace(); |
| | | } catch (Exception e) { |
| | | log.error("钉钉获取 access_token 失败,无法发送通知", e); |
| | | } |
| | | if (StringUtils.isNotEmpty(dingTalkReqVo.getNumber())) { |
| | | DingTalkClient clientT = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getbymobile"); |
| | | OapiV2UserGetbymobileRequest req = new OapiV2UserGetbymobileRequest(); |
| | | req.setMobile(dingTalkReqVo.getNumber()); |
| | | OapiV2UserGetbymobileResponse rsp = null; |
| | | try { |
| | | rsp = clientT.execute(req, accessToken); |
| | | body = rsp.getBody(); |
| | | String jsonObject = JSONObject.parseObject(body).getJSONObject("result").get("userid").toString(); |
| | | userIdlist.add(jsonObject); |
| | | if (StringUtils.isEmpty(accessToken)) { |
| | | log.error("钉钉 access_token 为空,无法发送通知"); |
| | | return false; |
| | | } |
| | | |
| | | } catch (ApiException e) { |
| | | e.printStackTrace(); |
| | | // ---- 解析收件人:优先手机号,其次部门 ---- |
| | | if (StringUtils.isNotEmpty(dingTalkReqVo.getNumber())) { |
| | | try { |
| | | DingTalkClient clientT = newClient("https://oapi.dingtalk.com/topapi/v2/user/getbymobile"); |
| | | OapiV2UserGetbymobileRequest req = new OapiV2UserGetbymobileRequest(); |
| | | req.setMobile(dingTalkReqVo.getNumber()); |
| | | OapiV2UserGetbymobileResponse rsp = clientT.execute(req, accessToken); |
| | | String body = rsp == null ? null : rsp.getBody(); |
| | | if (StringUtils.isEmpty(body)) { |
| | | log.error("钉钉按手机号查用户失败,mobile={},响应为空", dingTalkReqVo.getNumber()); |
| | | return false; |
| | | } |
| | | JSONObject resultObj = JSONObject.parseObject(body).getJSONObject("result"); |
| | | if (resultObj == null || resultObj.get("userid") == null) { |
| | | log.error("钉钉按手机号查用户失败,mobile={},响应={}", dingTalkReqVo.getNumber(), body); |
| | | return false; |
| | | } |
| | | userIdlist.add(resultObj.get("userid").toString()); |
| | | } catch (Exception e) { |
| | | log.error("钉钉按手机号查用户异常,mobile={}", dingTalkReqVo.getNumber(), e); |
| | | return false; |
| | | } |
| | | } else if (dingTalkReqVo.getDeptId() != null) { |
| | | try { |
| | | DingTalkClient client3 = newClient("https://oapi.dingtalk.com/topapi/user/listid"); |
| | | |
| | | OapiUserListidRequest request2 = new OapiUserListidRequest(); |
| | | request2.setDeptId(dingTalkReqVo.getDeptId()); // 设置要获取的部门 ID,这里以根部门为例675909059L |
| | | // 设置部门 ID,如果获取所有员工手机号,可以设置为根部门的 ID |
| | | OapiUserListidResponse response2 = client3.execute(request2, accessToken); |
| | | String body = response2 == null ? null : response2.getBody(); |
| | | if (StringUtils.isNotEmpty(body)) { |
| | | // 解析为JSONObject |
| | | JSONObject jsonObject = JSONObject.parseObject(body); |
| | | // 提取出JSONArray |
| | | JSONArray jsonArray = new JSONArray(jsonObject.getJSONObject("result").getJSONArray("userid_list")); |
| | | // 将JSONArray转为List列表 |
| | | String str = JSONObject.toJSONString(jsonArray); |
| | | userIdlist = JSONObject.parseObject(str, List.class); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("钉钉按部门查用户异常,deptId={}", dingTalkReqVo.getDeptId(), e); |
| | | return false; |
| | | } |
| | | } else { |
| | | DingTalkClient client3 = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/user/listid"); |
| | | // 既无手机号也无部门:原实现会走 listid(null) 被钉钉拒绝且异常被吞掉,表现为"静默发不出去" |
| | | log.warn("钉钉通知未提供收件人(手机号与部门均为空),跳过发送"); |
| | | return false; |
| | | } |
| | | |
| | | OapiUserListidRequest request2 = new OapiUserListidRequest(); |
| | | request2.setDeptId(dingTalkReqVo.getDeptId()); // 设置要获取的部门 ID,这里以根部门为例675909059L |
| | | // 设置部门 ID,如果获取所有员工手机号,可以设置为根部门的 ID |
| | | OapiUserListidResponse response2 = null; |
| | | try { |
| | | response2 = client3.execute(request2, accessToken); |
| | | } catch (ApiException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | body = response2.getBody(); |
| | | // 解析为JSONObject |
| | | JSONObject jsonObject = JSONObject.parseObject(body); |
| | | // 提取出JSONArray |
| | | JSONArray jsonArray = new JSONArray(jsonObject.getJSONObject("result").getJSONArray("userid_list")); |
| | | // 将JSONArray转为List列表 |
| | | String str = JSONObject.toJSONString(jsonArray); |
| | | userIdlist = JSONObject.parseObject(str, List.class); |
| | | if (CollectionUtils.isEmpty(userIdlist)) { |
| | | log.warn("钉钉通知未解析到任何收件人,跳过发送"); |
| | | return false; |
| | | } |
| | | |
| | | |
| | | //userid数组 |
| | | for (String urid : userIdlist) { |
| | | DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2"); |
| | | OapiMessageCorpconversationAsyncsendV2Request request = new OapiMessageCorpconversationAsyncsendV2Request(); |
| | | request.setUseridList(urid); |
| | | request.setAgentId(1779052814L); |
| | | request.setToAllUser(false); |
| | | |
| | | //发送内容处理 |
| | | List<ConcurrentHashMap<String, String>> contents = dingTalkReqVo.getContents(); |
| | | ArrayList<OapiMessageCorpconversationAsyncsendV2Request.Form> objects = new ArrayList<>(); |
| | | for (int i = 0; i < contents.size(); i++) { |
| | | ConcurrentHashMap<String, String> map = contents.get(i); |
| | | for (Map.Entry<String, String> entry : map.entrySet()) { |
| | | // ---- 组装消息体(所有收件人共用) ---- |
| | | // contents 中每行一个单键 map,用 List 顺序保证钉钉表单行序 |
| | | List<OapiMessageCorpconversationAsyncsendV2Request.Form> objects = new ArrayList<>(); |
| | | List<ConcurrentHashMap<String, Object>> contents = dingTalkReqVo.getContents(); |
| | | if (contents != null) { |
| | | for (ConcurrentHashMap<String, Object> map : contents) { |
| | | if (map == null) { |
| | | continue; |
| | | } |
| | | for (Map.Entry<String, Object> entry : map.entrySet()) { |
| | | Object value = entry.getValue(); |
| | | if (value == null) { |
| | | continue; |
| | | } |
| | | OapiMessageCorpconversationAsyncsendV2Request.Form formSeverityDesc = new OapiMessageCorpconversationAsyncsendV2Request.Form(); |
| | | formSeverityDesc.setKey(entry.getKey()); |
| | | formSeverityDesc.setValue(entry.getValue()); |
| | | if (entry.getKey().contains("审批时间") && value instanceof Date) { |
| | | // 24 小时制:原实现用 hh(12 小时制且无 AM/PM),下午的时间会显示成 01:xx |
| | | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); |
| | | formSeverityDesc.setValue(simpleDateFormat.format((Date) value)); |
| | | } else { |
| | | formSeverityDesc.setValue(value.toString()); |
| | | } |
| | | objects.add(formSeverityDesc); |
| | | } |
| | | } |
| | | } |
| | | |
| | | OapiMessageCorpconversationAsyncsendV2Request.Msg msg = new OapiMessageCorpconversationAsyncsendV2Request.Msg(); |
| | | msg.setOa(new OapiMessageCorpconversationAsyncsendV2Request.OA()); |
| | | msg.getOa().setMessageUrl(dingTalkReqVo.getUrl()); |
| | | msg.getOa().setPcMessageUrl(dingTalkReqVo.getUrl()); |
| | | msg.getOa().setHead(new OapiMessageCorpconversationAsyncsendV2Request.Head()); |
| | | // msg.getOa().getHead().setText("你在说啥呢绒顶替苛夺阿斯蒂芬阿斯蒂芬"); |
| | | msg.getOa().getHead().setBgcolor("00409eff"); |
| | | msg.getOa().setBody(new OapiMessageCorpconversationAsyncsendV2Request.Body()); |
| | | msg.getOa().getBody().setTitle(dingTalkReqVo.getTitle()); |
| | | // msg.getOa().getBody().setContent("待处理:" + "123" + "条"); |
| | | |
| | | msg.getOa().getBody().setForm(objects); |
| | | msg.setMsgtype("oa"); |
| | | request.setMsg(msg); |
| | | // log.info("获取发送通知消息体和获取发送通知人完成"); |
| | | // ---- 逐人发送;全部成功才算成功(原实现 result 写在循环内,只反映最后一个收件人) ---- |
| | | boolean allSuccess = true; |
| | | for (String urid : userIdlist) { |
| | | OapiMessageCorpconversationAsyncsendV2Response response = null; |
| | | try { |
| | | DingTalkClient client = newClient("https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2"); |
| | | OapiMessageCorpconversationAsyncsendV2Request request = new OapiMessageCorpconversationAsyncsendV2Request(); |
| | | request.setUseridList(urid); |
| | | request.setAgentId(agentId); |
| | | request.setToAllUser(false); |
| | | |
| | | OapiMessageCorpconversationAsyncsendV2Request.Msg msg = new OapiMessageCorpconversationAsyncsendV2Request.Msg(); |
| | | msg.setOa(new OapiMessageCorpconversationAsyncsendV2Request.OA()); |
| | | msg.getOa().setMessageUrl(dingTalkReqVo.getUrl()); |
| | | msg.getOa().setPcMessageUrl(dingTalkReqVo.getUrl()); |
| | | msg.getOa().setHead(new OapiMessageCorpconversationAsyncsendV2Request.Head()); |
| | | msg.getOa().getHead().setBgcolor("00409eff"); |
| | | msg.getOa().setBody(new OapiMessageCorpconversationAsyncsendV2Request.Body()); |
| | | msg.getOa().getBody().setTitle(dingTalkReqVo.getTitle()); |
| | | msg.getOa().getBody().setForm(objects); |
| | | msg.setMsgtype("oa"); |
| | | request.setMsg(msg); |
| | | |
| | | response = client.execute(request, accessToken); |
| | | } catch (ApiException e) { |
| | | e.printStackTrace(); |
| | | } catch (Exception e) { |
| | | log.error("钉钉消息发送异常,收件人={}", urid, e); |
| | | } |
| | | |
| | | // log.info("消息任务ID"+response.getTaskId()); |
| | | System.out.println(response.getTaskId()); |
| | | |
| | | result = response.isSuccess(); |
| | | if (response == null || !response.isSuccess()) { |
| | | allSuccess = false; |
| | | log.error("钉钉消息发送失败,收件人={},响应={}", urid, |
| | | response == null ? "null" : response.getBody()); |
| | | // 失败可能是 access_token 失效,清掉缓存让下次重新获取 |
| | | clearAccessToken(); |
| | | } else { |
| | | log.info("钉钉消息发送成功,收件人={},taskId={}", urid, response.getTaskId()); |
| | | } |
| | | } |
| | | return result; |
| | | return allSuccess; |
| | | } |
| | | |
| | | /** |
| | | * 免登陆接口 |
| | | * |
| | | * @param authCode |
| | | * @return |
| | | */ |
| | | public Map<String, Object> noLogin(String authCode) { |
| | | try { |
| | | String urlString = "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo"; |
| | | String params = "access_token=" + getAccessToken() + "&code=" + authCode; |
| | | String result = HttpUtils.sendPost(urlString, params); |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | Map<String, Object> map = objectMapper.readValue(result, Map.class); |
| | | return map; |
| | | } catch (Exception e) { |
| | | log.error(e.getMessage()); |
| | | new BaseException("登录出异常了,请联系管理员处理"); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public void deptidList(Long deptID, Integer begin) { |
| | | try { |
| | | //根据传过来的部门ID,去获取该部门下的用户信息 |
| | | if (begin != 1) DingUserInfoList(deptID); |
| | | //获取子部门的dept_id |
| | | DingTalkClient client = newClient("https://oapi.dingtalk.com/topapi/v2/department/listsub"); |
| | | OapiV2DepartmentListsubRequest req = new OapiV2DepartmentListsubRequest(); |
| | | req.setDeptId(deptID); |
| | | req.setLanguage("zh_CN"); |
| | | OapiV2DepartmentListsubResponse rsp = client.execute(req, getAccessToken()); |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | Map<String, Object> map = objectMapper.readValue(rsp.getBody(), Map.class); |
| | | if (ObjectUtils.isNotEmpty(map)) { |
| | | List<Map<String, Object>> dingDetpInfoList = (List<Map<String, Object>>) map.get("result"); |
| | | if (Integer.valueOf(map.get("errcode").toString()) == 0) { |
| | | for (int i = 0; i < dingDetpInfoList.size(); i++) { |
| | | Map<String, Object> map1 = dingDetpInfoList.get(i); |
| | | deptidList(Long.valueOf(map1.get("dept_id").toString()), 2); |
| | | } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error(e.getMessage()); |
| | | new BaseException("登录出异常了,请联系管理员处理"); |
| | | } |
| | | } |
| | | |
| | | public void DingUserInfoList(Long deptId) { |
| | | DingTalkClient client = null; |
| | | try { |
| | | client = newClient("https://oapi.dingtalk.com/topapi/v2/user/list?access_token=" + getAccessToken()); |
| | | OapiV2UserListRequest req = new OapiV2UserListRequest(); |
| | | req.setDeptId(deptId); |
| | | req.setCursor(0L); |
| | | req.setSize(100L); |
| | | req.setLanguage("zh_CN"); |
| | | OapiV2UserListResponse rsp = client.execute(req, ""); |
| | | //获取resp里的用户信息集合 |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | Map<String, Object> map = objectMapper.readValue(rsp.getBody(), Map.class); |
| | | Map<String, Object> mapResult = (Map<String, Object>) map.get("result"); |
| | | List<Map<String, Object>> userInfoMapList = (List<Map<String, Object>>) mapResult.get("list"); |
| | | if (!CollectionUtils.isEmpty(userInfoMapList)) { |
| | | //根据手机号,将userID写进对应的字段上 |
| | | for (Map<String, Object> objectMap : userInfoMapList) { |
| | | SysUser sysUser = new SysUser(); |
| | | sysUser.setPhonenumber(objectMap.get("mobile").toString()); |
| | | sysUser.setDingUserId(objectMap.get("userid").toString()); |
| | | sysUserMapper.updateUser(sysUser); |
| | | } |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 获取 access_token |
| | | * 获取 access_token(带缓存,失效自动重取) |
| | | */ |
| | | private String getAccessToken() throws ApiException { |
| | | DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken"); |
| | | OapiGettokenRequest request = new OapiGettokenRequest(); |
| | | //Appkey |
| | | request.setAppkey(dingAppid); |
| | | //Appsecret |
| | | request.setAppsecret(dingAppSecret); |
| | | /*请求方式*/ |
| | | request.setHttpMethod("GET"); |
| | | OapiGettokenResponse response = client.execute(request); |
| | | return response.getAccessToken(); |
| | | String token = cachedAccessToken; |
| | | if (StringUtils.isNotEmpty(token) && System.currentTimeMillis() < accessTokenExpireAt) { |
| | | return token; |
| | | } |
| | | synchronized (this) { |
| | | if (StringUtils.isNotEmpty(cachedAccessToken) |
| | | && System.currentTimeMillis() < accessTokenExpireAt) { |
| | | return cachedAccessToken; |
| | | } |
| | | DefaultDingTalkClient client = newClient("https://oapi.dingtalk.com/gettoken"); |
| | | OapiGettokenRequest request = new OapiGettokenRequest(); |
| | | //Appkey |
| | | request.setAppkey(dingAppid); |
| | | //Appsecret |
| | | request.setAppsecret(dingAppSecret); |
| | | /*请求方式*/ |
| | | request.setHttpMethod("GET"); |
| | | OapiGettokenResponse response = client.execute(request); |
| | | String newToken = response == null ? null : response.getAccessToken(); |
| | | if (StringUtils.isNotEmpty(newToken)) { |
| | | cachedAccessToken = newToken; |
| | | accessTokenExpireAt = System.currentTimeMillis() + ACCESS_TOKEN_TTL_MILLIS; |
| | | } |
| | | return newToken; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 清空 access_token 缓存。 |
| | | * 发送失败有可能是 token 失效(钉钉 errcode 40014/42001),清掉后下次发送会重新获取。 |
| | | */ |
| | | private void clearAccessToken() { |
| | | cachedAccessToken = null; |
| | | accessTokenExpireAt = 0L; |
| | | } |
| | | |
| | | } |