sinake
2 天以前 e24ab2fc4d092b4599ef44ddce5ebdbbc0bd8f9a
ruoyi-project/src/main/java/com/ruoyi/project/service/impl/DingTalkServiceImpl.java
@@ -2,22 +2,25 @@
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.Data;
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;
@@ -35,7 +38,7 @@
 * @date 2023-05-23
 */
@Service
@Log4j
@Slf4j
public class DingTalkServiceImpl implements DingTalkService {
@@ -48,132 +51,316 @@
    @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);
            if (jsonObject != null) {
                // 提取出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(agentId);
            request.setToAllUser(false);
            //发送内容处理
            List<ConcurrentHashMap<String, Object>> contents = dingTalkReqVo.getContents();
            ArrayList<OapiMessageCorpconversationAsyncsendV2Request.Form> objects = new ArrayList<>();
            for (int i = 0; i < contents.size(); i++) {
                ConcurrentHashMap<String, Object> map = contents.get(i);
        // ---- 组装消息体(所有收件人共用) ----
        // 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());
                    if (entry.getKey().contains("审批时间")) {
                        Date date = (Date) entry.getValue();
                        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
                        String format = simpleDateFormat.format(date);
                        formSeverityDesc.setValue(format);
                    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 {
                        if (entry.getValue() != null) {
                            formSeverityDesc.setValue(entry.getValue().toString());
                        }
                        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.getErrMsg();
            } 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;
    }
}