背景介绍

企业和客户(B端)进行对接时,往往需要同步客户的组织和成员信息,现在企业大多使用的办公软件有三样:企业微信、钉钉、飞书。现需要对接企业微信,在客户授权企业三方应用时,同步组织和用户信息,方便企业使用

需求分析

企业微信官方提供了开发者平台,当然你首先得需要进入一个企业,才可以登录查看开发文档
image-20230314194056845

开发模式

简单区分方式:企业和三方比较多,企业自己对自己,三方是别人对自己
基于这种背景需求,选择三方开发
image-20230314194739388
进入通讯录开发,进入之前建议阅读下开发必读
了解基本概念及参数

开发前提

该需求不是从0开发
已经有的

  1. 创建应用
  2. 注册一个测试账号安装了服务商应用(我们的应用)
  3. 通过授权拿到了基本信息(如公司id,秘钥等)
  4. 有这些表示可以直接去请求接口链接

所需接口

其实只需要两个接口

  1. 获得部门列表
  2. 获得部门成员

详细步骤官方已经写的很清楚,也不照本宣科了,只说一些关键点和坑点

获得部门列表

  • id参数可以不传,或者传0来获取所有部门信息
    access_token可以通过官方提供的方式获取,如果根据步骤发现获取不到那就是被加密了,要联系当时搭这个应用的人要
  • 无法获得部门名称,用id代替名称了

获得部门成员

  • id是必传的
  • access_token同上
  • 无法获得用户名称,同样用id代替名称

问题分析

现在问题出现了,无法获取到部门名称和用户名称。这个如果不解决,那同步了个寂寞。所以这个问题是一定是要解决的

方法选择

官方提供了两个方法,第一个是前端实现,第二个是后端实现
image-20230315120320281
但我推荐使用第二个,原因有下

  • 打通这个需求是后端实现的,不要加别人的工作量
  • 就算前端实现也是显示解决了问题,存数据库的还是加密的,别的地方用不了,而且前端官方给的SDK还需要进行企业微信授权登录才可以使用
  • 同步也不只是去企微显示用,可能有多个地方进行了依赖或者显示

当然第一个比第二个简单,为啥?因为不用你做啊

所以实现重点就是如图的通讯录ID转译部分
image-20230314203424527

通讯录转译

image-20230314204108507

先看看官方说的,我当时是理解不了,需要用户那边点击下载把文件给你?

在看看转译的一个流程
image-20230314204509404

最终会拿到url来下载这个解密的文件,前提是用户登录?我当时就觉得这个很麻烦,为此还请教了专业人员

image-20230314204805893

我这边想实现的一个效果是当我们点击“同步”的时候完成一系列操作,如果还需要多余的操作就应该考虑是否可以实现全自动化,如果不能,那难点在哪,能不能去解决

实现过程

ps:利用postman走一遍流程,在用代码实现
代码实现要逐层递进,保证各个环节单元测试通过

上传需要转译的文件

provider_access_token参数与之前的access_token不是同一个token,官方也提供了获取的方法
会返回media_id供下一步使用

异步通讯录id转译

output_file_name参数可以不要
output_file_format参数可以不要
会返回job_id供下一步使用

获取异步任务结果

url获取大多没什么问题,但下载需要条件,否则如图
image-20230314212333764
下载条件:用户通过oauth2授权登录或者单点登录才能进行下载。如果不懂官方说的什么屁话,且听我一言
需要客户登录后台(管理员权限)进入应用管理的三方小程序里,在进入服务商后台进行授权
第二种,第三方小程序使用企微登录
image-20230316210202511
测试小技巧:可以到服务商可信域名所在的网站进行测试(打开F12,自定义href标签插入下载)

需要注意的是,无法通过referer指定可信域名的方式投机取巧,登录态才是最重要的
a26ef569-c6c9-409c-8753-1605f6dfcc88
这表明无法通过一站式的方式解决,需要用户下载文件给到这边进行解密,中转一下才可实现

代码实现

编程语言Java

代码参考
gitHub(WxJava

微信开发Java SDK包括微信的所有功能,非常强大,从0开发推荐,我不是所以没用

github(qywx-third-java)

企微第三方开发demo,一些方法实现可以参考

转译文件excel格式如下
image-20230315114352905
企业id:企业在这边的标识
id:这边数据库的主键
name:部门/用户名称
ps:因为最后这个文件还要拿回来用,每个字段都有其对应的作用

关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 转译的一个大概流程
* 为什么是异步?
* 1. 请求需要多个接口,时间长
* 2. 解密成功与否不应该影响主流程
* 3. 成功率不是很高,可能由于文件大未能及时获取结果导致失败
* 接口请求用到了restTemplate
* 注意拿结果可能需要轮询获取
* 这里我推荐使用
*/
@Async
public void transferInfo(String tenant) {
log.info("执行任务");
// 获取media_id
String mediaId = getMediaId(tenant);
// 获取job_id
String jobId = getJobId(mediaId);
// 获取url
String url = getUrl(jobId);
// 根据url读取文件
try {
readExcel(url);
} catch (ExcelCommonException e) {
log.error("转译失败,未经过oauth2授权登录或者单点登录或者不为可信域名,无法下载该文件", e);
} catch (Exception e) {
log.error("未知异常", e);
}
}

各方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
private String getMediaId(String tenant) {
List<WxExcel> excelList = getExcelList(tenant);
// 相对路径,例如:/Users/dali/develop/om-auth
String rootPath = System.getProperty("user.dir");
String filePath= rootPath + "/wechat" + System.currentTimeMillis() + ".xlsx";
EasyExcel.write(filePath, WxExcel.class).sheet("企业微信转译").doWrite(excelList);
// 获得服务商token
String providerAccessToken = workWechatHelper.getProviderAccessToken();
String url = String.format(workWechatConfig.getUploadUrl(), providerAccessToken);
MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
FileSystemResource resource = new FileSystemResource(new File(filePath));
params.add("media",resource);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(params, headers);
Map<String, String> post = RestTemplateHelper.post(url, entity);
// 将缓存的信息删除
FileUtil.del(filePath);
if (post != null) {
String code = post.get("errcode");
if (Objects.equals(code, workWechatConfig.getSuccessCode())) {
return post.get("media_id");
} else {
log.error("调用失败,失败码为:{}, 失败原因为:{}", code, post.get("errmsg"));
}
}
return null;
}

private String getJobId(String mediaId, String tenant) {
String providerToken = workWechatHelper.getProviderAccessToken();
String url = String.format(workWechatConfig.getJobUrl(), providerToken);

Map<String, Object> map = new HashMap<>(1 << 3);

String corpId = workWechatHelper.getConfig("appId", tenant);

if (corpId == null) {
log.error("调用失败,获取corpId失败");
return null;
}
map.put("auth_corpid", corpId);

JSONArray mediaList = new JSONArray();
mediaList.add(mediaId);
map.put("media_id_list",mediaList);

Map<String, String> post = RestTemplateHelper.post(url, map);
if (post != null) {
String code = post.get("errcode");
if (Objects.equals(code, workWechatConfig.getSuccessCode())) {
return post.get("jobid");
} else {
log.error("调用失败,失败码为:{}, 失败原因为:{}", code, post.get("errmsg"));
}
}
return null;
}