0%

企业微信通讯录变更时同步到本地系统

记录操作日志

企业微信通讯录变更时同步到本地系统

企业微信API文档

第三方微信sdk: com.github.binarywang

1.申请企业微信企业测试账号

2.根据企业微信文档写回调接口,配置回调接口属性

从企业微信后台获取配置属性

(1) 超级管理员登录企业微信后台

企业微信后台地址:

https://work.weixin.qq.com/wework_admin/loginpage_wx?etype=expired#apps/contactsApi

(2) 获取参数

img

img

img

回调接口

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
59
60
61
62
63
64
65
66
@Api(tags = "企业微信-回调接口服务")
@RestController
@RequestMapping("/callback/{agentId}")
public class WxCallBackController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());

@GetMapping(produces = "text/plain;charset=utf-8")
public String authGet(@PathVariable Integer agentId,
@RequestParam(name = "msg_signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr) {
this.logger.info("\n接收到来自微信服务器的认证消息:signature = [{}], timestamp = [{}], nonce = [{}], echostr = [{}]",
signature, timestamp, nonce, echostr);

if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}

final WxCpService wxCpService = WxCpConfiguration.getCpService(agentId);
if (wxCpService == null) {
throw new IllegalArgumentException(String.format("未找到对应agentId=[%d]的配置", agentId));
}

if (wxCpService.checkSignature(signature, timestamp, nonce, echostr)) {
return new WxCpCryptUtil(wxCpService.getWxCpConfigStorage()).decrypt(echostr);
}

return "非法请求";
}

@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(@PathVariable Integer agentId,
@RequestBody String requestBody,
@RequestParam("msg_signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce) {
this.logger.info("\n接收微信请求:[signature=[{}], timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
signature, timestamp, nonce, requestBody);

final WxCpService wxCpService = WxCpConfiguration.getCpService(agentId);
WxCpXmlMessage inMessage = WxCpXmlMessage.fromEncryptedXml(requestBody, wxCpService.getWxCpConfigStorage(),
timestamp, nonce, signature);
this.logger.debug("\n消息解密内容为:\n{} ", JSONObject.toJSONString(inMessage));
WxCpXmlOutMessage outMessage = this.route(agentId, inMessage);
if (outMessage == null) {
return "";
}

String out = outMessage.toEncryptedXml(wxCpService.getWxCpConfigStorage());
this.logger.debug("\n回复信息:{}", out);
return out;
}

private WxCpXmlOutMessage route(Integer agentId, WxCpXmlMessage message) {
try {
return WxCpConfiguration.getRouters().get(agentId).route(message);
} catch (Exception e) {
this.logger.error(e.getMessage(), e);
}

return null;
}


}

属性配置

corpId,agentId,secret,token,aesKey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 企业微信帐号
wx:
cp:
application:
- corpId: ww234234223429bd7 # han申请的企业微信ID
appConfigs:
- agentId: 60023001 # 虚拟agentId 为注入配置使用
secret: q4asdfqedRasdfadfasdfasdfb5R93dRZasdfaXfkEk # 通讯录secret
token: uAasdfasdfasdf8u # 与企业微信回调一致
aesKey: AcYasdfasdfasdfasdfasdfasdfasd234234lAa # 与企业微信回调一致
- corpId: ww123123131213219d4 # 开发人员申请的企业微信ID
appConfigs:
- agentId: 40123002 # 虚拟agentId 为注入配置使用
secret: bSIyH1234123123123fsd1213123H1U
token:
aesKey:
- agentId: 62322303 # 虚拟agentId 为注入配置使用
secret: ix12312123123123123123b-NNc
token:
aesKey:

属性读取

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
@Data
@ConfigurationProperties(prefix = "wx.cp")
public class EnterpriseWechatApplication {

private List<WxCpProperties> application;

}

@Getter
@Setter
public class WxCpProperties {
/**
* 设置企业微信的corpId
*/
private String corpId;

private List<AppConfig> appConfigs;

@Getter
@Setter
public static class AppConfig {
/**
* 设置企业微信应用的AgentId
*/
private Integer agentId;

/**
* 设置企业微信应用的Secret
*/
private String secret;

/**
* 设置企业微信应用的token
*/
private String token;

/**
* 设置企业微信应用的EncodingAESKey
*/
private String aesKey;

}

@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}

