你的位置:首页 > Java教程

[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.

我们在导入Maven项目时,有时会发现不止一个pom.

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框架。我们可以通过在

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

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

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

即同样的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.

一个.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.目前靠手动覆盖。参考 Maven使用点滴 配置即可(webappDirectory我没设置,就设置了warSourceDirectory,能正常更新了)

Intellij Idea中有个Ant Build Window,默认显示的是主项目下build.

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


MyBatis

一个半ORM框架,SQL语句并不是像EF一样由框架解析,而是要预先写在

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.

以AuthFactory为例,其initProvider方法里有 JiveGlobals.migrateProperty("provider.auth.className"); ,

//按逗号拆分为数组String[] propName = parsePropertyName(name);// Search for this property by traversing down the 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(");  } }}

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

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,其中声明一个定义了异常的方法,

@FunctionalInterfacepublic 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.

也可以在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将配置项缓存起来,如果不修改

关于自定义包和扩展,后来才发现官网上有介绍: 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.

 

由于代码中用到了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.

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

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

 

转载请注明本文出处