钉钉微应用开发

651 阅读5分钟

项目由来

这个项目是我本科毕设,为即将读研的实验室开发一个绩效管理系统,当时钉钉相关的资料比较少,文档还不是十分完善,开发时,还是遇到了不少问题。后来疫情发生了,钉钉就突然火了🤣

项目地址

后端:dingtalk-springboot
前端:dingtalk-vue

钉钉鉴权

这些资料网上应该有不少了,列出我的代码

前端鉴权
  • 前端鉴权源码:dingtalk.js
  • import dingtalk-jsapi 部分,是按需导入, 为了减小依赖包的大小
  • import authenticate 用于从后端获取签名信息 :
import {
  authenticate
} from "@/api/common"; // jsapi 鉴权时获取签名信息
import * as dd from "dingtalk-jsapi/entry/union"; // 按需应用,微应用部分
import requestAuthCode from "dingtalk-jsapi/api/runtime/permission/requestAuthCode"; // 登陆用临时授权码
import choose from "dingtalk-jsapi/api/biz/contact/choose"; // PC 通讯录选人

/**
 * 鉴权
 * @param url 前端当前的url
 */
function ddconfig(url) {
  return authenticate(url).then(res => {
    dd.config({
      agentId: res.data.agentId, // 必填,微应用ID
      corpId: res.data.corpId, //必填,企业ID
      timeStamp: res.data.timeStamp, // 必填,生成签名的时间戳
      nonceStr: res.data.nonceStr, // 必填,生成签名的随机串
      signature: res.data.signature, // 必填,签名
      type: 0,
      jsApiList: [
        "runtime.info",
        "biz.contact.choose",
        "device.notification.confirm",
        "device.notification.alert",
        "device.notification.prompt",
        "biz.ding.post",
        "biz.util.openLink"
      ] // 必填,需要使用的jsapi列表,注意:不要带dd。
    });
  });
}

// 通讯录选人
export function contactChoose(url, userids) {
  return new Promise((resolve, reject) => {
    ddconfig(url)
      .then(() => {
        choose({
          users: userids,
          multiple: true, //是否多选:true多选 false单选; 默认true
          corpId: process.env.VUE_APP_CORPID, //企业id
          max: 10 //人数限制,当multiple为true才生效,可选范围1-1500
        }).then(res => {
          res = JSON.parse(JSON.stringify(res).replace(/emplId/g, "userid"));
          resolve(res);
        });
      })
      .catch(err => {
        reject(err);
      });
  });
}

// 获取登陆用临时授权码
export function getAuthCode(corpId) {
  return requestAuthCode({
    corpId: corpId
  });
}
后端鉴权
package com.softeng.dingtalk.component;

import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.*;
import com.dingtalk.api.response.*;
import com.taobao.api.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author zhanyeye
 * @description DingTalk 服务端API 工具组件
 * @date 11/13/2019
 */
@Slf4j
@Component
public class DingTalkUtils {
    private static String CORPID;
    private static String APP_KEY;
    private static String APP_SECRET;
    private static String CHAT_ID;
    private static String AGENTID;
    private static String DOMAIN;

    @Value("${my.corpid}")
    public void setCORPID(String corpid) {
        CORPID = corpid;
    }

    @Value("${my.app_key}")
    public void setAppKey(String appKey) {
        APP_KEY = appKey;
    }

    @Value("${my.app_secret}")
    public void setAppSecret(String appSecret) {
        APP_SECRET = appSecret;
    }

    @Value("${my.chat_id}")
    public void setChatId(String chatId) {
        CHAT_ID = chatId;
    }

    @Value("${my.agent_id}")
    public void setAGENTID(String agentid) {
        AGENTID = agentid;
    }
    @Value("${my.domain}")
    public void setDOMAIN(String domain) {
        DOMAIN = domain;
    }

    /**
     * 缓存时间 1小时 50分钟
     */
    private static final long cacheTime = 1000 * 60 * 55 * 2;
    /**
     * 缓存的accessToken: 不可直接调用,以防过期
     */
    private static String accessToken;
    /**
     * 缓存时间
     */
    private static long tokenTime;
    /**
     * 缓存的accessToken jsapi_ticket: 不可直接调用,以防过期
     */
    private static String jsapiTicket;
    /**
     * 缓存时间
     */
    private static long ticketTime;