@Configuration
@EnableConfigurationProperties(EnterpriseWechatApplication.class)
public class WxCpConfiguration {
private LogHandler logHandler;
private NullHandler nullHandler;
private LocationHandler locationHandler;
private MenuHandler menuHandler;
private MsgHandler msgHandler;
private UnsubscribeHandler unsubscribeHandler;
private SubscribeHandler subscribeHandler;
private ContactChangeHandler contactChangeHandler;
private EnterAgentHandler enterAgentHandler;

// private WxCpProperties properties;

private EnterpriseWechatApplication enterpriseWechatApplication;

private static Map<Integer, WxCpMessageRouter> routers = Maps.newHashMap();
private static Map<Integer, WxCpService> cpServices = Maps.newHashMap();

@Autowired
private RedissonClient redissonClient;

@Autowired
public WxCpConfiguration(LogHandler logHandler, NullHandler nullHandler, LocationHandler locationHandler,
MenuHandler menuHandler, MsgHandler msgHandler, UnsubscribeHandler unsubscribeHandler,
SubscribeHandler subscribeHandler, ContactChangeHandler contactChangeHandler,EnterAgentHandler enterAgentHandler
// , WxCpProperties properties
, EnterpriseWechatApplication enterpriseWechatApplication
) {
this.logHandler = logHandler;
this.nullHandler = nullHandler;
this.locationHandler = locationHandler;
this.menuHandler = menuHandler;
this.msgHandler = msgHandler;
this.unsubscribeHandler = unsubscribeHandler;
this.subscribeHandler = subscribeHandler;
// this.properties = properties;
this.enterpriseWechatApplication = enterpriseWechatApplication;
this.contactChangeHandler = contactChangeHandler;
this.enterAgentHandler = enterAgentHandler;
}


public static Map<Integer, WxCpMessageRouter> getRouters() {
return routers;
}

public static WxCpService getCpService(Integer agentId) {
return cpServices.get(agentId);
}


@PostConstruct // 初始化加载,加载Autowire之后
public void initServices() {
this.enterpriseWechatApplication.getApplication().forEach(wxCpProperties ->
{
Map<Integer, WxCpServiceImpl> wxCpServiceMap = wxCpProperties.getAppConfigs().stream().map(a -> {
WxCpRedissonConfigImpl configStorage = new WxCpRedissonConfigImpl(redissonClient);
configStorage.setCorpId(wxCpProperties.getCorpId());
configStorage.setAgentId(a.getAgentId());
configStorage.setCorpSecret(a.getSecret());
configStorage.setToken(a.getToken());
configStorage.setAesKey(a.getAesKey());

val service = new WxCpServiceImpl();
service.setWxCpConfigStorage(configStorage);
routers.put(a.getAgentId(), this.newRouter(service));
return service;
}).collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a));

cpServices.putAll(wxCpServiceMap);
}
);

}

private WxCpMessageRouter newRouter(WxCpService wxCpService) {
final val newRouter = new WxCpMessageRouter(wxCpService);

// 记录所有事件的日志 (异步执行)
newRouter.rule().handler(this.logHandler).next();
// 通讯录变更事件
newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT)
.event(WxCpConsts.EventType.CHANGE_CONTACT).handler(this.contactChangeHandler).end();
// 默认
newRouter.rule().async(false).handler(this.msgHandler).end();

return newRouter;
}
}

3.gatway服务配置企业微信回调地址token过滤白名单

bootstrap.yml

1
2
3
4
secure:
ignore:
urls: #配置白名单路径
- "/wx/callback/**"

4.使用企业微信回调接口工具测试

image-20210630114634938

5.企业微信管理后台设置通讯录同步-接收事件服务器

参考:https://open.work.weixin.qq.com/api/doc/90000/90135/90966

6.代码中获取通讯录变更事件,写变更时的处理逻辑

