企业微信脚手架(第三方)

在这里插入图片描述
在这里插入图片描述

开发文档地址:https://work.weixin.qq.com/api/doc/90001/90142/90594
企业微信服务商后台:https://open.work.weixin.qq.com/wwopen/developer/#/index


一、前言

1.企业微信于2016年4月上线,是腾讯微信团队打造的以办公沟通工具为主打定位的移动办公平台,它的slogan:让每个企业都有自己的微信。
2.企业微信提供了通讯录管理、应用管理、消息推送、身份验证、移动端SDK、素材、OA数据接口、企业支付、电子发票等API,管理员可以使用这些API,为企业接入更多个性化的办公应用。
3.企业微信也是一个平台,是一个统一的办公入口,可以集成公司内部的系统(OA系统、HR系统、ERP系统、CRM系统等),直接在企业微信手机端就可以接收内部系统的消息和通知。
4.企业微信与微信企业号区别:其实两个产品最大的其别就是微信企业号是基于微信,而企业微信是一个独立app。企业微信,倾向于将工作和生活完全分开,以独立app的形式去使用,更有着丰富的办公应用,如预设打卡、审批、日报、公告等OA应用,如果你对这些应用不满足,还可以通过API接入和第三方应用满足更多个性需求。有一种说法:
微信企业号要合并到企业微信,然后慢慢淡化微信企业号的概念。

第三方应用接口旨在方便企业微信管理员通过简单的操作来使用第三方服务商的云应用。实现该目标的核心的机制是:服务商预先在第三方管理端注册登记应用信息。企业选择使用第三方应用时,通过授权流程来一键安装应用。

脚手架源码下载:下载

在这里插入图片描述


二、脚手架预览

2.1 项目结构

在这里插入图片描述

2.2 服务商后台自建应用

在这里插入图片描述

SuiteID
Secret
Token
EncodingAESKey
可信域名配置
业务设置URL
数据回调UR(应用中发送消息,会回调该url)
指令回调URL(各种事件)

在这里插入图片描述
在这里插入图片描述

安装完后,在应用管理第三方应用里面就会多出该应用
在这里插入图片描述


2.2 消息自动回复

在这里插入图片描述


2.4 添加好友自动回复

在这里插入图片描述


2.5 名片配置小程序

在这里插入图片描述


2.6 拆分链接,用户授权

在这里插入图片描述
在这里插入图片描述


2.7 js-sdk测试demo

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


三、关键的模块

3.1 回调配置

在集成企业微信与内部系统时,我们往往需要搭建一个回调服务。回调服务,可以实现:

1.自定义丰富的服务行为。比如,用户向应用发消息时,识别消息关键词,回复不同的消息内容;用户点击应用菜单时,转化为指令,执行自动化任务。
2.可以及时获取到状态变化。比如,通讯录发生变化时,不需要定时去拉取通讯录对比,而是实时地获取到变化的通讯录结点,进行同步。

在这里插入图片描述
参数文

回调服务需要作出正确的响应才能通过URL验证,具体操作如下:

1.对收到的请求,解析上述的各个参数值(参数值需要做Urldecode处理) 根据已有的token,结合第1步获取的参数timestamp,
2.nonce, echostr重新计算签名,然后与参数msg_signature检查是否一致,确认调用者的合法性。计算方法参考:消息体签名检验
3.解密echostr参数得到消息内容(即msg字段)
4.在1秒内响应GET请求,响应内容为上一步得到的明文消息内容(不能加引号,不能带bom头,不能带换行符)


3.2 应用授权

企业微信的系统管理员可以授权安装第三方应用,安装后企业微信后台会将授权凭证、授权信息等推送给服务商后台。然后我们获取到授权信息,将授权信息保存起来。授权可以有两种发起方式:

1.从服务商网站发起
2.从企业微信应用市场发起

从服务商网站发起
在这里插入图片描述

从企业微信应用市场发起
在这里插入图片描述


3.3 推送suite_ticket处理

