刚刚更新:在线聊天系统设计(原理+思路+源码+效果图)

原创
2015/10/18 20:20
阅读数 1.7W

这周项目要做一个在线聊天系统,感觉不是特别困难,原理也很简单,分享给大家。

 

技术

Java(Spring)+Mysql+MemCache

Spring做的是事件驱动模型,所有DB,更新缓存操作改成异步的。

MemCache存放缓存,每个用户的聊天记录缓存,好友关系维护。

 

需求

用户分为虚拟用户,普通用户,高级用户(在线经理人),管理员用户(客服)。

虚拟,普通用户有一个好友列表,好友列表保存着用户的好友,对于虚拟,普通用户来说,他们的好友列表只有高级用户+管理员用户。

高级用户,管理员用户来说只要是用户给我发过消息,我都能看到,并且回复。

 

效果图

 

 

 

后台提供的接口列表

|--聊天列表
   |--普通用户获取动态聊天列表,目前固定是三位,客服+经理2
   |--特殊用户获取用户对自己提问的列表
|--聊天回复
   |--直接发送消息到后台
|--获取聊天数据
   |--获取该用户跟某用户的聊天记录,带分页
|--定时检查接口
   |--检测此用户是否有新消息提示

提供接口控制器的源码:

@Controller
public class CommunicateCtrl extends BaseController {
 @RequestMapping("/communicate/ask")
 @ResponseBody
 public 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,并使用虚拟userId
    generateVirUserInfoWhenUserIdEmpty(model);
  }
  model.setStatus(1); // 未回复
  model.setUserType(1); // 普通用户
  model.setBuildTime(new Date());
  communicateService.save(model); // 保存DB对象
  resultData.setData(model);
  putEvent(model);
 }
 @RequestMapping("/communicate/friends")
 @ResponseBody
 public 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)) { // 普通用户Id
    UserInfo 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")
 @ResponseBody
 public 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_list
   chatsList = 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")
 @ResponseBody
 public 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);
    else
     resultData.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("-", "")); // 生成虚拟UUID
  HttpUtils.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模式可以传入用户id
  String 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));
 }

 @Autowired
 CommunicateService communicateService;
 @Autowired
 CommunicateHandle communicateHandle;
 @Autowired
 UserInfoService 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";
 @Override
 public 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天
 }
 @Autowired
 CommunicateService communicateService;
}


项目启动时,根据聊天记录,初始化用户好友列表&用户与用户之间的聊天记录:


@Service
public class CommunicateServiceImpl extends BaseServiceImpl<Communicate, Integer> implements CommunicateService {

	@Override
	public void initAllChats() {
		List<Communicate> allList = getList("");
		try {
			// 清除原有缓存
			for (Communicate communicate : allList) {
				communicateListener.updateChatsCache(communicate.getUserId(), communicate.getChatId(), new ArrayList<Communicate>());
			}

			// 增加聊天记录缓存
			for (Communicate communicate : allList) {
				SpringContextUtil.getApplicationContext().publishEvent(new CommunicateEvent(communicate));
			}
		} catch (Exception e) {
			throw new RuntimeException("在线聊天缓存初始化出现了异常:" + e.getMessage());
		}
	}

	@Autowired
	CommunicateHandle communicateListener;

}



直接将数据push到缓存中,在Spring事件监听里已经做了处理:


if (null == model.getId())
   communicateService.save(model); // 保存DB对象

有些代码个人感觉写的还是很精妙的,希望你们能找出来,哈哈~

 

 

 优化版源码

提供接口控制器的源码:

新接口整合了friend与check接口,增加了虚拟用户转成真实用户后,经理人反查此真实用户以前的虚拟用户的聊天信息~ 增加了查询好友列表缓存功能,Boss再也不用担心程序性能~  一切依赖于缓存~~


@Controller
public class CommunicateCtrl extends BaseController {

