钉钉通讯录同步二开案例
一、背景
目前钉钉支持手机号或者邮箱同步钉钉通讯录,有些客户需要使用工号同步,或者客户使用了企业专属账号,苍穹当前版本产品接口不支持同步(v6.0.6版本以上支持),则需要现场二开解决。
二、方案
新增一个调度任务,在调度中调用钉钉的接口进行通讯录同步。
具体步骤
1. 获取token
根据配置好的应用信息获取token,方法如下:
请求方式:GET
请求地址:https://oapi.dingtalk.com/gettoken ?appkey=xxx&appsecret=xxx
请求参数:
appkey | String | 是 | dingeqqpkv3xxxx | 应用的唯一标识key。 |
appsecret | String | 是 | GT-lsu-taDAsTsxxxx | 应用的密钥。AppKey和AppSecret可在钉钉开发者后台 的应用详情页面获取。 |
2. 获取授权部门
在钉钉管理后台【权限管理】中需要配置授权部门,如果调用接口获取非授权部门的人员信息则会提示无权限,所以需要主动获取授权部门。
请求方式:GET
请求地址:https://oapi.dingtalk.com/auth/scopes ? access_token =xxxxx
请求参数:
access_token | String | 是 | 6ed1bxxx | 调用该接口的应用凭证。 |
3. 获取授权部门及其子部门
请求方式:GET
请求地址:
https://oapi.dingtalk.com/department/list? access_token =xxx& fetch_child =true&id=xxx
请求参数:
access_token | String | 是 | 6ed1bxxx | 调用服务端API的应用凭证。 |
lang | String | 否 | zh_CN | 通讯录语言,默认zh_CN。 |
fetch_child | Boolean | 否 | true | 是否递归部门的全部子部门。 |
id | String | 否 | 1 | 父部门ID。 如果不传,默认部门为根部门,根部门ID为1。 |
4. 获取部门下的人员信息
请求方式:POST
请求地址:https://oapi.dingtalk.com/topapi/v2/user/list ? access_token =xxxx
请求参数:
access_token | String | 是 | be3Fxxxx | 调用该接口的应用凭证。 |
Body参数:
dept_id | Number | 是 | 10 | 部门ID,可调用获取部门列表 获取,如果是根部门,该参数传1。 |
cursor | Number | 是 | 0 | 分页查询的游标,最开始传0,后续传返回参数中的next_cursor值。 |
size | Number | 是 | 10 | 分页大小。 |
order_field | String | 否 | modify_desc | 部门成员的排序规则,默认不传是按自定义排序(custom): entry_asc:代表按照进入部门的时间升序 entry_desc:代表按照进入部门的时间降序 modify_asc:代表按照部门信息修改时间升序 modify_desc:代表按照部门信息修改时间降序 custom:代表用户定义(未定义时按照拼音)排序 |
contain_access_limit | Boolean | 否 | false | 是否返回访问受限的员工: true:返回 false:不返回 |
language | String | 否 | zh_CN | 通讯录语言,取值。 |
5. 根据相关字段(如手机号或者邮箱)与苍穹(星瀚)人员信息进行匹配
6. 将钉钉人员信息保存在映射表中
参考代码:
package hnzb.sys.base.plugin.task;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import kd.bos.context.RequestContext;
import kd.bos.dataentity.utils.ObjectUtils;
import kd.bos.dd.service.DingDingServiceHelper;
import kd.bos.exception.KDException;
import kd.bos.lang.Lang;
import kd.bos.logging.Log;
import kd.bos.logging.LogFactory;
import kd.bos.org.utils.Consts;
import kd.bos.schedule.executor.AbstractTask;
import kd.bos.sec.user.task.SynUserTypeEnum;
import kd.bos.sec.user.utils.UserOperationUtils;
import kd.bos.util.HttpClientUtils;
import kd.bos.util.StringUtils;
import kd.sdk.plugin.Plugin;
import java.io.IOException;
import java.util.*;
/**
* @BelongsProject: hnzb-cosmic
* @BelongsPackage: hnzb.sys.base.plugin.task
* @Author:
* @CreateTime: 2023-12-21 17:37
* @Description: TODO 查询系统中指定日期修改的组织单元、人员、岗位,然后同步到主数据系统
* @Version: 1.0
* @PageNumber:
*/
public class SynDingUserToUserMappingTask extends AbstractTask {
private static Log logger = LogFactory.getLog(SynDingUserToUserMappingTask.class);
@Override
public void execute(RequestContext requestContext, Map<String, Object> map) throws KDException {
synDing();
}
/**
* 钉钉openid同步
*/
public static void synDing() {
String accessToken = DingDingServiceHelper.getAccess_token();
logger.info("authScope" + accessToken);
if (kd.bos.util.StringUtils.isEmpty(accessToken)) {
return;
}
Map<String, List<String>> authScope = getAuthScope(accessToken);
logger.info("authScope" + authScope);
//如果设置了权限范围 则只查询有权的组织及人员
if (isAuthScope(authScope)) {
List<String> authedUser = authScope.get("authed_user");
logger.info("authedUser" + authedUser);
int userSize = 0;
int deptSize = 0;
//授权人员
if (null != authedUser && !authedUser.isEmpty()) {
List<Map<String, String>> userList = getAuthScopeUsers(authedUser, accessToken);
logger.info("userList" + userList);
UserOperationUtils.saveIMMapping(userList, "mobile", Consts.PHONE, SynUserTypeEnum.DINGDING.getValue());
userSize = authedUser.size();
}
List<String> authedDept = authScope.get("authed_dept");
logger.info("authed_dept" + authedDept);
int deptCount = 0;
//授权部门
if (null != authedDept && !authedDept.isEmpty()) {
//获取子部门
List<String> allDept = getChildrenList(accessToken,authedDept);
authedDept.addAll(allDept);
deptSize = authedDept.size();
deptCount = saveUserMappingByDept(accessToken, authedDept, imTypeFieldName, sysFieldName);
logger.info("deptCount" + deptCount);
}
return;
}
List<String> deptList = DingDingServiceHelper.getDeptList(accessToken);
if (deptList == null || deptList.isEmpty()) {
return;
}
saveUserMappingByDept(accessToken, deptList);
}
private static List<String> getChildrenList(String accessToken, List<String> authedDept) {
List<String> allDept = new ArrayList<>(authedDept.size());
for (String deptId : authedDept) {
List<String> deptList = getDeptList(accessToken,deptId);
allDept.addAll(deptList);
}
return allDept;
}
private static int saveUserMappingByDept(String accessToken, List<String> deptList) {
int completedCount = 1;
for (String deptId : deptList) {
long offset = 0;
long size = 100;
while (true) {
List<Map<String, String>> userList = getDeptUserList(accessToken,
Long.parseLong(deptId), offset, size, null);
logger.error("saveUserMappingByDept**userList" + userList);
if (userList == null || userList.isEmpty()) {
break;
}
boolean mobile = UserOperationUtils.saveIMMapping(userList, "mobile", Consts.PHONE, SynUserTypeEnum.DINGDING.getValue());
logger.error("saveUserMappingByDept**mobile" + mobile);
if (userList.size() < 100) {
break;
}
offset += 100;
}
completedCount++;
}
return completedCount;
}
private static boolean isAuthScope(Map<String, List<String>> authScope) {
//授权范围为空
if (authScope.isEmpty()) {
logger.error("authScope is empty");
return false;
}
//授权范围为全部员工
List<String> authedDept = authScope.get("authed_dept");
if (null != authedDept && !authedDept.isEmpty()) {
String dept = authedDept.get(0);
if ("1".equals(dept)) {
logger.info("authed_dept : 1");
return false;
}
}
return true;
}
/**
* 获取钉钉应用的授权范围
*
* @param token
* @return
*/
public static Map<String, List<String>> getAuthScope(String token) {
String ddHost = "https://oapi.dingtalk.com";
StringBuilder url = new StringBuilder(ddHost).append("/auth/scopes?access_token=");
url.append(token);
try {
String data = HttpClientUtils.get(url.toString());
HashMap<String, Object> map = JSON.parseObject(data, HashMap.class);
//权限范围
Map<String, Object> authOrgScopes = (Map<String, Object>) map.get("auth_org_scopes");
//授权人员
JSONArray authed_user = (JSONArray) authOrgScopes.get("authed_user");
//授权部门
JSONArray authed_dept = (JSONArray) authOrgScopes.get("authed_dept");
HashMap<String, List<String>> dataMap = new HashMap<>(2);
if (authed_user != null) {
List<String> userList = authed_user.toJavaList(String.class);
dataMap.put("authed_user", userList);
}
if (authed_dept != null) {
List<String> deptList = authed_dept.toJavaList(String.class);
dataMap.put("authed_dept", deptList);
}
return dataMap;
} catch (Exception e) {
logger.error(e);
}
return Collections.emptyMap();
}
/**
* 根据授权范围获取人员信息
*
* @param authedUser 授权范围:授权人员
* @param accessToken
* @return
*/
public static List<Map<String, String>> getAuthScopeUsers(List<String> authedUser, String accessToken) {
List<Map<String, String>> userList = new ArrayList<>(16);
//1.获取授权人员信息
String ddHost = "https://oapi.dingtalk.com";
StringBuilder url = new StringBuilder(ddHost).append("/topapi/v2/user/get?access_token=");
url.append(accessToken);
HashMap<String, Object> params = new HashMap<>(2);
params.put("language", Lang.get().name());
try {
for (String userId : authedUser) {
params.put("userid", userId);
String data = HttpClientUtils.post(url.toString(), null, params);
Map<String, Object> map = JSON.parseObject(data, Map.class);
if ("0".equals(map.get("errcode").toString())) {
Map<String, String> result = (Map<String, String>) map.get("result");
//区号+手机号
String mobile = getMobileAndState(result);
result.put("mobile", mobile);
userList.add(result);
} else {
logger.error(map.get("errmsg").toString());
}
}
logger.info("222userList" + userList);
} catch (IOException e) {
logger.error(e);
}
return userList;
}
/**
* 带有区号的话根据区号拼接手机号
*
* @param result
* @return
*/
public static String getMobileAndState(Map<String, String> result) {
String state = getStateCode(result);
String mobile = result.get("mobile");
//86 默认不加
if (org.apache.commons.lang3.StringUtils.isBlank(state) || "86".equals(state)) {
return mobile;
}
return state + "-" + mobile;
}
private static String getStateCode(Map<String, String> result) {
if (StringUtils.isNotEmpty(result.get("state_code"))) {
return result.get("state_code");
}
if (StringUtils.isNotEmpty(result.get("stateCode"))) {
return result.get("stateCode");
}
if (StringUtils.isNotEmpty(result.get("statecode"))) {
return result.get("statecode");
}
return null;
}
public static List<String> getDeptList(String accesstoken, String deptId) {
if (null == accesstoken || accesstoken.trim().isEmpty()) {
logger.info("error ::: access_token is null !");
return null;
}
StringBuilder getDeptList_url = new StringBuilder("https://oapi.dingtalk.com").append("/department/list?access_token=");
getDeptList_url.append(accesstoken).append("&fetch_child=true");
if (org.apache.commons.lang3.StringUtils.isNotBlank(deptId)) {
getDeptList_url.append("&id=").append(deptId);
}
HashMap<String, Object> map = null;
try {
String data = HttpClientUtils.get(getDeptList_url.toString());
map = JSON.parseObject(data, HashMap.class);
logger.info("getUserId_url response: " + data);
} catch (Exception e) {
logger.error(e);
}
if (map == null)
return null;
else if ("0".equals(map.get("errcode").toString())) {
String department = map.get("department").toString();
List<Map<String, Object>> deptList = JSON.parseObject(department, List.class);
List<String> deptIds = new ArrayList<>(deptList.size());
for (Map<String, Object> dept : deptList) {
deptIds.add(dept.get("id").toString());
}
return deptIds;
} else {
logger.error(map.get("errmsg").toString());
return null;
}
}
public static List<Map<String, String>> getDeptUserList(String access_token, long department_id, long offset,
long size, String order) {
if (null == access_token || access_token.trim().isEmpty()) {
logger.info("error ::: access_token is null !");
return Collections.emptyList();
}
StringBuilder getDeptUserList_url = new StringBuilder("https://oapi.dingtalk.com").append("/topapi/v2/user/list?access_token=");
getDeptUserList_url.append(access_token);
Map<String, Object> body = new HashMap<>(4);
body.put("dept_id", department_id);
body.put("cursor", offset);
body.put("size", size);
HashMap<String, Object> map = null;
try {
String data = HttpClientUtils.post(getDeptUserList_url.toString(), null, body);
map = JSON.parseObject(data, HashMap.class);
} catch (Exception e) {
logger.error(e);
}
if (map == null) {
return Collections.emptyList();
}
if ("0".equals(map.get("errcode").toString())) {
List<Map<String, String>> userList = new ArrayList<>();
JSONObject result = (JSONObject) map.get("result");
if (ObjectUtils.isEmpty(result)) {
return Collections.emptyList();
}
JSONArray userListStr = (JSONArray) result.get("list");
userList = changeStateMobile(userListStr);
return userList;
}
logger.error(map.get("errmsg").toString());
return Collections.emptyList();
}
private static List<Map<String, String>> changeStateMobile(JSONArray userList) {
List<Map<String, String>> hashMaps = new ArrayList<>(userList.size());
for (Object o : userList) {
if (!ObjectUtils.isEmpty(o)) {
JSONObject jsonObject = (JSONObject) o;
Map hashMap = jsonObject.toJavaObject(Map.class);
String mobileAndState = getMobileAndState(hashMap);
hashMap.put("mobile", mobileAndState);
hashMaps.add(hashMap);
}
}
return hashMaps;
}
}
三、常见问题:
1. 海外手机号携带区号怎么同步?
钉钉将手机号字段与区号字段分开传递,需要代码手动将区号与手机号拼接在一起。如图:
2. 6.0版本之前不支持部分员工授权同步,如何二开?
主动获取授权部门进行同步,详情见二开步骤2,步骤3。
3. 企业(专属)账号怎么同步?
企业专属账号需要使用钉钉最新的接口获取人员信息。
4. 不使用手机号或者邮箱同步,使用其他字段,如工号进行同步,如何设置?
重写UserOperationUtils.saveIMMapping方法,将此方法中的手机号字段映射改成自己需要的字段进行映射
钉钉通讯录同步二开案例
本文2024-09-23 00:34:03发表“云苍穹知识”栏目。
本文链接:https://wenku.my7c.com/article/kingdee-cangqiong-140289.html