在发生授权、通讯录变更、ticket变化等事件时,企业微信服务器会向应用的“指令回调URL”推送相应的事件消息。消息结构体将使用创建应用时的EncodingAESKey进行加密。服务商在收到推送后都必须直接返回字符串 “success”,若返回值不是 “success”,企业微信会把返回内容当作错误信息。

企业微信服务器会定时(每十分钟)推送ticket。ticket会实时变更,并用于后续接口的调用。

    <xml>
        <SuiteId><![CDATA[ww4asffe99e54c0fxxxx]]></SuiteId>
        <InfoType> <![CDATA[suite_ticket]]></InfoType>
        <TimeStamp>1403610513</TimeStamp>
        <SuiteTicket><![CDATA[asdfasfdasdfasdf]]></SuiteTicket>
    </xml>


3.4 网页授权登录

企业微信提供了OAuth的授权登录方式,可以让从企业微信终端打开的网页获取成员的身份信息,从而免去登录的环节。
在这里插入图片描述

构造网页授权链接
获取访问用户身份
获取访问用户敏感信息


3.5 Js-Sdk配置

业微信JS-SDK是企业微信面向网页开发者提供的基于企业微信内的网页开发工具包。通过使用企业微信JS-SDK,网页开发者可借助企业微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用企业微信分享、扫一扫等企业微信特有的能力,为企业微信用户提供更优质的网页体验。

**** 所有的JS接口只能在企业微信应用的可信域名下调用(包括子域名),且可信域名必须有ICP备案且在管理端验证域名归属。验证域名归属的方法在企业微信的管理后台“我的应用”里,进入应用,设置应用可信域名。

JS-SDK使用权限签名算法:

获取企业的jsapi_ticket
签名算法
获取应用的jsapi_ticket

***生成签名之前必须先了解一下jsapi_ticket,jsapi_ticket是H5应用调用企业微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的api调用次数非常有限(一小时内,一个企业最多可获取400次,且单个应用不能超过100次),频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket。


四、怎么让脚手架快速跑起来

4.1 修改配置 application.xml
logging:
  level:
    org.springframework.web: info
    com.lxh.cp: debug
    cn.binarywang.wx.cp: debug

server:
  port: 80
  servlet:
    context-path: /

wx:
  cptp:
    corpId: ww4007bc86bexxxx
    suiteId: ww141f5c1b0bfexxxx
    secret: NpR7i2nC_lAdr-nhm4Chxxxxxxxxxxxxx
    aesKey: kYkZ8YiM7Xze6HbyHTzmmOs1xxxxxxxxxxxxxx


4.2 启动服务

在这里插入图片描述


4.3 添加redis缓存支持

自己扩展功能,在tp包下面:
在这里插入图片描述

package com.lxh.cptp.tp;

import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * created by lanxinghua@2dfire.com on 2020/2/25
 * 企业微信第三方,缓存支持
 * 1.suiteAccessToken
 */
public class WxCpTpRedisConfigImpl extends WxCpTpDefaultConfigImpl {
    // suite_ticket与suite_secret配套使用,用于获取suite_access_token 10min
    private static String SUITE_TICKET_KEY = "wx:cptp:suite_ticket";
    // 第三方应用凭证 2h (key按应用区分)
    private static String SUITE_ACCESS_TOKEN_KEY = "wx:cptp:suite_access_token:";
    // H5应用调用企业微信JS接口的临时票据 2h (跟着商家企业Id走)
    private static final String JS_API_TICKET_KEY = "wx:cptp:jsapi_ticket:";


    /**
     * 使用连接池保证线程安全.
     */
    private final JedisPool jedisPool;

    @Override
    public void setSuiteId(String corpId) {
        SUITE_ACCESS_TOKEN_KEY += corpId;
        super.setSuiteId(corpId);
    }

    public WxCpTpRedisConfigImpl(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }



