先看效果:

我的门户地址 你可以亲自试一试

点击试一下,微信扫码登陆,无感体验

门户页面登陆效果

微信扫码登陆

已经关注的用户返回

已关注的用户-公众号消息反馈
##首次扫码关注的信息反馈
首次关注的用户-公众号消息反馈

说明:

  • 页面是用vue3+vite做的,在移动端展示效果也好
  • 生成环境用的是wss协议
  • 无论是已经关注还是首次关注的,扫码均可登陆

前提准备:

必要的

认证的服务号类型的公众号

非必要

自己的域名 域名证书 等 生产环境https页面用的是wss协议

代码部分:

后端

依赖 本人用的是3.2.5

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
</parent>

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
      <groupId>com.github.binarywang</groupId>
      <artifactId>weixin-java-mp</artifactId>
      <version>4.4.0</version>
 </dependency>
 <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-websocket</artifactId>
 </dependency>

配置类

公众号账号配置

/**
 * @author
 */
@Data
@ConfigurationProperties(prefix = "cc.wechat.mp")
public class WeChatMpProperties  {
    /**
     * appId
     */
    private String appId;
    /**
     * secret
     */
    private String secret;
    /**
     * token
     */
    private String token;
    /**
     * aesKey
     */
    private String aesKey;
    /**
     * aesKey
     */
    private String baseHost;

}

公众号配置

@AllArgsConstructor
@Configuration
@EnableConfigurationProperties(WeChatMpProperties.class)
public class WxMpConfiguration {

    private final ScanHandler scanHandler;

    private final WeChatMpProperties weChatMpProperties;

    @Bean
    public WxMpService wxMpService() {
        WxMpService service = new WxMpServiceImpl();
        service.setMultiConfigStorages(List.of(weChatMpProperties)
                .stream().map(a -> {
                    WxMpDefaultConfigImpl configStorage;
                    configStorage = new WxMpDefaultConfigImpl();
                    configStorage.setAppId(a.getAppId());
                    configStorage.setSecret(a.getSecret());
                    configStorage.setToken(a.getToken());
                    configStorage.setAesKey(a.getAesKey());
                    return configStorage;
                }).collect(Collectors.toMap(WxMpDefaultConfigImpl::getAppId, a -> a, (o, n) -> o)));
        return service;
    }

    @Bean
    public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
        final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
        // 扫码事件
        newRouter.rule().async(false).msgType(EVENT).event(SCAN).handler(scanHandler).end();

        return newRouter;
    }

}

扫码拦截器(本人没有用到,供参考)

@Slf4j
@Component
public class ScanHandler implements WxMpMessageHandler {

    @Autowired
    private SystemBaseProperties systemBaseProperties;

    public static final String URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";


    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,
                                    WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
        String openId = wxMpXmlMessage.getFromUser();
        Integer code;
        log.info("handle openId:{}", openId);
        try {
            String eventKey = wxMpXmlMessage.getEventKey();
            eventKey = eventKey.replace("qrscene_", "");
            code = Integer.parseInt(eventKey);
        } catch (Exception e) {
            System.err.println("getEventKey error eventKey:{}" + wxMpXmlMessage.getEventKey() + e);
            return null;
        }

        // 推送链接让用户授权
        String authorizeUrl = String.format(URL, wxMpService.getWxMpConfigStorage().getAppId(), URLEncoder.encode(systemBaseProperties.getBaseHost() + "/api/open/wx/callBack"));
        String content = "请点击登录:<a href=\"" + authorizeUrl + "\">登录</a>";
        WxMpXmlOutTextMessage m = WxMpXmlOutMessage.TEXT().content(content)
                .fromUser(wxMpXmlMessage.getToUser()).toUser(wxMpXmlMessage.getFromUser())
                .build();
        return m;
    }

}

控制层

wx扫码登录

@GetMapping("/wx-scan-code")
    @Operation(summary = "wx扫码登录")
    public WxScanCodeRespVO wxcanode() throws WxErrorException {
        String code = CodeUtil.getRandomCode(10);
        WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, 60);
        String base64 = qrCodeService.generateQrCodeBase64(new GenerateQrCodeReqVO(wxMpQrCodeTicket.getUrl()));
        return new WxScanCodeRespVO(base64, wxMpQrCodeTicket.getExpireSeconds(), code);
    }

