欲先攻其事,必先利其器,我们想要发布钉钉待办通知首先需要把我们公司的用户id和钉钉的userId做一个绑定,这个绑定的途径有很多种,我们可以用钉钉扫码、手机号获取通过部门信息等方式来拿到钉钉的userId。下面我们重点说一下钉钉扫码绑定这个途径。
钉钉扫码这个方式拿到userId相对于其他方式比较安全,不用担心例如手机号方式数据更改的问题,也不会像通过获取部门信息来拿userId有点冗余。
话不多说,我们直接上具体实现。官方文档 https://developers.dingtalk.com/document/app/scan-qr-code-to-login-3rdapp
配置回调域名
1)创建企业内部应用
详情请参考创建应用,应用创建后,在基础信息页面可以查看到应用的AppKey和AppSecret。
2)进入应用详情页,然后单击钉钉登录与分享,添加应用回调的URL,以http或https开头
权限管理
需要开通以下权限
1、扫码绑定钉钉
a、显示二维码页面
我们用了钉钉提供的扫码登录页面,方便简单。
https://oapi.dingtalk.com/connect/qrconnect?appid=AppKey
&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI
参数 | 是否必填 | 说明 |
---|---|---|
appid | 是 | 企业内部应用的AppKey。 |
redirect_uri | 是 | 重定向地址。必须与开发者后台设置的回调域名保持一致。如果是第一种方式需要urlencode编码 |
state | 是 | 用于防止重放攻击,开发者可以根据此信息来判断redirect_uri只能执行一次来避免重放攻击。 |
response_type | 是 | 固定为code。 |
scope | 是 | 固定为snsapi_login。 |
loginTmpCode | 是 | 通过js获取到的loginTmpCode。 |
在钉钉用户扫码登录并确认后,会302到你指定的redirect_uri,并向url参数中追加临时授权码code及state两个参数。
b、实现流程
1)通过code获取用户的userId
- 服务端通过临时授权码获取授权用户的个人信息
调用sns/getuserinfo_bycode接口获取授权用户的个人信息,详情请参考根据sns临时授权码获取用户信息。
通过临时授权码Code获取用户信息,临时授权码只能使用一次。
- 根据unionid获取userid
调用user/getbyunionid接口获取userid,详情请参考根据unionid获取用户信息。
根据unionid获取userid,需要创建企业内部应用(小程序或微应用),使用内部应用的Appkey和AppSecret调用接口access_token。
c、实现代码
public class SysUserInfosServiceImpl extends ServiceImpl<SysUserInfosMapper, SysUserInfosPO> implements ISysUserInfosService {
@Value("${ding.talk.app-key}")
private String appKey;
@Value("${ding.talk.app-secret}")
private String appSecret;
@Override
public RespVO<Object> sweepCodeBinding(SweepCodeBindByDingTalkDTO dto) throws ApiException {
// 获取access_token,注意正式代码要有异常流处理
String accessToken = getAccessToken();
// 通过临时授权码获取授权用户的个人信息
DefaultDingTalkClient client2 = new DefaultDingTalkClient("https://oapi.dingtalk.com/sns/getuserinfo_bycode");
OapiSnsGetuserinfoBycodeRequest reqByCodeRequest = new OapiSnsGetuserinfoBycodeRequest();
// 通过扫描二维码,跳转指定的redirect_uri后,向url中追加的code临时授权码
reqByCodeRequest.setTmpAuthCode(dto.getAuthCode());
OapiSnsGetuserinfoBycodeResponse byCodeResponse = client2.execute(reqByCodeRequest, appKey, appSecret);
try {
byCodeResponse.getUserInfo().getUnionid();
} catch (NullPointerException e) {
return RespVO.success(JsonUtil.getJsonByString(byCodeResponse.getBody()));
}
// 根据unionId获取userId
String unionId = byCodeResponse.getUserInfo().getUnionid();
DingTalkClient clientDingTalkClient = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/user/getbyunionid");
OapiUserGetbyunionidRequest reqGetByUnionIdRequest = new OapiUserGetbyunionidRequest();
reqGetByUnionIdRequest.setUnionid(unionId);
OapiUserGetbyunionidResponse oapiUserGetbyunionidResponse = clientDingTalkClient.execute(reqGetByUnionIdRequest, accessToken);
return RespVO.success(JsonUtil.getJsonByString(oapiUserGetbyunionidResponse.getBody()));
}
public String getAccessToken() throws ApiException {
DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
OapiGettokenRequest request = new OapiGettokenRequest();
request.setAppkey(appKey);
request.setAppsecret(appSecret);
/*请求方式*/
request.setHttpMethod("GET");
OapiGettokenResponse response = client.execute(request);
return response.getAccessToken();
}
}
2)内部调用绑定钉钉接口
调用示例
@Value("${ding_talk.srv-url}")
private String srvUrl;
@Value("${ding_talk.fe-url}")
private String feUrl;
@Override
public RespVO<Object> sweepCodeBinding(SweepCodeBindByDingTalkDTO dto, String jwtToken) {
//解析出accountId
String accountId = CookieUtil.getOperator(jwtToken).toString();
Map<String, Object> mapParam = new HashMap<>();
mapParam.put("auth_code", dto.getAuthCode());
//调用内部接口
String response = SendHttpsUtil.sendPostByMap(srvUrl + "v1/ding-talk/backlog/sweep-code-binding-by-ding-talk", mapParam);
JSONObject data;
try {
data = JSON.parseObject(response).getJSONObject("data");
} catch (NullPointerException e) {
return RespVO.success(RespVO.failure(ApiResultCode.G1102, "绑定地址请求失败"));
}
String userId;
if (data.get("errcode").equals(0)) {
userId = data.getJSONObject("result").get("userid").toString();
} else {
return RespVO.success(RespVO.failure(ApiResultCode.G1100, "绑定失败"));
}
//判断用户表是否已经记录该员工信息
List<SysUserInfoPO> userInfoList = iSysUserInfoService.list(new QueryWrapper<SysUserInfoPO>().eq("account_id", accountId));
SysUserInfoPO sysUserInfo = new SysUserInfoPO();
if (userInfoList.size() > 0 && StringUtils.isNotBlank(userId)) {
//如果已记录,则直接给userId字段传值(修改)
sysUserInfo.setDtUserId(userId);
sysUserInfo.setModifier(accountId);
iSysUserInfoService.getBaseMapper().update(sysUserInfo, new UpdateWrapper<SysUserInfoPO>().eq("account_id", accountId));
}
return RespVO.success("绑定成功");
}
工具类
json工具类
public class JsonUtil {
/**
* json字符串转map
*
* @param str 字符串
* @return map
*/
public static LinkedHashMap<String, Object> getJsonByString(String str) {
return JSON.parseObject(str, new TypeReference<>() {
}, Feature.OrderedField);
}
}
RespVO
package com.dingTalk.common.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.suntime.sntp.common.vo.ApiResultCode;
import com.suntime.sntp.common.vo.DefaultApiResultCode;
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.server.ServerWebExchange;
@ResponseBody
public class RespVO<T> {
private int code;
private String message;
private Long count;
@JsonInclude(Include.NON_NULL)
private T data;
@JsonIgnore
private ApiResultCode apiResultCode;
@JsonProperty("execute_time")
@JsonInclude(Include.NON_NULL)
private Long executeTime;
private RespVO(){}
private RespVO(ApiResultCode code, T data) {
this.code = code.getCode();
this.message = code.getMessage();
this.data = data;
this.apiResultCode = code;
this.count = null;
}
private RespVO(ApiResultCode code, T data, Long count) {
this.code = code.getCode();
this.message = code.getMessage();
this.data = data;
this.apiResultCode = code;
this.count = count;
}
private RespVO(ApiResultCode code, String msg, T data) {
this.code = code.getCode();
this.message = msg;
this.data = data;
this.apiResultCode = code;
this.count = null;
}
public static <E> RespVO<E> success() {
return new RespVO(DefaultApiResultCode.SUCCESS, (Object) null);
}
public static <E> RespVO<E> success(E data) {
return new RespVO(DefaultApiResultCode.SUCCESS, data);
}
public static <E> RespVO<E> success(E data, Long count) {
return new RespVO(DefaultApiResultCode.SUCCESS, data, count);
}
public static <E> RespVO<E> of(ApiResultCode respCode, String msg, E body) {
return new RespVO(respCode, msg, body);
}
public static <E> RespVO<E> of(ApiResultCode apiResultCode, String msg) {
return of(apiResultCode, msg, null);
}
public static <E> RespVO<E> failure(ApiResultCode apiResultCode, String msg) {
return new RespVO(apiResultCode, msg, (Object) null);
}
public static <E> RespVO<E> failure(ApiResultCode apiResultCode) {
return failure(apiResultCode, apiResultCode.getMessage());
}
public static <E> RespVO<E> serverError(String overrideDefaultMsg) {
return new RespVO(DefaultApiResultCode.G500, overrideDefaultMsg, (Object) null);
}
public static <E> RespVO<E> serverError() {
return new RespVO(DefaultApiResultCode.G500, (Object) null);
}
public static RespVO<Object> emptyResp(ServerWebExchange exchange) {
return exchange.getRequest().getMethod() == HttpMethod.GET ? failure(ApiResultCode.G1001) : success();
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public Long getCount() {
return count;
}
public T getData() {
return data;
}
@JsonIgnore
public ApiResultCode getApiResultCode() {
return apiResultCode;
}
public Long getExecuteTime() {
return executeTime;
}
public void setExecuteTime(Long executeTime) {
this.executeTime = executeTime;
}
@JsonIgnore
public boolean cacheable() {
return this.getCode() == ApiResultCode.SUCCESS.getCode() || this.getCode() == ApiResultCode.G1001.getCode();
}
}
SendHttpsUtil
public class SendHttpsUtil {
/**
* 发送POST请求,参数是Map, contentType=x-www-form-urlencoded
*
* @param url 发送请求的 URL
* @param mapParam 请求参数
* @return 所代表远程资源的响应结果
*/
public static String sendPostByMap(String url, Map<String, Object> mapParam) {
Map<String, String> headParam = new HashMap<>(128);
headParam.put("Content-type", "application/json;charset=UTF-8");
return sendPost(url, mapParam, headParam);
}
/**
* 向指定 URL 发送POST方法的请求
*
* @param url 发送请求的 URL
* @param param 请求参数
* @return 所代表远程资源的响应结果
*/
public static String sendPost(String url, Map<String, Object> param, Map<String, String> headParam) {
PrintWriter out = null;
BufferedReader in = null;
StringBuilder result = new StringBuilder();
try {
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
// 设置通用的请求属性 请求头
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent",
"Fiddler");
if (headParam != null) {
for (Entry<String, String> entry : headParam.entrySet()) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
}
// 发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
// 获取URLConnection对象对应的输出流
out = new PrintWriter(conn.getOutputStream());
// 发送请求参数
out.print(JSON.toJSONString(param));
// flush输出流的缓冲
out.flush();
// 定义BufferedReader输入流来读取URL的响应
in = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}
//使用finally块来关闭输出流、输入流
finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
return result.toString();
}
}
d、实现效果
2、发送待办通知
咱们绑定完钉钉后,就可以回归主题啦,发送审核待办消息。
a、接收参数
参数名称 | 参数说明 | 是否必须 | 数据类型 |
---|---|---|---|
biz_id | 业务id | false | string |
dtl | 待办任务的内容 | true | string |
title | 待办任务的标题 | true | string |
url | 待办任务的跳转链接 | true | string |
user_id | 钉钉用户id | true | string |
package com.dingTalk.v1.webApi.entity.dto;
import com.baomidou.mybatisplus.annotation.TableField;
import com.dingtalk.api.request.OapiWorkrecordAddRequest;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
/**
* @author lixuan
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value = "RemindAuditDTO对象", description = "提醒审核")
public class RemindAuditDTO implements Serializable {
private static final long serialVersionUID = 1L;
@NotNull(message = "钉钉用户id不能为空")
@ApiModelProperty(name = "user_id", value = "钉钉用户id")
@TableField("user_id")
@JsonProperty("user_id")
private String userId;
@NotNull(message = "待办任务的标题不能为空")
@ApiModelProperty(name = "title", value = "待办任务的标题")
@TableField("title")
@JsonProperty("title")
private String title;
@NotNull(message = "跳转URL不能为空")
@ApiModelProperty(name = "url", value = "待办任务的跳转链接")
@TableField("url")
@JsonProperty("url")
private String url;
@ApiModelProperty(name = "biz_id", value = "业务id")
@TableField("biz_id")
@JsonProperty("biz_id")
private String bizId;
@NotNull(message = "待办任务的内容不能为空")
@ApiModelProperty(name = "dtl", value = "待办任务的内容")
@TableField("dtl")
@JsonProperty("dtl")
private String dtl;
}
b、实现代码
@Override
public RespVO<Object> remindAudit(RemindAuditDTO dto) throws ApiException {
OapiWorkrecordAddRequest req = new OapiWorkrecordAddRequest();
//接收人ID
req.setUserid(dto.getUserId());
req.setCreateTime(System.currentTimeMillis());
req.setTitle(sdf.format(System.currentTimeMillis()) + " " + dto.getTitle());
//手机端打开页面
req.setUrl(dto.getUrl());
//PC端打开页面
req.setPcUrl(dto.getUrl());
//发送内容
List<OapiWorkrecordAddRequest.FormItemVo> list = new ArrayList<>();
OapiWorkrecordAddRequest.FormItemVo vo = new OapiWorkrecordAddRequest.FormItemVo();
vo.setTitle(dto.getTitle());
String content = "您有一个待办审核";
if (StringUtils.isNotBlank(Html2PlainTextUtil.convert(Markdown2HtmlUtil.convert(dto.getDtl())))) {
//将markdown格式转换为text格式
content = Html2PlainTextUtil.convert(Markdown2HtmlUtil.convert(dto.getDtl()));
}
vo.setContent(content);
list.add(vo);
req.setFormItemList(list);
//pc端打开方式,2 钉钉内打开,4 浏览器内打开
req.setPcOpenType(4L);
//流程业务id,避免多个业务冲突,根据发送人、接收人、业务id和业务来源确定唯一
String bizId = String.valueOf(System.currentTimeMillis());
if (StringUtils.isNotBlank(dto.getBizId())) {
bizId = dto.getBizId();
}
req.setBizId(bizId);
DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/workrecord/add");
OapiWorkrecordAddResponse rsp = client.execute(req, getAccessToken());
return RespVO.success(JsonUtil.getJsonByString(rsp.getBody()));
}
c、内部调用
@Override
public RespVO<Object> remindAudits(RemindAuditsDTO dto) {
//判断有没有绑定钉钉
String userId = getDingIdByAccountId(dto.getAccountId());
if (StringUtils.isBlank(getDingIdByAccountId(dto.getAccountId()))) {
return RespVO.success(RespVO.failure(ApiResultCode.G500, "j产品负责人未绑定钉钉,请联系产品负责人"));
}
String pcUrl = feUrl + "middle-office/personal-center";
//流程业务id,避免多个业务冲突,根据发送人、接收人、业务id和业务来源确定唯一
String bizId = getBizId(pcUrl, dto.getAccountId());
//用户名
String realName = getRealNameByAccountId(dto.getAccountId());
Map<String, Object> mapParam = new HashMap<>();
mapParam.put("user_id", userId);
mapParam.put("title", dto.getTitle());
mapParam.put("dtl", dto.getDtl());
mapParam.put("url", pcUrl);
mapParam.put("biz_id", bizId);
String response = SendHttpsUtil.sendPostByMap(srvUrl + "v1/ding-talk/backlog/remind-audit-common", mapParam);
JSONObject data = JSON.parseObject(response).getJSONObject("data");
if (data.get("errcode").equals(0)) {
if (StringUtils.isNotBlank(data.get("record_id").toString())) {
//如果之前该需求已发送过待办了,新发送同一个需求时,将之前的待办任务取消
GetApprovedDTO getApprovedDTO = new GetApprovedDTO();
getApprovedDTO.setAccountId(dto.getAccountId());
getApprovedDTO.setRecordId(dto.getRecordId());
getAppRoved(getApprovedDTO);
String recordId = data.get("record_id").toString();
SysDtTaskRecPO dtTaskRec = new SysDtTaskRecPO(recordId, dto.getAccountId(), userId, realName, pcUrl, dto.getTitle(), dto.getDtl(), 1);
iSysDtTaskRecService.save(dtTaskRec);
SysDevDemandPO sysDevDemand = new SysDevDemandPO();
sysDevDemand.setApvlRecordId(recordId);
iSysDevDemandService.getBaseMapper().update(sysDevDemand, new UpdateWrapper<SysDevDemandPO>().eq("demand_id", dto.getRecordId()));
return RespVO.success("发送成功");
}
}
return RespVO.success(RespVO.failure(ApiResultCode.G1100, "发送失败"));
}