    /**-------------------------- suite ticket -------------------------**/
    @Override
    public String getSuiteTicket() {
        if (isSuiteTicketExpired()){
            String result = "{\"errcode\":40085, \"errmsg\":\"invaild suite ticket\"}";
            System.err.println(result);
            return null;
        }
        try (Jedis jedis = this.jedisPool.getResource()) {
            return jedis.get(SUITE_TICKET_KEY);
        }
    }


    @Override
    public boolean isSuiteTicketExpired() {
        try (Jedis jedis = this.jedisPool.getResource()) {
            return jedis.ttl(SUITE_TICKET_KEY) < 2;
        }
    }

    @Override
    public synchronized void updateSuiteTicket(String suiteTicket, int expiresInSeconds) {
        System.out.println("redis缓存更新成功:" + suiteTicket);
        try (Jedis jedis = this.jedisPool.getResource()) {
            jedis.setex(SUITE_TICKET_KEY, expiresInSeconds - 200, suiteTicket);
        }
    }

    @Override
    public void expireSuiteTicket() {
        try (Jedis jedis = this.jedisPool.getResource()) {
            jedis.expire(SUITE_TICKET_KEY, 0);
        }
    }

    /**-------------------------- suite access token -------------------------**/
    @Override
    public String getSuiteAccessToken() {
        if (isSuiteAccessTokenExpired()){
            return null;
        }
        try (Jedis jedis = this.jedisPool.getResource()) {
            return jedis.get(SUITE_ACCESS_TOKEN_KEY);
        }
    }

    @Override
    public boolean isSuiteAccessTokenExpired() {
        try (Jedis jedis = this.jedisPool.getResource()) {
            return jedis.ttl(SUITE_ACCESS_TOKEN_KEY) < 2;
        }
    }

    @Override
    public synchronized void updateSuiteAccessToken(WxAccessToken suiteAccessToken) {
        this.updateSuiteAccessToken(suiteAccessToken.getAccessToken(), suiteAccessToken.getExpiresIn());
    }

    @Override
    public synchronized void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds) {
        try (Jedis jedis = this.jedisPool.getResource()) {
            jedis.setex(SUITE_ACCESS_TOKEN_KEY, expiresInSeconds - 200, suiteAccessToken);
        }
    }

    @Override
    public void expireSuiteAccessToken() {
        try (Jedis jedis = this.jedisPool.getResource()) {
            jedis.expire(SUITE_ACCESS_TOKEN_KEY, 0);
        }
    }

    /**-------------------------- jsapi ticket -------------------------**/
    public String getJsApiTicket(String authCorpId) {
        System.out.println("缓存中获取jsApiTicket");
        if (isJsApiTicketExpired(authCorpId)){
            return null;
        }
        try (Jedis jedis = this.jedisPool.getResource()) {
            return jedis.get(JS_API_TICKET_KEY + authCorpId);
        }
    }

    public boolean isJsApiTicketExpired(String authCorpId) {
        try (Jedis jedis = this.jedisPool.getResource()) {
            return jedis.ttl(JS_API_TICKET_KEY + authCorpId) < 2;
        }
    }


    // authCorpId 使用第三方企业对应的id
    public synchronized void updateJsApiTicket(String jsApiTicket, int expiresInSeconds, String authCorpId) {
        try (Jedis jedis = this.jedisPool.getResource()) {
            jedis.setex(JS_API_TICKET_KEY + authCorpId, expiresInSeconds - 200, jsApiTicket);
        }
    }

    public void expireJsApiTicket(String authCorpId) {
        try (Jedis jedis = this.jedisPool.getResource()) {
            jedis.expire(JS_API_TICKET_KEY + authCorpId, 0);
        }
    }

    /**
     * This method will be destroy jedis pool
     */
    public void destroy() {
        this.jedisPool.destroy();
    }
}

在这里插入图片描述


五、核心代码

5.1 WxCpTpConfiguration.java
package com.lxh.cptp.config;

