接上一版,這一版的頁面與功能都有所優化,具體以下:css
一、優化登陸攔截html
二、登陸後獲取全部好友並區分顯示在線、離線好友,好友上線、下線都有標記前端
三、將先後端交互的值改爲用戶id、顯示值改爲暱稱nickNamejava
四、聊天消息存儲,點擊好友聊天,先追加聊天記錄git
五、登陸後獲取全部未讀消息並以小圓點的形式展現github
六、搜索好友、添加好友web
一、登陸攔截由以前的經過路徑中獲取帳號,判斷WebSocketServer.loginList中是否存在key改爲登陸的時候設置cookie,登陸攔截從cookie中取值ajax
登陸、登出的時候設置、刪除cookie,json
/** * 登陸 */ @PostMapping("login") public Result<ImsUserVo> login(ImsUserVo userVo, HttpServletResponse response) { //加密後再去對比密文 userVo.setPassword(MD5Util.getMD5(userVo.getPassword())); Result<List<ImsUserVo>> result = list(userVo); if (result.isFlag() && result.getData().size() > 0) { ImsUserVo imsUserVo = result.getData().get(0); //置空隱私信息 imsUserVo.setPassword(null); //add WebSocketServer.loginList WebSocketServer.loginList.put(imsUserVo.getUserName(), imsUserVo); //設置cookie Cookie cookie = new Cookie("imsLoginToken", imsUserVo.getUserName()); cookie.setMaxAge(60 * 30); //設置域 // cookie.setDomain("huanzi.cn"); //設置訪問路徑 cookie.setPath("/"); response.addCookie(cookie); return Result.of(imsUserVo); } else { return Result.of(null, false, "帳號或密碼錯誤!"); } } /** * 登出 */ @RequestMapping("logout/{username}") public ModelAndView loginOut(HttpServletResponse response, @PathVariable String username) { new WebSocketServer().deleteUserByUsername(username,response); return new ModelAndView("login.html"); }
改爲關閉websocket時不作操做,僅減減socket鏈接數後端
/** * 鏈接關閉調用的方法 */ @OnClose public void onClose(Session session) { //下線用戶名 String logoutUserName = ""; //從webSocketMap刪除下線用戶 for (Entry<String, Session> entry : sessionMap.entrySet()) { if (entry.getValue() == session) { sessionMap.remove(entry.getKey()); logoutUserName = entry.getKey(); break; } } deleteUserByUsername(logoutUserName,null); } /** 用戶下線 */ public void deleteUserByUsername(String username, HttpServletResponse response){ //在線人數減減 WebSocketServer.onlineCount--; if(WebSocketServer.onlineCount <= 0){ WebSocketServer.onlineCount = 0; } if(StringUtils.isEmpty(response)){ return; } //用戶集合delete WebSocketServer.loginList.remove(username); //刪除cookie 思路就是替換原來的cookie,並設置它的生存時間爲0 //設置cookie Cookie cookie = new Cookie("imsLoginToken", username); cookie.setMaxAge(0); //設置域 // cookie.setDomain("huanzi.cn"); //設置訪問路徑 cookie.setPath("/"); response.addCookie(cookie); //通知除了本身以外的全部人 sendOnlineCount(username, "{'type':'onlineCount','onlineCount':" + WebSocketServer.onlineCount + ",username:'" + username + "'}"); }
在登陸攔截器中從cookie取用戶帳戶
//其實存的是用戶帳號 String imsLoginToken = ""; Cookie[] cookies = request.getCookies(); if (null != cookies) { for (Cookie cookie : cookies) { if ("imsLoginToken".equals(cookie.getName())) { imsLoginToken = cookie.getValue(); } } } if(WebSocketServer.loginList.containsKey(imsLoginToken)){ //正常處理請求 filterChain.doFilter(servletRequest, servletResponse); }else{ //重定向登陸頁面 response.sendRedirect("/imsUser/loginPage.html"); }
二、登陸以後的用戶列表再也不是顯示websocket鏈接的用戶,而是登陸用戶的好友,同時要區分顯示好友的在線與離線,因此新增一個獲取在線好友的接口
/** * 獲取在線好友 */ @PostMapping("getOnlineList") private Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) { return imsFriendService.getOnlineList(imsFriendVo); } /** * 獲取在線好友 */ @Override public Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) { //好友列表 List<ImsFriendVo> friendList = list(imsFriendVo).getData(); //在線好友列表 ArrayList<ImsUserVo> onlineFriendList = new ArrayList<>(); //遍歷friendList for(ImsFriendVo imsFriendVo1 : friendList){ ImsUserVo imsUserVo = imsFriendVo1.getUser(); if (!StringUtils.isEmpty(WebSocketServer.getSessionMap().get(imsUserVo.getId().toString()))) { onlineFriendList.add(imsUserVo); } } return Result.of(onlineFriendList); }
//鏈接成功創建的回調方法 websocket.onopen = function () { //獲取好友列表 // $.post(ctx + "/imsFriend/list",{userId: username},function (data) { // console.log(data) // }); $.ajax({ type: 'post', url: ctx + "/imsFriend/list", contentType: 'application/x-www-form-urlencoded; charset=UTF-8', dataType: 'json', data: {userId: user.id}, success: function (data) { if (data.flag) { //列表 let friends = data.data; for (let i = 0; i < friends.length; i++) { let friend = friends[i].user; let $friendGroupList = $("<div class=\"hz-group-list\">" + "<img class='left' style='width: 23px;' src='https://avatars3.githubusercontent.com/u/31408183?s=40&v=4'/>" + "<span class='hz-group-list-username'>" + friend.nickName + "</span><span id=\"" + friend.id + "-status\" style='color: #9c0c0c;;'>[離線]</span>" + "<div id=\"hz-badge-" + friend.id + "\" class='hz-badge'>0</div>" + "</div>"); $friendGroupList.user = friend; $("#hz-group-body").append($friendGroupList); } //好友人數 $("#friendCount").text(friends.length); getOnlineList(user.id); } }, error: function (xhr, status, error) { console.log("ajax錯誤!"); } }); }; /** * 獲取在線好友 */ function getOnlineList(userId){ $.ajax({ type: 'post', url: ctx + "/imsFriend/getOnlineList", contentType: 'application/x-www-form-urlencoded; charset=UTF-8', dataType: 'json', data: {userId: userId}, success: function (data) { if (data.flag) { //列表 let onlineFriends = data.data; for (let i = 0; i < onlineFriends.length; i++) { let friend = onlineFriends[i]; $("#" + friend.id + "-status").text("[在線]"); $("#" + friend.id + "-status").css("color", "#497b0f"); } //好友人數 $("#onlineCount").text(onlineFriends.length); } }, error: function (xhr, status, error) { console.log("ajax錯誤!"); } }); }
三、將以前先後端傳遞用戶帳戶username改爲用戶id,同時,顯示的是nickName暱稱,改動的地方比較多,我就不貼代碼了
四、消息存儲
後端存儲關鍵代碼
/** * 服務器接收到客戶端消息時調用的方法 */ @OnMessage public void onMessage(String message, Session session) { try { //JSON字符串轉 HashMap HashMap hashMap = new ObjectMapper().readValue(message, HashMap.class); //消息類型 String type = (String) hashMap.get("type"); //來源用戶 Map srcUser = (Map) hashMap.get("srcUser"); //目標用戶 Map tarUser = (Map) hashMap.get("tarUser"); //若是點擊的是本身,那就是羣聊 if (srcUser.get("userId").equals(tarUser.get("userId"))) { //羣聊 groupChat(session,hashMap); } else { //私聊 privateChat(session, tarUser, hashMap); } //後期要作消息持久化 ImsFriendMessageVo imsFriendMessageVo = new ImsFriendMessageVo(); imsFriendMessageVo.setToUserId((Integer) tarUser.get("userId")); imsFriendMessageVo.setFromUserId((Integer) srcUser.get("userId")); //聊天內容 imsFriendMessageVo.setContent(hashMap.get("message").toString()); try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); imsFriendMessageVo.setCreatedTime(simpleDateFormat.parse(hashMap.get("date").toString())); imsFriendMessageVo.setUpdataTime(simpleDateFormat.parse(hashMap.get("date").toString())); } catch (ParseException e) { e.printStackTrace(); } imsFriendMessageService.save(imsFriendMessageVo); } catch (IOException e) { e.printStackTrace(); } }
前端點擊好友時,獲取聊天記錄關鍵代碼
//讀取聊天記錄 $.post(ctx + "/imsFriendMessage/getChattingRecords", { fromUserId: userId, toUserId: toUserId }, function (data) { if (data.flag) { for (let i = 0; i < data.data.length; i++) { let msgObj = data.data[i]; //當聊天窗口與msgUserName的人相同,文字在左邊(對方/其餘人),不然在右邊(本身) if (msgObj.fromUserId === userId) { //追加聊天數據 setMessageInnerHTML({ id: msgObj.id, isRead: msgObj.isRead, toUserId: msgObj.toUserId, fromUserId: msgObj.fromUserId, message: msgObj.content, date: msgObj.createdTime }); } else { //追加聊天數據 setMessageInnerHTML({ id: msgObj.id, isRead: msgObj.isRead, toUserId: msgObj.fromUserId, message: msgObj.content, date: msgObj.createdTime }); } } } });
/** * 獲取A-B的聊天記錄 */ @RequestMapping("getChattingRecords") public Result<List<ImsFriendMessageVo>> getChattingRecords(ImsFriendMessageVo imsFriendMessageVo){ return imsFriendMessageService.getChattingRecords(imsFriendMessageVo); } @Override public Result<List<ImsFriendMessageVo>> getChattingRecords(ImsFriendMessageVo imsFriendMessageVo) { //A對B的聊天記錄 List<ImsFriendMessageVo> allList = new ArrayList<>(super.list(imsFriendMessageVo).getData()); Integer fromUserId = imsFriendMessageVo.getFromUserId(); imsFriendMessageVo.setFromUserId(imsFriendMessageVo.getToUserId()); imsFriendMessageVo.setToUserId(fromUserId); //B對A的聊天記錄 allList.addAll(super.list(imsFriendMessageVo).getData()); //默認按時間排序 allList.sort(Comparator.comparingLong(vo -> vo.getCreatedTime().getTime())); return Result.of(allList); }
五、登陸後獲取全部未讀消息並以小圓點的形式展現
登陸成功後獲取與好友的未讀消息關鍵代碼,在獲取好友列表以後調用
//獲取未讀消息 $.post(ctx + "/imsFriendMessage/list",{toUserId:userId,isRead:0},function(data){ if(data.flag){ let friends = {}; //將fromUser合併 for (let i = 0; i < data.data.length; i++) { let fromUser = data.data[i]; if(!friends[fromUser.fromUserId]){ friends[fromUser.fromUserId] = {}; friends[fromUser.fromUserId].count = 1; }else{ friends[fromUser.fromUserId].count = friends[fromUser.fromUserId].count + 1; } } for (let key in friends) { let fromUser = friends[key]; //小圓點++ $("#hz-badge-" + key).text(fromUser.count); $("#hz-badge-" + key).css("opacity", "1"); } } });
六、搜索好友、添加好友
可按照帳號、暱稱進行搜索,其中帳號是等值查詢,暱稱是模糊查詢
關鍵代碼
//搜索好友 function findUserByUserNameOrNickName() { let userNameOrNickName = $("#userNameOrNickName").val(); if (!userNameOrNickName) { tip.msg("帳號/暱稱不能爲空"); return; } $.post(ctx + "/imsUser/findUserByUserNameOrNickName", { userName: userNameOrNickName, nickName: userNameOrNickName, }, function (data) { if (data.flag) { $("#friendList").empty(); for (let i = 0; i < data.data.length; i++) { let user = data.data[i]; let $userDiv = $("<div>" + "<img style='width: 23px;margin: 0 5px 0 0;' src='" + user.avatar + "'/>" + "<span>" + user.nickName + "(" + user.userName + ")</span>" + "<button onclick='tipUserInfo($(this).parent()[0].user)'>用戶詳情</button>" + "<button onclick=''>加好友</button>" + "</div>"); $userDiv[0].user = user; $("#friendList").append($userDiv); } } }); }
/** * 根據帳號或暱稱(模糊查詢)查詢 */ @PostMapping("findUserByUserNameOrNickName") public Result<List<ImsUserVo>> findUserByUserNameOrNickName(ImsUserVo userVo) { return imsUserService.findUserByUserNameOrNickName(userVo); } @Override public Result<List<ImsUserVo>> findUserByUserNameOrNickName(ImsUserVo userVo) { return Result.of(CopyUtil.copyList(imsUserRepository.findUserByUserNameOrNickName(userVo.getUserName(), userVo.getNickName()), ImsUserVo.class)); } @Query(value = "select * from ims_user where user_name = :userName or nick_name like %:nickName%",nativeQuery = true) List<ImsUser> findUserByUserNameOrNickName(@Param("userName") String userName,@Param("nickName") String nickName);
添加好友
首先要修改ims_friend結構,SQL以下,添加了一個字段is_agree,是否已經贊成好友申請 0已申請但未贊成 1贊成 -1拒絕,以前查詢好友列表的post請求則須要新增參數isAgree=1
/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 50528 Source Host : localhost:3306 Source Schema : test Target Server Type : MySQL Target Server Version : 50528 File Encoding : 65001 Date: 14/05/2019 17:25:35 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for ims_friend -- ---------------------------- DROP TABLE IF EXISTS `ims_friend`; CREATE TABLE `ims_friend` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', `user_id` int(11) NULL DEFAULT NULL COMMENT '用戶id', `friend_id` int(11) NULL DEFAULT NULL COMMENT '好友id', `friend_type` int(11) NULL DEFAULT NULL COMMENT '好友分組id', `friend_remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '好友備註', `is_agree` int(1) NULL DEFAULT NULL COMMENT '是否已經贊成好友申請 0已申請但未贊成 1贊成 -1拒絕', `created_time` datetime NULL DEFAULT NULL COMMENT '建立時間', `updata_time` datetime NULL DEFAULT NULL COMMENT '更新時間', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '好友表' ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;
在工具欄新加一個系統消息,貼出對應關鍵代碼
//監聽單擊系統消息,彈出窗口 $("body").on("click", "#sysNotification", function () { //此處爲單擊事件要執行的代碼 if ($(".sysNotification").length <= 0) { tip.dialog({ title: "系統消息", class: "sysNotification", content: "<div></div>", shade: 0 }); } else { $(".sysNotification").click(); } $("#sysNotification").find(".hz-badge").css("opacity",0); $("#sysNotification").find(".hz-badge").text(0); //已拒絕 //申請好友 $.post(ctx + "/imsFriend/list", { friendId: userId, isAgree: 0, }, function (data) { if (data.flag) { for (let i = 0; i < data.data.length; i++) { let user = data.data[i].user; let $userDiv = $("<div>" + "<img style='width: 23px;margin: 0 5px 0 0;' src='" + user.avatar + "'/>" + "<span>" + user.nickName + "(" + user.userName + ")</span> 申請添加好友<br/>" + "<button onclick='tipUserInfo($(this).parent()[0].user)'>用戶詳情</button>" + "<button onclick='agreeAddFriend(" + data.data[i].id + ")'>贊成</button>" + "</div>"); $userDiv[0].user = user; $(".sysNotification .tip-content").append($userDiv); } } }); }); //申請添加好友 function applyToAddFriend(friendUserId) { let nowTime = commonUtil.getNowTime(); $.post(ctx + "/imsFriend/save", { userId: userId, friendId: friendUserId, friendType: 1, friendRemark: "", isAgree: 0, createdTime: nowTime, updataTime: nowTime, }, function (data) { if (data.flag) { tip.msg({text:"已爲你遞交好友申請,對方贊成好便可成爲好友!",time:3000}); } }); } //贊成好友添加 function agreeAddFriend(id){ let nowTime = commonUtil.getNowTime(); $.post(ctx + "/imsFriend/save", { id:id, isAgree: 1, updataTime: nowTime, }, function (data) { if (data.flag) { $.post(ctx + "/imsFriend/save", { userId: data.data.friendId, friendId: data.data.userId, friendType: 1, friendRemark: "", isAgree: 1, createdTime: nowTime, updataTime: nowTime, }, function (data) { if (data.flag) { tip.msg({text:"大家已是好友了,能夠開始聊天!",time:2000}); } }); } }); } //獲取個人申請好友,並作小圓點提示 function getApplyFriend(userId){ $.post(ctx + "/imsFriend/list", { friendId: userId, isAgree: 0, }, function (data) { if (data.flag && data.data.length > 0) { $("#sysNotification").find(".hz-badge").css("opacity",1); $("#sysNotification").find(".hz-badge").text(data.data.length); } }); }
在線、離線提示出來小bug...
2019-05-17更新
問題找到了,是由於咱們將關聯的好友對象屬性名改爲了
@OneToOne @JoinColumn(name = "friendId",referencedColumnName = "id", insertable = false, updatable = false) @NotFound(action= NotFoundAction.IGNORE) private ImsUser friendUser;//好友
但在獲取在線好友那裏仍是,getUser();,致使數據錯亂,bug修改:改爲getFriendUser();便可
/** * 獲取在線好友 */ @Override public Result<List<ImsUserVo>> getOnlineList(ImsFriendVo imsFriendVo) { imsFriendVo.setIsAgree(1); //好友列表 List<ImsFriendVo> friendList = list(imsFriendVo).getData(); //在線好友列表 ArrayList<ImsUserVo> onlineFriendList = new ArrayList<>(); //遍歷friendList for(ImsFriendVo imsFriendVo1 : friendList){ ImsUserVo imsUserVo = imsFriendVo1.getUser(); if (!StringUtils.isEmpty(WebSocketServer.getSessionMap().get(imsUserVo.getId().toString()))) { onlineFriendList.add(imsUserVo); } } return Result.of(onlineFriendList); }
第二版暫時記錄到這,第三版持續更新中...
2019-06-18補充:HashMap不支持併發操做,線程不安全,ConcurrentHashMap支持併發操做線程安全,所以,咱們應該用後者,而不是前者,今天在這裏補充一下,就再也不其餘地方作補充說明了
PS:ConcurrentHashMap是一個 Segment 數組,Segment 經過繼承 ReentrantLock 來進行加鎖,因此每次須要加鎖的操做鎖住的是一個 segment,這樣只要保證每一個 Segment 是線程安全的,也就實現了全局的線程安全。