公众号鉴权监听控制层(这个要自己在官网配置,不多叙述)

@GetMapping(value = "/auth")
    public void authGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (StringUtils.isNotBlank(request.getParameter("signature"))) {
            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);
            if (SignUtil.checkSignature(signature, timestamp, nonce)) {
                log.info("数据源为微信后台,将echostr[{}]返回!", echostr);
                response.getOutputStream().println(echostr);
            }
        }
    }

    @PostMapping(value = "/auth")
    public  void authPost(HttpServletResponse response,
                           HttpServletRequest request,
                           @RequestBody String body) throws IOException {
        log.info("body -> {}", body);

        String sendMsg = wxService.processRequest(body);
        response.setCharacterEncoding("UTF-8");
        // 将sendMsg写入到response的body中
        response.getWriter().print(sendMsg);;

    }

业务层

QrCodeServiceImpl

@Slf4j
@Service
public class QrCodeServiceImpl implements IQrCodeService {
    // 中间图片占二维码的最大比例,降低此值以提高扫描成功率
    private static final double MAX_LOGO_RATIO = 0.8;
    // 中间图片周围的白色边框比例
    private static final double LOGO_PADDING_RATIO = 0.1;
    // 强制使用最高容错级别
    private static final ErrorCorrectionLevel ERROR_CORRECTION_LEVEL = ErrorCorrectionLevel.H;

    @Autowired
    private IRedisService redisService;

    /**
     * 生成带中间图片的二维码并返回Base64编码
     *
     * @param reqVO
     * @return 二维码图片的Base64编码
     */
    @Override
    public String generateQrCodeBase64(GenerateQrCodeReqVO reqVO) {

        try {
            log.info("开始生成二维码,参数: {}", reqVO);
            String hash = HashUtils.calculateSHA256FromToString(reqVO);
            String redisKey = RedisPreKey.CACHE_QR_CODE + hash;
            Boolean hasKey = redisService.hasKey(redisKey);
            if (Boolean.TRUE.equals(hasKey)) {
                return String.valueOf(redisService.get(redisKey));
            }

            // 配置二维码参数
            QrConfig config = new QrConfig(reqVO.getWidth(), reqVO.getHeight());
            config.setMargin(reqVO.getMargin());
            config.setForeColor(reqVO.getForeColor());
            config.setBackColor(reqVO.getBackgroundColor());

            // 设置最高容错级别
            config.setErrorCorrection(ERROR_CORRECTION_LEVEL);

            BufferedImage qrImage = null;

            if (StringUtils.isNotBlank(reqVO.getImageUrl())) {
                // 下载中间图片
                Image logoImage = downloadImage(reqVO.getImageUrl());

                // 计算合适的中间图片尺寸
                int logoWidth = (int) (reqVO.getWidth() * MAX_LOGO_RATIO);
                int logoHeight = (int) (reqVO.getHeight() * MAX_LOGO_RATIO);

                // 缩放图片
                logoImage = logoImage.getScaledInstance(
                        logoWidth,
                        logoHeight,
                        Image.SCALE_SMOOTH
                );

                // 将图片转换为带白色边框的BufferedImage
                BufferedImage logoWithPadding = addWhitePadding(
                        logoImage,
                        logoWidth,
                        logoHeight,
                        (int) (logoWidth * LOGO_PADDING_RATIO)
                );

                // 设置中间图片
                config.setImg(logoWithPadding);

                log.info("中间图片处理完成,尺寸: {}x{},带边框尺寸: {}x{}",
                        logoWidth, logoHeight,
                        logoWithPadding.getWidth(),
                        logoWithPadding.getHeight()
                );
            }

            // 生成带中间图片的二维码
            qrImage = QrCodeUtil.generate(reqVO.getContent(), config);

            // 转换为Base64
            String base64Result = imageToBase64(qrImage);
            log.info("二维码生成成功,尺寸: {}x{}", qrImage.getWidth(), qrImage.getHeight());
            redisService.set(redisKey, base64Result, 1, TimeUnit.DAYS);
            return base64Result;
        } catch (Exception e) {
            log.error("生成二维码失败", e);
            return null;
        }
    }