import com.google.common.collect.Maps;
import com.lxh.cptp.handler.*;
import com.lxh.cptp.tp.WxCpTpRedisConfigImpl;
import me.chanjar.weixin.cp.api.WxCpTpService;
import me.chanjar.weixin.cp.api.impl.WxCpTpServiceImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.annotation.PostConstruct;
import java.util.Map;

/**
 * created by lanxinghua@2dfire.com on 2020/2/23
 * 支持一个第三方
 */
@Configuration
@EnableConfigurationProperties(WxCpTpProperties.class)
public class WxCpTpConfiguration {
    private static final Logger logger = LoggerFactory.getLogger(WxCpTpConfiguration.class);
    public static String SUIT_ID = "ww141f5c1b0bfe0bc2";

    private static Map<String/*suitId*/, Map<String/*key*/, AbstractHandler>> routers = Maps.newHashMap();
    private static Map<String/*suitId*/, WxCpTpService> cpTpServices = Maps.newHashMap();

    @Autowired
    private WxCpTpProperties properties;

    // 更新2020.2.25 加入redis
    @Autowired
    private JedisPool jedisPool;

    public static WxCpTpService getCpTpService(String suitId) {
        return cpTpServices.get(suitId);
    }

    public static Map<String, AbstractHandler> getHandlerMap(){
        return routers.get(SUIT_ID);
    }

    public static AbstractHandler getHandler(String key){
        return routers.get(SUIT_ID).get(key);
    }

    /**
     * 初始化服务
     */
    @PostConstruct
    public void initServices() {
        WxCpTpService tpService = new WxCpTpServiceImpl();
        WxCpTpDefaultConfigImpl configStorage = new WxCpTpDefaultConfigImpl();
        // 加入redis缓存
        if (redisIsOk()){
            configStorage = new WxCpTpRedisConfigImpl(jedisPool);
        }
        configStorage.setSuiteId(properties.getSuiteId());
        configStorage.setAesKey(properties.getAesKey());
        configStorage.setToken(properties.getToken());
        configStorage.setSuiteSecret(properties.getSecret());
        configStorage.setCorpId(properties.getCorpId());

        tpService.setWxCpTpConfigStorage(configStorage);
        cpTpServices.put(properties.getSuiteId(), tpService);
        routers.put(properties.getSuiteId(), initHandler());
    }


    /**
     *
     * @return
     */
    private Map<String, AbstractHandler> initHandler(){
        Map<String, AbstractHandler> handlerMap = Maps.newHashMap();
        handlerMap.put("log", new LogHandler());
        handlerMap.put("msg", new MsgHandler());
        return handlerMap;
    }


    /**
     * redis是否可用
     * @return
     */
    private boolean redisIsOk(){
        try {
            Jedis jedis = jedisPool.getResource();
            jedis.ping();
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }
}


5.2 各种事件Handler

在这里插入图片描述


5.3 构建器

在这里插入图片描述


5.4 WxCpTpController.java(关键)
package com.lxh.cptp.controller;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Pair;
import cn.hutool.json.JSONUtil;
import com.lxh.cptp.config.WxCpTpConfiguration;
import com.lxh.cptp.constant.ChangeTypeEnum;
import com.lxh.cptp.constant.InfoTypeEnum;
import com.lxh.cptp.constant.WxCpTpInnerConstant;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.cp.api.WxCpTpService;
import me.chanjar.weixin.cp.bean.WxCpTpCorp;
import me.chanjar.weixin.cp.bean.WxCpTpXmlMessage;
import me.chanjar.weixin.cp.bean.WxCpTpXmlPackage;
import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.util.crypto.WxCpTpCryptUtil;
import me.chanjar.weixin.cp.util.xml.XStreamTransformer;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.json.XML;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;

/**
 * created by lanxinghua@2dfire.com on 2020/2/14
 * 企业微信第三方应用开发
 */
@RestController
public class WxCpTpController extends BaseController{
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private String SUIT_ID = "ww1410bfe0bc2";
    private WxCpTpService tpService;

