文章目录

  • 前言
  • 一、聊天系统为什么使用短连接?
  • 二、技术方案
    • 后端技术方案:
    • 前端技术方案
    • 原生端
  • 三、代码详细设计
    • 1.数据库设计
    • 2.后端程序
    • 3.前端程序
  • 四、效果展示
  • 五、源码-GitHub
  • 六、后期计划

前言

客服系统比较常见,主流的还是采用三方SDK接入,这些SDK的实现方式大都采用长连接,性能要求比较高,费用也偏高。此系列文章采用短连接的形成,快速开发一个实用性客服系统。

规划:

1.通过短连接实现客服系统,代码全部开源在github上(已完成)
2.将此客服系统通过SDK的方式供别人使用(已完成)
3.通过长连接实现IM聊天系统+客服系统,并开源(未完成)


一、聊天系统为什么使用短连接?

  1. 客服系统的及时性不是很高,客服一般要处理多个用户的聊天咨询,在一般情况下,客服和用户之间的聊天实时性不是很高,一般会有几秒的等待时间。
  2. 开发成本:短连接通过http协议实现,收发消息只需要发送http请求即可,开发简单。
  3. 性能:长连接需要客户端和服务器一直保持连接,比较消耗服务器性能,用户量一大,服务器的压力很大。

二、技术方案


通过短连接轮询的方式,达到收发消息。

后端技术方案:

数据库:MySQL
项目框架:Sping Boot
缓存:Redis
消息队列:Rabbit

前端技术方案

VUE

原生端

安卓:未开发
IOS:未开发
目前原生端接入方式为:跳转H5聊天页面,以内嵌的方式,短连接的方案目前不考虑原生端,后面长连接的方式会考虑原生端。

三、代码详细设计

1.数据库设计

表名:account
主要功能:后台管理账号,客服人员登录
核心字段:app_id,name

表名:user
主要功能:聊天用户表,每个需要聊天的用户都需要自动注册该表,通过该表的id来收发消息
核心字段:id,type(用户类型:1游客,2管理员,3登录用户),app_key,client_type(客户端类型:1H5,2PC,3安卓,4IOS),out_user_id(外部系统的用户id)

表名:app
主要功能:应用表,每个后台管理员账号下可以新增多个应用,每个应用都归于一个后台管理员
核心字段:app_key,app_secret,state,user_id(管理员id,对应account表)

表名:conversation
主要功能:会话表,每个聊天窗口都会新建一个会话
核心字段:from_user_id,to_user_id,last_text,from_unread_count(未读消息数),to_unread_count(未读消息数),extra(扩展字段,用户昵称、头像或其他字段在这里)

表名:conversation
主要功能:会话表,每个聊天窗口都会新建一个会话
核心字段:from_user_id,to_user_id,text,type(消息类型:1文本,2图片,3语音,4视频,5其他),file_url(文件对于的URL,发送图片/文件),file_small_url(文件小图的URL),state(消息状态),extra(扩展字段,用户昵称、头像或其他字段在这里),conversation_id,cover_img_url(封面图片的URL,如发布的视频)

2.后端程序

1.发消息

