微信垄断否?对接微信已成为企业绕不开的功能---开箱即用源码贡献对接微信

前言

  • 作为企业管理现在使用钉钉已经很常见了。没个集团没有公众号也说不过去。而针对公众号和钉钉这种公共服务。他们都提供了个性化的定制功能。下面我们来看看如何对接微信,钉钉平台吧

源码

微信

开发者配置

image-20220107131910935.png

开发者工具

  • 配置完成之后,我们就需要在【开发者工具】中查看我们的接口文档。包括我们可以使用微信提供的接口测试工具快速测试

image-20220107132258219.png

  • 为了方便我们测试,后续的开发我们将使用测试账号来完成。关于测试账号的配置和我们上面提到的公众号开发者配置如出一辙。

image-20220107132445027.png

消息

  • 关注公众号给用户发消息,这个功能还是很好用的。试想下你做订单仓库管理。当出库等节点需要给用户发送消息时,所以我们对接微信公众号的消息发送功能是很常见的功能的。
  • 关于消息发送微信给我们提出了两种类型。一种是公众号内客服消息,一种是待办通知消息。确切的说我们还有一种公众号内会话消息,会话内回复和客服消息实际上是一类消息。

消息通知

image-20220107132951905.png

  • 我们需要实现的通知就是利用「模版消息接口」来实现的。模版消息的初衷主要是服务号给用户发送通知使用的。为了杜绝骚扰用户和消息的滥用。微信针对模版消息接口做了条数和次数的限制。
  • 每个账号最多可以使用25套模版
  • 每个模版可以被无限使用,但是账号每天发送的模版消息次数限制为10万次。正常情况没有10万粉,但是如果你的公众号的确超过10万粉将会按照10W、100W、1000W来进行匹配限制

image-20220107134502593.png

  • 上面是我在公众号的测试账号中添加的两套模版。关于模版的获取的接口这里就不描述了。主要实现下通过模版发送通知的功能

image-20220107134704851.png

请求方式 url 参数
POST api.weixin.qq.com/cgi-bin/mes… json
  • 关于参数体格式如下
{
  "touser": "openId",
  "template_id": "templateId",
  "url": "url
  "miniprogram": {},
  "data": {}
}
  • 关于各个参数解释官网给的很详细了,这里就直接贴官网的说明了
参数 是否必填 说明
touser 接收者openid
template_id 模板ID
url 模板跳转链接(海外帐号没有跳转能力)
miniprogram 跳小程序所需数据,不需跳小程序可不用传该数据
appid 所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系,暂不支持小游戏)
pagepath 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar),要求该小程序已发布,暂不支持小游戏
data 模板数据
color 模板内容字体颜色,不填默认为黑色

代码实现

  • 源码已经放在github了,有需要自取。最终发送模版消息的实现如下WechatMessageServiceImpl
private Integer sendMsgBaseOpenId(String openId, String templateId, List<MsgData> dataList) {
        if (StringUtils.isEmpty(templateId)) {
            throw new RuntimeException("未检测到消息模版,无法发送message");
        }
        Map<String, Object> map = new HashMap();
        map.put("touser", openId);//你要发送给某个用户的openid 前提是已关注该公众号,该openid是对应该公众号的,不是普通的openid
        map.put("template_id", templateId);
        Map<String,Object> dataMap = new HashMap();
        if (CollectionUtils.isNotEmpty(dataList)) {
            for (MsgData msgData : dataList) {
                dataMap.put(msgData.getName(), msgData);
            }
        }
        map.put("data", dataMap);
        String msgUrl = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + this.tokenService.accessAndGetDingDingToken();
        HttpUtil.post(msgUrl, JSON.toJSONString(map));
        return 1;
    }
  • 入口见three-party-message/message-demo/src/test/java/com/github/zxhtom/message/demo/WechatTest#sendMsgOpenId
  • 最终我们看下实现效果

image-20220107140803976.png

  • 最终我们会在公众号中收到消息通知。如果我们发送时候设置了跳转小程序或者跳转链接的话,点击之后就会进行页面跳转。

客服消息

  • 还记得我们在开发者配置的时候需要我们配置一个外网的服务接口地址吗。我是通过ngrok进行玩网映射的。为什么微信需要我们配置一个交互接口呢?因为微信会将用户与微信公众号的交互动作转发到我们配置的服务上。这样我们就能够基于用户的反馈进行回应了。
  • 同样的我们这里只关心【客服接口-发消息】。其他接口我们就不看了。有需要的我们自己封装就行了。因为实际上就是调用接口而已。没必要每个地方都指出来。

image-20220107142144048.png