注意:失败重试+持久化需要同步的数据

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
@Component
@Slf4j
public class ContactChangeHandler extends AbstractHandler {

@Autowired
private MdmService mdmService;

@Override
public WxCpXmlOutMessage handle(WxCpXmlMessage wxMessage, Map<String, Object> context, WxCpService cpService,
WxSessionManager sessionManager) {
String content = "收到通讯录变更事件,内容:" + JSONObject.toJSONString(wxMessage);
this.logger.info(content);

// 企业微信修改手机号、邮箱后调用wanma和crm接口,同步数据
if (WxCpConsts.EventType.CHANGE_CONTACT.equals(wxMessage.getEvent())) {// 通讯录变更事件
if (WxCpConsts.ContactChangeType.UPDATE_USER.equals(wxMessage.getChangeType())) {// 从通讯录变更-更新成员
log.info("企业微信更新成员消息:" + wxMessage.toString());
// 通过微信userId查询变更人信息
Result<MdmUserInfoDTO> mdmUserInfoDTOResult = mdmService.queryUserDetailByWxUserId(wxMessage.getUserId());
if (mdmUserInfoDTOResult.getCode() != 0) {
log.error("通过wxUserId获取用户信息失败:", wxMessage.toString());
}
MdmUserInfoDTO oldUser = mdmUserInfoDTOResult.getData();
log.info("企业微信通讯录变更前用户信息:" + mdmUserInfoDTOResult.getData().toString());
MdmUserInfoDTO newUser = new MdmUserInfoDTO();
newUser.setUserId(oldUser.getUserId());
newUser.setWxUserId(wxMessage.getUserId());// 变更信息的成员UserID
newUser.setGender("1".equals(wxMessage.getGender()) ? "1" : "0");// 企业微信的性别 1男2女
newUser.setUserEmail(StringUtils.isNotBlank(wxMessage.getEmail()) ? AESUtil.encryptHex(wxMessage.getEmail()) : null);// 变更信息的邮箱 加密
newUser.setUserMobile(StringUtils.isNotBlank(wxMessage.getMobile()) ? AESUtil.encryptHex(wxMessage.getMobile()) : null);// 变更信息的手机号 加密
newUser.setUserName(wxMessage.getName());// 姓名

// 调用本地系统同步接口
Result result = mdmService.updateUser(newUser);

if (result.getCode() != 0) {
log.error("企业微信通讯录更新同步本地系统失败,同步数据:"+newUser);
}
//调用crm同步接口
Result result1 = mdmService.userDataSync(oldUser.getUserId());
if (result1.getCode() != 0) {
log.error("企业微信通讯录更新同步crm失败,同步userId:"+oldUser.getUserId());
}
}
}

return new TextBuilder().build(content, wxMessage, cpService);
}

}

7.IP白名单配置定时更新任务,更新定时变化的企业微信IP

7.1quartz设置定时任务

任务job

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
public class WxIpWhitelistJob extends QuartzJobBean {

@Autowired
private WxcpService wxcpService;//wx服务

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
log.info("拉取企业微信IP并更新白名单任务执行开始: ==> ... ");
wxcpService.ipWhitelist();
log.info("拉取企业微信IP并更新白名单任务执行开始: ==> end ");
}

}

启动quartz服务时,将job和触发器加入scheduler(任务调度)

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@Component
public class SchedulerInitListener implements CommandLineRunner {// 启动监听并执行run

@Override
public void run(String... args) throws SchedulerException {
List<QuartzJob> quartzJobs = quartzJobMapper.selectByExample(QuartzJobExample.newAndCreateCriteria()
.andStatusEqualTo(true).andIsDelEqualTo(false).example());// 从库汇中查询定时任务
if (!Optional.ofNullable(quartzJobs).isPresent() || quartzJobs.size() <= 0) {
return;
}
for (QuartzJob quartzJob : quartzJobs) {
try {
quartzManager.addQuartzJob(quartzJob);// 将job和触发器加入scheduler(任务调度)
} catch (Exception e) {
log.info("定时任务启动失败, jobName = {}, 异常信息: {}", quartzJob.getJobName(), e.getMessage());
quartzJob.setStatus(false);
quartzJobMapper.updateByPrimaryKeySelective(quartzJob);
continue;
}
}
}
}