    /**
     * 下载远程图片并转换为Image对象
     */
    private Image downloadImage(String imageUrl) throws IOException {
        try (InputStream inputStream = HttpUtil.createGet(imageUrl).execute().bodyStream()) {
            return ImageIO.read(inputStream);
        }
    }

    /**
     * 为图片添加白色边框
     */
    private BufferedImage addWhitePadding(Image image, int width, int height, int padding) {
        BufferedImage result = new BufferedImage(
                width + padding * 2,
                height + padding * 2,
                BufferedImage.TYPE_INT_ARGB
        );

        Graphics2D g2d = result.createGraphics();
        try {
            // 填充白色背景
            g2d.setColor(Color.WHITE);
            g2d.fillRect(0, 0, result.getWidth(), result.getHeight());

            // 绘制图片到中间
            g2d.drawImage(
                    image,
                    padding,
                    padding,
                    width,
                    height,
                    null
            );
        } finally {
            g2d.dispose();
        }

        return result;
    }

    /**
     * 将图片转换为Base64编码
     */
    private String imageToBase64(BufferedImage image) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            ImageIO.write(image, "png", baos);
            byte[] bytes = baos.toByteArray();
            return Base64.encode(bytes);
        }
    }
}

WxServiceImpl

@Slf4j
@Service
public class WxServiceImpl implements IWxService {


    @Autowired
    private IChatService chatService;

    @Autowired
    private IUserService userService;

    @Autowired
    private MyEventPubLisher myEventPubLisher;

    @Autowired
    private IRedisService redisService;

    @Autowired
    private SystemBaseProperties systemBaseProperties;

    public static final String URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";

    @Autowired
    private WxMpService wxMpService;