请求方式 url 参数
POST api.weixin.qq.com/cgi-bin/mes… json
  • 关于请求体这里需要着重说明下。因为在客服消息中支持发送【文本】、【图片】、【语音】、【视频】、【音乐】、【图文】,甚至我们还可以通过客服消息接口发送【菜单】消息,【卡券】消息。
  • 为了方便后续各种消息结构的扩展,在代码中我将消息进行抽象化。
public class Message {
    //文本消息
    protected static final String MESSAGE_TEXT="text";
    //图片消息
    protected static final String MESSAGE_IMAGE="image";
    //语音消息
    protected static final String MESSAGE_VOICE="voice";
    //音乐
    protected static final String MESSAGE_MUSIC="music";
    //图文
    protected static final String MESSAGE_NEWS="news";
    //文件
    protected static final String MESSAGE_FILE="file";
    //链接消息
    protected static final String MESSAGE_LINK="link";
    //markdown消息
    protected static final String MESSAGE_MARKDOWN="markdown";
    //卡片消息
    protected static final String MESSAGE_CARD="action_card";

    private String type;
    public Message(String type) {
        this.type = type;
    }

    public Map<String, Object> initMap() {
        Map<String, Object> map = new HashMap<>();
        map.put("msgtype", this.type);
        return map;
    }
}
  • 目前列举所有相关类型。后续如果新增了消息类型我们可以在此处新增,也完全可以不在此处枚举,只需要保证每个子类对应的拥有type就行了。其中有些类型是钉钉中的类型的。因为我对比了两遍的消息结构基本上一致的。都是通过msgtype指定消息类型的。所以基类中提供了初始化请求体方法,就是将对应的type填充进去。
public class ImageMessage extends Message {
    private String mediaId;
    public ImageMessage(String mediaId) {
        super(MESSAGE_IMAGE);
        this.mediaId = mediaId;
    }

    @Override
    public String toString() {
        Map<String, Object> map = initMap();
        Map<String, Object> childMap = new HashMap<>();
        childMap.put("media_id", this.mediaId);
        map.put(getType(), childMap);
        return JSON.toJSONString(map);
    }
}
  • 比如我们需要发送图片消息,那么我们只需要实现继承Message的子类,并制定自己对应的type .
{
    "touser":"OPENID",
    "msgtype":"image",
    "image":
    {
      "media_id":"MEDIA_ID"
    }
}
  • 自信观察分析下,图文消息除了类型以外,我们还需要一个media_id就可以完成消息的构建。所以我们在ImageMessage构造中再接收一个media_id就可以了。最终重写toString构建最终的消息体就可以了。

素材上传

  • 发送文本消息很简单,我们直接构建对应的TextMessage就可以了。但是发送图片消息时,微信为了加快性能响应,所以他要求开发者必须先将图片上传至素材库。

image-20220107144048956.png

  • 而上文提到的media_id就是在素材库中的一个唯一ID,我们发送的时候只需要将ID给微信,微信会自动在素材库中寻找对应的素材发送给用户。

image-20220107144305067.png

  • 在【素材管理】中提供了上传素材的接口。我们根据自己需要看时上传临时的还是永久的。我们这里就临时的解决。
请求方式 url 参数
POST api.weixin.qq.com/cgi-bin/med… json
  • 官网上也明确提到了支持的素材格式为图片(image)、语音(voice)、视频(video)和缩略图(thumb);同样为了后期的扩展,这里的文件格式我也进行了抽象
@Data
public class Meterial {
    //图片
    //钉钉 :图片,图片最大1MB。支持上传jpg、gif、png、bmp格式
    //微信  :10M,支持PNG\JPEG\JPG\GIF格式
    protected static final String METERIAL_IMAGE="image";
    //语音
    //钉钉 : 语音,语音文件最大2MB。支持上传amr、mp3、wav格式
    //微信 : 2M,播放长度不超过60s,支持AMR\MP3格式
    protected static final String METERIAL_VOICE="voice";

    //视频
    //钉钉: 视频,视频最大10MB。支持上传mp4格式。
    //微信: 10MB,支持MP4格式
    protected static final String METERIAL_VIDEO="video";

    //文件
    //仅钉钉支持 : 普通文件,最大10MB。支持上传doc、docx、xls、xlsx、ppt、pptx、zip、pdf、rar格式
    protected static final String METERIAL_FILE="file";
    //缩略图
    //仅微信支持 : 64KB,支持JPG格式
    protected static final String METERIAL_THUMB="thumb";

    private String type;

    public Meterial(String type) {
        this.type = type;
    }
}
  • 而子类只需制定类型就行。不需要做其他的工作。上传素材时需要上传文件。所以我们在HttpUtils中还需要在准备一个文件上传的功能。为了方便在服务器间交互,这里我们接收InputStream 。