@Slf4j
@Component
public class QuartzManager {
@Autowired
private Scheduler scheduler;

public void addQuartzJob(QuartzJob quartzTask) {
try {
// 创建jobDetail实例,绑定Job实现类
// 指明job的名称,所在组的名称,以及绑定job类
Class<? extends QuartzJobBean> jobClass = (Class<? extends QuartzJobBean>) (Class.forName(quartzTask.getBeanClass()).newInstance().getClass());
// 任务名称和组构成任务key
// 判断参数是否为空
JobDetail jobDetail = null;
if (!quartzTask.getParam().isEmpty()) {
JSONObject jsonObject = quartzTask.getParam();
JobDataMap dataMap = new JobDataMap();
Set<String> strings = jsonObject.keySet();
strings.forEach(str -> {
dataMap.put(str, jsonObject.getString(str));
});
jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(quartzTask.getJobName(), quartzTask.getGroup())
.usingJobData(dataMap)
.build();
} else {
jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(quartzTask.getJobName(), quartzTask.getGroup())
.build();
}

// 定义调度触发规则
// 使用cornTrigger规则
// 触发器key
// 判断任务类型,生成对应类型的调度器
Trigger trigger = null;
if (JobTypeEnum.SIMPLE.getType().equals(quartzTask.getJobType())) {
trigger = TriggerBuilder.newTrigger()
.withIdentity(quartzTask.getJobName(), quartzTask.getGroup())
// 立即执行
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
// 执行间隔,单位:毫秒
.withIntervalInMilliseconds(quartzTask.getMilliSeconds())
// 一直执行
.repeatForever()
)
.build();
} else {
trigger = TriggerBuilder.newTrigger()
.withIdentity(quartzTask.getJobName(), quartzTask.getGroup())
.startAt(DateBuilder.futureDate(1, DateBuilder.IntervalUnit.SECOND))
.withSchedule(CronScheduleBuilder.cronSchedule(quartzTask.getCronExpression())).startNow().build();
}
// 把作业和触发器注册到任务调度中
if (Optional.ofNullable(jobDetail).isPresent() && Optional.ofNullable(trigger).isPresent()) {
scheduler.scheduleJob(jobDetail, trigger);
}
// 启动
if (!scheduler.isShutdown()) {
scheduler.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

7.2wx服务获取企业微信ip

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
@Api(tags = "企业微信-IP白名单接口")
@RestController
@RequestMapping("/ip")
@Slf4j
public class WxIPController {

@Value("${wx.cp.application[0].appConfigs[0].agentId}")
private Integer mobileAgentId;

@Autowired
private MdmService mdmService;// 主数据服务

@ApiOperation(value = "获取企业微信IP集合", notes = "获取企业微信IP集合", httpMethod = "POST")
@RequestMapping("/getIpList")
public Result getIpList() {
log.info("开始获取企业微信IP集合: ");
try {
String[] callbackIp = WxCpTempConfiguration.getCpService(mobileAgentId).getCallbackIp();
log.info("获取企业微信IP集合成功: "+Arrays.toString(callbackIp));
// 白名单更新企业微信IP
if (ArrayUtil.isNotEmpty(callbackIp)){
mdmService.updateWxIp(callbackIp);
} else{
log.error("获取的企业微信IP集合为空");
}

return Result.success();
} catch (WxErrorException e) {
log.info("获取企业微信IP集合失败: ", e);
return Result.error(e.getMessage());
}
}

}

7.3主数据服务更新企业微信ip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
@Transactional(rollbackFor = Exception.class)
public Result updateWxIp(String[] callbackIp) {
// 插入
LocalDateTime now = LocalDateTime.now().minusSeconds(30l);
if (ArrayUtils.isNotEmpty(callbackIp)){
List<MdmLoginIp> mdmLoginIpList = new ArrayList<>();
for (String ip : callbackIp) {
MdmLoginIp mdmLoginIp = new MdmLoginIp();
mdmLoginIp.setLoginIpId(IdWorker.nextId(CommonConstant.MDM_LOGIN_IP_ID_PREFIX));
mdmLoginIp.setLoginIp(ip);
mdmLoginIp.setIsDel(false);
mdmLoginIp.setCreateTime(LocalDateTime.now());
mdmLoginIp.setLoginIpType("QYWX");
mdmLoginIpList.add(mdmLoginIp);
}
mdmLoginIpMapper.batchInsert(mdmLoginIpList);
// 删除
mdmLoginIpMapper.deleteByExample(MdmLoginIpExample.newAndCreateCriteria().andLoginIpTypeEqualTo("QYWX").andCreateTimeLessThan(now).example());
return Result.success();
} else {
return Result.error("ip集合为空");
}
}