刚刚更新:在线聊天系统设计(原理+思路+源码+效果图)
2019獨角獸企業重金招聘Python工程師標準>>>
這周項目要做一個在線聊天系統,感覺不是特別困難,原理也很簡單,分享給大家。
?
技術
Java(Spring)+Mysql+MemCache
Spring做的是事件驅動模型,所有DB,更新緩存操作改成異步的。
MemCache存放緩存,每個用戶的聊天記錄緩存,好友關系維護。
?
需求
用戶分為虛擬用戶,普通用戶,高級用戶(在線經理人),管理員用戶(客服)。
虛擬,普通用戶有一個好友列表,好友列表保存著用戶的好友,對于虛擬,普通用戶來說,他們的好友列表只有高級用戶+管理員用戶。
高級用戶,管理員用戶來說只要是用戶給我發過消息,我都能看到,并且回復。
?
效果圖
?
?
?
后臺提供的接口列表
|--聊天列表
?? |--普通用戶獲取動態聊天列表,目前固定是三位,客服+經理2
?? |--特殊用戶獲取用戶對自己提問的列表
|--聊天回復
?? |--直接發送消息到后臺
|--獲取聊天數據
?? |--獲取該用戶跟某用戶的聊天記錄,帶分頁
|--定時檢查接口
?? |--檢測此用戶是否有新消息提示
?
提供接口控制器的源碼:
@Controller public class CommunicateCtrl extends BaseController {@RequestMapping("/communicate/ask")@ResponseBodypublic void doAsk(@RequestAttr ResultData resultData, Communicate model, HttpServletRequest request) throws Exception {model.checkChatIdEmpty("聊天對象Id不能為空");model.checkContentEmpty("聊天內容不能為空");model.checkContentIllegal("您的聊天內容帶有敏感詞");UserInfo userInfo = getUserInfo();if (null != userInfo) { // 如果聊天者已經登錄model.setUserId(String.valueOf(userInfo.getUserId()));model.setMobile(userInfo.getMobile());model.setName(StringUtil.isNullOrEmpty(userInfo.getUserName()) ? "" : userInfo.getUserName());} else {model.setUserId(getUserId());if (Str.isEmpty(model.getUserId())) // 當傳過來的cookie為空,則生成一個cookie,并使用虛擬userIdgenerateVirUserInfoWhenUserIdEmpty(model);}model.setStatus(1); // 未回復model.setUserType(1); // 普通用戶model.setBuildTime(new Date());communicateService.save(model); // 保存DB對象resultData.setData(model);putEvent(model);}@RequestMapping("/communicate/friends")@ResponseBodypublic void doFriendList(@RequestAttr ResultData resultData, HttpServletRequest request) throws Exception {// 如果為普通用戶if (null == getUserInfo() || getUserInfo().getType() != 3) {List<UserInfo> userInfos = userInfoService.getList(" and type = 3 "); // 加載特殊角色,提供在線聊天功能for (UserInfo userInfo : userInfos)userInfo.getDicMap().put("userType", 2); // userType 0 虛擬用戶 1普通用戶 2經紀人resultData.setData(userInfos);return;}// 特殊用戶獲取好友列表List<String> friendList = communicateHandle.getFriendListCache(getUserId()); // 獲取好友列表List<Object> list = new ArrayList<Object>(friendList.size());for (String userId : friendList) {if (Str.isEmpty(userId))continue;Object o = null;if (ZhengzeValidate.isInteger(userId)) { // 普通用戶IdUserInfo userInfo = userInfoService.getById(Integer.parseInt(userId));if (null != (o = userInfo)) {userInfo.setHeadImg(Str.isEmpty(userInfo.getHeadImg()) ? defaultImg : userInfo.getHeadImg());userInfo.getDicMap().put("userType", 1); // userType 0 虛擬用戶 1普通用戶 2經紀人}} else {// user.dicMap.userType// userType 0 虛擬用戶 1普通用戶 2經紀人o = MapBean.getNew().set("userId", userId).set("headImg", defaultImg).set("dicMap", MapBean.getNew("userType", 0));}list.add(o);}resultData.setData(list);}@RequestMapping("/communicate/check")@ResponseBodypublic void doCheck(@RequestAttr ResultData resultData, String updateStatusUserId) throws Exception {String userId = getUserId();List<MapBean> dataMapList = new ArrayList<MapBean>(); // 用戶是否有新消息列表List<String> friendList = communicateHandle.getFriendListCache(userId); // 獲取好友列表List<Communicate> chatsList = null;List<Communicate> unReaderList = null; // 未讀消息列表,提供給前端// 循環所有好友的聊天數據,檢測是否有新數據for (String friendUserId : friendList) {// 110&8_chart_list// 8&110_chart_listchatsList = communicateHandle.getChatsCache(friendUserId, userId); // 取得與每個好友的聊天記錄,注意與生成key的順序區別if (!chatsList.isEmpty()) {// 如果存在聊天數據int size = 0;String lastMsg = null;if (Str.isNotEmpty(updateStatusUserId))unReaderList = new ArrayList<Communicate>();for (Communicate communicate : chatsList) {lastMsg = communicate.getContent();if (!communicate.getUserId().equals(userId) && communicate.getStatus() == 1) { // 只查詢我未讀的消息,過濾我的消息size += 1;if (communicate.getUserId().equals(updateStatusUserId)) { // 如果聊天對象一致,則更新狀態,并返回未讀消息列表communicate.setStatus(2);// 內存與db一致Communicate communicateDB = communicateService.getById(communicate.getId());if (communicateDB.getStatus() == 2) // 如果其他線程已更新狀態,這里則不返回continue;communicateDB.setStatus(communicate.getStatus());communicateService.updateById(communicateDB);unReaderList.add(communicate);}// // 如果需要更新狀態 --- 性能更好的一種批量更新方式// if (Str.isNotEmpty(isUpdateStatus)) {// communicateService.updateStatus(list.get(0), " and user_id = '" + userId + "' and status = " + Communicate.STATUS_MGR_REPLY);// }}}MapBean dataMap = MapBean.getNew("userId", friendUserId, "oper", "normal"); // 返回最后的頁數,操作為正常(沒有新消息)// 返回新消息if (size > 0) {if (null != unReaderList && !unReaderList.isEmpty())communicateHandle.updateChatsCache(updateStatusUserId, userId, chatsList); // 更新緩存dataMap.set("oper", "new").set("msg", tl("你有:0條未讀消息", size));dataMap.set("lastMsg", lastMsg).set("unReadMsgCount", size);dataMap.set("unReaderList", unReaderList);}dataMapList.add(dataMap);}}resultData.setData(dataMapList); // 設置與所有用戶聊天數據// 如果出現某一個用戶的聊天數據,則返回該用戶的聊天數據if (Str.isNotEmpty(updateStatusUserId)) {for (MapBean dataMap : dataMapList) {if (dataMap.getString("userId").equals(updateStatusUserId)) {resultData.setData(dataMap);break;}}}}@RequestMapping("/communicate/chats")@ResponseBodypublic void doChats(@RequestAttr ResultData resultData, String chatId) throws Exception {if (Str.isEmpty(chatId))throw new RuntimeException("聊天對象Id不能為空");int pageSize = Tool.convertInt(RequestTool.getParameter("pageSize"), 10);int msgId = Tool.convertInt(RequestTool.getParameter("msgId"), 0);List<Communicate> chatsList = communicateHandle.getChatsCache(chatId, getUserId()); // 取得與每個好友的聊天記錄int size = chatsList.size();int lastIndex = size - 1; // List索引可能出現 1-1=0的情況,if中做兼容if (lastIndex >= 0) { // 如果存在聊天數據if (msgId <= 0) { // 如果消息Id為空,則取最后數據N條if (size <= pageSize)resultData.setData(chatsList);elseresultData.setData(chatsList.subList(size - pageSize, size)); // 倒序,取最后一節數據return;}// 根據msgId來取數據int msgIdIndex = binarySearch(chatsList, msgId);if (msgIdIndex == -1 || msgIdIndex == 0) // -1則表示此msgId不存在,0則表示在它之前已經沒有了任何數據return;int subIndex = msgIdIndex - pageSize;resultData.setData(chatsList.subList(subIndex < 0 ? 0 : subIndex, msgIdIndex)); // 取出比msgId小的Id// msgIdIndex += 1;// +1 過濾掉自己// int subSize = msgIdIndex + pageSize;// resultData.setData(chatsList.subList(msgIdIndex, subSize > size ? size : subSize)); //取出比msgId大的Id}}// 二分法查找,查找線性表必須是有序列表int binarySearch(List<Communicate> chatsList, int key) {int low = 0, high = chatsList.size() - 1, mid;while (low <= high) {mid = (low + high) >>> 1;if (key == chatsList.get(mid).getId()) {return mid;} else if (key < chatsList.get(mid).getId()) {high = mid - 1;} else {low = mid + 1;}}return -1;}void generateVirUserInfoWhenUserIdEmpty(Communicate communicate) {communicate.setUserId(UUID.randomUUID().toString().replace("-", "")); // 生成虛擬UUIDHttpUtils.addCookie(RequestTool.getResponse(), Constants.VIR_USER_ID, communicate.getUserId(), 24 * 60 * 60 * 1000 * 7); // 保存cookie一周}String getUserId() {String userId = null;if (null != getUserInfo()) {userId = getUserInfo().getUserId() + "";} else {Cookie cookie = getCookieByName(Constants.VIR_USER_ID);if (null != cookie)userId = cookie.getValue();}// debug模式可以傳入用戶idString id = RequestTool.getParameter("id");return isDebug() && Str.isNotEmpty(id) ? id : userId;}@SuppressWarnings("unchecked")Map<Class<?>, Set<String>> setResultJsonFilter(Class<?> clazz, Set<String> set) {Map<Class<?>, Set<String>> includeMap = (Map<Class<?>, Set<String>>) RequestTool.getRequest().getAttribute("includeMap");if (null == includeMap)RequestTool.getRequest().setAttribute("includeMap", includeMap = new HashMap<Class<?>, Set<String>>());includeMap.put(clazz, set);RequestTool.getRequest().setAttribute("jsonFilter", new ComplexPropertyPreFilter(includeMap));return includeMap;}void putEvent(Communicate model) {SpringContextUtil.getApplicationContext().publishEvent(new CommunicateEvent(model));}@AutowiredCommunicateService communicateService;@AutowiredCommunicateHandle communicateHandle;@AutowiredUserInfoService userInfoService;String defaultImg = ConfigLoader.loader.getString("user_default_img"); }
Spring異步觀察者事件處理:
@Component @SuppressWarnings("unchecked") public class CommunicateHandle implements ApplicationListener<CommunicateEvent> {static final String chartsKey = "_chart_list";static final String friendsKey = "_friend_list";@Overridepublic void onApplicationEvent(CommunicateEvent event) {Communicate model = (Communicate) event.getSource();if (null == model.getId())communicateService.save(model); // 保存DB對象// 查詢并更新自己的好友列表getAndAddFriendList(model);// 查詢并更新聊天對象的好友列表getAndAddFriendList(model, "friend");// 查詢并添加自己與聊天對象的記錄列表getAndAddChats(model);}List<Communicate> getAndAddChats(Communicate model) {List<Communicate> list = null; // 用戶所有的聊天記錄try {list = getChatsCache(model.getUserId(), model.getChatId());list.add(model);Collections.sort(list); // 排序此用戶的消息隊列updateChatsCache(model.getUserId(), model.getChatId(), list);// 保存至緩存} catch (Exception e) {e.printStackTrace();}return list;}List<String> getAndAddFriendList(Communicate model, String... friends) {List<String> list = null; // 所有用戶的好友try {list = friends.length == 0 ? getFriendListCache(model.getUserId()) : getFriendListCache(model.getChatId());if (friends.length == 0 ? !list.contains(model.getChatId()) && list.add(model.getChatId()) : !list.contains(model.getUserId()) && list.add(model.getUserId()))setCache(getKey(model, friends) + friendsKey, list); // 自動追加為好友} catch (Exception e) {e.printStackTrace();}return list;}String getKey(Communicate model, String... friends) {String key = model.getUserId();if (friends.length > 0)key = model.getChatId();return key;}public List<String> getFriendListCache(Object userId) throws Exception {List<String> list = (List<String>) MemCacheClient.get(userId + friendsKey);if (Str.isNull(list))list = new ArrayList<String>();return list;}public List<Communicate> getChatsCache(Object userId, Object chatsUserId) throws Exception {List<Communicate> list = (List<Communicate>) MemCacheClient.get(userId + "&" + chatsUserId + chartsKey);if (Str.isNull(list))list = new ArrayList<Communicate>();return list;}public boolean updateChatsCache(Object userId, Object chatsUserId, Object o) throws Exception {setCache(chatsUserId + "&" + userId + chartsKey, o); // 1296000秒 = 15天setCache(userId + "&" + chatsUserId + chartsKey, o); // 1296000秒 = 15天return true;}boolean setCache(String key, Object o) throws Exception {return MemCacheClient.set(key, 1296000, o); // 1296000秒 = 15天}@AutowiredCommunicateService communicateService; }
項目啟動時,根據聊天記錄,初始化用戶好友列表&用戶與用戶之間的聊天記錄:
有些代碼個人感覺寫的還是很精妙的,希望你們能找出來,哈哈~
?
?
?優化版源碼
提供接口控制器的源碼:
新接口整合了friend與check接口,增加了虛擬用戶轉成真實用戶后,經理人反查此真實用戶以前的虛擬用戶的聊天信息~ 增加了查詢好友列表緩存功能,Boss再也不用擔心程序性能~? 一切依賴于緩存~~
?
?
?
?
?
?
?
?
?
?
?
?
?
?
轉載于:https://my.oschina.net/linapex/blog/518651
總結
以上是生活随笔為你收集整理的刚刚更新:在线聊天系统设计(原理+思路+源码+效果图)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 流水线经典讲解!!!!!
- 下一篇: python视频教程大全