	@RequestMapping("/communicate/ask")
	@ResponseBody
	public void doAsk(@RequestAttr ResultData resultData, Communicate model, HttpServletRequest request) throws Exception {
		model.checkChatIdEmpty("聊天对象Id不能为空");
		model.checkContentEmpty("聊天内容不能为空");
		model.checkContentIllegal("您的聊天内容带有敏感词");

		UserInfo userInfo = getUserInfo();
		model.setUserId(getUserId());
		if (null != userInfo) { // 如果聊天者已经登录
			model.setMobile(userInfo.getMobile());
			model.setName(userInfo.getUserName());
		} else {
			if (Str.isEmpty(model.getUserId())) // 当传过来的cookie为空,则生成一个cookie,并使用虚拟userId
				generateVirUserInfoWhenUserIdEmpty(model);
		}
		model.setStatus(1); // 未回复
		model.setUserType(1); // 普通用户
		model.setBuildTime(new Date());
		communicateService.save(model); // 保存DB对象
		SpringContextUtil.getApplicationContext().publishEvent(new CommunicateEvent(model));
		resultData.setData(model);
	}

	@RequestMapping("/communicate/check")
	@ResponseBody
	public void doCheck(@RequestAttr ResultData resultData, String updateStatusUserId) throws Exception {
		String userId = getUserId();
		List<MapBean> dataMapList = new ArrayList<MapBean>(); // 用户是否有新消息列表&用户列表

		if (null == getUserInfo() || getUserInfo().getType() != 3) { // 如果为普通用户
			List<UserInfo> userInfos = communicateHandle.getCache("db_friend_list");
			if (null == userInfos)
				communicateHandle.setCache("db_friend_list", 3600, userInfos = userInfoService.getList(" and type = 3 ")); // 从DB中加载特殊角色,提供在线聊天功能,3600秒=1小时
			List<String> friendList = new ArrayList<String>(userInfos.size());
			for (UserInfo userInfo : userInfos)
				friendList.add(userInfo.getUserId() + "");
			dataMapList = getAndUpdateNewMsgList(userId, updateStatusUserId, friendList); // 用户获取特殊好友列表
		} else
			dataMapList = getAndUpdateNewMsgList(userId, updateStatusUserId, communicateHandle.getFriendListCache(userId)); // 特殊用户获取好友列表

		resultData.setData(dataMapList); // 设置与所有用户聊天数据
		// 如果出现某一个用户的聊天数据,则返回该用户的聊天数据
		if (Str.isNotEmpty(updateStatusUserId)) {
			for (MapBean dataMap : dataMapList) {
				Object oUser = dataMap.get("user");
				if (oUser instanceof UserInfo) {
					if (((UserInfo) oUser).getUserId().toString().equals(updateStatusUserId)) { // 真实用户id是纯数字
						resultData.setData(dataMap);
						break;
					}
				} else {
					if (((MapBean) oUser).getString("userId").equals(updateStatusUserId)) { // 虚拟用户id对比
						resultData.setData(dataMap);
						break;
					}
				}// end else
			}// end for
		}// end if
	}