    @Override
    public String processRequest(String requestBody) {
        ReceivedWXMessage receivedWXMessage = XMLUtil.parseXML(requestBody, ReceivedWXMessage.class);
        SendWXMessage sendWXMessage = BeanHelper.copyProperties(receivedWXMessage, SendWXMessage.class);
        String question = receivedWXMessage.getContent();
        String fromUserName = receivedWXMessage.getFromUserName();
        String msgType = receivedWXMessage.getMsgType();
        String event = receivedWXMessage.getEvent();
        String eventKey = receivedWXMessage.getEventKey();
        log.info("wx processRequest mgType -> {}, event -> {}, eventKey -> {}", msgType, event, eventKey);
        if ("event".equals(msgType)) {
            // 关注事件和扫码事件
            if ("SCAN".equals(event)) {
                // 先找到该用户
                User user = userService.getUserByOpenId(fromUserName);
                if (user == null) {
                    user = userService.addPortalUser(fromUserName);
                }
                log.info("user -> {}", user);
                if (StringUtils.isNotBlank(eventKey) && eventKey.length() == 10) {
                    userService.wxScanCodeLogin(eventKey, user);
                    // 推送链接让用户授权
//                    String authorizeUrl = String.format(URL, wxMpService.getWxMpConfigStorage().getAppId(), URLEncoder.encode(systemBaseProperties.getBaseHost() + "/api/open/wx/callBack"));
//                    String content = "请点击登录:<a href=\"" + authorizeUrl + "\">登录</a>";
                    String content = "扫码登陆成功,请返回浏览器";
                    sendWXMessage.setContent(content);
                    sendWXMessage.setMsgType("text");
                    sendWXMessage.setCreateTime(System.currentTimeMillis() / 1000);
                    sendWXMessage.setToUserName(receivedWXMessage.getFromUserName());
                    sendWXMessage.setFromUserName(receivedWXMessage.getToUserName());
                    String xml = XMLUtil.genXML(sendWXMessage, "xml");
                    log.info("xml -> {}", xml);
                    return xml;
                }

            } else if ("subscribe".equals(event)) {
                String content = "感谢你的关注";
                if (StringUtils.isNotBlank(eventKey) && eventKey.startsWith("qrscene_")) {
                    // 扫码关注
                    String code = eventKey.replace("qrscene_", "");
                    User user = userService.getUserByOpenId(fromUserName);
                    if (user == null) {
                        user = userService.addPortalUser(fromUserName);
                    }
                    userService.wxScanCodeLogin(code, user);
                    content += ",扫码登陆成功,请返回浏览器";
                }
                sendWXMessage.setContent(content);
                sendWXMessage.setMsgType("text");
                sendWXMessage.setCreateTime(System.currentTimeMillis() / 1000);
                sendWXMessage.setToUserName(receivedWXMessage.getFromUserName());
                sendWXMessage.setFromUserName(receivedWXMessage.getToUserName());
                String xml = XMLUtil.genXML(sendWXMessage, "xml");
                log.info("xml -> {}", xml);
                return xml;
            } else if ("unsubscribe".equals(event)) {
                log.warn("用户取消了关注 -> fromUserName:{}", fromUserName);
            }
        } else if ("text".equals(msgType)) {
            String lastQuestionKey = String.format("cache:chat:lastQuestion:%s:%s", fromUserName, question.hashCode());
            Boolean hasKey = redisService.hasKey(lastQuestionKey);
            String call = null;
            boolean dateTimeShow = false;
            if (hasKey) {
                call = redisService.get(lastQuestionKey).toString();
            } else {
                List<ChatMessageReqVO> messages = userService.getContext(receivedWXMessage.getFromUserName());
                dateTimeShow = DtsUtil.dateTimeShow(messages);
                messages.add(new ChatMessageReqVO(question, "user", dateTimeShow));
                call = chatService.sendMessage(messages);
            }
            boolean success = StringUtils.isNotBlank(call);
            // 去掉call中所有的换行
            call = success ? call : "抱歉,我暂时无法回答你的问题。";
            sendWXMessage.setCreateTime(System.currentTimeMillis() / 1000);
            sendWXMessage.setContent(call);
            sendWXMessage.setToUserName(receivedWXMessage.getFromUserName());
            sendWXMessage.setFromUserName(receivedWXMessage.getToUserName());
            String xml = XMLUtil.genXML(sendWXMessage, "xml");
            log.info("xml -> {}", xml);
            LifyMsgModel lifyMsgModel = new LifyMsgModel(receivedWXMessage.getFromUserName(), question, receivedWXMessage, sendWXMessage, dateTimeShow);
            // 发布事件
            myEventPubLisher.pushLifyMsgEvent(JSON.toJSONString(lifyMsgModel));
            return xml;
        }

        return null;

    }
}

POJO

@Data
@Schema(name = "微信扫码结果")
@AllArgsConstructor
public class WxScanCodeRespVO {
    @Schema(name = "base64编码的图片")
    private String base64Code;
    @Schema(name = "过期时间")
    private int expireSeconds;
    @Schema(name = "二维码ID")
    private String qrCodeId;
}
@Data
@NoArgsConstructor
public class WebSocketMessageModel<T> {
    @NotBlank(message = "消息类型不能为空")
    private String type;
    private String message;
    private T data;

    public WebSocketMessageModel(WebSocketMessageTypeEnum webSocketMessageTypeEnum, String message) {
        this.type = webSocketMessageTypeEnum.getType();
        this.message = message;
    }

    public WebSocketMessageModel(WebSocketMessageTypeEnum webSocketMessageTypeEnum, T data) {
        this.type = webSocketMessageTypeEnum.getType();
        this.data = data;
        this.message = "success";
    }

}
@Getter
@Schema(name = "WebSocket消息类型", description = "WebSocket消息类型枚举")
public enum WebSocketMessageTypeEnum {

    /**
     * 全局通知
     */
    GLOBAL_NOTIFICATION("globalNotification", "全局通知"),
    /**
     * 心跳
     */
    BASE_HEARTBEAT("baseHeartbeat", "基础心跳"),
    /**
     * 我的照片点评数量
     */
    MY_PHOTO_REVIEW_COUNTS("myPhotoReviewCounts", "我的照片点评数量"),
    /**
     * 微信扫二维码登录
     */
    WX_SCAN_QR_CODE_LOGIN("wxScanQrCodeLogin", "微信扫二维码登录"),

    ;



    private String type;
    private String name;