public static String upload(InputStream inputStream , String fileName,String urlLink) throws IOException {
        String result = StringUtils.EMPTY;
        URL url=new URL(urlLink);
        HttpsURLConnection conn=(HttpsURLConnection) url.openConnection();
        conn.setRequestMethod("POST");//以POST方式提交表单
        conn.setDoInput(true);
        conn.setDoOutput(true);
        conn.setUseCaches(false);//POST方式不能使用缓存
        //设置请求头信息
        conn.setRequestProperty("Connection", "Keep-Alive");
        conn.setRequestProperty("Charset", "UTF-8");
        //设置边界
        String BOUNDARY="----------"+System.currentTimeMillis();
        conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
        //请求正文信息
        //第一部分
        StringBuilder sb=new StringBuilder();
        sb.append("--");//必须多两条道
        sb.append(BOUNDARY);
        sb.append("\r\n");
        sb.append("Content-Disposition: form-data;name=\"media\"; filename=\"" + fileName+"\"\r\n");
        sb.append("Content-Type:application/octet-stream\r\n\r\n");
        System.out.println("sb:"+sb);

        //获得输出流
        OutputStream out=new DataOutputStream(conn.getOutputStream());
        //输出表头
        out.write(sb.toString().getBytes("UTF-8"));
        //文件正文部分
        //把文件以流的方式 推送道URL中
        DataInputStream din=new DataInputStream(inputStream);
        int bytes=0;
        byte[] buffer=new byte[1024];
        while((bytes=din.read(buffer))!=-1){
            out.write(buffer,0,bytes);
        }
        din.close();
        //结尾部分
        byte[] foot=("\r\n--" + BOUNDARY + "--\r\n").getBytes("UTF-8");//定义数据最后分割线
        out.write(foot);
        out.flush();
        out.close();
        if(HttpsURLConnection.HTTP_OK==conn.getResponseCode()){

            StringBuffer strbuffer=null;
            BufferedReader reader=null;
            try {
                strbuffer=new StringBuffer();
                reader=new BufferedReader(new InputStreamReader(conn.getInputStream()));
                String lineString=null;
                while((lineString=reader.readLine())!=null){
                    strbuffer.append(lineString);

                }
                if(StringUtils.isEmpty(result)){
                    result=strbuffer.toString();
                    System.out.println("result:"+result);
                }
            } catch (IOException e) {
                System.out.println("发送POST请求出现异常!"+e);
                e.printStackTrace();
            }finally{
                if(reader!=null){
                    reader.close();
                }
            }

        }
        return result;
    }

测试用例

  • three-party-message/message-demo/src/test/java/com/github/zxhtom/message/demo/WechatTest.upload用于上传素材
  • three-party-message/message-demo/src/test/java/com/github/zxhtom/message/demo/WechatTest.sendPic用于发送图片给用户

image-20220107145815907.png

授权登陆

  • 除了发送消息外,我们还有一个必须要做的功能就是获取到登陆用户。才能针对性的开发。在【微信网页开发】--【网页授权】中详细描述了我们如何接入用户认证功能。
  • 因为授权就涉及了微信回掉我们的地址,微信为了安全考虑会要求我们设置域名回掉地址。在测试账号中可以配置IP。比如我现在要回掉的地址是10.0.20.139:8081/test.index.html . 那么我们在授权回掉中配置的域名就是10.0.20.139:8081

image-20220107153704760.png

image-20220107155732451.png

  • 大概梳理下授权登陆的流程图。对于前端页面来说不需要做改动,只需在获取code之后调用java服务获取当前登陆用户就行了。code的获取也很容易,在微信回掉的时候会将code生成并拼接在回掉地址的请求参数中。
  • 比如官网提供的地址,我们用微信打开https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx520c15f417810387&redirect_uri=https%3A%2F%2Fchong.qq.com%2Fphp%2Findex.php%3Fd%3D%26c%3DwxAdapter%26m%3DmobileDeal%26showwxpaytitle%3D1%26vb2ctag%3D4_2030_5_1194_60&response_type=code&scope=snsapi_base&state=123#wechat_redirect
  • 这个时候微信在跳转到QQ充值页面时页面地址是如下地址https://chong.qq.com/tws/entra/getpanel?id=147&code=031DFQ000EZT4N1h7b100x0P0L1DFQ0R&state=qqchongzhi
  • 我们能够看到此时code的值,这里需要注意的是我们的回掉地址是需要通过urlEncode 对链接进行处理.
  • 获取到code之后我们就后台完成后续的一些列操作。

代码测试

  • three-party-message/message-demo/src/test/java/com/github/zxhtom/message/demo/WechatTest#selectUserByCode

  • 逻辑就是按照官网进行调用接口,需要注意的是在调用https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN接口的时候access_token时根据code获取的,和根据appId ,appSecret获取的access_token不同哦。

  • 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7124120052114128909