zoukankan      html  css  js  c++  java
  • 客服系统

    客服系统

    假如网站需要提供客服功能,如果只是简单的聊天咨询可以考虑营销QQ、百度商桥等(目前大部分网站采用此方式,包括一些知名行业电商);如果需要更精细化的管理,比如客服人员安排、各项数据统计汇总,那么需要对接专业的第三方客服平台,比如网易七鱼,当然价格不菲;然而若是如京东本身就是一个平台,需要为每个商家提供各自的客服管理,首先目前第三方提供商并无此类产品(网易七鱼据说已经开发出来了,但是官网上没找到),其次即使有,价格也肯定不便宜,而且数据在别人那里总归不好。所以电商平台的客服系统,一般都是自己开发。当然了,借助优秀的开源项目,自主开发[一套简单能用的]也变得轻松很多。

    我采用了openfire+spark+layim,前两者基于java平台,layim是国人开发的一个webim前端组件。

    先来看大致效果(左边是浏览器layim-客户提咨询,右边是spark聊天窗口-客服解答)

    图示:

    本文涉及到的知识点(杂乱,后续会不定期添加内容):

    Java基础

    Intellij Idea:Java IDE

    Mybatis:半ORM

    XMPP协议

    smack:XMPP协议的Java封装

    openfire

    fastpath:openfire插件,我们需要依赖它实现客服功能

    wechat

    spark


    一秒钟入门Java

    Java SE(J2SE):Standard Edition,可认为是基础库,用于开发和部署桌面、服务器以及嵌入设备(J2ME)和实时环境中的Java应用程序。

    Java EE(J2EE):基于SE的高级库,提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构。

    可以知道J2EE比J2SE多了Web相关的组件和API,但是本人在使用SpringMVC框架开发Web应用程序时,去官网Java SE页面下载的JDK,也能正常开发。后来查看官网的Java EE的下载页面,发现提供的SDK中主要包含一个叫GlassFish的开源组件和一些示例及文档,而Java EE刚开始是以一种规范提出,GlassFish可以看作是实现了这些规范的JEE容器,而我们开发Web站点时部署到服务器(比如Tomcat),实现了JEE规范其中的Servlet容器部分,所以以JDK开发Web并不会出现问题。

    JNDI 是什么

    目前流行的IDE有Eclipse和IntelliJ IDEA,前者免费且由于历史关系占有率一直很高,后者也有社区版,据说使用性上目前完胜前者。

    final关键词:类似于.NET的readonly

    匿名内部类

    定义一个类A(可以为abstract),为方便说明,在A中定义一个[抽象]方法dosth。在调用方法里可以直接new A,并且同时给dosth赋方法体。

    复制代码
    public abstract A{    
        public void dosth() {
        }
    }
    
    public abstract B{    
        public void call() {
            final A a = new A() {
                public void dosth() {
                    //这里写方法体
                }
            };
        }
    }
    复制代码

    看着是实例化了A的一个对象,其实是实例化了A类的匿名子类。

    Access restriction:eclipse对某些java包(or 类?)有access rules,比如 sun.awt.shell.ShellFolder。因为这些JAR默认包含了一系列的代码访问规则(Access Rules),如果代码中引用了这些访问规则所禁止引用类,那么就会提示这个错误信息。解决方法:既然存在访问规则,那么修改访问规则即可。打开项目的Build Path Configuration页面,打开引用的[报错]JAR包,选中Access rules条目,选择右侧的编辑按钮,添加一个访问规则即可。

    Java NIO

    Apache Mina

    CopyOnWrite:CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。

    Maven:项目管理工具。不像VS,eclipse是更纯粹的编码工具,在维护jar包和项目之间的依赖关系、项目的构建目标等方面的功能比较弱(比如拷贝了一个项目,我们需要手动去Configure Build Path),而Maven就是补足于此。Maven独立于IDE,eclipse有一个插件叫M2E,里面内置了Maven。Maven项目的配置信息保存在pom.xml文件中。

    我们在导入Maven项目时,有时会发现不止一个pom.xml,那是因为项目中有子项目(module,module有自己的pom.xml),只要选择最顶层的pom.xml文件即可,会自动加载引用到的子项目。

    JavaBean:一般可看作是POJO,可参看 Java Bean 是个什么概念? (不过这个问题里有个答主说Java没有事件的概念,让我大吃一惊,不过转念一想,Java主要用于开发服务端应用,确实不怎么涉及到[自定义]事件。其实Java中是有事件机制的,只是不知变通,就一个半成品的观察者模式,想想C#的委托,其实就一个函数指针的事)

    MVC:当.Neter们在被Asp.Net的重量压得踹不过气来的时候,Java已经有MVC的概念了。很多模式,.Net界都是直接copy,.Neter们并没有对其历史的认知,所以接收不能,MVC就是如此。其实在Asp.Net时代已经有MVC的影子,就是一般处理程序.ashx。很早以前,用户提交都是提交到具体的一个页面,于是会经常导致一个页面并不是用于显示,而是用于业务逻辑的处理,于是后来把业务逻辑单独拎出来,这便是Controller,用户请求的是Controller,不再是具体页面,并且Controller里不再使用类似HttpRequest或者HttpResponse获取数据和返回响应,而是使用对象的形式(M),这便是MVC模式。可参看 Java Web开发模式

    Java中的注解相当于.NET中的Attribute。

    Spring是一个IOC和AOP框架。我们可以通过在xml文件中配置bean,然后在代码中使用@Autowired或@Resource注入bean实例,不过配置的环节稍显繁琐,能不能省略呢?答案是肯定的,Sping2.5开始支持注解注入,具体可看 spring注解注入:<context:component-scan>详解。需要注意的是,@Component及相关的几个注解类,在应用到interface上的时候,可能并不如预期工作,因为interface并不能实例化,而这几个注解类貌似又没有@Inherited修饰,所以就算有实现类或运行时的动态实现类,也不会注册到上下文中;且修饰的类要有公共构造函数。另外注入[被注入方]一般只能在注入方本身是已注册的bean里,若在普通类里想通过@Autowired或@Resource的方式注入bean,则稍微有点绕,可参看 Java普通类获取Spring XML中Bean的方法总结

    关于Servlet、Struts、Spring、SpringMVC的关系与区别可参看 Java开发web的几种开发模式 和 SpringMVC与Struts2的对比

    SpringMVC竟然URL和参数大小写敏感,虽然有办法配置,但这种预设没有道理吧。。。

    Servlet url-pattern /与/*区别:两者的长度不同,根据最长路径匹配的优先级,/*比/更容易被选中,而/的真正含义是,缺省匹配。既所有的URL都无法被选中的时候,就一定会选中/,可见它的优先级是最低的,这就两者的区别。

    xml文件也可以打包进jar包,但是访问jar包里的xml文件就不能按文件目录的方式来了,可参看 http://blog.csdn.net/jianxin1009/article/details/18814799 

    application.getInitParameter:jsp中9个内置对象之一application,它的数据对整个web应用都有效,application有一个重要的用途就是通过getInitParameter()获取web.xm中的配置参数,这样可以提高代码的移植性。

    dwr:简化ajax调用,使得调用远程服务器方法看上去像调用本地方法一样。

    在java项目中必不可少的是我们要指定一个jdk。在指定jdk的同时,还可以指定jdk的Language level,这个有点像我们工程最低支持版本。比如Language level 设置了5.0 只是就不能出现使用6.0/7.0特性的代码,因为这些特性在5.0的环境下是无法编译的。或者可以理解ide会安装Language level指定的jdk版本来对我们的代码进行编译,以及错误检查。即同样的jdk对应不同的Language Level会采用[可能]不同的编译和优化方式。

    Java中也有类似.Net的字符串池的概念,请看 String中intern的方法

    Java插件技术: OSGi

    貌似在同一package下,protected可见。(和.NET不同)

    Java的泛型类型只能是引用类型,而不能是基础类型,但是Java针对每个基础类型有对应的封装类型,比如boolean对应Boolean,后者是引用类型,可以为null,当封装类型不为null时,可以隐式转换,但写代码时null的情况要自己处理,如

    private boolean existUser(String username) {
        Boolean result = null; 
        return result != null && result.booleanValue();
    }

    Ant:类似于.NET的MSBuild,其构建文件默认为build.xml(可以在其中指定构建基于的Java平台版本),每个构建文件都对应于一个项目,但是大型项目经常包含大量的子项目,每一个子项目都可以有自己的构建文件。

    一个.java文件中可以定义多个类,但是public修饰的只能至多有一个,且要与文件名相同,编译后,有几个类就会产生几个对应的.class文件。jar包类似.Net的dll,它将多个.class文件打包一块。大多数 JAR 文件包含一个 META-INF 目录,它用于存储包和扩展的配置数据,如安全性和版本信息。Java 2 平台识别并解释 META-INF 目录中的下述文件和目录,以便配置应用程序、扩展和类装载器。具体可看 MANIFEST.MF 文件内容完全详解

    System.getProperty()获取系统/项目全局变量,比如Java运行时版本,当然我们也可以通过System.setProperty()设置自定义变量。

    Java桌面客户端编程:Java Swing 。桌面程序毕竟不是Java的主流领域,因此各IDE貌似也并未作太多努力,相较VS的所见即所得的控件拖拽开发模式,Java GUI编程就吃力很多了。

    Java国际化:i18n,注意中文的资源文件,貌似需要先UTF-8转码,大约就是像这样。(可以使用JDK自带的native2ascii.exe)


    Intellij Idea

    使用Intellij Idea创建spring mvc时(没用maven),run都报 Error during artifact deployment. See server log for details 错误,后来把lib文件夹拷到WEB-INFO文件夹下就没问题了,不知何故。

    原因:tomcat默认是去web-info/lib/下找依赖的jar包。手动拷jar包毕竟不是一个好办法,其实我们可以在下图处进行Artifacts设置

    运行项目,项目目录下会多出一个out文件夹,生成所有的站点文件,依赖包会自动拷贝到下面的WEB-INF/lib/下,如下图:

    IDEA配置artifacts中Web Application:Exploded和Web Application:Archive的区别:前者以文件夹形式(War Exploded)发布项目,后者以war包形式(每次都会重新打包全部的)。Tomcat会自动解压war包并启动站点,缺点是会造成一段时间的站点不可用,而以文件夹形式发布的话,则支持热部署(需进行额外的一些配置)。

    当然我们也可以使用Maven进行依赖包的管理。在当前项目右键->Add Framework Support->Maven即可。注意需要在Project Structure-> Project Settings中移除之前非Maven引用的包依赖。此时运行项目,项目目录下会多出一个target文件夹,其下有生成的站点文件。但是运行时发现WEB-INF下的文件除了web.xml外,其它的文件都不会覆盖,貌似用maven管理的web工程,需要将applicationContext.xml等资源文件放在resource目录下,然后以classpath的方式去访问。后来发现jsp页面也无法自动更新到target目录,再后来听说maven有一套约定的目录结构,貌似又可以通过pom.xml进行自定义配置,神烦!目前靠手动覆盖。参考 Maven使用点滴 配置即可(webappDirectory我没设置,就设置了warSourceDirectory,能正常更新了)

    Intellij Idea中有个Ant Build Window,默认显示的是主项目下build.xml中的targets,and by default, IDEA only shows the default target and targets that have descriptions。对这个有疑问可参看 How to get Ant Build to list all targets in a hierarchy of build files.

    可以在Run/Debug Configurations Window中设置自定义系统变量,如下图(-D不能省):


    MyBatis

    一个半ORM框架,SQL语句并不是像EF一样由框架解析,而是要预先写在xml中或者写在Java注解(同.Net的Attribute)中,且不支持匿名类型(即select出来的数据要么是基础类型,要么要有对应的Java Bean)。一般情况下,我们使用resultType映射查询结果和对象即可(MyBatis 会在幕后自动创建一个 ResultMap),当只想映射部分字段或者包含复杂类型属性的时候,我们需要自定义ResultMap。

    MyBatis不支持方法重载,因为它是通过方法名称(不加参数)去查找执行方法,因此我们设置不同的方法名,或者使用动态sql。


    XMPP协议

    JID表示一个地址,由三部分组成——node、domain和resource。例如:xiaoming@xiaoming.home/sleeping,xiaoming就是node ,xiaoming.home就是domain,sleeping就是resource。node domain 和resource任何一部分都不能超过1023 字节 ,加上@和 /,一个JID 总共不能超过3071字节。BareJid就是去掉resource,只包含node@domain。

    XMPP包含IQ, message and presence 三种packet。


    smack

    ConnectionConfiguration.Builder的setXmppDomain和setHost的区别?一个是域(服务器集群),一个是其中的一台服务器,应该只要设置其中一个就可以了。

    使用XMPPTCPConnectionConfiguration建立连接时报空指针错误,调试发现有个base64encoder未赋值,需要引用smack-java7包,该包会初始化base64encoder,如果是安卓开发,那么就引用smack-android。


    openfire 

    使用idea导入openfire代码,过程可参考将openfire源码部署到IDEA中 或者 IntelliJ IDEA搭建openfire4.1.3开发环境 。使用openfire配置界面只能配置一个数据库,且我也不打算完全依赖它生成的数据库。我需要openfire部分功能使用现有的数据库(比如用户表),而openfire的业务数据仍然使用生成的数据库,因此涉及到多库连接。这只能去修改源码了。

    上面说到的配置界面设置的项最终存储在ofproperty表中。在配置界面完成配置后,我们也可以在conf/openfire.xml中重新设置值,重启openfire,配置文件中的值会更新到数据库中。

    以AuthFactory为例,其initProvider方法里有 JiveGlobals.migrateProperty("provider.auth.className"); ,XMLProperties根据"provider.auth.className"读取xml文件中的值(getProperty方法)

    复制代码
    //按逗号拆分为数组
    String[] propName = parsePropertyName(name);
    // Search for this property by traversing down the XML hierarchy.
    Element element = document.getRootElement();
    for (String aPropName : propName) {
        element = element.element(aPropName);
        if (element == null) {
            return null;
        }
    }
    value = element.getTextTrim();
    复制代码

    对应的配置节写法如下(可以看到,propName对应各层级element,而非attribute形式)

    <provider>
        <auth>
            <className>org.jivesoftware.openfire.auth.JDBCAuthProvider</className>
        </auth>
    </provider>    

    而后覆盖数据库值

    复制代码
    public void migrateProperty(String name) {
        if (getProperty(name) != null) {
            if (JiveGlobals.getProperty(name) == null) {
                JiveGlobals.setProperty(name, getProperty(name));
                deleteProperty(name);
            }
            else if (JiveGlobals.getProperty(name).equals(getProperty(name))) {
                deleteProperty(name);
            }
            else if (!JiveGlobals.getProperty(name).equals(getProperty(name))) {
                Log.warn("XML Property '"+name+"' differs from what is stored in the database.  Please make property changes in the database instead of the configuration file.");
            }
        }
    }
    复制代码

    当然,若是我们有数据库权限,直接进入数据库修改也一样。

    openfire源码采用JDBC方式操作数据库,而且没有做很好的封装,重复代码较多,如下图所示

    相似代码在与数据库交互的地方随处可见。部分逻辑的抽取,莫过于lambda(回调函数)的方式。考虑到Java8已经支持lambda表达式,重构如下:

    复制代码
    public <T> T excuteQuery(String queryText, Function<ResultSet, T> func) {
        T result = null;
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = getConnection();
            pstmt = con.prepareStatement(queryText);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                result = func.apply(rs);
            }
        } catch (SQLException e) {
            Log.error(e.getMessage(), e);
        } finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }
        return result;
    }
    复制代码

    但是在写调用代码的时候提示:

    虽然我们在excuteQuery方法中已经catch了这个异常,但是编译器并不买账。而且就算我们在方法定义时已经throws了相关异常,也没用,如下图:

    解决方法有两种:可以在lambda体内catch异常后不再throw;或者自定义一个Functional Interface,其中声明一个定义了异常的方法,

    @FunctionalInterface
    public interface CheckedSQLExceptionFunction<T, R> {
        R apply(T t) throws SQLException;
    }

    然后将Function<Result,T>的地方替换为CheckedSQLExceptionFunction<ResultSet, T>。这两种都显得别扭与不合理,导致这一问题的是,Java Lambda规定如果Lambda中抛出了异常,那么这个异常一定要在Functional Interface中的abstract方法上定义。这是一个让人无法理解的规定。

    遇到lambda的另一个坑:

    由于username有重新赋值,所以编译报错,是不是很喜感?我不得不用一个临时变量解决。。

    官方提供了一种集成外部用户体系的方法(Custom Database Integration Guide),然后并不支持加盐密码,于是我只能自己撸码解决。关键是实现两个接口:AuthProvider 和 UserProvider,只要实现部分方法即可,很简单不赘述。

    部署

    部署到centos7。首先 rpm -qa | grep openjdk 查看所有已安装的jdk,如果版本不满足则先 rpm -e --nodeps [java-1.7.0-openjdk[-headless]] 卸载掉。然后去官网上下载合适版本的server jre/jre/sdk包(下面会进一步说明),然后解压,设置环境变量,就算安装完毕了(不过这种安装方式通过rpm -qa可是找不到的哦)。具体可看 Centos7 JDK8安装配置。

    讲道理,jdk是开发时候用的,部署的话我们只要安装jre就可以了。我刚开始下载的是server jre包,在ant的时候报 package javafx.util does not exist 的错(因为我在代码里用到了Pair<>二元组,属于javafx.util包),然而网上查了下,貌似javaFX是用于客户端GUI方面的组件(不知道是否我这里报错的javafx同个概念)。我懒得探究,马上去官网下了jre包(官网说Covers most end-users needs. Contains everything required to run Java applications on your system.),载下来之后发现果然有jfxrt.jar(包含javafx.util),欢欣鼓舞,但是ant之后报无法找到/lib/tools.jar——因为build.xml里有用到这个jar——之前server jre是有的,也是日了狗了。马上去下jdk,疯狂操作之后终于编译通过。

    也可以在windows平台编译打包,然后拷贝到linux系统。

    官网上是说./openfire start启动openfire,然而我只找到openfire.bat和openfirectl,先试了./openfirectl start 报错:Could not find Openfire installation under /opt,/usr/share,or /usr/local,查看openfirectl的shell代码,发现当OPENFIRE_HOME未设置时,会去这三个目录下找openfire,于是为其设置真实根目录,然而虽没报错,但还是没有运行起来。试了下openfire.bat,报Permission denied,尼玛,我可是用root登录的。先不管原因,我再去官网下了4.1.6(目前最新版)的tar包,发现bin目录下果然有个openfire文件,拷到服务器上后报同样的Permission denied的错误——网上说root并不默认就有所有文件的最高权限,但是他可以随意给自己增加权限——好吧,设置了权限之后,执行./openfire start 没报错,但是依旧没有运行起来。。。后来发现没有输出错误信息,是因为shell里写了/dev/null 2>&1,去掉之后终于提示——Could not find or load main class com.install4j.runtime.launcher.UnixLauncher——shell代码里该类指向的目录本地编译不存在,最后在官网tar包里发现有一个名为.install4j的隐藏文件夹,拷贝后总算运行起来了。 

    记得打开相应端口。


    webchat

    用户一般都是通过浏览器进行咨询,有个webchat示例可以参考(openfire4.2 配置fastpath、webchat、spark实现客服系统),但那是基于很久以前的smack版本,转过来也费了不少劲,特别是QueueUpdate包扩展已经不再内置支持,调试了半天在smack中找到几个关键文件,这些都是内置资源文件,项目运行时会读取这些文件,调用ProviderManager.addExtensionProvider将配置项缓存起来,如果不修改xml的话,那么在外部调用该方法也是可以的。参照着写了一个QueueUpdateProvider,顺便了解了下XmlPullParser的用法。

    关于自定义包和扩展,后来才发现官网上有介绍: Provider Architecture: Stanza Extensions and Custom IQ's,也是心累。

    再后来,发现部分非内置的扩展的Provider已经在扩展类里[作为内部类]定义好了,比如QueueUpdate.Provider。。。吐血。关于内部类可参看 java中的内部类总结


    fastpath

    增加几个http接口,如新增客服组,添加客服等,示例代码如下:

    复制代码
    public class MasonServlet extends HttpServlet {
        @Override
        public void init(ServletConfig config) throws ServletException {
            super.init(config);
            AuthCheckFilter.addExclude("fastpath/mason/*"); // 公共接口不需身份校验
        }
    
        @Override
        public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
            String action = request.getRequestURI();
            action = action.substring(action.indexOf("mason/") + 6);
            OPResult result = null;
            if (action.toLowerCase().equals("createworkgroup")) {
                String wgName = request.getParameter("wgName");
                String description = request.getParameter("description");
                String agents = request.getParameter("agents");
    
                result = createWorkgroup(wgName, description, agents);
            }
            if (result == null) {
                result = new OPResult();
                result.setSuccess(false);
                result.setMessage("未找到对应方法");
            }
            response.setContentType("application/json; charset=utf-8");
            response.setCharacterEncoding("UTF-8");
            Genson genson = new Genson();
            String json = genson.serialize(result);
            response.getOutputStream().write(json.getBytes("UTF-8"));
        }
    
        // 新增工作组(会同时建立一个默认客服组,每个工作组可以包含多个客服组)
        private OPResult createWorkgroup(String wgName, String description, String agents) {
            OPResult result = new OPResult();
            Map errors = WorkgroupUtils.createWorkgroup(wgName, description, agents);
            if (errors.size() == 0) {
                Workgroup workgroup = WorkgroupManager.getInstance().getWorkgroup(wgName);
                result.setData(workgroup.getJID());
                result.setSuccess(true);
            } else
                result.setSuccess(false);
            return result;
        }
    }
    复制代码

    完了我们就可以重新构建该插件了,在intellij中可以在窗口中设置(看了下build.xml,发现plugin任务可以构建单个插件,它接收plugin的参数表明构建的是哪个插件):

     

    由于代码中用到了genson这个第三方jar包,虽然直接编译没问题(项目的其它地方有引用),但用ant构建的时候会报错,提示找不到这个组件,原因官网说了:Any JAR files your plugin needs during compilation should be put into the lib directory,因此我们需要将该jar包复制一份到fastpath/lib目录下。


    spark

    此spark非彼spark,而是一个开源IM桌面客户端。下载下来2.8.3代码,导入到IntelliJ,运行输出了空指针异常,调试发现找不到资源文件 "META-INF/plugins.xml",查看编译后的jar文件,里面已经包含了resources/META-INF/plugins.xml。再查看Project Structure,发现没有为主模块Spark设置Resource Folders,添加了resources文件夹后编译运行正常,此时再看jar文件,里面并没有resources目录,META-INF直接在根目录体现。

    也就是说,将某个目录设置为资源文件夹(Resource Folders),意即将该目录下的子目录一起打包进jar包(不包含该目录本身),而getResource()方法获取特定路径的资源时,是直接去jar包根目录下查找对应文件。

    似乎还要设置VM arguments:-Djava.library.path=build/lib/dist/windows64,具体值按照操作系统来。参看 openfire-spark 二次开发-(二)运行环境配置

    转载请注明本文出处:http://www.cnblogs.com/newton/p/7269373.html

  • 相关阅读:
    聊聊和关系型数据库相关的一些概念
    Spring-MongoDB 关键类的源码分析
    Studio 3T 如何使用 Query Builder 查询数据
    MySQL error : Deadlock found when trying to get lock; try restarting transaction
    The java.util.concurrent Synchronizer Framework笔记
    JDK 8
    MongoDB Data Model 浅谈
    Java 泛型分析
    DCL(双检锁)的失效:现实与初衷的背离
    vue之虚拟dom
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/8031211.html
Copyright © 2011-2022 走看看