public void sendMsg(SendMsgParam param, ResponseDataBase responseDataBase) {String lastText = param.text;if (TextUtil.isEmpty(lastText)){lastText = "["+MsgType.getMsgType(param.type).desc+"]";}else {if (lastText.length()>8){lastText = lastText.substring(0,8);lastText += "...";}}boolean isFirstCreateConversation = false;if (param.conversationId<=0){//会话id为空,则有可能是第一次聊天//1.查询是否以前有聊天会话ConversationExample conversationExample = new ConversationExample();ConversationExample.Criteria criteria1 = conversationExample.createCriteria();criteria1.andFromUserIdEqualTo(param.fromUserId);criteria1.andToUserIdEqualTo(param.toUserId);ConversationExample.Criteria criteria2 = conversationExample.createCriteria();criteria2.andFromUserIdEqualTo(param.toUserId);criteria2.andToUserIdEqualTo(param.fromUserId);conversationExample.or(criteria2);List<Conversation> conversations = conversationMapper.selectByExample(conversationExample);if (!CollectionUtils.isEmpty(conversations)){param.conversationId = conversations.get(0).getId();}else {//第一次会话,建立新的会话Conversation conversation = new Conversation();conversation.setFromUserId(param.fromUserId);conversation.setToUserId(param.toUserId);conversation.setTimestamp(System.currentTimeMillis());conversation.setState(1);conversation.setToUnreadCount(1);conversation.setFromUnreadCount(0);conversation.setLastText(lastText);conversation.setExtra(param.userInfoExtra);conversationMapper.insert(conversation);param.conversationId = conversation.getId();isFirstCreateConversation = true;}}Message message = new Message();message.setType(param.type);message.setFromUserId(param.fromUserId);message.setToUserId(param.toUserId);message.setText(param.text);message.setExtra(param.extra);message.setConversationId(param.conversationId);message.setState(0);message.setTimestamp(System.currentTimeMillis());message.setDatetime(new Date());if (!TextUtil.isEmpty(param.fileUrl)){message.setFileUrl(param.fileUrl);message.setFileSmallUrl(param.fileSmallUrl);}int insert = messageMapper.insert(message);if (insert>0){//成功if(!isFirstCreateConversation){//更新会话Conversation c = getConversationByFromUserId(param.conversationFromUserId,param.conversationId);;c.setLastText(lastText);c.setTimestamp(System.currentTimeMillis());//c.setState(1);c.setExtra(param.userInfoExtra);if(param.conversationFromUserId<=0){param.conversationFromUserId = c.getFromUserId();}//设置未读消息数量if (param.fromUserId == param.conversationFromUserId){c.setToUnreadAddCount(1);  //未读消息数量+1}else {c.setFromUnreadAddCount(1);  //未读消息数量+1}conversationMapper.updateByPrimaryKeySelective(c);}responseDataBase.data = message;}else {responseDataBase.code = HttpUtil.ErrorDec.RequestError.value;}}

2.查询会话列表

<select id="queryConversationList" resultMap="BaseResultMap" parameterType="com.ideaout.im.http.param.ListParam" >SELECT * from conversationwhere (from_user_id=#{param.userId,jdbcType=INTEGER} or to_user_id=#{param.userId,jdbcType=INTEGER}) and state!='2'order by  timestamp desc<if test="param.pageIndex != null">limit ${param.pageIndex*param.pageSize},${param.pageSize};</if></select>

3.查询消息

public List<Message> queryMsgList(QueryMsgListParam param) {if (param.conversationId>0){Conversation conversation = getConversationByFromUserId(param.conversationFromUserId,param.conversationId);if(param.conversationFromUserId<=0){param.conversationFromUserId = conversation.getFromUserId();}//把当前查询者的未读消息设置为0if (param.userId == param.conversationFromUserId){conversation.setFromUnreadCount(0);}else {conversation.setToUnreadCount(0);}conversationMapper.updateByPrimaryKeySelective(conversation);}return messageMapper.queryMsgList(param);}

4.初始化SDK

public void initSdk(InitSdkParam param, ResponseDataBase responseDataBase) {//聊天im初始化,游客/管理员/普通用户 都调用此方法进行初始化if (!initVerification(param,responseDataBase)) {return;}User user = getImUser(param.appKey,param.outUserId,param.clientType,param.deviceUniqueId,param.imUserType);InitSdkResult initSdkResult = new InitSdkResult();//注册成功if (user!=null){initSdkResult.imUserId = user.getId();String token = TokenUtils.token(new TokenAttr(UserRoleType.IMUser.value,user.getId(), param.clientType, user.getType(),param.deviceUniqueId));initSdkResult.token = token;//redis存入tokenredisUtils.set( CacheUtil.getImUserTokenRedisKey(user.getId(),param.clientType),token, Config.imUserTokenExpireDay, TimeUnit.DAYS);  //7天//对方用户不为空时注册imif (!TextUtil.isEmpty(param.otherOutUserId)){User otherUser = getImUser(param.appKey,param.otherOutUserId,0,"",0);if (otherUser!=null){initSdkResult.otherImUserId = otherUser.getId();}}}responseDataBase.data = initSdkResult;}private boolean initVerification(InitSdkParam param,ResponseDataBase responseDataBase){if (TextUtil.isEmpty(param.appKey)){responseDataBase.code = HttpUtil.ErrorDec.RequestError.value;responseDataBase.errorDec = "初始化异常:appKey为空";return false;}/*else if (TextUtil.isEmpty(param.outUserId)){responseDataBase.code = HttpUtil.ErrorDec.RequestError.value;responseDataBase.errorDec = "外部应用用户id为空";return;}*/else if (param.clientType<=0){responseDataBase.code = HttpUtil.ErrorDec.RequestError.value;responseDataBase.errorDec = "初始化异常:客户端类型为空";return false;}//校验appKeyAppExample appExample = new AppExample();AppExample.Criteria criteria = appExample.createCriteria();criteria.andAppKeyEqualTo(param.appKey);criteria.andStateEqualTo(1);List<App> apps = appMapper.selectByExample(appExample);if (CollectionUtils.isEmpty(apps)){responseDataBase.code = HttpUtil.ErrorDec.RequestError.value;responseDataBase.errorDec = "初始化异常:appKey无效";return false;}return true;}private User getImUser(String appKey,String outUserId,int clientType,String deviceUniqueId,int imUserType){UserExample userExample = new UserExample();UserExample.Criteria userCriteria = userExample.createCriteria();userCriteria.andAppKeyEqualTo(appKey);userCriteria.andOutUserIdEqualTo(outUserId);List<User> users = userMapper.selectByExample(userExample);int updateUserResult = -1;User user = null;if (!CollectionUtils.isEmpty(users)){user = users.get(0);user.setLastTimestamp(System.currentTimeMillis());//在被注册的情况下,这些信息初始化的时候没有,需要补充上if (TextUtil.isEmpty(user.getDeviceUniqueId())){user.setDeviceUniqueId(deviceUniqueId);}if (user.getClientType()==null || user.getClientType()==0){user.setClientType(clientType);}if (user.getType()==null || user.getType()==0){user.setType(imUserType);}updateUserResult = userMapper.updateByPrimaryKeySelective(user);}else {user = new User();user.setAppKey(appKey);user.setOutUserId(outUserId);user.setClientType(clientType);user.setDeviceUniqueId(deviceUniqueId);user.setState(UserState.Normal.value); //状态为默认user.setChannel(0);user.setType(ImUserType.getImUserType(imUserType).value); //im类型user.setDatetime(new Date());user.setTimestamp(System.currentTimeMillis());user.setLastTimestamp(user.getTimestamp());updateUserResult = userMapper.insert(user);}return user;}

3.前端程序

前端通过轮询的方式更新会话列表和消息列表,详细代码见源码
1.初始化代码:

/*
* 初始化sdk,返回token
* imUserType:1游客,2管理员,3已登录用户
* */
this.init = function (appKey,outUserId,imUserType,otherOutUserId,callback) {var param = {};param.appKey = appKey;param.outUserId = outUserId +"";param.clientType = DevicesUtil.getClientType();param.imUserType = imUserType;param.deviceUniqueId = DevicesUtil.getDeviceUniqueId();param.otherOutUserId = otherOutUserId +"";HttpUtil.sendPost(param,"CODE0011",function (data) {UserUtil.saveIMUserToken(data.token);  //保存tokenif (callback) {callback(data.imUserId,data.otherImUserId);}},function (data) {console.log("error:" + JSON.stringify(data));},true);
};

2.定时器轮询消息

setInterval(function () {console.log("会话轮询时间到:"+getNowFormatDate());app_content.loopQueryConversationList(true);
},ComConfig.CONVERSATION_LOOPER_TIME);

四、效果展示

1.后台应用列表
用户登录账号后,可以新建多个应用,新建应用会自动生成appKey和appSecret,在聊天建立之前需要通过这2个值初始化,初始化成功后才可以通信。

1.后台会话列表

2.后台聊天界面(发送消息界面)
目前消息类型支持文字和图片

3.前端用户会话列表

五、源码-GitHub

本系列代码全部开源放在github上,欢迎大家使用和指出问题。

同时本系统支持以三方SDK的方式供别人使用,SDK接入方式:

第一种.代码部署在我这边服务器,只需要跳转H5对应的链接即可,5分钟即可完成
第二种.代码自己部署,把前端+后端代码部署在自己服务器,更改相应的配置,1小时左右可以完成。

GitHub地址
前端:https://github.com/1812507678/LightIMWeb
后端:https://github.com/1812507678/LightIMServer

demo体验

PC客服端:http://94.191.22.221/LightIMWeb/page/admin-login.html
用户名:test
密码:123456

用户端会话列表:
http://94.191.22.221/LightIMWeb/page/conversation.html?appKey=YmnTRIiI&userId=1

用户端打开聊天:
http://94.191.22.221/LightIMWeb/page/message.html?appKey=YmnTRIiI&fromUserId=1&toUserId=2

遇到问题可以加我微信交流(添加时备注IM):mwhjjy591


六、后期计划

1.优化此聊天客服系统,以SDK的方式提供给大家使用
2.通过长连接实现im聊天系统+客服系统,以保证消息的实时性,在开发完成后将会把代码开源,或以SDK的方式供大家使用,大家赶兴趣的小伙伴可以一起加入开发。

一周开发一个轻量级客服系统(代码开源)相关推荐

  1. 怎么联系vue客服_Vue在线客服系统【开源项目】

    1. 项目介绍 一个基于Vue2.0的在线客服系统. 技术栈包含:Vue.VueX.Vue Router.Element UI. 2. 功能介绍 项目包含了2个模块:客服端和访客端. 2.1 客服端功 ...

  2. 一个智能客服系统的设计思路

    一.目标 随着移动互联网发展,许多线下服务都搬到了网络平台上,人们也越来越习惯于通过互联网来获得服务,这大大节省了时间,提高了办事效率.但线上服务面向的用户群体大,同类问题多,这就要求互联网平台提高服 ...

  3. 设计一个智能客服系统

    背景: 最近在设计一个公司的智能客服系统,通过对现有人工客服语料作为样本,通过训练样本完成整个QA过程或业务办理过程. 整体思路 AliceBot负责闲聊,这里用了开源的语料,也可以添加语料到DB,基 ...

  4. 在线客服系统代码_h5客服_对接公众号_支持APP_支持多语言

    前言 客服系统比较常见,主流的还是采用三方SDK接入,这些SDK的实现方式大都采用长连接,性能要求比较高,费用也偏高.我们在此的目标是开发一个属于自己的客服系统,完全的无依赖第三方,完全自己控制. 一 ...

  5. IM在线客服系统_开源在线客服系统附源码

    在线客服系统比以往任何时候都更受欢迎,随着即时通讯巨头WhatsApp.Facebook Messenger和微信的崛起,即时通讯平台也正在接管商业通信.精简和用户友好的消息传递解决方案已经取代了电话 ...

  6. 在线客服系统代码安装 (附移动版APP下载)

    GOFLY,一套可私有化部署的免费开源客服系统,独立部署后,无服务器限制,无域名限制,无坐席限制 网站只需嵌入一段js或跳转直连地址即可快速接入客服,访客端支持电脑.手机页面自适应,全渠道支持,这个项 ...

  7. 微信群控SCRM客服系统SDK定制开发教程

    微信群控SCRM客服系统SDK定制开发教程!出自秋天不穿秋裤,天冷也要风度的程序猿之手,必属精品! 今天给大家介绍如何使用聚播群控sdk(微信二次开发SDK)快速开发一个群控客服系统!使用此SDK也可 ...

  8. 智能客服系统开发(技术方案)

    一个智能客服系统,负责全部的开发任务:主要包括前端页面开发,后台逻辑设计,中间客服与用户之间的对话流设计(算法部分),以及系统部署. 前端页面采用vue框架(最近比较火的一个框架,vuejs比较贴合p ...

  9. JAVA 开发升讯威在线客服系统:调用百度翻译接口实现实时自动翻译

    业余时间用 .net core 写了一个在线客服系统.并在博客园写了一个系列的文章,写介绍这个开发过程. 我把这款业余时间写的小系统丢在网上,陆续有人找我要私有化版本,我都给了,毕竟软件业的初衷就是免 ...

最新文章

  1. Anchor-free目标检测 | 工业应用更友好的新网络(附大量相关论文下载)
  2. java POI导出多张图片到表格(占位符方式)
  3. 能源36号文解读_中国能源报
  4. 数据结构和算法 —— 绪论
  5. 技巧实例:如何在.NET中访问MySQL数据库
  6. java websocket原理_Java WebSocket基本原理
  7. php输出因子,如何在PHP因子程序中返回值
  8. windows命令行设置和系统快捷键
  9. 华为手机像素密度排行_「屏幕像素密度」(全解析)屏幕尺寸,分辨率,像素,PPI之间到底什么关系? - seo实验室...
  10. 程序员的第一次相亲,还没开始,就已经结束了
  11. 自定义结构体及初始化
  12. 微信小程序——如何实现账号的注册、登录?
  13. 年薪五万程序员的生活及他的理财梦
  14. 这就是你日日夜夜想要的docker!!!---------docker+consul+ nginx集成分布式的服务发现与注册架构
  15. rtsp流媒体播放器----ffmpeg相关代码走读(一)
  16. ASP新闻发布网站(一) 首页
  17. git与github的使用方法
  18. 翻转课堂融入计算机课,“翻转课堂”教学模式在职业院校计算机课程中的应用...
  19. 01.java后台三层架构
  20. 服务器机架位置,服务器的安装与机架的使用

热门文章

  1. MySQL数据库开发
  2. 软件工程专业的“乐趣“
  3. bpmn基础使用(一)
  4. HTML生日快乐代码 (粉色主题)(HTML5+CSS3+JS)520表白代码/七夕情人节网页/告白/求婚/生日快乐...
  5. 电子元件二极管封装SMA,SMB,SMC的区别
  6. 尚观第15天nagios安装配置
  7. 华中数控机器人编程循环三次_我研究了下公司旗下的各机器人子公司的专利一直在同行业名列前茅,应该说技术不错,而_华中数控(300161)股吧_东方财富网股吧...
  8. 使用kitbashing实现快速的概念设计
  9. 刘剑 计算机科学与技术,刘剑-控制科学与工程学院
  10. 新手看完学会,5分钟教你制作吸引人的片头片尾,操作简单