    WebSocketMessageTypeEnum(String type, String name) {
        this.type = type;
        this.name = name;
    }
}

websocket相关

websocket处理器 我自己用的jwt 你替换自己的认证

@Slf4j
@Component
@SuppressWarnings("all")
public class WebSocketHandler extends TextWebSocketHandler {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 使用ConcurrentHashMap存储所有在线会话
     * key为sessionId,value为WebSocketSession
     */
    private static final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
    /**
     * 存储用户和sessionIds的映射关系
     */
    private static final ConcurrentHashMap<Long, List<String>> userMap = new ConcurrentHashMap<>();
    /**
     * 微信扫码登陆 qrCodeId和sessionId的映射关系
     */
    private static final ConcurrentHashMap<String, String> wxScanQrCodeLoginMap = new ConcurrentHashMap<>();

    /**
     * 接收消息并处理
     *
     * @param session     会话
     * @param textMessage 消息
     * @throws Exception 异常
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws Exception {
        String payload = textMessage.getPayload();
        String sessionId = session.getId();
        WebSocketMessageModel wsmm = JSON.parseObject(payload, WebSocketMessageModel.class);

        if (wsmm == null) {
            log.error("收到了空白的WebSocketMessageModel-->{}", payload);
            return;
        }
        String type = wsmm.getType();
        Object message = wsmm.getMessage();
        // 处理心跳消息
        if (WebSocketMessageTypeEnum.BASE_HEARTBEAT.getType().equals(type)) {
            if ("ping".equals(String.valueOf(message))) {
                session.sendMessage(new TextMessage(JSON.toJSONString(WebSocketMessageModelEnum.PONG.getWebSocketMessageModel())));
                return;
            }
        } else if (WebSocketMessageTypeEnum.WX_SCAN_QR_CODE_LOGIN.getType().equals(type)) {
            String wxScanQrCodeLogin = String.valueOf(message);
            wxScanQrCodeLoginMap.put(wxScanQrCodeLogin, sessionId);
            return;
        }

        log.error("收到了无法识别的WebSocketMessageModel类型-->{}", payload);
    }

    /**
     * 处理连接关闭
     *
     * @param session 会话
     * @param status  关闭状态
     * @throws Exception 异常
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String sessionId = session.getId();
        sessions.remove(sessionId);
        // 从 userMap 中移除对应的会话 ID
        userMap.forEach((userId, sessionIds) -> {
            sessionIds.remove(sessionId);
            if (sessionIds.isEmpty()) {
                userMap.remove(userId);
            }
        });
        // 从 wxScanQrCodeLoginMap 中移除对应的会话 ID
        wxScanQrCodeLoginMap.entrySet().removeIf(entry -> entry.getValue().equals(sessionId));
        broadcastOnlineCount();
        log.info("连接关闭: " + sessionId + ",当前在线连接数: " + sessions.size() + ",当前在线用户数: " + userMap.size());
    }

    /**
     * 处理传输错误
     *
     * @param session   会话
     * @param exception 错误
     * @throws Exception 异常
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        String sessionId = session.getId();
        log.error("传输错误发生在 session: " + sessionId + ", 错误: " + exception.getMessage());
        if (session.isOpen()) {
            session.close();
        }
        sessions.remove(sessionId);
        broadcastOnlineCount();
    }

    /**
     * 处理连接建立
     *
     * @param session 会话
     * @throws Exception 异常
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String sessionId = session.getId();
        String authToken = extractAuthTokenFromUri(session.getUri().toString());
        if (StringUtils.isNotBlank(authToken)) {
            try {
                Payload<UserInfo> payload = JwtUtils.getInfoFromToken(authToken, jwtProperties.getPublicKey(), UserInfo.class);
                Long userId = payload.getUserInfo().getId();
                if (userId != null) {
                    List<String> orDefault = userMap.computeIfAbsent(userId, k -> new CopyOnWriteArrayList<>());
                    orDefault.add(sessionId);
                }
            } catch (Exception e) {
                log.error("WebSocket解析token失败-->{}", e.getMessage());
            }
        }
        sessions.put(sessionId, session);
        broadcastOnlineCount();
        log.info("新连接建立: " + sessionId + ",当前在线连接数: " + sessions.size() + ",当前在线用户数: " + userMap.size());
    }

    /**
     * 从URI中提取auth参数
     */
    private String extractAuthTokenFromUri(String uri) {
        int index = uri.indexOf("auth=");
        if (index != -1) {
            String param = uri.substring(index + "auth=".length());
            // 处理可能的其他参数(如&符号)
            return param.split("&")[0];
        }
        return null;
    }

