实现基于springboot的web管理端与微信公众号关注用户的消息会话

实现基于springboot的web管理端与微信公众号用户的消息会话(ws/wss)

一、需求:

小程序用户在进入小程序进行下单消费,然后关注公众号,在公众号里发消息到微信公众号服务器,此时web端需要展示该用户发过来的消息,web后台管理系统的管理人员收到消息可以进行回复,本功能暂时只支持文本消息交互,后期会扩展到音视频、图片等消息交互
web界面

二、前期准备工作以及注意的地方

1、用户需要进入过小程序,并且授权登录小程序,微信小程序关联微信公众号,并且微信公众号在微信开放平台绑定认证,后台服务器将获取openId以及unionId,并且将unionId和小程序用户的openid(本文标记为openidA)关联在一起缓存在redis
2、开发者在微信公众号平台后台配置接受微信消息的业务后台路径(注意路径不可被拦截,该路径不需要登录或者业务后台自定义的token令牌验证)
3、业务服务器根据公众号的 Appid 以及 Appsecret获取 access_token,并且需要过期刷新,公众号的access_token只能在一个服务换取,如果有两个服务获取,第一个服务调用会报错:access_token失效但是不过期
4、进入小程序的微信用户关注公众号,当后台服务器收到微信公众号发来的 {CreateTime=1545034549, EventKey=, Event=subscribe, ToUserName=XXXXXXXX, FromUserName=XXXXXXXXXX, MsgType=event}

ToUserName:微信公众号原始id
FromUserName:微信公众号openid
Event:时间类型
MsgType:消息类型
CreateTime:消息产生时间

消息时,业务后台会根据 FromUserName(微信用户在微信公众号的openidB) 来获取微信公众号关注用户在公众号的unionid,将两者关联起来缓存在redis,并且给微信公众号用户推送小程序文字链
5、websocket连接需要在nginx配置协议转换,wss需要在nginx配置ssl证书以及路径
6、本文小程序用户openidopenidA 表示,微信公众号用户 openidopenidB 表示

三、流程简介:

在这里插入图片描述

四、代码:

本文主要讲述大概的业务流程以及思路,代码没有一 一贴出,读者有需要可以留言联系

首先要加入websocket必须的依赖

WebSocket配置:

@Component
public class WebSocketConfig {
	//在本地调试时需要加上配置,部署到服务器注释掉此段配置,使用外部tomcat的包,不需要这个配置
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

SocketServer(webSocket服务类)

@Slf4j
@ServerEndpoint(value = "/socketServer")
@Component
public class SocketServer {
    private static Logger logger = LoggerFactory.getLogger(SocketServer.class);
    /**
     * 客服接口发送消息
     */
    private static final String CLIENT_SENT_MESSAGE = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN";

    private Session session;
    private static Map<Session, String> sessionPool = new HashMap<>();
    private static Map<String, String> sessionIds = new HashMap<>();
    //concurrent包的线程安全Set,用来存放每个客户端对应的SocketServer对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
    private static CopyOnWriteArraySet<SocketServer> socketServers = new CopyOnWriteArraySet<SocketServer>();
    @Autowired
    private ImService imService;

    private static RedisService redisService;
    private static RedisUtil redisUtil;
    private static UserService userService;

	/**
     * 这里需要注意的是,websocket的对象是不受spring管理的
     * 在websocket连接的时候新建一个websocket对象,这个新建的对象不受spring管理,所以会注入不进去,所以通过如下方法来注入,
     * 用受管理的对象,注入依赖对象,并且将其放到websocket的static成员种,这样其他对象就可以调用了
     */
    @Autowired
    private void setRedisUtil(RedisUtil redisUtil, RedisService redisService, UserService userService) {
        SocketServer.redisUtil = redisUtil;
        SocketServer.redisService = redisService;
        SocketServer.userService = userService;
    }