    /**
     * 获取 AccessToken
     * @return java.lang.String
     * @Date 9:10 PM 11/13/2019
     **/
    public String getAccessToken() {
        long curTime = System.currentTimeMillis();
        if (accessToken == null || curTime - tokenTime >= cacheTime ) {
            DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
            OapiGettokenRequest request = new OapiGettokenRequest();
            request.setAppkey(APP_KEY);
            request.setAppsecret(APP_SECRET);
            request.setHttpMethod("GET");
            try {
                OapiGettokenResponse response = client.execute(request);
                accessToken = response.getAccessToken();
                tokenTime = System.currentTimeMillis();
            } catch (ApiException e) {
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "DingtalkUtils 获取accesstoken失败");
            }
            log.debug("AccessToken 快要过期,重新获取");
        }
        return accessToken;
    }


    /**
     * 获取 Jsapi Ticket
     * @return java.lang.String
     * @Date 8:20 AM 2/23/2020
     **/
    public String getJsapiTicket()  {
        long curTime = System.currentTimeMillis();
        if (jsapiTicket == null || curTime - ticketTime >= cacheTime) {
            DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/get_jsapi_ticket");
            OapiGetJsapiTicketRequest req = new OapiGetJsapiTicketRequest();
            req.setTopHttpMethod("GET");
            try {
                OapiGetJsapiTicketResponse response = client.execute(req, getAccessToken());
                jsapiTicket = response.getTicket();
                ticketTime = System.currentTimeMillis();
            } catch (ApiException e) {
                throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "DingtalkUtils 获取JsapiTicket失败");
            }
            log.debug("JsapiTicket 快要过期,重新获取");
        }
        return jsapiTicket;
    }


    /**
     * 获得userid : 通过 access_token 和 requestAuthcode;在内部调用了getAccessToken(),不用传参
     * @param requestAuthCode
     * @return java.lang.String
     * @Date 5:07 PM 1/13/2020
     **/
    public String getUserId(String requestAuthCode) {
        String userId = null;
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/getuserinfo");
        OapiUserGetuserinfoRequest request = new OapiUserGetuserinfoRequest();
        request.setCode(requestAuthCode);
        request.setHttpMethod("GET");
        try {
            OapiUserGetuserinfoResponse response = client.execute(request, getAccessToken());
            userId = response.getUserid();
        } catch (ApiException e) {
            e.printStackTrace();
        }
        return userId;
    }


    /**
     * 根据 userid 获取用户详细信息
     * @param userid
     * @return
     */
    public OapiUserGetResponse fetchUserDetail(String userid) {
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/get");
        OapiUserGetRequest request = new OapiUserGetRequest();
        request.setUserid(userid);
        request.setHttpMethod("GET");
        OapiUserGetResponse response;
        try {
            response = client.execute(request, getAccessToken());
        } catch (ApiException e) {
            log.error("getUserDetail fail", e);
            throw new RuntimeException();
        }
        return response;
    }


    /**
     * 获取周报信息
     * @param userid
     * @param date
     * @return
     */
    public Map getReport(String userid, LocalDate date) {
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/report/list");
        OapiReportListRequest request = new OapiReportListRequest();
        request.setUserid(userid);
        Long startTime = LocalDateTime.of(date, LocalTime.of(8,0)).toInstant(ZoneOffset.of("+8")).toEpochMilli();
        //开始时间
        request.setStartTime(startTime);
        //结束时间
        request.setEndTime(startTime + TimeUnit.DAYS.toMillis(5));
        request.setCursor(0L);
        request.setSize(1L);
        OapiReportListResponse response;
        try {
            response = client.execute(request, getAccessToken());
        } catch (ApiException e) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "获取周报失败");
        }
        if (response.getResult().getDataList().size() == 0) {
            // 无数据
            return Map.of();
        } else {
            List<OapiReportListResponse.JsonObject> contents = response.getResult().getDataList().get(0).getContents().stream()
                    .filter((item) -> !item.getValue().isEmpty())
                    .collect(Collectors.toList());
            return Map.of("contents", contents);
        }
    }


    /**
     * 获取部门id
     * @return
     */
    public List<String> listDepid() {
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/department/list");
        OapiDepartmentListRequest request = new OapiDepartmentListRequest();
        request.setHttpMethod("GET");
        OapiDepartmentListResponse response;
        try {
            response = client.execute(request, getAccessToken());
        } catch (ApiException e) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "获取部门Id 失败");
        }
        return response.getDepartment().stream().map(x -> String.valueOf(x.getId())).collect(Collectors.toList());
    }


    /**
     * 查询所有部门信息
     * @return
     */
    public List<OapiDepartmentListResponse.Department> fetchDeptInfo() {
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/department/list");
        OapiDepartmentListRequest request = new OapiDepartmentListRequest();
        request.setHttpMethod("GET");
        OapiDepartmentListResponse response;
        try {
            response = client.execute(request, getAccessToken());
        } catch (ApiException e) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "获取部门Id 失败");
        }
        return response.getDepartment();
    }


    /**
     * 获取整个部门的userid
     * @param depid
     * @return
     */
    public List<String> listUserId(String depid) {
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/getDeptMember");
        OapiUserGetDeptMemberRequest req = new OapiUserGetDeptMemberRequest();
        req.setDeptId(depid);
        req.setHttpMethod("GET");
        OapiUserGetDeptMemberResponse response;
        try {
            response = client.execute(req, getAccessToken());
        } catch (ApiException e) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "获取getUserIds失败");
        }

        return response.getUserIds();
    }

    // 生成钉钉内部跳转链接

    /**
     * 生成跳转到投票页面的钉钉链接
     * @param isInternal 是否是内部论文评审投票
     * @param pid 对应的论文id
     * @return
     */
    private String createDingTalkLink(boolean isExternal, int pid) {
        StringBuffer curl = null;
        if (isExternal) {
            // 外部论文评审的链接
            curl = new StringBuffer().append("dingtalk://dingtalkclient/action/openapp?corpid=").append(CORPID)
                    .append("&container_type=work_platform&app_id=0_").append(AGENTID).append("&redirect_type=jump&redirect_url=")
                    .append(DOMAIN).append("/paper/ex-detail/").append(pid).append("/vote");
        } else {
            // 内部论文评审的链接
            curl = new StringBuffer().append("dingtalk://dingtalkclient/action/openapp?corpid=").append(CORPID)
                    .append("&container_type=work_platform&app_id=0_").append(AGENTID).append("&redirect_type=jump&redirect_url=")
                    .append(DOMAIN).append("/paper/in-detail/").append(pid).append("/vote");
        }
        log.debug(curl.toString());
        return curl.toString();
    }

    /**
     * 发起投票时向群中发送消息
     * @param pid
     * @param title
     * @param endtime
     * @param namelist
     */
    public void sendVoteMsg(int pid, boolean isExternal, String title, String endtime, List<String> namelist) {
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/chat/send");
        OapiChatSendRequest request = new OapiChatSendRequest();
        request.setChatid(CHAT_ID);
        OapiChatSendRequest.ActionCard actionCard = new OapiChatSendRequest.ActionCard();

        if (!isExternal) {
            // 如果是内部评审投票
            StringBuffer content = new StringBuffer().append(" #### 投票 \n ##### 论文: ").append(title).append(" \n ##### 作者: ");
            for (String name : namelist) {
                content.append(name).append(", ");
            }
            content.append(" \n 截止时间: ").append(endtime);
            actionCard.setTitle("内部评审投票");
            actionCard.setMarkdown(content.toString());
        } else {
            // 如果是外部评审投票
            StringBuffer content = new StringBuffer().append(" #### 投票 \n ##### 论文: ").append(title);
            content.append(" \n 截止时间: ").append(endtime);
            actionCard.setTitle("外部评审投票");
            actionCard.setMarkdown(content.toString());

        }

        actionCard.setSingleTitle("前往投票");
        actionCard.setSingleUrl(createDingTalkLink(isExternal, pid));

        request.setActionCard(actionCard);
        request.setMsgtype("action_card");

        try {
            OapiChatSendResponse response = client.execute(request, getAccessToken());
        } catch (ApiException e) {
            e.printStackTrace();
        }

    }


    /**
     * 发送投票结果
     * @param pid
     * @param title
     * @param result
     * @param accept
     * @param total
     */
    public void sendVoteResult(int pid, String title, boolean result, int accept, int total, boolean isExternal) {
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/chat/send");
        OapiChatSendRequest request = new OapiChatSendRequest();
        request.setChatid(CHAT_ID);
        OapiChatSendRequest.ActionCard actionCard = new OapiChatSendRequest.ActionCard();

        StringBuffer content = new StringBuffer().append(" #### 投票结果 \n ##### 论文: ").append(title)
                .append(" \n 最终结果: ").append(result ? "Accept" : "reject")
                .append("  \n  Accept: ").append(accept).append(" 票  \n ")
                .append("Reject: ").append(total-accept).append(" 票  \n ")
                .append("已参与人数: ").append(total).append("人  \n ");


        actionCard.setTitle("投票结果");
        actionCard.setMarkdown(content.toString());
        actionCard.setSingleTitle("查看详情");
        actionCard.setSingleUrl(createDingTalkLink(isExternal, pid));


        request.setActionCard(actionCard);
        request.setMsgtype("action_card");

        try {
            OapiChatSendResponse response = client.execute(request, getAccessToken());

            log.debug(response.getBody());
        } catch (ApiException e) {
            e.printStackTrace();
        }
    }


    /**
     * 字节数组转化成十六进制字符串
     * @param hash
     * @return
     */
    private String bytesToHex(final byte[] hash) {
        Formatter formatter = new Formatter();
        for (byte b : hash) {
            formatter.format("%02x", b);
        }
        String result = formatter.toString();
        formatter.close();
        return result;
    }


    /**
     * 计算鉴权 signature
     * @param ticket
     * @param nonceStr
     * @param timeStamp
     * @param url
     * @return
     */
    private String sign(String ticket, String nonceStr, long timeStamp, String url)  {
        String plain = "jsapi_ticket=" + ticket + "&noncestr=" + nonceStr + "&timestamp=" + String.valueOf(timeStamp)
                + "&url=" + url;
        try {
            MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
            sha1.reset();
            sha1.update(plain.getBytes("UTF-8"));
            return bytesToHex(sha1.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 返回鉴权结果
     * @param url
     * @return
     */
    public Map authentication(String url) {
        long timeStamp = System.currentTimeMillis();
        String nonceStr = "todowhatliesclearathand";
        String signature = sign(getJsapiTicket(),nonceStr, timeStamp, url);
        return Map.of("agentId", AGENTID,"url", url, "nonceStr", nonceStr, "timeStamp", timeStamp, "corpId", CORPID, "signature", signature);
    }
}