	@RequestMapping("/communicate/chats")
	@ResponseBody
	public void doChats(@RequestAttr ResultData resultData, String chatId) throws Exception {
		if (Str.isEmpty(chatId))
			throw new RuntimeException("聊天对象Id不能为空");

		doCheck(resultData, chatId); // 更新聊天信息
		int pageSize = Tool.convertInt(RequestTool.getParameter("pageSize"), 10);
		int msgId = Tool.convertInt(RequestTool.getParameter("msgId"), 0);

		List<Communicate> chatsList = communicateHandle.getChatsCache(chatId, getUserId(), getVirUserId()); // 取得与每个好友的聊天记录
		int size = chatsList.size();
		if (size > 0) { // 如果存在聊天数据
			if (msgId <= 0) { // 如果消息Id为空,则取最后数据N条
				if (size <= pageSize)
					resultData.setData(chatsList);
				else
					resultData.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
		}
	}

	// 二分法查找,查找线性表必须是有序列表
	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("-", "")); // 生成虚拟UUID
		HttpUtils.addCookie(RequestTool.getResponse(), Constants.VIR_USER_ID, communicate.getUserId(), 0); // 保存cookie永久有效
	}

	String getUserId() {
		String userId = null != getUserInfo() ? userId = getUserInfo().getUserId() + "" : getVirUserId();
		// debug模式可以传入用户id
		String id = RequestTool.getParameter("id");
		return isDebug() && Str.isNotEmpty(id) ? id : userId;
	}

	String getVirUserId() {
		Cookie cookie = getCookieByName(Constants.VIR_USER_ID);
		if (null != cookie)
			return cookie.getValue();
		return null;
	}

	List<MapBean> getAndUpdateNewMsgList(String userId, String updateStatusUserId, List<String> friendList) throws Exception {
		List<MapBean> dataMapList = new ArrayList<MapBean>();

		List<Communicate> chatsList = null;
		List<Communicate> unReaderList = null; // 未读消息列表,提供给前端
		for (String friendUserId : friendList) {
			if (Str.isEmpty(friendUserId))
				continue;

			String userBindVirUser = null; // 真实用户绑定的虚拟用户
			Object o = null;
			if (ZhengzeValidate.isInteger(friendUserId)) { // 普通用户Id
				UserInfo userInfo = communicateHandle.getCache(tl("db:0_friend_list", friendUserId));
				if (null == userInfo)
					communicateHandle.setCache(tl("db:0_friend_list", friendUserId), 3600, userInfo = userInfoService.getById(Integer.parseInt(friendUserId))); // 从DB中加载特殊角色,提供在线聊天功能,3600秒=1小时
				if (null != (o = userInfo)) {
					userInfo.setHeadImg(Str.isEmpty(userInfo.getHeadImg()) ? defaultImg : userInfo.getHeadImg());
					userInfo.getDicMap().put("userType", userInfo.getType()); // userType 0 虚拟用户 1普通用户 2经纪人,如果是普通用户获取的特殊用户的好友列表,则用户类型为经纪人,如果是特殊用户获取的用户列表,则获取的用户类型为普通用户
					userBindVirUser = Str.isNotEmpty(userInfo.getVirUserId()) ? userInfo.getVirUserId() : null; // 取出用户绑定虚拟用户
				}
			} else
				o = MapBean.getNew().set("userId", friendUserId).set("headImg", defaultImg).set("dicMap", MapBean.getNew("userType", 0)); // userType 0 虚拟用户 1普通用户 2经纪人

			chatsList = communicateHandle.getChatsCache(friendUserId, userId, userBindVirUser); // 取得与每个好友的聊天记录
			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("user", o, "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);
		}
		return dataMapList;
	}

	@Autowired
	CommunicateService communicateService;

	@Autowired
	CommunicateHandle communicateHandle;

	@Autowired
	UserInfoService userInfoService;

	String defaultImg = ConfigLoader.loader.getString("user_default_img");

}



 

 

 

 

 

 

 

 

 

 

 

 

 

 

展开阅读全文
打赏
7
194 收藏
分享
加载中
这个主要后台的业务设计前台到没什么,关键是聊天信息的保存与回查.
2015/10/26 17:03
回复
举报
linapex博主

引用来自“若水191”的评论

只能呵呵
有什么指教你?
2015/10/26 13:29
回复
举报
只能呵呵
2015/10/25 21:11
回复
举报
987899[10]
2015/10/24 16:52
回复
举报
嘎嘎嘎嘎
2015/10/24 16:51
回复
举报
默默路过,期待前端实现设计思路
2015/10/22 08:50
回复
举报
mark
2015/10/21 13:19
回复
举报
linapex博主

引用来自“osenlin”的评论

这样的架构去设计聊天系统。。
只需要两张表,一个用户信息,一个聊天信息,好友关系自动维护,自动缓存,支持emoji表情,还有什么好架构?
2015/10/21 09:46
回复
举报
这样的架构去设计聊天系统。。
2015/10/21 09:40
回复
举报
linapex博主

引用来自“roylieu”的评论

可以看看ejabberd(erlang)或者openfire(java)
这些可控性,安全性,扩展性未必很强,ejabberd 还要使用erlang语言,openfire 挺好,但也还是Socket, 这些都需要单独的部署服务器~~ 他们协议支持的比较好, 但我们这个比较简单的聊天功能,直接用Java就好,目前用的是轮询,扩展成WebSocket很方便,集群也都OK~
2015/10/20 09:34
回复
举报
更多评论
打赏
23 评论
194 收藏
7
分享
在线直播报名
返回顶部
顶部