    //连接时存放当前的websocket对象
    @OnOpen
    public void open(Session session) {
        this.session = session;
        socketServers.add(this);

        logger.info("========>【IM会话连接成功,有新的客户端 sessionId:{} 加入】<========", session.getId());

        try {
            logger.info("Connection Successfully:Ready Send Msg");
            //异步
            this.session.getAsyncRemote().sendText("【From Server】:Connection Successfully!");
        } catch (Exception e) {
            logger.error("Client {} Connection Successfully,CallBack Msg Exception",session.getId());
            e.printStackTrace();
        }

        logger.info("检查缓存中所有微信公众号发过来的消息(单个用户最多缓存十条,可在后台系统配置条数) ");

        List<User> userList = userService.selectList(new EntityWrapper<User>().eq("is_deleted", DeleteStatus.NOT_DELETED.getCode()));
        //查询所有用户,在websocket建立连接时把redis缓存的所有用户消息都发送到前端
        if(!ParamUtil.isNullOrEmptyOrZero(userList)){
            for (User user : userList) {
                String openid = user.getOpenid();
                List userMsgsFromWx = redisService.getUserMsgsFromWx(openid);
                //该用户没有消息缓存
                if (ParamUtil.isNullOrEmptyOrZero(userMsgsFromWx)) {
                    continue;
                }
                logger.info("用户 {} 缓存的消息条数 {},消息内容 【{}】", openid, userMsgsFromWx.size(), userMsgsFromWx);

                //发送该用户的所有缓存消息到所有 websocket 客户端
                for (Object msgsFromWx : userMsgsFromWx) {
                    sendMessage(msgsFromWx.toString());
                }
                //发送完消息,销毁该用户在缓存中的消息
                redisService.deleteUserMsgsFromWx(openid);
            }
        }
    }
    //接收前端(客户端)的消息
    @OnMessage
    public void onMessage(String message) {
        JSONObject messageObj = JSONObject.parseObject(message);
        String content = messageObj.getString("content");
        String msgType = messageObj.getString("msgType");

        logger.info("========>【来自客户端 sessionid {} 的消息 {}<========】", session.getId(), message);

        //维持心跳连接(系统消息)
        if ("ping".equals(content) && MsgType.SYSTEM_MSG.getCode().equals(Integer.valueOf(msgType)) && !ParamUtil.isNullOrEmptyOrZero(session)) {
            try {
                logger.info("from client request : ping");
                session.getAsyncRemote().sendText("from server response : pong");
                return;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        String openid = messageObj.getString("openid");

        //通过客服接口发送前端输入的信息给微信关注公众号的用户
        // step1:获取token
        String wxAppId = SysConfigUtils.get(CommonSystemConfig.class).getWxAppId();
        //从redis获取微信公众号的access_token
        BasicAccessToken wxAccessToken = redisUtil.getWxAccessToken(wxAppId);

        if (ParamUtil.isNullOrEmptyOrZero(wxAccessToken )) {
            LeaseException.throwSystemException(LeaseExceEnums.WX_TOKEN_EXCEPTION);
        }
        String accessToken = wxAccessToken .getAccess_token();
        if (ParamUtil.isNullOrEmptyOrZero(accessToken)) {
            LeaseException.throwSystemException(LeaseExceEnums.WX_TOKEN_EXCEPTION);
        }

        logger.info("微信公众号 accessToken:{}", accessToken);
        
        String client_url = CLIENT_SENT_MESSAGE.replace("ACCESS_TOKEN", accessToken);
        //step2:通过小程序的openid获取unionid,
        String openIdToUnionId = redisService.getOpenIdToUnionId(openid);
        //step3:通过unionid获取公众号openid
        String wxUnionIdToOpenid = redisUtil.getWxUnionIdToOpenid(openIdToUnionId );

        logger.info("小程序用户 openid {},微信公众号 unionid {},微信公众号 openid {}", openid, openIdToUnionId, wxUnionIdToOpenid);

        //文本消息,msgType:text
        String result = "{\n" +
                "    \"touser\":\"" + wxUnionIdToOpenid + "\",\n" +
                "    \"msgtype\":\"text\",\n" +
                "    \"text\":\n" +
                "    {\n" +
                "         \"content\":\"" + content + "\"\n" +
                "    }\n" +
                "}";

        logger.info("调用客服接口发送消息给微信公众号的用户 {}", wxUnionIdToOpenid);

        //step4:通过微信公众号客服接口发送消息给小程序用户
        String responseStr = null;
        try {
            responseStr = HttpUtil.executePost(client_url, result);
        }catch (Exception e){
            e.printStackTrace();
        }
        JSONObject jsonObject = JSONObject.parseObject(responseStr);
        Integer errcode = jsonObject.getInteger("errcode");

        logger.info("调用微信客服接口发送消息响应 :{}", jsonObject);

		//这里后期会将消息体定义一个结构体,统一调用
        if (errcode != 0) {
            messageObj.put("content", "发送给微信公众号用户消息失败,请重试!");
            messageObj.put("msgType",MsgType.SYSTEM_MSG.getCode());
            sendMessage(messageObj.toString());
        } else {
            messageObj.put("content", "发送给微信公众号用户消息成功!");
            messageObj.put("msgType",MsgType.SYSTEM_MSG.getCode());
            sendMessage(messageObj.toString());
        }
    }

    @OnClose
    public void onClose( ) {
        //移除当前的websocket对象
        socketServers.remove(this);
        logger.info("========>【关闭与客户端 {} 的websocket连接】<========", session.getId());
    }

    @OnError
    public void onError(Session session, Throwable error) {
        session.getRequestParameterMap();
        logger.error("error message: {}", error.getMessage());
        logger.error("error error msg:{}", error.toString());
        logger.error("========>【客户端 {} websocket 连接 发生未知错误】<========", session.getId());
        error.printStackTrace();
    }

    //发送消息给前端的每一个websocket连接
    public static void sendMessage(String message) {
        for (SocketServer socketSet : socketServers) {
            try {
                logger.info("========>【收到微信公众号消息 {},发送给客户端 sessionId {}】<========", message, socketSet.session.getId());
                socketSet.session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

redisService:

 //缓存微信公众号发过来的十条消息到redis缓存,防止websocket断开重连的时候接收不到消息
    public void cacheFromWxUserMsg(String openid, JSONObject msgObj) {
        if (ParamUtil.isNullOrEmptyOrZero(openid) || ParamUtil.isNullOrEmptyOrZero(msgObj)) {
            return;
        }
        //添加在当前元素的右边
        redisTemplate.opsForList().rightPush(openid, msgObj);
    }

    //更新用户的消息内容,超过十条就把最开始的那条删除
    public void updateUserMsgFromWx(String openid) {
        if (ParamUtil.isNullOrEmptyOrZero(openid)) {
            return;
        }
        //获取配置的消息条数,可在后台修改配置增加或者减少
        String cacheUserMsgSize = SysConfigUtils.get(CommonSystemConfig.class).getCacheUserMsgSize();
        if (redisTemplate.opsForList().size(openid) > Long.valueOf(cacheUserMsgSize)) {
            //弹出list左边的元素,也就是时间最久的消息
            redisTemplate.opsForList().leftPop(openid);
        }
    }
    //获取指定openid缓存的消息总数
    public List getUserMsgsFromWx(String openid) {
        List msgList = redisTemplate.opsForList().range(openid, 0, -1);
        if (msgList.size() <= 0) {
            return null;
        }
        return msgList;
    }

    //在websocket链接成功,发送到web前端以后,销毁消息
    public void deleteUserMsgsFromWx(String openid) {
        if (ParamUtil.isNullOrEmptyOrZero(openid)) {
            return;
        }
        redisTemplate.delete(openid);
        logger.info("============【删除用户 {} 的消息内容缓存】============", openid);
    }

WechatController(微信消息处理类):

@Api(description = "微信服务器校验接口")
@RestController
@EnableSwagger2
@RequestMapping("/wx")
@Slf4j
public class WeChatController extends BaseController {
    /*
     * 微信公众号服务器
     */
    @Autowired
    private ImService imService;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private RedisService redisService;
    @Autowired
    private UserService userService;
    
//通过微信公众号openid获取微信公众号UnionId
private static final String GET_WX_UNIONID_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";
/**
 * 客服发送消息
 */
private static final String CLIENT_SENT_MESSAGE = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN";

private static final String WX_SEND_TEMPLATE_MSG = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN";


//微信公众号验证
@IgnoreLogin//无需登录
@GetMapping("/system")
public void doGet(HttpServletRequest request, HttpServletResponse response) throws AesException {

    //获取参数值
    String signature = request.getParameter("signature");
    String timeStamp = request.getParameter("timestamp");
    String nonce = request.getParameter("nonce");
    String echostr = request.getParameter("echostr");
    log.info("微信公众号服务器验证请求:signature {},timestamp {},nonce {},echostr {}", signature, timeStamp, nonce, echostr);
    //微信服务器校验的token,可在系统配置
    String token = SysConfigUtils.get(CommonSystemConfig.class).getWxToken();
    log.info("服务器保存的token:{}", token);

    PrintWriter out = null;
    try {
        out = response.getWriter();
        //开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
        if (WXPublicUtils.verifyUrl(signature, timeStamp, nonce)) {
            out.println(echostr);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        out.close();
    }
}

//接收公众号post请求消息,
//接收到的消息直接返回给前端页面
@IgnoreLogin
@PostMapping("/system")
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, DocumentException {

    JSONObject messageObj = new JSONObject();
    response.setCharacterEncoding("utf-8");
    PrintWriter out = null;
    //将微信请求xml转为map格式,获取所需的参数
    Map<String, String> map = XMLUtil.xml2Map(request);
    String ToUserName = map.get("ToUserName");//开发者
    String FromUserName = map.get("FromUserName");//微信用户openID
    String MsgType = map.get("MsgType");//消息类型
    String event = map.get("Event");//事件类型,subscribe :关注事件,unsubscribe:取消事件
    String createTime = map.get("CreateTime");//消息的创建时间

    BasicAccessToken wxAccessToken= redisUtil.getWxAccessToken(SysConfigUtils.get(CommonSystemConfig.class).getWxAppId());
    String accessToken = null;
    if (!ParamUtil.isNullOrEmptyOrZero(wxAccessToken)) {
        accessToken = wxAccessToken.getAccess_token();
        log.info("微信公众号的 accessToken :{}", accessToken);

    }

    String wxUnionIdUrl = GET_WX_UNIONID_URL.replace("ACCESS_TOKEN", accessToken).replace("OPENID", FromUserName);
    String wxUnionIdRes = null;
    try {
        wxUnionIdRes = HttpClientUtil.httpRequest(wxUnionIdUrl, null, "", HttpClientUtil.MethodType.METHOD_GET);
    }catch (Exception e){
        e.printStackTrace();
    }

    log.info("通过微信公众号 openId 获取 unionId响应 :", wxUnionIdRes);
    JSONObject jsonObject = JSONObject.parseObject(wxUnionIdRes);
    Object unionid = jsonObject.get("unionid");
    String unionIdToOpenid = null;
    if (!ParamUtil.isNullOrEmptyOrZero(unionid)) {

        log.info("通过微信公众号 openId 获取 unionId {}", unionid);
        //缓存获取到的unionId与微信公众号openid的关联
        redisUtil.cacheWxUnionIdToOpenid(unionid.toString(), FromUserName);

        //通过unionid获取缓存里的小程序openid
        unionIdToOpenid = redisService.getUnionIdToOpenid(unionid.toString());

    }
    String message = null;
    log.info("map:{}", map);
    log.info("----接收微信公众号的消息 end:------");

    //判断接收的内容类型
    switch (MsgType) {
        case "event":
            if (event.equals("subscribe")) {
                //如果是用户的关注事件,就发送消息到用户进入小程序
                String client_url = CLIENT_SENT_MESSAGE.replace("ACCESS_TOKEN", accessToken);

                message = SysConfigUtils.get(CommonSystemConfig.class).getMiniProgramSubscribeText();
                log.info("小程序用户关注公众号,小程序推送文本:{} ", message);
                //文本消息json
                HashMap<Object, Object> contentMap = new HashMap<>();
                JSONObject msgJson = new JSONObject();
                msgJson.put("touser", FromUserName);
                msgJson.put("msgtype", "text");
                contentMap.put("content", message);
                msgJson.put("text", contentMap);
                String responseStr = null;
                try {
                    responseStr = HttpClientUtil.httpRequest(client_url, null, msgJson.toString(), HttpClientUtil.MethodType.METHOD_POST);
                } catch (Exception e) {
                    e.printStackTrace();
                }

                JSONObject sendMsgRes = JSONObject.parseObject(responseStr);
                Integer errcode = sendMsgRes.getInteger("errcode");

                log.info("用户关注微信公众号,发送小程序文字链给微信公众号的用户 {},响应:{}", FromUserName, responseStr);

                if (errcode != 0) {
                    messageObj.put("content", "发送小程序文字链给微信公众号用户失败,请重试!");
                    messageObj.put("openid", bshUnionIdToOpenid);
                    messageObj.put("msgType", com.gizwits.lease.bsenums.MsgType.SYSTEM_MSG.getCode());
                    SocketServer.sendMessage(messageObj.toString());
                } else {
                    messageObj.put("content", "发送小程序文字链给微信公众号用户成功!");
                    messageObj.put("openid", bshUnionIdToOpenid);
                    messageObj.put("msgType", com.gizwits.lease.bsenums.MsgType.SYSTEM_MSG.getCode());
                    SocketServer.sendMessage(messageObj.toString());
                }
            }
            break;
        case "text":
            String Content = map.get("Content");//文本内容
            String textMsg = Content + bshUnionIdToOpenid;

            //发送文本给前端
            messageObj.put("openid", bshUnionIdToOpenid);
            messageObj.put("content", Content);
            messageObj.put("msgType", com.gizwits.lease.bsenums.MsgType.USER_MSG.getCode());
            messageObj.put("msgTime",createTime);

            User user = userService.selectOne(new EntityWrapper<User>()
                    .eq("openid", bshUnionIdToOpenid).eq("is_deleted", DeleteStatus.NOT_DELETED.getCode()));

            if(!ParamUtil.isNullOrEmptyOrZero(user)){
                String username = user.getUsername();
                log.info("当前发送消息到服务端的微信用户 :{}", username);
                messageObj.put("fromUser",username);
            }

            //保存微信公众号的openid以及对应的消息,最多十条
            redisService.cacheFromWxUserMsg(unionIdToOpenid, messageObj);
            
            //更新缓存里当前用户的消息内容,最多十条
            redisService.updateUserMsgFromWx(unionIdToOpenid);
            SocketServer.sendMessage(messageObj.toString());

            log.info("接收来自微信公众号用户 {} 的消息 {} 发送文本消息给前端展示", unionIdToOpenid, Content);
            break;
        case "image"://图片
            break;
        case "voice"://音频
            break;
    }
}

微信校验

public class WXPublicUtils {

    private static String WX_TOKEN = "6FwrtuhdgMiGfe4ZoSjbOyWpE0XINmUk";

    /**
     * 验证Token
     *
     * @param msgSignature 签名串,对应URL参数的signature
     * @param timeStamp    时间戳,对应URL参数的timestamp
     * @param nonce        随机串,对应URL参数的nonce
     * @return 是否为安全签名
     * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
     */
    public static boolean verifyUrl(String msgSignature, String timeStamp, String nonce)
            throws AesException {
        String signature = SHA1.getSHA1(WX_TOKEN, timeStamp, nonce);
        if (!signature.equals(msgSignature)) {
            throw new AesException(AesException.ValidateSignatureError);
        }
        return true;
    }

}

猜你喜欢

转载自blog.csdn.net/qq_37189624/article/details/84240836