    /**
     * 数据回调url,应用中发送消息接收
     * @param signature
     * @param timestamp
     * @param nonce
     * @param echostr
     * @return
     */
    @RequestMapping("/callback/data")
    public String data(@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,
                       HttpServletRequest request) {
        logger.info("数据回调");
        logger.info("signature = [{}], timestamp = [{}], nonce = [{}], echostr = [{}]", signature, timestamp, nonce, echostr);
        tpService = WxCpTpConfiguration.getCpTpService(SUIT_ID);
        if (StringUtils.isEmpty(echostr)){
            try {
                BufferedReader reader = request.getReader();
                StringBuffer buffer = new StringBuffer();
                String line = " ";
                while (null != (line = reader.readLine())) {
                    buffer.append(line);
                }
                String postData = buffer.toString();
                String decryptMsgs = new WxCpTpCryptUtil(tpService.getWxCpTpConfigStorage()).decrypt(signature, timestamp, nonce, postData);
                System.out.println("数据回调-解密后的xml数据:" + decryptMsgs);
                WxCpTpXmlPackage tpXmlPackage = WxCpTpXmlPackage.fromXml(decryptMsgs);
                System.out.println(JSONUtil.toJsonStr(tpXmlPackage));

                // 消息回复
                WxCpXmlOutMessage outMessage = this.route(SUIT_ID, tpXmlPackage);
                String plainXml = XStreamTransformer.toXml((Class) outMessage.getClass(), outMessage);
                logger.info("\n组装回复信息:{}", plainXml);

                WxCpTpCryptUtil pc = new WxCpTpCryptUtil(tpService.getWxCpTpConfigStorage());
                return pc.encrypt(plainXml);
            }catch (Exception e){
                logger.error("校验失败:" + e.getMessage());
                return "success";
            }
        }
        try {
            if (tpService.checkSignature(signature, timestamp, nonce, echostr)) {
                return new WxCpTpCryptUtil(tpService.getWxCpTpConfigStorage()).decrypt(echostr);
            }
        } catch (Exception e) {
            logger.error("校验签名失败:" + e.getMessage());
        }
        return "success";
    }

    private WxCpXmlOutMessage route(String suitId, WxCpTpXmlPackage message) {
        // 日志处理
        WxCpTpConfiguration.getHandler("log").handle(message, null);
        // 消息处理
        return WxCpTpConfiguration.getHandler("msg").handle(message, null);
    }