    // 广播在线人数到所有客户端
    private void broadcastOnlineCount() {
        String message = "{\"type\":\"onlineCount\",\"count\":" + sessions.size() + "}";
        TextMessage textMessage = new TextMessage(message);

        sessions.forEach((sessionId, session) -> {
            try {
                if (session.isOpen()) {
                    session.sendMessage(textMessage);
                }
            } catch (IOException e) {
                log.info("发送在线人数消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
                try {
                    session.close();
                } catch (IOException ex) {
                    // 忽略
                }
                sessions.remove(sessionId);
            }
        });
    }

    // 获取所有会话
    public static ConcurrentHashMap<String, WebSocketSession> getSessions() {
        return sessions;
    }

    // 获取所有用户和sessionIds的映射关系
    public static ConcurrentHashMap<Long, List<String>> getUserMap() {
        return userMap;
    }

    // 获取所有微信扫码登陆 qrCodeId和sessionId的映射关系
    public static ConcurrentHashMap<String, String> getWxScanQrCodeLoginMap() {
        return wxScanQrCodeLoginMap;
    }

    // 获取总体信息
    public static JSONObject getGeneralInfo() {
        return new JSONObject()
                .set("sessionsSzie", getSessions().size())
                .set("userMapSize", getUserMap().size())
                .set("wxScanQrCodeLoginMapSize", getWxScanQrCodeLoginMap().size());
    }

    /**
     * 广播消息到所有客户端
     *
     * @param message 要广播的消息
     */
    public int broadcastMessage(WebSocketMessageModel message) {
        TextMessage textMessage = new TextMessage(JSON.toJSONString(message));
        AtomicInteger count = new AtomicInteger();
        sessions.forEach((sessionId, session) -> {
            try {
                if (session.isOpen()) {
                    session.sendMessage(textMessage);
                    count.getAndIncrement();
                }
            } catch (IOException e) {
                log.info("广播消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
                try {
                    session.close();
                } catch (IOException ex) {
                    log.error("关闭 session 失败: " + sessionId + ", 错误: " + ex.getMessage(), ex);
                }
                sessions.remove(sessionId);
            }
        });
        return count.get();
    }

    /**
     * 发送消息到指定会话
     *
     * @param sessionId 会话 ID
     * @param message   要发送的消息
     */
    public int sendMessageToSession(String sessionId, WebSocketMessageModel message) {
        AtomicInteger count = new AtomicInteger();
        WebSocketSession session = sessions.get(sessionId);
        if (session != null && session.isOpen()) {
            try {
                TextMessage textMessage = new TextMessage(JSON.toJSONString(message));
                session.sendMessage(textMessage);
                count.getAndIncrement();
            } catch (IOException e) {
                log.info("发送消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
                try {
                    session.close();
                } catch (IOException ex) {

                }
                sessions.remove(sessionId);
            }
        }
        return count.get();
    }

    public int sendMessageToUser(Long userId, WebSocketMessageModel message) {
        AtomicInteger count = new AtomicInteger();
        List<String> sessionIds = userMap.get(userId);
        if (sessionIds != null) {
            sessionIds.forEach(sessionId -> {
                WebSocketSession session = sessions.get(sessionId);
                if (session != null && session.isOpen()) {
                    try {
                        TextMessage textMessage = new TextMessage(JSON.toJSONString(message));
                        session.sendMessage(textMessage);
                        count.getAndIncrement();
                    } catch (IOException e) {
                        log.info("发送消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
                        try {
                            session.close();
                        } catch (IOException ex) {

                        }
                        sessions.remove(sessionId);
                    }
                }
            });
        }
        return count.get();
    }

    public int wxScanQrCodeLoginResult(String qrCodeId, WebSocketMessageModel message) {
        AtomicInteger count = new AtomicInteger(0);
        String sessionId = wxScanQrCodeLoginMap.get(qrCodeId);
        if (sessionId != null) {
            WebSocketSession session = sessions.get(sessionId);
            if (session != null && session.isOpen()) {
                try {
                    TextMessage textMessage = new TextMessage(JSON.toJSONString(message));
                    session.sendMessage(textMessage);
                    count.getAndIncrement();
                } catch (IOException e) {
                    log.info("发送消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
                    try {
                        session.close();
                    } catch (IOException ex) {

                    }
                    sessions.remove(sessionId);
                }
            }

        }
        return count.get();
    }

}

注册websocket

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private WebSocketHandler webSocketHandler;
    @Autowired
    private  HttpHandshakeInterceptor handshakeInterceptor;

    public WebSocketConfig(WebSocketHandler webSocketHandler,
                           HttpHandshakeInterceptor handshakeInterceptor) {
        this.webSocketHandler = webSocketHandler;
        this.handshakeInterceptor = handshakeInterceptor;
    }

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, "/ws")
                .addInterceptors(handshakeInterceptor)
                .setAllowedOrigins("*"); // 生产环境应设置具体域名
    }

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        container.setMaxSessionIdleTimeout(30 * 1000L); // 30秒空闲超时
        return container;
    }
}

前端

websocketservice.js 在全局配置里配置好自己的后端ws的host

import {ref, computed, onMounted, onUnmounted} from 'vue';

export default function useWebSocket() {
    const AUTH_COOKIE_NAME = import.meta.env.VITE_APP_AUTH_COOKIE_NAME;
    const WS_URL = import.meta.env.VITE_APP_WS_URL;
    const ENV_PROD = import.meta.env.VITE_ENV_PROD;
    const socket = ref(null);
    const connectionStatus = ref('disconnected'); // disconnected, connecting, connected
    const onlineCount = ref(0);
    const errorMessage = ref('');
    let heartbeatInterval = null;
    let reconnectTimer = null;
    const RECONNECT_DELAY = 5000; // 5秒后重连
    const MAX_RECONNECT_ATTEMPTS = 5; // 最大重连次数
    let reconnectAttempts = 0;
    const onAuthSuccess = ref(null);
    const globalNotification = ref(null);
    const myPhotoReviewCounts = ref(null);
    const baseHeartbeat = ref(null);
    const wxScanQrCodeLogin = ref(null);
    const PING = {
        type: 'baseHeartbeat',
        message: "ping"
    };
    const PONG = {
        type: 'baseHeartbeat',
        message: "pong"
    }

    // 消息处理器映射
    const messageHandlers = {
        onlineCount: (data) => {
            onlineCount.value = data.message;
        }
        , onAuthSuccess: (data) => {
            if (onAuthSuccess.value) {
                onAuthSuccess.value(data.message);
            }
        }, globalNotification: (data) => {
            if (globalNotification.value) {
                globalNotification.value(data.message);
            }
        }, myPhotoReviewCounts: (data) => {
            if (myPhotoReviewCounts.value) {
                myPhotoReviewCounts.value(data.message);
            }
        }, baseHeartbeat: (data) => {
            if (baseHeartbeat.value) {
                baseHeartbeat.value(data.message);
            }
        }, wxScanQrCodeLogin: (data) => {
            if (wxScanQrCodeLogin.value) {
                wxScanQrCodeLogin.value(data.data);
            }
        }
    };

    // 连接到 WebSocket 服务器
    const connect = () => {
        if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
            errorMessage.value = '达到最大重连次数,请检查网络或服务器状态';
            return;
        }

        if (socket.value && (socket.value.readyState === WebSocket.CONNECTING || socket.value.readyState === WebSocket.OPEN)) {
            return;
        }

        connectionStatus.value = 'connecting';
        errorMessage.value = '';

        try {
            // 获取用户信息
            let auth = localStorage.getItem(AUTH_COOKIE_NAME);
            let wsUrl = WS_URL;
            // 如果是null 改为''
            if (auth === null) {
                auth = '';
            }
            // 创建 WebSocket 连接,指向你的后端 8080 端口
            socket.value = new WebSocket(`${wsUrl}?auth=${auth}`);

            // 连接建立成功
            socket.value.onopen = () => {
                connectionStatus.value = 'connected';
                console.log('WebSocket 连接已建立');
                reconnectAttempts = 0; // 连接成功,重置重连次数

                // 启动心跳机制
                startHeartbeat();

                // 清除重连计时器
                if (reconnectTimer) {
                    clearTimeout(reconnectTimer);
                    reconnectTimer = null;
                }
            };

            // 接收到消息
            socket.value.onmessage = (event) => {
                try {
                    const edata = JSON.parse(event.data);
                    const {type, message, data} = edata;
                    console.log('收到消息:', data);
                    if (messageHandlers[type]) {
                        messageHandlers[type]({type, message,data});
                    } else {
                        console.log('收到未知类型消息:', edata);
                    }
                } catch (error) {
                    console.log('收到非 JSON 消息:', event.data);
                }
            };

            // 连接关闭
            socket.value.onclose = (event) => {
                connectionStatus.value = 'disconnected';
                console.log(`WebSocket 连接已关闭,代码: ${event.code}, 原因: ${event.reason}`);

                // 停止心跳
                stopHeartbeat();

                // 自动重连
                reconnectAttempts++;
                if (reconnectTimer === null) {
                    reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
                }
            };

            // 发生错误
            socket.value.onerror = (error) => {
                connectionStatus.value = 'disconnected';
                errorMessage.value = `WebSocket 错误: ${error.message}`;
                console.error('WebSocket 错误:', error);

                // 自动重连
                reconnectAttempts++;
                if (reconnectTimer === null) {
                    reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
                }
            };
        } catch (error) {
            connectionStatus.value = 'disconnected';
            errorMessage.value = `创建 WebSocket 失败: ${error.message}`;
            console.error('创建 WebSocket 失败:', error);

            // 自动重连
            reconnectAttempts++;
            if (reconnectTimer === null) {
                reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
            }
        }
    };