    /**
     * 指令回调url  通讯录,部门变更,  授权变更,  ticket数据
     * @param signature
     * @param timestamp
     * @param nonce
     * @param echostr
     * @param request
     * @param response
     * @return
     * @throws IOException
     */
    @RequestMapping(value = "/suite/receive")
    public String suite(@RequestParam("msg_signature") String signature,
                        @RequestParam("timestamp") String timestamp,
                        @RequestParam("nonce") String nonce,
                        @RequestParam(value = "echostr", required = false) String echostr,
                        HttpServletRequest request,
                        HttpServletResponse response) throws Exception {
        logger.info("指令回调URL-调用我了");
        logger.info("signature = [{}], timestamp = [{}], nonce = [{}], echostr = [{}]", signature, timestamp, nonce, echostr);

        tpService = WxCpTpConfiguration.getCpTpService(SUIT_ID);

        // 不为空为回调配置请求
        if (null != echostr) {
            try {
                if (tpService.checkSignature(signature, timestamp, nonce, echostr)) {
                    return new WxCpTpCryptUtil(tpService.getWxCpTpConfigStorage()).decrypt(echostr);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }

        BufferedReader reader = request.getReader();
        StringBuffer buffer = new StringBuffer();
        String line = " ";
        while (null != (line = reader.readLine())) {
            buffer.append(line);
        }
        String postData = buffer.toString();
        String decryptMsgs = null;
        try {
            decryptMsgs = new WxCpTpCryptUtil(tpService.getWxCpTpConfigStorage()).decrypt(signature, timestamp, nonce, postData);
            System.out.println("解密后的xml数据:" + decryptMsgs);
        }catch (Exception e){
            logger.error("校验失败:" + e.getMessage());
            return "success";
        }
        WxCpTpXmlMessage wxCpTpXmlMessage = WxCpTpXmlMessage.fromXml(decryptMsgs);
        InfoTypeEnum infoTypeEnum = InfoTypeEnum.getByInstance(wxCpTpXmlMessage.getInfoType());
        if (infoTypeEnum == null){
            throw new Exception("不支持该类型操作");
        }
        Map<String, Object> jsonObject = wxCpTpXmlMessage.getAllFieldsMap();
        switch (infoTypeEnum) {
            //(每十分钟)推送ticket。ticket会实时变更,并用于后续接口的调用。
            case SUITE_TICKET: {
                String suitId = wxCpTpXmlMessage.getSuiteId();
                String suiteTicket = wxCpTpXmlMessage.getSuiteTicket();
                Integer timeStamp = Convert.toInt(wxCpTpXmlMessage.getTimeStamp());
                logger.info("推送ticket成功:" + jsonObject.toString());
                WxCpTpConfigStorage tpConfig = tpService.getWxCpTpConfigStorage();
                tpConfig.updateSuiteTicket(suiteTicket, 20*60);
                logger.info("suit ticket缓存更新成功");

                String suitAccessToken = tpService.getSuiteAccessToken();
                logger.info("suitAccessToken:" + suitAccessToken);
                break;
            }
            // 企业微信应用市场发起授权时,企业微信后台会推送授权成功通知
            case CREATE_AUTH:{
                logger.info("创建授权:" + jsonObject.toString());
                String suiteId = wxCpTpXmlMessage.getSuiteId();
                String authCode = wxCpTpXmlMessage.getAuthCode();
                String timeStamp = wxCpTpXmlMessage.getTimeStamp();
                WxCpTpCorp permanentCode = tpService.getPermanentCode(authCode);
                logger.info("永久授权码:" + permanentCode);
                break;
            }
            case CHANGE_AUTH:{
                logger.info("变更授权");
                // TODO: 2020/2/14
                break;
            }
            case CHANGE_CONTACT:{
                logger.info("通讯录变更");
                String changeType = jsonObject.get(WxCpTpInnerConstant.CHANGE_TYPE).toString();
                ChangeTypeEnum changeTypeEnum = ChangeTypeEnum.getByInstance(changeType);
                if (Objects.isNull(changeTypeEnum)){
                    throw new Exception("该类型通讯录变更不存在");
                }
                switch (changeTypeEnum){
                    case DELETE_USER:{
                        logger.info("删除用户");
                        break;
                    }
                    case UPDATE_TAG:{
                        logger.info("更新标签");
                        break;
                    }
                    case DELETE_PARTY:{
                        logger.info("删除部门");
                        break;
                    }
                    default:{
                        break;
                    }
                }
                break;
            }
            case CHANGE_EXTERNAL_CONTACT:{
                logger.info("外部联系人");
                String changeType = jsonObject.get(WxCpTpInnerConstant.CHANGE_TYPE).toString();
                ChangeTypeEnum changeTypeEnum = ChangeTypeEnum.getByInstance(changeType);
                if (Objects.isNull(changeTypeEnum)){
                    throw new Exception("该类型CHANGE_EXTERNAL_CONTACT不存在");
                }
                switch (changeTypeEnum){
                    case ADD_EXTERNAL_CONTACT:{
                        logger.info("添加客户" + jsonObject.toString());
                        String externalUserID = jsonObject.get("ExternalUserID").toString();
                        String welcomeCode = jsonObject.get("WelcomeCode").toString();
                        if (StringUtils.isEmpty(welcomeCode)){
                            logger.warn("welcomeCode没获取到,用户可能已经设置了欢迎语!!!");
                            break;
                        }
                        String userID = jsonObject.get("UserID").toString();
                        String suiteId = jsonObject.get("SuiteId").toString();
                        String authCorpId = jsonObject.get("AuthCorpId").toString();
                        System.out.println("欢迎code:" + welcomeCode);
                        sendWelcome(tpService, welcomeCode);
                        break;
                    }
                    default:{
                        break;
                    }
                }
            }
            default: {
                break;
            }
        }
        return "success";
    }


    /**
     * 获取操作方式type
     * @param decryptMsgs
     * @return
     */
    public static Pair<String, JSONObject> doType(String decryptMsgs){
        try {
            JSONObject jsonObject = XML.toJSONObject(decryptMsgs.replace("<xml>", "").replace("</xml>", ""));
            String infoType = jsonObject.getString(WxCpTpInnerConstant.INFO_TYPE);
            if (StringUtils.isBlank(infoType)){
                throw new Exception("infoType为空");
            }
            return new Pair<String, JSONObject>(infoType, jsonObject);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 发送欢迎语
     * @param tpService
     * @param welcome_code
     * @throws Exception
     */
    public void sendWelcome(WxCpTpService tpService, String welcome_code) throws Exception{
        String auth_corpId = "ww4007de5885";
        String permCode = authMap.get(auth_corpId);
        WxAccessToken wxAccessToken = tpService.getCorpToken(auth_corpId, permCode);
        logger.info("获取企业凭证:" + JSONUtil.toJsonStr(wxAccessToken));

        String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token=%s";
        // image、link和miniprogram只能有一个
        String postData_back = "{\n" +
                "    \"welcome_code\":\""+welcome_code+"\",\n" +
                "    \"text\": {\n" +
                "        \"content\":\"欢迎,自定义文本消息内容\"\n" +
                "    },\n" +
                "    \"image\": {\n" +
                "        \"media_id\": \"1Qio5JosY6ylJN9LzyOSrhCYkZQA0IzDYV-PSOXca4bWaX1B-uzTRBXXS1H3bPcg2\"\n" +
                "    },\n" +
                "    \"link\": {\n" +
                "        \"title\": \"消息标题\",\n" +
                "        \"picurl\": \"https://assets.2dfire.com/frontend/277ec5572aad73d80b3bcaa5b57fc65b.png\",\n" +
                "        \"desc\": \"消息描述\",\n" +
                "        \"url\": \"http://2dfire.com\"\n" +
                "    },\n" +
                "    \"miniprogram\": {\n" +
                "        \"title\": \"消息标题找店\",\n" +
                "        \"pic_media_id\": \"1Qio5JosY6ylJN9LzyOSrhCYkZQA0IzDYV-PSOXca4bWaX1B-uzTRBXXS1H3bPcg2\",\n" +
                "        \"appid\": \"wx3c2dd923aea\",\n" +
                "        \"page\": \"/pages/findShop\"\n" +
                "    }\n" +
                "}\n";

        String postData = "{\n" +
                "    \"welcome_code\":\""+welcome_code+"\",\n" +
                "    \"text\": {\n" +
                "        \"content\":\"欢迎,自定义文本消息内容\"\n" +
                "    },\n" +
                "    \"miniprogram\": {\n" +
                "        \"title\": \"消息标题找店\",\n" +
                "        \"pic_media_id\": \"1xWyPP9Pcf2oOso6fBCTsELmjTyehO-aOOOWZs54NNCQLfNCrW8PXsoYIp1jaTX\",\n" +
                "        \"appid\": \"wx3c2dd9236aea\",\n" +
                "        \"page\": \"/pages/findShop\"\n" +
                "    }\n" +
                "}\n";
        String result = tpService.post(String.format(url, wxAccessToken.getAccessToken()), postData);
        //JsonObject jsonObject = new JsonParser().parse(result).getAsJsonObject();
        System.out.println("发送欢迎语:"+ result);
    }

}


5.5 WxCpTpOauthController.java
package com.lxh.cptp.controller;

import com.alibaba.fastjson.JSONObject;
import com.lxh.cptp.config.WxCpTpConfiguration;
import com.lxh.cptp.utils.JsonUtils;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.common.util.RandomUtils;
import me.chanjar.weixin.common.util.crypto.SHA1;
import me.chanjar.weixin.cp.api.WxCpTpService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

import static com.lxh.cptp.config.WxCpTpConfiguration.SUIT_ID;

/**
 * created by lanxinghua@2dfire.com on 2020/2/23
 * 授权相关(第三方应用)
 */
@RestController
@RequestMapping("/wx/oauth")
public class WxCpTpOauthController extends BaseController{
    private WxCpTpService tpService;
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private String oauthUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&state=%s#wechat_redirect";
    private String loginUrl = "http://chenxingxing.51vip.biz/wx/oauth/login";


    /**
     * 拆分链接
     * @param url
     */
    @GetMapping("/jump")
    public void jump(String url,
                     HttpServletResponse response) throws IOException {
        if (StringUtils.isBlank(url)) {
            throw new IllegalArgumentException("url is empty");
        }
        oauthUrl = String.format(oauthUrl, SUIT_ID, loginUrl, url);
        logger.info("跳转url:" + oauthUrl);
        response.sendRedirect(oauthUrl);
    }


    /**
     * 授权链接  通过code换取用户信息
     */
    @GetMapping("/login")
    public void login(String code,
                      String state,
                      HttpServletRequest request,
                      HttpServletResponse response) {
        if (StringUtils.isBlank(code)) {
            throw new IllegalArgumentException("code is empty");
        }
        try {
            tpService = WxCpTpConfiguration.getCpTpService(SUIT_ID);
            JSONObject userInfo3rd = getUserInfo3rd(tpService, code);
            if (userInfo3rd == null){
                throw new Exception("用户信息获取失败");
            }
            JSONObject userInfoDetail = getUserDetail3rd(tpService, userInfo3rd.get("user_ticket").toString());
            request.getSession().setAttribute("token", userInfoDetail);
            response.sendRedirect(state);
        } catch (Exception e) {
            this.logger.error(e.getMessage(), e);
        }
    }


    /**
     * 创建js-sdk签名
     * @param url
     * @return
     * @throws Exception
     */
    @RequestMapping("/create/jsapi_sign")
    @ResponseBody
    public Object jssdk(@RequestParam  String url,
                        HttpServletRequest request) throws Exception{
        // 从url里面解析test参数
        String auth_corpId = "wwd282e1e3";
        if (url.contains("entityId")){
            Map<String, Object> parameter = getParameter(url);
            auth_corpId = testMap.get(parameter.get("entityId").toString());
        }
        if (StringUtils.isEmpty(auth_corpId)){
            throw new Exception("企业授权id为空.");
        }
        logger.info("authCorpId:{}  url:{}", auth_corpId, url);
        tpService = WxCpTpConfiguration.getCpTpService(SUIT_ID);
        long timestamp = System.currentTimeMillis() / 1000;
        String nonceStr = RandomUtils.getRandomStr();
        JSONObject object = getJsApiTicket(auth_corpId, tpService);
        String jsApiTicket = object.getString("ticket");
        String signature = SHA1.genWithAmple(
                "jsapi_ticket=" + jsApiTicket,
                "noncestr=" + nonceStr,
                "timestamp=" + timestamp,
                "url=" + url
        );
        WxJsapiSignature jsapiSignature = new WxJsapiSignature();
        jsapiSignature.setTimestamp(timestamp);
        jsapiSignature.setNonceStr(nonceStr);
        jsapiSignature.setUrl(url);
        jsapiSignature.setSignature(signature);
        jsapiSignature.setAppId(auth_corpId);
        logger.info("data:" + JsonUtils.toJson(jsapiSignature));
        return jsapiSignature;
    }
}


有什么问题,可以下面留言,一起交流…