    // 启动心跳机制
    const startHeartbeat = () => {
        // 每25秒发送一次心跳,比服务器的超时时间短
        heartbeatInterval = setInterval(() => {
            if (socket.value && socket.value.readyState === WebSocket.OPEN) {
                socket.value.send(JSON.stringify(PING));
                console.log('发送心跳');
            } else {
                console.log('无法发送心跳,连接未打开');
            }
        }, 25000);
    };

    // 停止心跳机制
    const stopHeartbeat = () => {
        if (heartbeatInterval) {
            clearInterval(heartbeatInterval);
            heartbeatInterval = null;
        }
    };

    // 手动断开连接
    const disconnect = () => {
        if (socket.value) {
            stopHeartbeat();

            // 清除重连计时器
            if (reconnectTimer) {
                clearTimeout(reconnectTimer);
                reconnectTimer = null;
            }

            socket.value.close();
            socket.value = null;
            connectionStatus.value = 'disconnected';
            console.log('WebSocket 连接已手动关闭');
        }
    };

    // 发送消息到服务器
    const sendMessage = (message) => {
        if (socket.value && socket.value.readyState === WebSocket.OPEN) {
            socket.value.send(JSON.stringify(message));
            console.log('发送消息:', message);
        } else {
            console.error('无法发送消息,连接未打开');
            errorMessage.value = '无法发送消息,连接未打开';
        }
    };

    // 计算属性:连接是否活跃
    const isConnected = computed(() => {
        return connectionStatus.value === 'connected';
    });

    // 生命周期钩子:组件挂载时连接
    onMounted(() => {
        connect();
    });

    // 生命周期钩子:组件卸载时断开连接
    onUnmounted(() => {
        disconnect();
    });

    return {
        connectionStatus,
        onlineCount,
        errorMessage,
        isConnected,
        connect,
        disconnect,
        sendMessage,
        onAuthSuccess,
        wxScanQrCodeLogin,
        globalNotification,
        myPhotoReviewCounts
    };
}

在登陆页面用websocket回调

// 扫码登录回调处理
wxScanQrCodeLogin.value = (res) => {
  handleLoginSuccess(res);
};
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