Explorar el Código

init from phab

xielq hace 5 años
padre
commit
2109c3aefd
Se han modificado 100 ficheros con 5659 adiciones y 0 borrados
  1. 0 0
      README.md
  2. 55 0
      build.gradle
  3. 9 0
      develop.md
  4. 16 0
      gradle/dependencies-base.gradle
  5. 11 0
      gradle/tasks.gradle
  6. BIN
      gradle/wrapper/gradle-wrapper.jar
  7. 6 0
      gradle/wrapper/gradle-wrapper.properties
  8. 164 0
      gradlew
  9. 90 0
      gradlew.bat
  10. 2 0
      settings.gradle
  11. 6 0
      src/main/docker/Dockerfile
  12. 12 0
      src/main/java/com/uas/demo/Application.java
  13. 74 0
      src/main/java/com/uas/demo/api/ChatApiController.java
  14. 32 0
      src/main/java/com/uas/demo/api/ChatSessionController.java
  15. 61 0
      src/main/java/com/uas/demo/api/FileController.java
  16. 53 0
      src/main/java/com/uas/demo/api/MessageController.java
  17. 15 0
      src/main/java/com/uas/demo/configuration/CorsConfig.java
  18. 21 0
      src/main/java/com/uas/demo/configuration/MongoConfig.java
  19. 25 0
      src/main/java/com/uas/demo/dao/ChatInfoDao.java
  20. 17 0
      src/main/java/com/uas/demo/dao/ChatSessionDao.java
  21. 16 0
      src/main/java/com/uas/demo/dao/EnterpriseDao.java
  22. 39 0
      src/main/java/com/uas/demo/dao/MessageDao.java
  23. 17 0
      src/main/java/com/uas/demo/dao/UserDao.java
  24. 23 0
      src/main/java/com/uas/demo/dao/UserInfoDao.java
  25. 23 0
      src/main/java/com/uas/demo/facade/ChatInfoFacade.java
  26. 16 0
      src/main/java/com/uas/demo/facade/MessageFacade.java
  27. 120 0
      src/main/java/com/uas/demo/facade/impl/ChatInfoFacadeImpl.java
  28. 41 0
      src/main/java/com/uas/demo/facade/impl/MessageFacadeImpl.java
  29. 143 0
      src/main/java/com/uas/demo/model/ChatInfo.java
  30. 165 0
      src/main/java/com/uas/demo/model/ChatInfoDto.java
  31. 284 0
      src/main/java/com/uas/demo/model/ChatSession.java
  32. 109 0
      src/main/java/com/uas/demo/model/Enterprise.java
  33. 208 0
      src/main/java/com/uas/demo/model/Message.java
  34. 6 0
      src/main/java/com/uas/demo/model/MessageType.java
  35. 8 0
      src/main/java/com/uas/demo/model/StoreType.java
  36. 93 0
      src/main/java/com/uas/demo/model/User.java
  37. 97 0
      src/main/java/com/uas/demo/model/UserInfo.java
  38. 6 0
      src/main/java/com/uas/demo/model/UserType.java
  39. 19 0
      src/main/java/com/uas/demo/monitor/MongoMonitor.java
  40. 22 0
      src/main/java/com/uas/demo/service/ChatInfoService.java
  41. 22 0
      src/main/java/com/uas/demo/service/ChatService.java
  42. 29 0
      src/main/java/com/uas/demo/service/ChatSessionService.java
  43. 14 0
      src/main/java/com/uas/demo/service/EnterpriseService.java
  44. 44 0
      src/main/java/com/uas/demo/service/MessageService.java
  45. 14 0
      src/main/java/com/uas/demo/service/UserInfoService.java
  46. 75 0
      src/main/java/com/uas/demo/service/impl/ChatInfoServiceImpl.java
  47. 91 0
      src/main/java/com/uas/demo/service/impl/ChatServiceImpl.java
  48. 102 0
      src/main/java/com/uas/demo/service/impl/ChatSessionServiceImpl.java
  49. 31 0
      src/main/java/com/uas/demo/service/impl/EnterpriseServiceImpl.java
  50. 220 0
      src/main/java/com/uas/demo/service/impl/MessageServiceImpl.java
  51. 50 0
      src/main/java/com/uas/demo/service/impl/UserInfoServiceImpl.java
  52. 68 0
      src/main/java/com/uas/demo/utils/JacksonUtils.java
  53. 69 0
      src/main/java/com/uas/demo/web/ChatController.java
  54. 15 0
      src/main/java/com/uas/demo/web/HomeController.java
  55. 13 0
      src/main/java/com/uas/demo/web/LoginController.java
  56. 25 0
      src/main/java/com/uas/demo/web/UserController.java
  57. 15 0
      src/main/resources/application.yml
  58. 559 0
      src/main/resources/static/app/client.js
  59. 105 0
      src/main/resources/static/app/common/data-service.js
  60. 31 0
      src/main/resources/static/app/common/http-utils.js
  61. 197 0
      src/main/resources/static/app/common/xmpp-client.js
  62. 109 0
      src/main/resources/static/app/index.js
  63. 25 0
      src/main/resources/static/app/model/message.js
  64. 14 0
      src/main/resources/static/app/service/chat-service.js
  65. 13 0
      src/main/resources/static/app/service/file-service.js
  66. 30 0
      src/main/resources/static/app/service/message-service.js
  67. 14 0
      src/main/resources/static/app/service/session-service.js
  68. 165 0
      src/main/resources/static/lib/css/jquery.emoji.css
  69. 0 0
      src/main/resources/static/lib/css/jquery.mCustomScrollbar.min.css
  70. 381 0
      src/main/resources/static/lib/jquery.emoji.js
  71. 0 0
      src/main/resources/static/lib/jquery.emoji.min.js
  72. 2 0
      src/main/resources/static/lib/jquery.json.min.js
  73. 1 0
      src/main/resources/static/lib/jquery.mCustomScrollbar.min.js
  74. 1 0
      src/main/resources/static/lib/jquery.min.js
  75. 12 0
      src/main/resources/static/lib/jquery.mousewheel-3.0.6.min.js
  76. 201 0
      src/main/resources/static/lib/md5.min.js
  77. 149 0
      src/main/resources/static/lib/onfire.js
  78. 1 0
      src/main/resources/static/lib/onfire.min.js
  79. 1 0
      src/main/resources/static/lib/strophe.min.js
  80. 442 0
      src/main/resources/static/style/css/chat.css
  81. 185 0
      src/main/resources/static/style/css/page.css
  82. BIN
      src/main/resources/static/style/img/face/1.gif
  83. BIN
      src/main/resources/static/style/img/face/10.gif
  84. BIN
      src/main/resources/static/style/img/face/11.gif
  85. BIN
      src/main/resources/static/style/img/face/12.gif
  86. BIN
      src/main/resources/static/style/img/face/13.gif
  87. BIN
      src/main/resources/static/style/img/face/14.gif
  88. BIN
      src/main/resources/static/style/img/face/15.gif
  89. BIN
      src/main/resources/static/style/img/face/16.gif
  90. BIN
      src/main/resources/static/style/img/face/17.gif
  91. BIN
      src/main/resources/static/style/img/face/18.gif
  92. BIN
      src/main/resources/static/style/img/face/19.gif
  93. BIN
      src/main/resources/static/style/img/face/2.gif
  94. BIN
      src/main/resources/static/style/img/face/20.gif
  95. BIN
      src/main/resources/static/style/img/face/21.gif
  96. BIN
      src/main/resources/static/style/img/face/22.gif
  97. BIN
      src/main/resources/static/style/img/face/23.gif
  98. BIN
      src/main/resources/static/style/img/face/24.gif
  99. BIN
      src/main/resources/static/style/img/face/25.gif
  100. BIN
      src/main/resources/static/style/img/face/26.gif

+ 0 - 0
README.md


+ 55 - 0
build.gradle

@@ -0,0 +1,55 @@
+buildscript {
+	ext {
+		springBootVersion = '1.5.3.RELEASE'
+		// 0.12.0 升级到了 Gradle 3.4,Gradle 2.14.1 只能使用 0.11.0及以下
+		dockerVersion = '0.11.0'
+		dcokerRegistry = "10.10.100.200:5000"
+	}
+	repositories {
+		maven { url "https://plugins.gradle.org/m2/" }
+		maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
+		mavenCentral()
+		jcenter()
+	}
+	dependencies {
+		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+		classpath("gradle.plugin.com.palantir.gradle.docker:gradle-docker:${dockerVersion}")
+	}
+}
+
+group 'com.uas.demo.im'
+version '0.2.0'
+
+apply plugin: 'java'
+apply plugin: "com.palantir.docker"
+apply plugin: "org.springframework.boot"
+
+apply from: "$rootDir/gradle/tasks.gradle"
+apply from: "$rootDir/gradle/dependencies-base.gradle"
+
+allprojects {
+	sourceCompatibility = 1.8
+	targetCompatibility = 1.8
+}
+
+jar {
+	baseName = project.name
+	version = ''
+}
+
+dependencies {
+	compile("org.springframework.boot:spring-boot-starter-web")
+	compile("org.springframework.boot:spring-boot-starter-thymeleaf")
+	compile("org.springframework.boot:spring-boot-starter-data-jpa")
+	compile("org.springframework.boot:spring-boot-starter-data-mongodb")
+	compile("commons-fileupload:commons-fileupload:1.3.1")
+	compile("com.oracle:ojdbc6:11.2.0")
+
+	compile("org.webjars:webjars-locator")
+	compile("org.webjars:bootstrap:3.3.7")
+	compile("org.webjars:jquery:3.1.0")
+
+	compile("org.springframework.boot:spring-boot-starter-actuator")
+
+	testCompile("org.springframework.boot:spring-boot-starter-test")
+}

+ 9 - 0
develop.md

@@ -0,0 +1,9 @@
+# Develop Log
+
+version 0.1.9
+* feature 002: add emoji support
+
+version 0.2.0
+* feature 003: change message to 2 types, text, image and file.Dispatch message to one enterprise's someone.Count unread message for notifying the contact.
+
+

+ 16 - 0
gradle/dependencies-base.gradle

@@ -0,0 +1,16 @@
+// Gradle Base Dependencies Configurations
+// Created by huxz on 2017-3-17 14:39:36
+repositories {
+	mavenLocal()
+	maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
+	maven {
+		url 'http://10.10.101.21:8081/artifactory/libs-release'
+	}
+	maven {
+		url 'http://10.10.101.21:8081/artifactory/libs-snapshot'
+	}
+	maven {
+		url 'http://10.10.101.21:8081/artifactory/plugins-snapshot'
+	}
+	mavenCentral()
+}

+ 11 - 0
gradle/tasks.gradle

@@ -0,0 +1,11 @@
+// Gradle Tasks Configurations
+// Created by huxz on 2017-3-17 14:39:36
+bootRun {
+	addResources = true
+}
+
+docker {
+	name "${dcokerRegistry}/${project.name}:${project.version}"
+	dockerfile "${projectDir}/src/main/docker/Dockerfile"
+	files "${buildDir}/libs/${project.name}.jar"
+}.dependsOn build

BIN
gradle/wrapper/gradle-wrapper.jar


+ 6 - 0
gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Wed Jun 21 17:18:48 CST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip

+ 164 - 0
gradlew

@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

+ 90 - 0
gradlew.bat

@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 2 - 0
settings.gradle

@@ -0,0 +1,2 @@
+rootProject.name = 'web-chat'
+

+ 6 - 0
src/main/docker/Dockerfile

@@ -0,0 +1,6 @@
+FROM hub.c.163.com/library/java:8-jre-alpine
+VOLUME /tmp
+ADD web-chat.jar app.jar
+RUN sh -c "touch /app.jar"
+ENV JAVA_OPTS=""
+ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar --spring.profiles.active=test"]

+ 12 - 0
src/main/java/com/uas/demo/Application.java

@@ -0,0 +1,12 @@
+package com.uas.demo;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+
+@SpringBootApplication
+public class Application {
+
+	public static void main(String[] args) {
+		new SpringApplicationBuilder(Application.class).web(true).run(args);
+	}
+}

+ 74 - 0
src/main/java/com/uas/demo/api/ChatApiController.java

@@ -0,0 +1,74 @@
+package com.uas.demo.api;
+
+import com.uas.demo.facade.ChatInfoFacade;
+import com.uas.demo.model.ChatInfoDto;
+import com.uas.demo.service.ChatService;
+import org.apache.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping(value = "/api/chat/infos")
+public class ChatApiController {
+
+	private Logger logger = Logger.getLogger(getClass());
+
+	private final ChatService chatService;
+
+	private final ChatInfoFacade chatInfoFacade;
+
+	@Autowired
+	public ChatApiController(ChatService chatService, ChatInfoFacade chatInfoFacade) {
+		this.chatService = chatService;
+		this.chatInfoFacade = chatInfoFacade;
+	}
+
+	/**
+	 * 获取聊天双方的用户信息
+	 *
+	 * @param from		信息发送者的手机号
+	 * @param to		信息接收者的手机号
+	 */
+	@RequestMapping(method = RequestMethod.GET, params = "condition=phone")
+	public Map<String, String> findChatUserInfo(String from, String to) {
+		logger.info(String.format("User find information by phone[from: %s, to: %s]", from, to));
+		return chatService.findChatUserInfo(from, to);
+	}
+
+	/**
+	 * 获取聊天双方的用户信息
+	 *
+	 * @param from		信息发送者的UserId
+	 * @param to		信息接收者的UserId
+	 */
+	@RequestMapping(method = RequestMethod.GET, params = "condition=userid")
+	public Map<String, String> findChatUserInfoByUserId(String from, String to) {
+		logger.info(String.format("User find information by user id[from: %s, to: %s]", from, to));
+		return chatService.findChatUserInfoByUserId(from, to);
+	}
+
+	/**
+	 * 当用户访问会话列表或发起客服,生成会话信息
+	 *
+	 * @param chatInfoDto		聊天信息
+	 */
+	@RequestMapping(method = RequestMethod.POST, params = "condition=chat_info")
+	public Map<String, Object> generateChatInfoWhenUserVisitListOrChat(@RequestBody ChatInfoDto chatInfoDto) {
+		logger.info(String.format("Generate chat info when user visit list or chat, chat info %s", chatInfoDto.toString()));
+		return chatInfoFacade.generateChatInfoWhenUserVisitListOrChat(chatInfoDto);
+	}
+
+	/**
+	 * 用户访问网站时,根据聊天信息id获取聊天信息
+	 *
+	 * @param id    聊天信息ID
+	 */
+	@RequestMapping(value = "/{id}", method = RequestMethod.GET)
+	public Map<String, Object> queryChatInfoWhenUserVisitWebSite(@PathVariable("id") String id) {
+		logger.info(String.format("Query chat info[%s] when user visit web site", id));
+		return chatInfoFacade.queryChatInfoWhenUserVisitWebSite(id);
+	}
+
+}

+ 32 - 0
src/main/java/com/uas/demo/api/ChatSessionController.java

@@ -0,0 +1,32 @@
+package com.uas.demo.api;
+
+import com.uas.demo.model.ChatSession;
+import com.uas.demo.service.ChatSessionService;
+import org.apache.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping(value = "/api/chat/session")
+public class ChatSessionController {
+
+	private Logger logger = Logger.getLogger(getClass());
+
+	private final ChatSessionService chatSessionService;
+
+	@Autowired
+	public ChatSessionController(ChatSessionService chatSessionService) {
+		this.chatSessionService = chatSessionService;
+	}
+
+	@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
+	public List<ChatSession> updateSessionStateWhenUserSwitchNewSession(@PathVariable("id") String id) {
+		logger.info("Update session when user switch new session [" + id + "]");
+		return chatSessionService.updateSessionStateWhenUserSwitchNewSession(id);
+	}
+
+
+
+}

+ 61 - 0
src/main/java/com/uas/demo/api/FileController.java

@@ -0,0 +1,61 @@
+package com.uas.demo.api;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.log4j.Logger;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+
+@RestController
+@RequestMapping(value = "/api/utils/file")
+public class FileController {
+
+	private final Logger logger = Logger.getLogger(getClass());
+
+	private RestTemplate restTemplate;
+
+	public FileController() {
+		this.restTemplate = new RestTemplate();
+	}
+
+	@RequestMapping(method = RequestMethod.POST)
+	@ResponseStatus(HttpStatus.CREATED)
+	public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file, UriComponentsBuilder ucb) {
+
+		String fileUrl = "";
+		try {
+			File uploadFile = File.createTempFile("tmp", "." + FilenameUtils.getExtension(file.getOriginalFilename()));
+			file.transferTo(uploadFile);
+
+			FileSystemResource resource = new FileSystemResource(uploadFile);
+
+			MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
+			param.add("file", resource);
+			fileUrl = restTemplate.postForObject("http://10.10.100.23:20070/file/part", param, String.class);
+			uploadFile.deleteOnExit();
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+
+		HttpHeaders headers = new HttpHeaders();
+		URI uri = ucb.replacePath(fileUrl)
+				.build()
+				.toUri();
+		headers.setLocation(uri);
+
+		logger.info(String.format("Upload file[%s]", fileUrl));
+
+		return new ResponseEntity<>(fileUrl, headers, HttpStatus.CREATED);
+	}
+}

+ 53 - 0
src/main/java/com/uas/demo/api/MessageController.java

@@ -0,0 +1,53 @@
+package com.uas.demo.api;
+
+import com.uas.demo.facade.MessageFacade;
+import com.uas.demo.model.ChatSession;
+import com.uas.demo.model.Message;
+import com.uas.demo.service.MessageService;
+import org.apache.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping(value = "/api/chat/message")
+public class MessageController {
+
+	private final Logger logger = Logger.getLogger(getClass());
+
+	private final MessageService messageService;
+
+	private final MessageFacade messageFacade;
+
+	@Autowired
+	public MessageController(MessageService messageService, MessageFacade messageFacade) {
+		this.messageService = messageService;
+		this.messageFacade = messageFacade;
+	}
+
+	@RequestMapping(method = RequestMethod.POST)
+	public List<ChatSession> cacheMessageWhenClientReceive(@RequestBody Message message) {
+		logger.info(String.format("cache message %s when client receive", message.toString()));
+		return messageFacade.cacheMessageWhenClientReceive(message);
+	}
+
+	@RequestMapping(method = RequestMethod.GET, params = "operate=user_read")
+	public List<Message> loadReadableMessageWhenUserRead(String owner, String contact) {
+		logger.info(String.format("Load readable message when user %s read message from contact %s", owner, contact));
+		return messageService.loadReadableMessageWhenUserRead(owner, contact);
+	}
+
+	@RequestMapping(method = RequestMethod.GET, params = "operate=count_unread")
+	public Map<String, String> countUnReadMessageWhenUserQuery(String phone, Long enUU) {
+		logger.info(String.format("Count unread message when user %s %s query", phone, enUU));
+		return messageService.countUnReadMessageWhenUserQuery(phone, enUU);
+	}
+
+	@RequestMapping(method = RequestMethod.GET, params = "operate=history_message")
+	public List<Message> loadHistoryMessage(String owner, String contact, @RequestParam(required = false) Long max, @RequestParam(required = false) Long min) {
+		logger.info(String.format("Load history message(%s, %s) from %d to %d", owner, contact, min, max));
+		return messageService.loadHistoryMessage(owner, contact, max, min);
+	}
+}

+ 15 - 0
src/main/java/com/uas/demo/configuration/CorsConfig.java

@@ -0,0 +1,15 @@
+package com.uas.demo.configuration;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
+
+@Configuration
+public class CorsConfig extends WebMvcConfigurerAdapter {
+
+	@Override
+	public void addCorsMappings(CorsRegistry registry) {
+		registry.addMapping("/api/**")
+				.allowedOrigins("*");
+	}
+}

+ 21 - 0
src/main/java/com/uas/demo/configuration/MongoConfig.java

@@ -0,0 +1,21 @@
+package com.uas.demo.configuration;
+
+import com.mongodb.Mongo;
+import com.mongodb.MongoClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.core.MongoTemplate;
+
+@Configuration
+public class MongoConfig {
+
+	public @Bean
+	Mongo mongo() throws Exception {
+		return new MongoClient("10.10.100.22:27017");
+	}
+
+	public @Bean
+	MongoTemplate mongoTemplate() throws Exception {
+		return new MongoTemplate(mongo(), "im_infos");
+	}
+}

+ 25 - 0
src/main/java/com/uas/demo/dao/ChatInfoDao.java

@@ -0,0 +1,25 @@
+package com.uas.demo.dao;
+
+import com.uas.demo.model.ChatInfo;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ChatInfoDao extends MongoRepository<ChatInfo, String> {
+
+	/**
+	 * 获取当前用户当前企业的聊天信息
+	 *
+	 * @param userId	当前用户User Id
+	 * @param enUU		当前企业UU
+	 */
+	ChatInfo findByUserIdAndEnUU(String userId, Long enUU);
+
+	/**
+	 * 获取当前用户当前企业的聊天信息
+	 *
+	 * @param phone		当前用户手机号
+	 * @param enUU		当前企业UU
+	 */
+	ChatInfo findByPhoneAndEnUU(String phone, Long enUU);
+}

+ 17 - 0
src/main/java/com/uas/demo/dao/ChatSessionDao.java

@@ -0,0 +1,17 @@
+package com.uas.demo.dao;
+
+import com.uas.demo.model.ChatSession;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface ChatSessionDao extends MongoRepository<ChatSession, String> {
+
+	List<ChatSession> findByOwner(String owner);
+
+	List<ChatSession> findByOwnerAndCurrent(String owner, Boolean current);
+
+	ChatSession findByOwnerAndLinkman(String owner, String linkman);
+}

+ 16 - 0
src/main/java/com/uas/demo/dao/EnterpriseDao.java

@@ -0,0 +1,16 @@
+package com.uas.demo.dao;
+
+import com.uas.demo.model.Enterprise;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface EnterpriseDao extends MongoRepository<Enterprise, String> {
+
+	/**
+	 * 根据企业UU获取缓存的企业信息(包含店铺信息,如果有)
+	 *
+	 * @param enUU		企业UU
+	 */
+	Enterprise findByEnUU(Long enUU);
+}

+ 39 - 0
src/main/java/com/uas/demo/dao/MessageDao.java

@@ -0,0 +1,39 @@
+package com.uas.demo.dao;
+
+import com.uas.demo.model.Message;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface MessageDao extends MongoRepository<Message, String> {
+
+	/**
+	 * 根据发起者和接收者获取相应状态的消息缓存
+	 *
+	 * @param senderInfo		消息的发送者信息
+	 * @param receiverInfo		消息的接受者信息
+	 * @param read				消息是否已读
+	 */
+	List<Message> findBySenderInfoAndReceiverInfoAndReadOrderByTimeSendAsc(String senderInfo, String receiverInfo, Boolean read);
+
+	/**
+	 * 获取当前用户的前3条消息
+	 *
+	 * @param own			消息的拥有者
+	 * @param communicator	消息的关联者
+	 * @param read			消息是否已读
+	 */
+	List<Message> findTop3ByOwnAndCommunicatorAndReadOrderByTimeSendDescStyleAsc(String own, String communicator, Boolean read);
+
+	/**
+	 * 统计用户的未读或已读消息的数量
+	 *
+	 * @param own		用户User Id
+	 * @param read		消息阅读状态
+	 */
+	Long countByOwnAndRead(String own, Boolean read);
+
+	List<Message> findByOwnAndCommunicatorAndTimeSendBetweenOrderByTimeSendAsc(String own, String communicator, Long max, Long min);
+}

+ 17 - 0
src/main/java/com/uas/demo/dao/UserDao.java

@@ -0,0 +1,17 @@
+package com.uas.demo.dao;
+
+import com.uas.demo.model.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface UserDao extends JpaRepository<User, Long> {
+
+	@Query(value = "SELECT u FROM User u where u.phone = :phone and u.app = 'im'")
+	User findByPhone(@Param("phone") String phone);
+
+	@Query(value = "SELECT u FROM User u where u.userId = :userId and u.app = 'im'")
+	User findByUserId(@Param("userId") String userId);
+}

+ 23 - 0
src/main/java/com/uas/demo/dao/UserInfoDao.java

@@ -0,0 +1,23 @@
+package com.uas.demo.dao;
+
+import com.uas.demo.model.UserInfo;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface UserInfoDao extends MongoRepository<UserInfo, String> {
+
+	/**
+	 * 根据手机号获取用户信息
+	 *
+	 * @param phone		手机号
+	 */
+	UserInfo findByPhone(String phone);
+
+	/**
+	 * 根据用户ID获取用户信息
+	 *
+	 * @param userId	用户User Id
+	 */
+	UserInfo findByUserId(String userId);
+}

+ 23 - 0
src/main/java/com/uas/demo/facade/ChatInfoFacade.java

@@ -0,0 +1,23 @@
+package com.uas.demo.facade;
+
+import com.uas.demo.model.ChatInfoDto;
+
+import java.util.Map;
+
+public interface ChatInfoFacade {
+
+	/**
+	 * 当用户访问会话列表或发起客服,生成会话信息
+	 *
+	 * @param chatInfoDto		聊天信息
+	 */
+	Map<String, Object> generateChatInfoWhenUserVisitListOrChat(ChatInfoDto chatInfoDto);
+
+	/**
+	 * 当用户访问客服系统时,根据联系信息id获取聊天信息,并带出当前用户当前账套的
+	 * 会话列表信息
+	 *
+	 * @param id	聊天信息Id
+	 */
+	Map<String, Object> queryChatInfoWhenUserVisitWebSite(String id);
+}

+ 16 - 0
src/main/java/com/uas/demo/facade/MessageFacade.java

@@ -0,0 +1,16 @@
+package com.uas.demo.facade;
+
+import com.uas.demo.model.ChatSession;
+import com.uas.demo.model.Message;
+
+import java.util.List;
+
+public interface MessageFacade {
+
+	/**
+	 * 缓存客户端收到的消息信息,并获取更新后的会话信息
+	 *
+	 * @param message		消息
+	 */
+	List<ChatSession> cacheMessageWhenClientReceive(Message message);
+}

+ 120 - 0
src/main/java/com/uas/demo/facade/impl/ChatInfoFacadeImpl.java

@@ -0,0 +1,120 @@
+package com.uas.demo.facade.impl;
+
+import com.uas.demo.facade.ChatInfoFacade;
+import com.uas.demo.model.ChatInfo;
+import com.uas.demo.model.ChatInfoDto;
+import com.uas.demo.model.ChatSession;
+import com.uas.demo.service.ChatInfoService;
+import com.uas.demo.service.ChatSessionService;
+import com.uas.demo.service.EnterpriseService;
+import com.uas.demo.service.UserInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.*;
+
+@Service
+public class ChatInfoFacadeImpl implements ChatInfoFacade {
+
+	private final EnterpriseService enterpriseService;
+
+	private final UserInfoService userInfoService;
+
+	private final ChatSessionService chatSessionService;
+
+	private final ChatInfoService chatInfoService;
+
+	@Autowired
+	public ChatInfoFacadeImpl(EnterpriseService enterpriseService, UserInfoService userInfoService, ChatSessionService chatSessionService, ChatInfoService chatInfoService) {
+		this.enterpriseService = enterpriseService;
+		this.userInfoService = userInfoService;
+		this.chatSessionService = chatSessionService;
+		this.chatInfoService = chatInfoService;
+	}
+
+	@Override
+	@Transactional
+	public Map<String, Object> generateChatInfoWhenUserVisitListOrChat(ChatInfoDto chatInfoDto) {
+		if (chatInfoDto == null) {
+			return createErrorMessage("聊天信息不能为空");
+		}
+
+		// 创建或获取当前用户的聊天信息
+		ChatInfo chatInfo = chatInfoService.generateChatInfoWhenUserVisitListOrChat(chatInfoDto.getUserPhone(), chatInfoDto.getEnterprise());
+		if (chatInfo == null) {
+			return createErrorMessage("创建或获取聊天信息失败");
+		}
+
+		if ("CHAT".equals(chatInfoDto.getType())) {
+			// 创建或获取当前用户联系人的聊天信息
+			ChatInfo otherChatInfo = chatInfoService.generateChatInfoWhenUserVisitListOrChat(chatInfoDto.getToPhone(), chatInfoDto.getOtherEnterprise());
+			if (otherChatInfo == null) {
+				return createErrorMessage("创建或获取聊天信息失败");
+			}
+
+			ChatSession session = createSessionIfUserFindNone(chatInfo.getId(), otherChatInfo.getId(), chatInfoDto);
+			if (session == null) {
+				return createErrorMessage("生成或更新会话信息失败");
+			}
+		}
+		return createSuccessContent(chatInfo.getId());
+	}
+
+	@Override
+	public Map<String, Object> queryChatInfoWhenUserVisitWebSite(String id) {
+		if (StringUtils.isEmpty(id)) {
+			return createErrorMessage("聊天信息ID不能为空");
+		}
+
+		ChatInfo chatInfo = chatInfoService.queryChatInfoWhenUserVisitWebSite(id);
+		if (chatInfo == null) {
+			return createErrorMessage("聊天用户信息不存在");
+		}
+
+		List<ChatSession> sessions = chatSessionService.loadSessionsWhenUserVisitWebSite(id);
+		if (CollectionUtils.isEmpty(sessions)) {
+			sessions = Collections.emptyList();
+		}
+
+		// 保存获取信息
+		Map<String, Object> result = new HashMap<>();
+		result.put("chatInfo", chatInfo);
+		result.put("sessions", sessions);
+		result.put("success", true);
+		return result;
+	}
+
+	private ChatSession createSessionIfUserFindNone(String owner, String linkman, ChatInfoDto chatInfoDto) {
+		if (StringUtils.isEmpty(owner) || StringUtils.isEmpty(linkman)) {
+			return null;
+		}
+
+		ChatSession session = new ChatSession();
+		session.setOwner(owner);
+		session.setLinkman(linkman);
+		session.setCommunicatorType(chatInfoDto.getOtherUserType());
+		session.setTimeSend(new Date().getTime() / 1000);
+		session.setContent("");
+		session.setTopic(chatInfoDto.getTopic());
+		session.setRead(true);
+		session = chatSessionService.createSessionIfUserFindNone(session);
+		return session;
+	}
+
+	private Map<String, Object> createErrorMessage(String message) {
+		Map<String, Object> result = new HashMap<>();
+		result.put("message", message);
+		result.put("success", false);
+		return result;
+	}
+
+	private Map<String, Object> createSuccessContent(String content) {
+		Map<String, Object> result = new HashMap<>();
+		result.put("content", content);
+		result.put("success", true);
+		return result;
+	}
+}

+ 41 - 0
src/main/java/com/uas/demo/facade/impl/MessageFacadeImpl.java

@@ -0,0 +1,41 @@
+package com.uas.demo.facade.impl;
+
+import com.uas.demo.facade.MessageFacade;
+import com.uas.demo.model.ChatSession;
+import com.uas.demo.model.Message;
+import com.uas.demo.service.ChatSessionService;
+import com.uas.demo.service.MessageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+@Service
+public class MessageFacadeImpl implements MessageFacade {
+
+	private final ChatSessionService chatSessionService;
+
+	private final MessageService messageService;
+
+	@Autowired
+	public MessageFacadeImpl(ChatSessionService chatSessionService, MessageService messageService) {
+		this.chatSessionService = chatSessionService;
+		this.messageService = messageService;
+	}
+
+	@Override
+	public List<ChatSession> cacheMessageWhenClientReceive(Message message) {
+		if (message == null) {
+			return Collections.emptyList();
+		}
+		messageService.cacheMessageWhenClientReceive(message);
+
+		List<ChatSession> sessions = chatSessionService.loadSessionsWhenUserVisitWebSite(message.getOwn());
+		if (CollectionUtils.isEmpty(sessions)) {
+			return Collections.emptyList();
+		}
+		return sessions;
+	}
+}

+ 143 - 0
src/main/java/com/uas/demo/model/ChatInfo.java

@@ -0,0 +1,143 @@
+package com.uas.demo.model;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.CompoundIndex;
+import org.springframework.data.mongodb.core.index.CompoundIndexes;
+import org.springframework.data.mongodb.core.mapping.DBRef;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * 聊天信息
+ */
+@Document(collection = "chat_info")
+@CompoundIndexes(value = {
+		@CompoundIndex(name = "user_enterprise_unique", def = "{'userId': 1, 'enUU': 1}", unique = true),
+		@CompoundIndex(name = "user_enterprise_unique_1", def = "{'phone': 1, 'enUU': 1}", unique = true)
+})
+
+public class ChatInfo {
+
+	/**
+	 * 主键
+	 */
+	@Id
+	private String id;
+
+	/**
+	 * 当前用户User Id
+	 */
+	private String userId;
+
+	/**
+	 * 当前用户手机号
+	 */
+	private String phone;
+
+	/**
+	 * 当前用户当前企业UU
+	 */
+	private Long enUU;
+
+	/**
+	 * 缓存用户信息
+	 */
+	@DBRef
+	private UserInfo userInfo;
+
+	/**
+	 * 缓存企业信息
+	 */
+	@DBRef
+	private Enterprise enterprise;
+
+	/**
+	 * 时间戳
+	 */
+	private long version;
+
+	/**
+	 * 统计未读消息数量
+	 */
+	private long count = 0L;
+
+	public ChatInfo() {
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getUserId() {
+		return userId;
+	}
+
+	public void setUserId(String userId) {
+		this.userId = userId;
+	}
+
+	public String getPhone() {
+		return phone;
+	}
+
+	public void setPhone(String phone) {
+		this.phone = phone;
+	}
+
+	public Long getEnUU() {
+		return enUU;
+	}
+
+	public void setEnUU(Long enUU) {
+		this.enUU = enUU;
+	}
+
+	public UserInfo getUserInfo() {
+		return userInfo;
+	}
+
+	public void setUserInfo(UserInfo userInfo) {
+		this.userInfo = userInfo;
+	}
+
+	public Enterprise getEnterprise() {
+		return enterprise;
+	}
+
+	public void setEnterprise(Enterprise enterprise) {
+		this.enterprise = enterprise;
+	}
+
+	public long getVersion() {
+		return version;
+	}
+
+	public void setVersion(long version) {
+		this.version = version;
+	}
+
+	public long getCount() {
+		return count;
+	}
+
+	public void setCount(long count) {
+		this.count = count;
+	}
+
+	@Override
+	public String toString() {
+		return "ChatInfo{" +
+				"id='" + id + '\'' +
+				", userId='" + userId + '\'' +
+				", phone='" + phone + '\'' +
+				", enUU=" + enUU +
+				", userInfo=" + userInfo +
+				", enterprise=" + enterprise +
+				", version=" + version +
+				", count=" + count +
+				'}';
+	}
+}

+ 165 - 0
src/main/java/com/uas/demo/model/ChatInfoDto.java

@@ -0,0 +1,165 @@
+package com.uas.demo.model;
+
+/**
+ * 聊天信息
+ */
+public class ChatInfoDto {
+
+	/**
+	 * 当前用户手机号
+	 */
+	private String userPhone;
+
+	/**
+	 * 缓存用户信息Id
+	 */
+	private UserInfo userInfo;
+
+	/**
+	 * 用户企业信息
+	 */
+	private Enterprise enterprise;
+
+	/**
+	 * 当年用户类型,企业(买家) or 店铺(卖家)
+	 */
+	private UserType userType;
+
+	/**
+	 * 联系人手机号
+	 */
+	private String toPhone;
+
+	/**
+	 * 联系人缓存用户信息Id
+	 */
+	private String otherUserInfo;
+
+	/**
+	 * 联系人企业信息
+	 */
+	private Enterprise otherEnterprise;
+
+	/**
+	 * 交谈者用户类型,企业(买家) or 店铺(卖家)
+	 */
+	private UserType otherUserType;
+
+	/**
+	 * 交涉内容
+	 */
+	private String topic;
+
+	/**
+	 * 通信类型
+	 */
+	private String type;
+
+	public ChatInfoDto() {
+	}
+
+	public String getUserPhone() {
+		return userPhone;
+	}
+
+	public void setUserPhone(String userPhone) {
+		this.userPhone = userPhone;
+	}
+
+	public UserInfo getUserInfo() {
+		return userInfo;
+	}
+
+	public void setUserInfo(UserInfo userInfo) {
+		this.userInfo = userInfo;
+	}
+
+	public Enterprise getEnterprise() {
+		return enterprise;
+	}
+
+	public void setEnterprise(Enterprise enterprise) {
+		this.enterprise = enterprise;
+	}
+
+	public UserType getUserType() {
+		return userType;
+	}
+
+	public void setUserType(UserType userType) {
+		this.userType = userType;
+
+		// 设置联系人用户类型
+		if (UserType.STORE.equals(this.userType)) {
+			this.otherUserType = UserType.ENTERPRISE;
+		} else if (UserType.ENTERPRISE.equals(this.userType)) {
+			this.otherUserType = UserType.STORE;
+		} else {
+			this.otherUserType = null;
+		}
+	}
+
+	public String getToPhone() {
+		return toPhone;
+	}
+
+	public void setToPhone(String toPhone) {
+		this.toPhone = toPhone;
+	}
+
+	public String getOtherUserInfo() {
+		return otherUserInfo;
+	}
+
+	public void setOtherUserInfo(String otherUserInfo) {
+		this.otherUserInfo = otherUserInfo;
+	}
+
+	public Enterprise getOtherEnterprise() {
+		return otherEnterprise;
+	}
+
+	public void setOtherEnterprise(Enterprise otherEnterprise) {
+		this.otherEnterprise = otherEnterprise;
+	}
+
+	public UserType getOtherUserType() {
+		return otherUserType;
+	}
+
+	public void setOtherUserType(UserType otherUserType) {
+		this.otherUserType = otherUserType;
+	}
+
+	public String getTopic() {
+		return topic;
+	}
+
+	public void setTopic(String topic) {
+		this.topic = topic;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	@Override
+	public String toString() {
+		return "ChatInfoDto{" +
+				"userPhone='" + userPhone + '\'' +
+				", userInfo=" + userInfo +
+				", enterprise=" + enterprise +
+				", userType=" + userType +
+				", toPhone='" + toPhone + '\'' +
+				", otherUserInfo='" + otherUserInfo + '\'' +
+				", otherEnterprise=" + otherEnterprise +
+				", otherUserType=" + otherUserType +
+				", topic='" + topic + '\'' +
+				", type='" + type + '\'' +
+				'}';
+	}
+}

+ 284 - 0
src/main/java/com/uas/demo/model/ChatSession.java

@@ -0,0 +1,284 @@
+package com.uas.demo.model;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.CompoundIndex;
+import org.springframework.data.mongodb.core.mapping.DBRef;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * 聊天会话记录
+ */
+@Document(collection = "chat_session")
+@CompoundIndex(name = "owner_linkman_unique", def = "{'owner': 1, 'linkman': 1}", unique = true)
+public class ChatSession {
+
+	//===============================================================
+	// 主键信息
+	//===============================================================
+
+	/**
+	 * 主键
+	 */
+	@Id
+	private String id;
+
+	/**
+	 * 会话持有者聊天信息ID
+	 */
+	private String owner;
+
+	/**
+	 * 会话持有者联系人聊天信息ID
+	 */
+	private String linkman;
+
+
+	//===============================================================
+	// 基础信息
+	//===============================================================
+
+	/**
+	 * 会话持有者联系人聊天信息
+	 */
+	@DBRef
+	private ChatInfo linkmanInfo;
+
+	/**
+	 * 会话所属人 User Id
+	 */
+	private String own;
+
+	/**
+	 * 沟通者 User Id
+	 */
+	private String communicator;
+
+	/**
+	 * 沟通者姓名
+	 */
+	private String communicatorName;
+
+	/**
+	 * 缓存企业Id
+	 */
+	private String enterpriseId;
+
+	/**
+	 * 缓存企业信息
+	 */
+	@DBRef
+	private Enterprise enterprise;
+
+	/**
+	 * 沟通者所属,企业 or 店铺
+	 */
+	private UserType communicatorType;
+
+	/**
+	 * 话题
+	 */
+	private String topic;
+
+	/**
+	 * 消息发送者
+	 */
+	private String sender;
+
+	/**
+	 * 是否当前会话
+	 */
+	private Boolean current = false;
+
+	//===============================================================
+	// 消息基础信息
+	//===============================================================
+
+	/**
+	 * 消息发送时间戳
+	 */
+	private Long timeSend;
+
+	/**
+	 * 消息类型
+	 */
+	private int type = 1;
+
+	/**
+	 * 消息内容
+	 */
+	private String content;
+
+	/**
+	 * 是否已读
+	 */
+	private Boolean read = true;
+
+	/**
+	 * 是否自己发送
+	 */
+	private MessageType style = MessageType.SEND;
+
+	public ChatSession() {
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getOwn() {
+		return own;
+	}
+
+	public void setOwn(String own) {
+		this.own = own;
+	}
+
+	public Long getTimeSend() {
+		return timeSend;
+	}
+
+	public void setTimeSend(Long timeSend) {
+		this.timeSend = timeSend;
+	}
+
+	public int getType() {
+		return type;
+	}
+
+	public void setType(int type) {
+		this.type = type;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
+	public Boolean getRead() {
+		return read;
+	}
+
+	public void setRead(Boolean read) {
+		this.read = read;
+	}
+
+	public String getCommunicator() {
+		return communicator;
+	}
+
+	public void setCommunicator(String communicator) {
+		this.communicator = communicator;
+	}
+
+	public String getCommunicatorName() {
+		return communicatorName;
+	}
+
+	public void setCommunicatorName(String communicatorName) {
+		this.communicatorName = communicatorName;
+	}
+
+	public UserType getCommunicatorType() {
+		return communicatorType;
+	}
+
+	public void setCommunicatorType(UserType communicatorType) {
+		this.communicatorType = communicatorType;
+	}
+
+	public MessageType getStyle() {
+		return style;
+	}
+
+	public void setStyle(MessageType style) {
+		this.style = style;
+	}
+
+	public String getSender() {
+		return sender;
+	}
+
+	public void setSender(String sender) {
+		this.sender = sender;
+	}
+
+	public Boolean getCurrent() {
+		return current;
+	}
+
+	public void setCurrent(Boolean current) {
+		this.current = current;
+	}
+
+	public String getTopic() {
+		return topic;
+	}
+
+	public void setTopic(String topic) {
+		this.topic = topic;
+	}
+
+	public String getEnterpriseId() {
+		return enterpriseId;
+	}
+
+	public void setEnterpriseId(String enterpriseId) {
+		this.enterpriseId = enterpriseId;
+	}
+
+	public Enterprise getEnterprise() {
+		return enterprise;
+	}
+
+	public void setEnterprise(Enterprise enterprise) {
+		this.enterprise = enterprise;
+	}
+
+	public String getOwner() {
+		return owner;
+	}
+
+	public void setOwner(String owner) {
+		this.owner = owner;
+	}
+
+	public String getLinkman() {
+		return linkman;
+	}
+
+	public void setLinkman(String linkman) {
+		this.linkman = linkman;
+	}
+
+	public ChatInfo getLinkmanInfo() {
+		return linkmanInfo;
+	}
+
+	public void setLinkmanInfo(ChatInfo linkmanInfo) {
+		this.linkmanInfo = linkmanInfo;
+	}
+
+	@Override
+	public String toString() {
+		return "ChatSession{" +
+				"id='" + id + '\'' +
+				", own='" + own + '\'' +
+				", communicator='" + communicator + '\'' +
+				", communicatorName='" + communicatorName + '\'' +
+				", communicatorType=" + communicatorType +
+				", sender='" + sender + '\'' +
+				", timeSend=" + timeSend +
+				", type=" + type +
+				", content='" + content + '\'' +
+				", read=" + read +
+				", style=" + style +
+				'}';
+	}
+}

+ 109 - 0
src/main/java/com/uas/demo/model/Enterprise.java

@@ -0,0 +1,109 @@
+package com.uas.demo.model;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * 缓存企业及其店铺信息
+ */
+@Document(collection = "enterprise")
+public class Enterprise {
+
+	/**
+	 * 主键
+	 */
+	@Id
+	private String id;
+
+	/**
+	 * 企业UU
+	 */
+	@Indexed(unique = true)
+	private Long enUU;
+
+	/**
+	 * 企业名称
+	 */
+	@Indexed(unique = true)
+	private String name;
+
+	/**
+	 * 店铺名称
+	 */
+	private String storeName;
+
+	/**
+	 * 店铺类型
+	 */
+	private String storeType;
+
+	/**
+	 * 店铺信息JSON
+	 */
+	private String storeInfo;
+
+	// 后续新增
+
+	public Enterprise() {
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public Long getEnUU() {
+		return enUU;
+	}
+
+	public void setEnUU(Long enUU) {
+		this.enUU = enUU;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getStoreName() {
+		return storeName;
+	}
+
+	public void setStoreName(String storeName) {
+		this.storeName = storeName;
+	}
+
+	public String getStoreType() {
+		return storeType;
+	}
+
+	public void setStoreType(String storeType) {
+		this.storeType = storeType;
+	}
+
+	public String getStoreInfo() {
+		return storeInfo;
+	}
+
+	public void setStoreInfo(String storeInfo) {
+		this.storeInfo = storeInfo;
+	}
+
+	@Override
+	public String toString() {
+		return "Enterprise{" +
+				"id='" + id + '\'' +
+				", enUU=" + enUU +
+				", name='" + name + '\'' +
+				", storeName='" + storeName + '\'' +
+				", storeType='" + storeType + '\'' +
+				'}';
+	}
+}

+ 208 - 0
src/main/java/com/uas/demo/model/Message.java

@@ -0,0 +1,208 @@
+package com.uas.demo.model;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * 聊天记录-缓存
+ */
+@Document(collection = "message")
+public class Message {
+
+	/**
+	 * 主键
+	 */
+	@Id
+	private String id;
+
+	//-----------------------------------------------------
+	// 消息基础信息
+	//-----------------------------------------------------
+
+	/**
+	 * 发送时间
+	 */
+	private Long timeSend;
+
+	/**
+	 * 消息缓存拥有者[alias owner]
+	 */
+	private String own;
+
+	/**
+	 * 联系人
+	 */
+	private String communicator;
+
+	//-----------------------------------------------------
+	// 消息内容信息
+	//-----------------------------------------------------
+
+	/**
+	 * 消息类型
+	 */
+	private String type;
+
+	/**
+	 * 消息发送者
+	 */
+	private String senderInfo;
+
+	/**
+	 * 消息接收者
+	 */
+	private String receiverInfo;
+
+	/**
+	 * 发送者
+	 */
+	private String sender;
+
+	/**
+	 * 发送者类型
+	 */
+	private UserType senderType;
+
+	/**
+	 * 接收者
+	 */
+	private String receiver;
+
+	/**
+	 * 消息内容
+	 */
+	private String content;
+
+	/**
+	 * 是否已读
+	 */
+	private Boolean read = false;
+
+	private MessageType style = MessageType.SEND;
+
+	public Message() {
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public Long getTimeSend() {
+		return timeSend;
+	}
+
+	public void setTimeSend(Long timeSend) {
+		this.timeSend = timeSend;
+	}
+
+	public String getOwn() {
+		return own;
+	}
+
+	public void setOwn(String own) {
+		this.own = own;
+	}
+
+	public String getCommunicator() {
+		return communicator;
+	}
+
+	public void setCommunicator(String communicator) {
+		this.communicator = communicator;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	public String getSenderInfo() {
+		return senderInfo;
+	}
+
+	public void setSenderInfo(String senderInfo) {
+		this.senderInfo = senderInfo;
+	}
+
+	public String getReceiverInfo() {
+		return receiverInfo;
+	}
+
+	public void setReceiverInfo(String receiverInfo) {
+		this.receiverInfo = receiverInfo;
+	}
+
+	public String getSender() {
+		return sender;
+	}
+
+	public void setSender(String sender) {
+		this.sender = sender;
+	}
+
+	public UserType getSenderType() {
+		return senderType;
+	}
+
+	public void setSenderType(UserType senderType) {
+		this.senderType = senderType;
+	}
+
+	public String getReceiver() {
+		return receiver;
+	}
+
+	public void setReceiver(String receiver) {
+		this.receiver = receiver;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
+	public Boolean getRead() {
+		return read;
+	}
+
+	public void setRead(Boolean read) {
+		this.read = read;
+	}
+
+	public MessageType getStyle() {
+		return style;
+	}
+
+	public void setStyle(MessageType style) {
+		this.style = style;
+	}
+
+	@Override
+	public String toString() {
+		return "Message{" +
+				"id='" + id + '\'' +
+				", timeSend=" + timeSend +
+				", own='" + own + '\'' +
+				", communicator='" + communicator + '\'' +
+				", type='" + type + '\'' +
+				", senderInfo='" + senderInfo + '\'' +
+				", receiverInfo='" + receiverInfo + '\'' +
+				", sender='" + sender + '\'' +
+				", senderType=" + senderType +
+				", receiver='" + receiver + '\'' +
+				", content='" + content + '\'' +
+				", read=" + read +
+				", style=" + style +
+				'}';
+	}
+}

+ 6 - 0
src/main/java/com/uas/demo/model/MessageType.java

@@ -0,0 +1,6 @@
+package com.uas.demo.model;
+
+public enum MessageType {
+	SEND, 		// 发送消息
+	RECEIVE, 	// 接受消息
+}

+ 8 - 0
src/main/java/com/uas/demo/model/StoreType.java

@@ -0,0 +1,8 @@
+package com.uas.demo.model;
+
+public enum StoreType {
+	AGENCY, 			// 代理
+	DISTRIBUTION,		// 经销
+	ORIGINAL_FACTORY,	// 原厂
+	CONSIGNMENT,		// 库存寄售
+}

+ 93 - 0
src/main/java/com/uas/demo/model/User.java

@@ -0,0 +1,93 @@
+package com.uas.demo.model;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+@Entity
+@Table(name = "ac$users")
+public class User {
+
+	@Id
+	@Column(name = "id")
+	private Long id;
+
+	@Column(name = "uid_")
+	private String phone;
+
+	@Column(name = "name")
+	private String name;
+
+	@Column(name = "password")
+	private String password;
+
+	@Column(name = "dialectuid")
+	private String userId;
+
+	@Column(name = "appid")
+	private String app;
+
+	public User() {
+	}
+
+	public Long getId() {
+		return id;
+	}
+
+	public void setId(Long id) {
+		this.id = id;
+	}
+
+	public String getPhone() {
+		return phone;
+	}
+
+	public void setPhone(String phone) {
+		this.phone = phone;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getPassword() {
+		return password;
+	}
+
+	public void setPassword(String password) {
+		this.password = password;
+	}
+
+	public String getUserId() {
+		return userId;
+	}
+
+	public void setUserId(String userId) {
+		this.userId = userId;
+	}
+
+	public String getApp() {
+		return app;
+	}
+
+	public void setApp(String app) {
+		this.app = app;
+	}
+
+	@Override
+	public String toString() {
+		return "User{" +
+				"id=" + id +
+				", phone='" + phone + '\'' +
+				", name='" + name + '\'' +
+				", password='" + password + '\'' +
+				", userId='" + userId + '\'' +
+				", app='" + app + '\'' +
+				'}';
+	}
+}

+ 97 - 0
src/main/java/com/uas/demo/model/UserInfo.java

@@ -0,0 +1,97 @@
+package com.uas.demo.model;
+
+import org.springframework.data.mongodb.core.index.CompoundIndex;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import javax.persistence.Id;
+
+/**
+ * 缓存用户或联系人信息
+ */
+@Document(collection = "user_info")
+@CompoundIndex(name = "phone_user_id_unique", def = "{'phone': 1, 'userId': 1}", unique = true)
+public class UserInfo {
+
+	/**
+	 * 主键
+	 */
+	@Id
+	private String id;
+
+	/**
+	 * 手机号
+	 */
+	@Indexed(unique = true)
+	private String phone;
+
+	/**
+	 * 用户User Id[IM]
+	 */
+	@Indexed(unique = true)
+	private String userId;
+
+	/**
+	 * 资质
+	 */
+	private String certification;
+
+	/**
+	 * 用户名称
+	 */
+	private String name;
+
+	public UserInfo() {
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getPhone() {
+		return phone;
+	}
+
+	public void setPhone(String phone) {
+		this.phone = phone;
+	}
+
+	public String getUserId() {
+		return userId;
+	}
+
+	public void setUserId(String userId) {
+		this.userId = userId;
+	}
+
+	public String getCertification() {
+		return certification;
+	}
+
+	public void setCertification(String certification) {
+		this.certification = certification;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	@Override
+	public String toString() {
+		return "UserInfo{" +
+				"id='" + id + '\'' +
+				", phone='" + phone + '\'' +
+				", userId='" + userId + '\'' +
+				", certification='" + certification + '\'' +
+				", name='" + name + '\'' +
+				'}';
+	}
+}

+ 6 - 0
src/main/java/com/uas/demo/model/UserType.java

@@ -0,0 +1,6 @@
+package com.uas.demo.model;
+
+public enum UserType {
+	ENTERPRISE,		// 企业
+	STORE			// 店铺
+}

+ 19 - 0
src/main/java/com/uas/demo/monitor/MongoMonitor.java

@@ -0,0 +1,19 @@
+package com.uas.demo.monitor;
+
+import com.mongodb.MongoClient;
+import org.springframework.boot.actuate.health.AbstractHealthIndicator;
+import org.springframework.boot.actuate.health.Health;
+
+public class MongoMonitor extends AbstractHealthIndicator {
+
+	private final MongoClient client;
+
+	public MongoMonitor(MongoClient client) {
+		this.client = client;
+	}
+
+	@Override
+	protected void doHealthCheck(Health.Builder builder) throws Exception {
+		builder.up().withDetail("mongoClient", client.getAddress());
+	}
+}

+ 22 - 0
src/main/java/com/uas/demo/service/ChatInfoService.java

@@ -0,0 +1,22 @@
+package com.uas.demo.service;
+
+import com.uas.demo.model.ChatInfo;
+import com.uas.demo.model.Enterprise;
+
+public interface ChatInfoService {
+
+	/**
+	 * 当用户访问会话列表或发起客服,创建或获取聊天信息
+	 *
+	 * @param userPhone		用户手机号
+	 * @param enterprise	用户对应的企业信息
+	 */
+	ChatInfo generateChatInfoWhenUserVisitListOrChat(String userPhone, Enterprise enterprise);
+
+	/**
+	 * 用户访问网站时,根据聊天信息id获取聊天信息
+	 *
+	 * @param id	聊天信息ID
+	 */
+	ChatInfo queryChatInfoWhenUserVisitWebSite(String id);
+}

+ 22 - 0
src/main/java/com/uas/demo/service/ChatService.java

@@ -0,0 +1,22 @@
+package com.uas.demo.service;
+
+import java.util.Map;
+
+public interface ChatService {
+
+	/**
+	 * 获取聊天双方的用户信息
+	 *
+	 * @param fromPhone		信息发送者的手机号
+	 * @param toPhone		信息接收者的手机号
+	 */
+	Map<String, String> findChatUserInfo(String fromPhone, String toPhone);
+
+	/**
+	 * 获取聊天双方的用户信息
+	 *
+	 * @param from		信息发送者的UserId
+	 * @param to		信息接收者的UserId
+	 */
+	Map<String, String> findChatUserInfoByUserId(String from, String to);
+}

+ 29 - 0
src/main/java/com/uas/demo/service/ChatSessionService.java

@@ -0,0 +1,29 @@
+package com.uas.demo.service;
+
+import com.uas.demo.model.ChatSession;
+
+import java.util.List;
+
+public interface ChatSessionService {
+
+	/**
+	 * 用户刷新页面时加载会话记录数据
+	 *
+	 * @param ownerId		聊天信息ID
+	 */
+	List<ChatSession> loadSessionsWhenUserVisitWebSite(String ownerId);
+
+	/**
+	 * 如果用户没有找到会话记录,则创建一个新的会话
+	 *
+	 * @param session	会话信息
+	 */
+	ChatSession createSessionIfUserFindNone(ChatSession session);
+
+	/**
+	 * 当用户切换新的会话时,更新选中会话的状态
+	 *
+	 * @param sessionId    会话ID
+	 */
+	List<ChatSession> updateSessionStateWhenUserSwitchNewSession(String sessionId);
+}

+ 14 - 0
src/main/java/com/uas/demo/service/EnterpriseService.java

@@ -0,0 +1,14 @@
+package com.uas.demo.service;
+
+import com.uas.demo.model.Enterprise;
+
+public interface EnterpriseService {
+
+	/**
+	 * 当用户访问系统时,如果不存在缓存的企业信息,则保存外部系统传入的企业
+	 * 信息;否则,根据企业UU获取企业信息
+	 *
+	 * @param enterprise		企业信息
+	 */
+	Enterprise cacheEnterpriseInfoWhenUserVisitSystem(Enterprise enterprise);
+}

+ 44 - 0
src/main/java/com/uas/demo/service/MessageService.java

@@ -0,0 +1,44 @@
+package com.uas.demo.service;
+
+import com.uas.demo.model.Message;
+
+import java.util.List;
+import java.util.Map;
+
+public interface MessageService {
+
+	/**
+	 * 缓存客户端收到的消息信息
+	 *
+	 * @param message		消息
+	 */
+	Message cacheMessageWhenClientReceive(Message message);
+
+	/**
+	 * 用户读取消息时获取未读消息,并更新为已读
+	 *
+	 * @param owner        消息缓存拥有者聊天信息ID
+	 * @param contact      消息缓存聊天联系人聊天信息ID
+	 */
+	List<Message> loadReadableMessageWhenUserRead(String owner, String contact);
+
+	/**
+	 * 用户查询未读会话数据
+	 *
+	 * @param userPhone		用户手机号
+	 * @param enUU			用户企业UU
+	 */
+	Map<String, String> countUnReadMessageWhenUserQuery(String userPhone, Long enUU);
+
+	/**
+	 * 获取用户的聊天历史记录,如果小于30天,则返回某个区间的消息历史,否则,返回余下所有
+	 * 的消息历史记录
+	 *
+	 * @param owner		消息缓存拥有者聊天信息ID
+	 * @param contact	消息缓存聊天联系人聊天信息ID
+	 * @param max		最近时间点
+	 * @param min		最早时间点
+	 */
+	List<Message> loadHistoryMessage(String owner, String contact, Long max, Long min);
+
+}

+ 14 - 0
src/main/java/com/uas/demo/service/UserInfoService.java

@@ -0,0 +1,14 @@
+package com.uas.demo.service;
+
+import com.uas.demo.model.UserInfo;
+
+public interface UserInfoService {
+
+	/**
+	 * 当用户访问系统时,如果用户信息不存在,则缓存用户信息或联系人信息;
+	 * 否则返回已缓存的用户或联系人信息
+	 *
+	 * @param phone		用户或联系人手机号
+	 */
+	UserInfo cacheUserInfoWhenUserVisitSystem(String phone);
+}

+ 75 - 0
src/main/java/com/uas/demo/service/impl/ChatInfoServiceImpl.java

@@ -0,0 +1,75 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.ChatInfoDao;
+import com.uas.demo.dao.UserDao;
+import com.uas.demo.model.ChatInfo;
+import com.uas.demo.model.Enterprise;
+import com.uas.demo.model.UserInfo;
+import com.uas.demo.service.ChatInfoService;
+import com.uas.demo.service.EnterpriseService;
+import com.uas.demo.service.UserInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.Date;
+
+@Service
+public class ChatInfoServiceImpl implements ChatInfoService {
+
+	private final UserDao userDao;
+
+	private final UserInfoService userInfoService;
+
+	private final EnterpriseService enterpriseService;
+
+	private final ChatInfoDao chatInfoDao;
+
+	@Autowired
+	public ChatInfoServiceImpl(UserDao userDao, UserInfoService userInfoService, EnterpriseService enterpriseService
+			, ChatInfoDao chatInfoDao) {
+		this.userDao = userDao;
+		this.userInfoService = userInfoService;
+		this.enterpriseService = enterpriseService;
+		this.chatInfoDao = chatInfoDao;
+	}
+
+	@Override
+	public ChatInfo generateChatInfoWhenUserVisitListOrChat(String userPhone, Enterprise enterprise) {
+		// 验证参数信息
+		if (StringUtils.isEmpty(userPhone) || enterprise == null || enterprise.getEnUU() == null) {
+			return null;
+		}
+
+		// 获取用户缓存信息
+		UserInfo userInfo = userInfoService.cacheUserInfoWhenUserVisitSystem(userPhone);
+		assert userInfo != null;
+
+		ChatInfo chatInfo = chatInfoDao.findByUserIdAndEnUU(userInfo.getUserId(), enterprise.getEnUU());
+		if (chatInfo == null) {
+			chatInfo = new ChatInfo();
+			chatInfo.setUserInfo(userInfo);
+			chatInfo.setUserId(userInfo.getUserId());
+			chatInfo.setPhone(userInfo.getPhone());
+
+			// 获取用户缓存企业信息
+			enterprise = enterpriseService.cacheEnterpriseInfoWhenUserVisitSystem(enterprise);
+			assert enterprise != null;
+			chatInfo.setEnterprise(enterprise);
+			chatInfo.setEnUU(enterprise.getEnUU());
+		}
+
+		// 更新聊天信息版本信息,并持久化
+		chatInfo.setVersion(new Date().getTime());
+		chatInfo = chatInfoDao.save(chatInfo);
+		return chatInfo;
+	}
+
+	@Override
+	public ChatInfo queryChatInfoWhenUserVisitWebSite(String id) {
+		if (StringUtils.isEmpty(id)) {
+			return null;
+		}
+		return chatInfoDao.findOne(id);
+	}
+}

+ 91 - 0
src/main/java/com/uas/demo/service/impl/ChatServiceImpl.java

@@ -0,0 +1,91 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.UserDao;
+import com.uas.demo.model.User;
+import com.uas.demo.service.ChatService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@Service
+public class ChatServiceImpl implements ChatService {
+
+	private final UserDao userDao;
+
+	@Autowired
+	public ChatServiceImpl(UserDao userDao) {
+		this.userDao = userDao;
+	}
+
+	@Override
+	public Map<String, String> findChatUserInfo(String fromPhone, String toPhone) {
+		if (StringUtils.isEmpty(fromPhone)) {
+			return Collections.emptyMap();
+		}
+
+		// 设置聊天发起人信息
+		User fromUser = userDao.findByPhone(fromPhone);
+		if (fromUser == null) {
+			return Collections.emptyMap();
+		}
+		Map<String, String> userInfos = new HashMap<>();
+		userInfos.put("userId", fromUser.getUserId());
+		userInfos.put("userName", fromUser.getName());
+		userInfos.put("certification", fromUser.getPassword());
+
+		if (StringUtils.isEmpty(toPhone)) {
+			userInfos.put("type", "USER_LIST");
+			return userInfos;
+		}
+
+		// 设置聊天接受人信息
+		User toUser = userDao.findByPhone(toPhone);
+		if (toUser != null) {
+			userInfos.put("contactId", toUser.getUserId());
+			userInfos.put("contactName", toUser.getName());
+			userInfos.put("type", "CHAT");
+		} else {
+			userInfos.put("type", "USER_LIST");
+		}
+
+		return userInfos;
+	}
+
+	@Override
+	public Map<String, String> findChatUserInfoByUserId(String from, String to) {
+		if (StringUtils.isEmpty(from)) {
+			return Collections.emptyMap();
+		}
+
+		// 设置聊天发起人信息
+		User fromUser = userDao.findByUserId(from);
+		if (fromUser == null) {
+			return Collections.emptyMap();
+		}
+		Map<String, String> userInfos = new HashMap<>();
+		userInfos.put("userId", fromUser.getUserId());
+		userInfos.put("userName", fromUser.getName());
+		userInfos.put("certification", fromUser.getPassword());
+
+		if (StringUtils.isEmpty(to)) {
+			userInfos.put("type", "USER_LIST");
+			return userInfos;
+		}
+
+		// 设置聊天接受人信息
+		User toUser = userDao.findByUserId(to);
+		if (toUser == null) {
+			userInfos.put("type", "USER_LIST");
+		} else {
+			userInfos.put("contactId", toUser.getUserId());
+			userInfos.put("contactName", toUser.getName());
+			userInfos.put("type", "CHAT");
+		}
+
+		return userInfos;
+	}
+}

+ 102 - 0
src/main/java/com/uas/demo/service/impl/ChatSessionServiceImpl.java

@@ -0,0 +1,102 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.ChatInfoDao;
+import com.uas.demo.dao.ChatSessionDao;
+import com.uas.demo.model.ChatInfo;
+import com.uas.demo.model.ChatSession;
+import com.uas.demo.service.ChatSessionService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+@Service
+public class ChatSessionServiceImpl implements ChatSessionService {
+
+	private final ChatInfoDao chatInfoDao;
+
+	private final ChatSessionDao chatSessionDao;
+
+	@Autowired
+	public ChatSessionServiceImpl(ChatInfoDao chatInfoDao, ChatSessionDao chatSessionDao) {
+		this.chatInfoDao = chatInfoDao;
+		this.chatSessionDao = chatSessionDao;
+	}
+
+	@Override
+	public List<ChatSession> loadSessionsWhenUserVisitWebSite(String ownerId) {
+		if (StringUtils.isEmpty(ownerId)) {
+			return Collections.emptyList();
+		}
+
+		return chatSessionDao.findByOwner(ownerId);
+	}
+
+	@Override
+	public ChatSession createSessionIfUserFindNone(ChatSession session) {
+		if (session == null || StringUtils.isEmpty(session.getOwner()) || StringUtils.isEmpty(session.getLinkman())) {
+			return null;
+		}
+
+		// 删除遗留的会话激活状态
+		List<ChatSession> sessions = chatSessionDao.findByOwnerAndCurrent(session.getOwner(), true);
+		if (CollectionUtils.isEmpty(sessions)) {
+			sessions = Collections.emptyList();
+		}
+		for (ChatSession chatSession : sessions) {
+			chatSession.setCurrent(false);
+			chatSessionDao.save(chatSession);
+		}
+
+		ChatInfo chatInfo = chatInfoDao.findOne(session.getLinkman());
+		if (chatInfo == null) {
+			return null;
+		}
+		session.setLinkmanInfo(chatInfo);
+
+		// 如果已存在会话记录,则更新相应的会话
+		ChatSession existSession = chatSessionDao.findByOwnerAndLinkman(session.getOwner(), session.getLinkman());
+		if (existSession != null) {
+			existSession.setTopic(session.getTopic());
+			session = existSession;
+		}
+
+		// 创建会话信息
+		session.setCurrent(true);
+		session = chatSessionDao.save(session);
+		return session;
+	}
+
+	@Override
+	public List<ChatSession> updateSessionStateWhenUserSwitchNewSession(String sessionId) {
+		if (StringUtils.isEmpty(sessionId)) {
+			return null;
+		}
+
+		ChatSession session = chatSessionDao.findOne(sessionId);
+		if (session == null || StringUtils.isEmpty(session.getOwner())) {
+			return null;
+		}
+
+		// 删除遗留的会话激活状态
+		List<ChatSession> sessions = chatSessionDao.findByOwnerAndCurrent(session.getOwner(), true);
+		if (CollectionUtils.isEmpty(sessions)) {
+			sessions = Collections.emptyList();
+		}
+		for (ChatSession chatSession : sessions) {
+			chatSession.setCurrent(false);
+			chatSessionDao.save(chatSession);
+		}
+
+		session.setRead(true);
+		session.setCurrent(true);
+		chatSessionDao.save(session);
+
+		sessions = chatSessionDao.findByOwner(session.getOwner());
+		return sessions;
+	}
+
+}

+ 31 - 0
src/main/java/com/uas/demo/service/impl/EnterpriseServiceImpl.java

@@ -0,0 +1,31 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.EnterpriseDao;
+import com.uas.demo.model.Enterprise;
+import com.uas.demo.service.EnterpriseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class EnterpriseServiceImpl implements EnterpriseService {
+
+	private final EnterpriseDao enterpriseDao;
+
+	@Autowired
+	public EnterpriseServiceImpl(EnterpriseDao enterpriseDao) {
+		this.enterpriseDao = enterpriseDao;
+	}
+
+	@Override
+	public Enterprise cacheEnterpriseInfoWhenUserVisitSystem(Enterprise enterprise) {
+		if (enterprise == null || enterprise.getEnUU() == null) {
+			return null;
+		}
+
+		Enterprise findEnterprise = enterpriseDao.findByEnUU(enterprise.getEnUU());
+		if (findEnterprise == null) {
+			findEnterprise = enterpriseDao.save(enterprise);
+		}
+		return findEnterprise;
+	}
+}

+ 220 - 0
src/main/java/com/uas/demo/service/impl/MessageServiceImpl.java

@@ -0,0 +1,220 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.ChatInfoDao;
+import com.uas.demo.dao.ChatSessionDao;
+import com.uas.demo.dao.MessageDao;
+import com.uas.demo.dao.UserDao;
+import com.uas.demo.model.*;
+import com.uas.demo.service.MessageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.*;
+
+@Service
+public class MessageServiceImpl implements MessageService {
+
+	private final UserDao userDao;
+
+	private final ChatInfoDao chatInfoDao;
+
+	private final MessageDao messageDao;
+
+	private final ChatSessionDao chatSessionDao;
+
+	private final MongoTemplate mongoTemplate;
+
+	@Autowired
+	public MessageServiceImpl(UserDao userDao, ChatInfoDao chatInfoDao, MessageDao messageDao, ChatSessionDao chatSessionDao, MongoTemplate mongoTemplate) {
+		this.userDao = userDao;
+		this.chatInfoDao = chatInfoDao;
+		this.messageDao = messageDao;
+		this.chatSessionDao = chatSessionDao;
+		this.mongoTemplate = mongoTemplate;
+	}
+
+	@Override
+	@Transactional
+	public Message cacheMessageWhenClientReceive(Message message) {
+		if (message == null) {
+			return null;
+		}
+
+		Boolean read = message.getRead();
+		UserType communicatorType = message.getSenderType();
+		String linkman = message.getSenderInfo();
+		// 当前用户发送消息
+		if (Objects.equals(message.getOwn(), message.getSenderInfo())) {
+			linkman = message.getReceiverInfo();
+			if (UserType.STORE == message.getSenderType()) {
+				communicatorType = UserType.ENTERPRISE;
+			}
+			if (UserType.ENTERPRISE == message.getSenderType()) {
+				communicatorType = UserType.STORE;
+			}
+		}
+		setCommunicator(message);
+		message = messageDao.save(message);
+
+		ChatSession session = chatSessionDao.findByOwnerAndLinkman(message.getOwn(), linkman);
+		if (session == null) {
+			session = new ChatSession();
+			session.setOwner(message.getOwn());
+			session.setLinkman(linkman);
+			session.setCommunicatorType(communicatorType);
+
+			ChatInfo chatInfo = chatInfoDao.findOne(linkman);
+			if (chatInfo == null) {
+				return null;
+			}
+			session.setLinkmanInfo(chatInfo);
+		}
+
+		session.setSender(message.getSender());
+		session.setTimeSend(message.getTimeSend());
+		session.setType(Integer.parseInt(message.getType()));
+		session.setContent(message.getContent());
+		session.setStyle(message.getStyle());
+
+		if (!read) {
+			session.setRead(read);
+		}
+		chatSessionDao.save(session);
+
+		// 发送消息时,联系人未读消息 + 1
+		if (MessageType.SEND == message.getStyle()) {
+			System.out.println(message.getCommunicator() + " MESSAGE_UN_READ: +1");
+
+			ChatInfo chatInfo = chatInfoDao.findOne(message.getCommunicator());
+			assert chatInfo != null;
+			chatInfo.setCount(chatInfo.getCount() + 1);
+			chatInfoDao.save(chatInfo);
+		}
+		// 当前用户接收消息时,统计未读数量并更新记录
+		if (MessageType.RECEIVE == message.getStyle()) {
+			Long count = messageDao.countByOwnAndRead(message.getOwn(), false);
+			System.out.println(message.getOwn() + " MESSAGE_UN_READ: " + count);
+
+			ChatInfo chatInfo = chatInfoDao.findOne(message.getOwn());
+			assert chatInfo != null;
+			chatInfo.setCount(count);
+			chatInfoDao.save(chatInfo);
+		}
+
+		return message;
+	}
+
+	private void setCommunicator(Message message) {
+		if (message == null) return;
+
+		if (MessageType.RECEIVE == message.getStyle()) {
+			message.setCommunicator(message.getSenderInfo());
+		}
+		if (MessageType.SEND == message.getStyle()) {
+			message.setCommunicator(message.getReceiverInfo());
+		}
+	}
+
+	@Override
+	public List<Message> loadReadableMessageWhenUserRead(String owner, String contact) {
+		if (StringUtils.isEmpty(owner) || StringUtils.isEmpty(contact)) {
+			return Collections.emptyList();
+		}
+
+		List<Message> readMessages = messageDao.findTop3ByOwnAndCommunicatorAndReadOrderByTimeSendDescStyleAsc(owner, contact, true);
+		if (CollectionUtils.isEmpty(readMessages)) {
+			readMessages = new ArrayList<>();
+		}
+		Collections.reverse(readMessages);
+
+		// 获取未读消息列表
+		List<Message> messages = messageDao.findBySenderInfoAndReceiverInfoAndReadOrderByTimeSendAsc(contact, owner, false);
+		if (CollectionUtils.isEmpty(messages)) {
+			messages = new ArrayList<>();
+		}
+
+		// 更新未读消息为已读消息
+		for (Message message : messages) {
+			message.setRead(true);
+			messageDao.save(message);
+		}
+
+		// 更新当前用户未读消息数量
+		Long count = messageDao.countByOwnAndRead(owner, false);
+		System.out.println(owner + " MESSAGE_UN_READ: " + count);
+
+		ChatInfo chatInfo = chatInfoDao.findOne(owner);
+		assert chatInfo != null;
+		chatInfo.setCount(count);
+		chatInfoDao.save(chatInfo);
+
+		readMessages.addAll(messages);
+		return readMessages;
+	}
+
+	@Override
+	public Map<String, String> countUnReadMessageWhenUserQuery(String userPhone, Long enUU) {
+		// 验证方法参数
+		if (StringUtils.isEmpty(userPhone)) {
+			return createErrorMessage("用户手机号不能为空");
+		}
+		if (enUU == null) {
+			return createErrorMessage("用户企业UU不能为空");
+		}
+
+		ChatInfo chatInfo = chatInfoDao.findByPhoneAndEnUU(userPhone, enUU);
+		if (chatInfo == null) {
+			return createSuccessMessage(0L);
+		}
+		return createSuccessMessage(chatInfo.getCount());
+	}
+
+	@Override
+	public List<Message> loadHistoryMessage(String owner, String contact, Long max, Long min) {
+		if (StringUtils.isEmpty(owner) || StringUtils.isEmpty(contact) || (max == null && min == null)) {
+			return Collections.emptyList();
+		}
+
+		Query query = new Query();
+		query.addCriteria(Criteria.where("own").is(owner));
+		query.addCriteria(Criteria.where("communicator").is(contact));
+
+		if (max != null && min != null) {
+			query.addCriteria(Criteria.where("timeSend").lte(max).gt(min));
+		} else if (max == null) {
+			query.addCriteria(Criteria.where("timeSend").gt(min));
+		} else {
+			query.addCriteria(Criteria.where("timeSend").lte(max));
+		}
+		query.with(new Sort(Sort.Direction.ASC, "timeSend"));
+
+		List<Message> messages = mongoTemplate.find(query, Message.class);
+		if (CollectionUtils.isEmpty(messages)) {
+			return Collections.emptyList();
+		}
+		Collections.reverse(messages);
+		return messages;
+	}
+
+	private Map<String, String> createErrorMessage(String message) {
+		Map<String, String> map = new HashMap<>();
+		map.put("success", Boolean.toString(false));
+		map.put("message", message);
+		return map;
+	}
+
+	private Map<String, String> createSuccessMessage(Long count) {
+		Map<String, String> map = new HashMap<>();
+		map.put("success", Boolean.toString(true));
+		map.put("count", Long.toString(count != null ? count : 0L));
+		return map;
+	}
+
+}

+ 50 - 0
src/main/java/com/uas/demo/service/impl/UserInfoServiceImpl.java

@@ -0,0 +1,50 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.UserDao;
+import com.uas.demo.dao.UserInfoDao;
+import com.uas.demo.model.User;
+import com.uas.demo.model.UserInfo;
+import com.uas.demo.service.UserInfoService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+@Service
+public class UserInfoServiceImpl implements UserInfoService {
+
+	private final UserDao userDao;
+
+	private final UserInfoDao userInfoDao;
+
+	@Autowired
+	public UserInfoServiceImpl(UserDao userDao, UserInfoDao userInfoDao) {
+		this.userDao = userDao;
+		this.userInfoDao = userInfoDao;
+	}
+
+	@Override
+	public UserInfo cacheUserInfoWhenUserVisitSystem(String phone) {
+		if (StringUtils.isEmpty(phone)) {
+			return null;
+		}
+
+		UserInfo userInfo = userInfoDao.findByPhone(phone);
+		if (userInfo == null) {
+			// 获取账号中心的用户信息
+			User user = userDao.findByPhone(phone);
+			if (user == null) {
+				return null;
+			}
+
+			// 缓存外部用户信息到系统中
+			userInfo = new UserInfo();
+			userInfo.setPhone(user.getPhone());
+			userInfo.setUserId(user.getUserId());
+			userInfo.setCertification(user.getPassword());
+			userInfo.setName(user.getName());
+
+			userInfo = userInfoDao.save(userInfo);
+		}
+		return userInfo;
+	}
+}

+ 68 - 0
src/main/java/com/uas/demo/utils/JacksonUtils.java

@@ -0,0 +1,68 @@
+package com.uas.demo.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * JSON 工具类
+ *
+ * history:
+ * Created by huxz on 2017-3-1 10:16:03
+ */
+public final class JacksonUtils {
+
+	private static ObjectMapper objectMapper;
+
+	/**
+	 * 把JSON文本parse为JavaBean
+	 */
+	public static <T> T fromJson(String text, Class<T> clazz) {
+		if (objectMapper == null) {
+			objectMapper = new ObjectMapper();
+		}
+		try {
+			return objectMapper.readValue(text, clazz);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+
+	/**
+	 * 把JSON文本parse成JavaBean集合
+	 */
+	public static <T> List<T> fromJsonArray(String text, Class<T> clazz) {
+		if (objectMapper == null) {
+			objectMapper = new ObjectMapper();
+		}
+		JavaType javaType = objectMapper.getTypeFactory().constructParametricType(ArrayList.class, clazz);
+		try {
+			return objectMapper.readValue(text, javaType);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return Collections.emptyList();
+	}
+
+	/**
+	 * 将JavaBean序列化为JSON文本
+	 */
+	public static String toJson(Object object) {
+		if (objectMapper == null) {
+			objectMapper = new ObjectMapper();
+		}
+		try {
+			return objectMapper.writeValueAsString(object);
+		} catch (JsonProcessingException e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+
+}

+ 69 - 0
src/main/java/com/uas/demo/web/ChatController.java

@@ -0,0 +1,69 @@
+package com.uas.demo.web;
+
+import com.uas.demo.model.ChatInfo;
+import com.uas.demo.service.ChatInfoService;
+import com.uas.demo.service.ChatService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+
+@Controller
+@RequestMapping(value = "/chat")
+public class ChatController {
+
+	private final ChatService chatService;
+
+	private final ChatInfoService chatInfoService;
+
+	@Autowired
+	public ChatController(ChatService chatService, ChatInfoService chatInfoService) {
+		this.chatService = chatService;
+		this.chatInfoService = chatInfoService;
+	}
+
+	/**
+	 * 跳转私聊页面
+	 *
+	 * @param from		信息发送者的手机号
+	 * @param to		信息接收者的手机号
+	 * @param model		模型属性
+	 */
+	@Deprecated
+	@RequestMapping(method = RequestMethod.GET)
+	public String chatIndex(String from, String to, Model model, HttpServletResponse response) throws IOException {
+		Map<String, String> map = chatService.findChatUserInfo(from, to);
+
+		String type = map.get("type");
+		if (StringUtils.isEmpty(type)) {
+			response.sendRedirect("/login");
+		} else if ("USER_LIST".equals(type)) {
+			// TODO huxz 跳转到好友列表,暂时跳转到首页
+			response.sendRedirect("/index");
+		}
+		for (Map.Entry<String, String> entry :map.entrySet()) {
+			model.addAttribute(entry.getKey(), entry.getValue());
+		}
+		return "chat/private";
+	}
+
+	@RequestMapping(value = "/visit", method = RequestMethod.GET)
+	public String visitWebIm(@RequestParam(value = "gid", required = false) String id, HttpServletResponse response) throws IOException {
+		if (StringUtils.isEmpty(id)) {
+			response.sendRedirect("/");
+		} else {
+			ChatInfo chatInfo = chatInfoService.queryChatInfoWhenUserVisitWebSite(id);
+			if (chatInfo == null || chatInfo.getUserInfo() == null || StringUtils.isEmpty(chatInfo.getUserInfo().getUserId())) {
+				response.sendRedirect("/");
+			}
+		}
+		return "chat/chat-list";
+	}
+}

+ 15 - 0
src/main/java/com/uas/demo/web/HomeController.java

@@ -0,0 +1,15 @@
+package com.uas.demo.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+@Controller
+public class HomeController {
+
+	@RequestMapping(value = {"/index", "/"}, method = RequestMethod.GET)
+	public String index() {
+		// TODO 之后修改为实际的首页HTML
+		return "index-a";
+	}
+}

+ 13 - 0
src/main/java/com/uas/demo/web/LoginController.java

@@ -0,0 +1,13 @@
+package com.uas.demo.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@Controller
+public class LoginController {
+
+	@RequestMapping(value = "/login")
+	public String login() {
+		return "auth/login";
+	}
+}

+ 25 - 0
src/main/java/com/uas/demo/web/UserController.java

@@ -0,0 +1,25 @@
+package com.uas.demo.web;
+
+import com.uas.demo.dao.UserDao;
+import com.uas.demo.model.User;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping(value = "/user")
+public class UserController {
+
+	private final UserDao userDao;
+
+	@Autowired
+	public UserController(UserDao userDao) {
+		this.userDao = userDao;
+	}
+
+	@RequestMapping(value = "/phone", method = RequestMethod.GET)
+	public User findUser(String phone) {
+		return userDao.findByPhone(phone);
+	}
+}

+ 15 - 0
src/main/resources/application.yml

@@ -0,0 +1,15 @@
+server:
+  port: 20220
+
+spring:
+  datasource:
+    url: jdbc:oracle:thin:@//10.10.100.215:1521/orcl
+    username: platformmanager
+    password: select!#%*(
+    driver-class-name: oracle.jdbc.driver.OracleDriver
+  thymeleaf:
+    cache: false
+  data:
+    mongodb:
+      uri: mongodb://10.10.100.22:27017/im_infos
+      database: im_infos

+ 559 - 0
src/main/resources/static/app/client.js

@@ -0,0 +1,559 @@
+window.debug = true;
+
+// 记录聊天双方信息
+var dataInfo = {};
+
+// 存储应用数据
+var app = {};
+app.gid = undefined;
+// 储存聊天的会话记录信息
+app.sessions = {};
+// 聊天记录是否已经展开
+app.isShowRecord = false;
+
+$(document).ready(ready);
+
+function connectServer() {
+	// 连接XMPP服务器
+	Client.connect(app.userInfo.userId, app.userInfo.certification);
+}
+
+// 接收到<message>
+/**
+ * 处理接收到Message
+ *
+ * @param message	用户接收到的消息
+ */
+function onMessage(message) {
+	console.log('subscribe received message', message);
+
+	// 如果是当前会话,则直接显示消息
+	if (app.currentSession && message.senderInfo === app.currentSession.linkman) {
+		// 显示消息
+		drawMessage(message, false);
+	}
+
+	// 缓存消息
+	console.log(message);
+	if (message.senderInfo && message.receiverInfo) {
+		saveMessageToLocal(message, false);
+	}
+	return true;
+}
+
+/**
+ * 用户发送消息
+ *
+ * @param message	已填充的消息
+ */
+function sendMessage(message) {
+	if (!message) {
+		return ;
+	}
+
+	if (Client.isConnected()) {
+		if (!message.receiver || message.receiver == '') {
+			alert("请输入联系人!");
+			return;
+		}
+
+		message = Client.sendMessage(message);
+
+		// 显示消息
+		drawMessage(message || {}, true);
+		saveMessageToLocal(message, true);
+		$("#input-msg").empty();
+	} else {
+		alert("请先登录!");
+	}
+}
+
+
+function activate() {
+	app = httpUtils.queryParams() || {};
+
+	if (window.debug) {
+		console.log('[DEBUG]App Info', app);
+	}
+
+	onfire.on('chat.message', onMessage);
+
+	ChatService.queryChatInfoWhenUserVisitWebSite(app.gid, function (data) {
+		if (!data.success) {
+			console.log('Message', data.message);
+			return ;
+		}
+
+		app.userInfo = data.chatInfo.userInfo;
+		app.enterprise = data.chatInfo.enterprise;
+
+		// 绘制用户信息
+		drawUserInfo();
+
+		// 绘制会话列表
+		app.sessions = handlerSessionData(data.sessions || []);
+		drawSessionList(app.sessions);
+
+		// 绘制会话区域
+		if (app.currentSession) {
+			drawTitle();
+			showChatArea();
+			handlerUnReadMessage();
+		}
+
+		connectServer();
+	}, function (error) {
+		console.log(error);
+		window.location.href = '/';
+	});
+}
+
+function handlerUnReadMessage() {
+	// 获取未读消息缓存
+	messageService.loadReadableMessageWhenUserRead(app.currentSession.owner, app.currentSession.linkman, function (messages) {
+		if (window.debug) {
+			console.log('message.unread', messages);
+		}
+
+		// 显示消息
+		messages.forEach(function (message) {
+			drawMessage(message, message.style == 'SEND');
+		});
+	}, function (error) {
+		console.log(error);
+	});
+}
+
+function handlerSessionData(data) {
+	var sessions = {};
+	if (data && data.length > 0) {
+		for (var i = 0; i < data.length; i++) {
+			if (!data[i].linkman || data[i].linkman === '') {
+				continue;
+			}
+			if (!sessions[data[i].linkman]) {
+				sessions[data[i].linkman] = data[i];
+				sessions[data[i].linkman].userInfo = sessions[data[i].linkman].linkmanInfo.userInfo;
+				sessions[data[i].linkman].enterprise = sessions[data[i].linkman].linkmanInfo.enterprise;
+			} else {
+				if (sessions[data[i].linkman].timeSend < data[i].timeSend) {
+					sessions[data[i].linkman] = data[i];
+				}
+			}
+		}
+	}
+
+	// 获取当前会话
+	var current = false;
+	for (var property in sessions) {
+		if (sessions.hasOwnProperty(property) && sessions[property] && sessions[property].current) {
+			current = true;
+			app.currentSession = sessions[property] || {};
+			break ;
+		}
+	}
+
+	if (!current) {
+		delete app.currentSession;
+	}
+	return sessions;
+}
+
+function formatTime(timestamp) {
+	if (timestamp == null) return '';
+	var date = new Date(timestamp * 1000),
+		year = date.getFullYear(),
+		month = '' + (date.getMonth() + 1),
+		day = '' + date.getDate();
+	if (month.length < 2) month = '0' + month;
+	if (day.length < 2) day = '0' + day;
+
+	return [year, month, day].join('-');
+}
+
+function formatFullTime(timestamp) {
+	if (timestamp == null) return '';
+	var date = new Date(timestamp * 1000),
+		year = date.getFullYear(),
+		month = '' + (date.getMonth() + 1),
+		day = '' + date.getDate(),
+		hour = '' + date.getHours(),
+		minute = '' + date.getMinutes(),
+		second = '' + date.getSeconds();
+	if (month.length < 2) month = '0' + month;
+	if (day.length < 2) day = '0' + day;
+	if (hour.length < 2) hour = '0' + hour;
+	if (minute.length < 2) minute = '0' + minute;
+	if (second.length < 2) second = '0' + second;
+
+	return [year, month, day].join('-') + ' ' + [hour, minute, second].join(':');
+}
+
+function showChatArea() {
+	$('#chat-area').show();
+}
+
+function hideChatArea() {
+	$('#chat-area').hide();
+}
+
+function drawUserInfo() {
+	console.log('APP', app);
+	$('#member-title').html('<div class="img"><img src="/style/img/photo01.png" /></div>' +
+		'<div class="content">' +
+		'<p>' + app.userInfo.name + '</p>' +
+		'<p>' + app.enterprise.name + '</p>' +
+		'</div>');
+}
+
+function drawTitle() {
+	var name = '';
+	if (app.currentSession) {
+		if ('ENTERPRISE' === app.currentSession.communicatorType) {
+			name = app.currentSession.linkmanInfo.userInfo.name + '-' + app.currentSession.linkmanInfo.enterprise.name;
+		} else {
+			name = app.currentSession.linkmanInfo.enterprise.name;
+		}
+	}
+
+	$('#chat-title').html('<h3>' + name + '<em></em></h3>');
+}
+
+/**
+ * 清空聊天记录
+ */
+function clearMessage() {
+	$("#chat-receiver-log").empty();
+}
+
+/**
+ * 展示消息内容
+ *
+ * @param message	消息内容
+ * @param isSend	是否为发送消息
+ */
+function drawMessage(message, isSend) {
+	var userName = '';
+
+	if (!isSend) {
+		if (message.senderType === 'STORE') {
+			userName = app.currentSession.enterprise.name;
+		}
+		if (message.senderType === 'ENTERPRISE') {
+			userName = app.currentSession.userInfo.name + '-' + app.currentSession.enterprise.name;
+		}
+	} else {
+		userName = app.userInfo.name;
+	}
+
+	$('#chat-receiver-log').append('<dl class="' + (isSend ? 'send-msg' : 'receive-msg') + '">' +
+		'<dt><img src="/style/img/photo01.png"/></dt>' +
+		'<dd>' +
+		'<p class="name">' + userName + '</p>' +
+		'<p class="message-content">' + message.content + '</p>' +
+		'</dd>' +
+		'</dl>');
+
+	var height = $('#chat-receiver-log')[0].scrollHeight - $("#chat-receiver-log")[0].clientHeight;
+
+	$("#chat-receiver-log").animate({
+		scrollTop : height
+	}, 300);
+}
+
+function drawSessionList(sessions) {
+	$('#session-list .list-group').text('');
+	for (var property in sessions) {
+		if (sessions.hasOwnProperty(property) && sessions[property]) {
+
+			var sessionStyle = '', record = '';
+			if (!sessions[property].read) {
+				//sessionStyle = 'bg-warning';
+			}
+			if (app.currentSession && app.currentSession.linkman && app.currentSession.linkman !== '') {
+				if (sessions[property].id === app.currentSession.id) {
+					sessionStyle = 'active';
+				}
+			}
+
+			if (sessions[property].type == 1) {
+				record = sessions[property].content.replace(/<img class="emoji_icon".*?>/, '[表情]');
+			} else if (sessions[property].type == 2) {
+				record = '[图片]';
+			} else if (sessions[property].type == 3) {
+				record = '[文件]';
+			}
+
+			var flag = "'" + sessions[property].linkman + "'";
+
+			$('#session-list .list-group').append('<dl class="' + sessionStyle + '">' +
+				'<a href="javascript:void(0);" onclick="switchNewSession(' + flag + ')">' +
+				'<dt><em>寄售</em><img src="/style/img/photo01.png"/></dt>' +
+				'<dd>' +
+				'<span class="name">' + (sessions[property].userInfo.name + '</span>' +
+				'<span class="time">' + formatTime(sessions[property].timeSend) + '</span>' +
+				'</dd>' +
+				'<dd class="record">' + sessions[property].enterprise.name) + '</dd>' +
+				'</a>' +
+				'</dl>');
+		}
+	}
+}
+
+/**
+ * 缓存消息到服务器
+ *
+ * @param stanza		消息内容
+ * @param isMySend		是否为自己发送
+ */
+function saveMessageToLocal(stanza, isMySend) {
+	console.log('message.body', stanza);
+
+	if (isMySend === null || isMySend === undefined) {
+		isMySend = stanza.senderInfo === app.currentSession.owner;
+	}
+
+	stanza.own = app.gid;
+	stanza.mySend = isMySend;
+	stanza.style = isMySend ? 'SEND' : 'RECEIVE';
+	if (isMySend || (!isMySend && app.currentSession && stanza.senderInfo === app.currentSession.linkman)) {
+		stanza.read = true;
+	} else {
+		stanza.read = false;
+	}
+
+	// 缓存消息
+	messageService.cacheMessageWhenClientReceive(stanza, function (sessions) {
+		if (window.debug) {
+			console.log('[DEBUG]Update Sessions:', sessions);
+		}
+
+		// 绘制会话列表
+		app.sessions = handlerSessionData(sessions);
+		drawSessionList(app.sessions);
+	}, function (error) {
+		console.log('Cache Fail', error);
+	});
+}
+
+function ready() {
+	activate();
+
+	// 添加Emoji
+	$('#input-msg').emoji({
+		button: "#show-emoji",
+		showTab: false,
+		animation: 'slide',
+		icons: [{
+			name: "QQ表情",
+			path: "/style/img/qq/",
+			maxNum: 91,
+			excludeNums: [41, 45, 54],
+			file: ".gif"
+		}]
+	});
+
+	// 点击回车发送消息
+	$('#input-msg').keydown(function (event) {
+		if (event.keyCode == 13) {
+			clickSendMessage();
+		}
+	});
+
+	// 点击回车发送消息后,清除换行字符
+	$('#input-msg').keyup(function (event) {
+		if (event.keyCode == 13) {
+			$("#input-msg").empty();
+		}
+	});
+
+	// 发送消息
+	$("#btn-send").click(clickSendMessage);
+
+	// 关闭聊天窗口
+	$("#btn-close").click(closeChatArea);
+
+	// 上传图片
+	$('#upload-image-btn').click(function () {
+		// 触发文件上传的点击
+		$('#upload-image').click();
+	});
+
+	// 上传图片获取图片信息
+	$('#upload-image').change(uploadImage);
+
+	// 显示聊天历史
+	$('#show-chat-record').click(showChatRecord);
+}
+
+/**
+ * 点击发送按钮,发送消息
+ */
+function clickSendMessage() {
+
+	var message = new Message();
+	message.type = 1;
+	message.senderInfo = app.currentSession.owner;
+	message.receiverInfo = app.currentSession.linkman;
+	message.senderType = app.currentSession.communicatorType;
+
+	message.sender = app.userInfo.userId;
+	message.receiver = app.currentSession.userInfo.userId;
+	message.content = $("#input-msg").html();
+
+	if (!message.content || message.content === '') {
+		$("#input-msg").empty();
+		return ;
+	}
+
+	if (window.debug) {
+		console.log('[DEBUG]Send Message', message);
+	}
+
+	sendMessage(message);
+}
+
+/**
+ * 发送图片
+ *
+ * @param url	图片URL
+ */
+function sendImage(url) {
+
+	var message = new Message();
+	message.type = 2;
+	message.senderInfo = app.currentSession.owner;
+	message.receiverInfo = app.currentSession.linkman;
+	message.senderType = app.currentSession.communicatorType;
+
+	message.sender = app.userInfo.userId;
+	message.receiver = app.currentSession.userInfo.userId;
+	message.content = '<img class="content-image" src="' + (url || '') + '"/>';
+
+	if (window.debug) {
+		console.log('[DEBUG]Send Message', message.content);
+	}
+
+	sendMessage(message);
+}
+
+/**
+ * 点击图片按钮发送图片
+ */
+function uploadImage() {
+	var file = $('#upload-image')[0].files[0];
+	console.log(file);
+
+	if (file) {
+		var fileSize = 0;
+		if (file.size > 1024 * 1024) {
+			fileSize = (Math.round(file.size * 100 / (1024 * 1024)) / 100).toString() + 'MB';
+		} else {
+			fileSize = (Math.round(file.size * 100 / 1024) / 100).toString() + 'KB';
+		}
+
+		console.log('File Information', fileSize);
+
+		var formData = new FormData();
+		formData.append('file', file);
+
+		FileService.uploadImage(formData, function (data) {
+			sendImage(data);
+			$('#upload-image')[0].files[0] = [];
+		}, function (error) {
+			console.log(error);
+			$('#upload-image')[0].files[0] = [];
+		})
+	}
+}
+
+function drawHistoryMessage(message, isSend) {
+	var name = '', enterpriseName = '', classStyle = '';
+
+	if (!isSend) {
+		name = app.currentSession.userInfo.name;
+		enterpriseName = app.currentSession.enterprise.name;
+	} else {
+		name = app.userInfo.name;
+		enterpriseName = app.enterprise.name;
+		classStyle = 'customer';
+	}
+
+	$('#chat-record>ul').prepend('<li class="' + classStyle + '">' +
+		'<p>' + name + ' ( ' + enterpriseName + ' ) ' + '<time>' + formatFullTime(message.timeSend) + '</time></p>' +
+		'<p class="content">' + message.content + '</p>' +
+		'</li>');
+}
+
+function loadHistoryMessage(max, min) {
+	messageService.loadHistoryMessage(app.gid, app.currentSession.linkman, max, min, function (messages) {
+		console.log(messages);
+		app.history = messages;
+		if (messages && messages.length > 0) {
+			messages.forEach(function (message) {
+				drawHistoryMessage(message, message.style === 'SEND');
+			});
+		}
+
+		// 绘制聊天历史记录
+	}, function (error) {
+		console.log(error);
+	})
+
+}
+
+/**
+ * 显示聊天历史信息
+ */
+function showChatRecord() {
+	if (!app.isShowRecord) {
+		app.isShowRecord = true;
+		$('#chat-record').show();
+		$('#chat-receiver-log').hide();
+		$('#show-chat-record').html('<img src="/style/img/icon04.png"/>&nbsp;关闭聊天记录');
+		$('#show-chat-record').css('color', 'red');
+		$('#chat-record>ul').html('');
+
+		var max = Math.round(new Date().getTime() / 1000) - (60 * 60 * 24);
+		loadHistoryMessage(max, null);
+	} else {
+		app.isShowRecord = false;
+		$('#chat-record').hide();
+		$('#chat-receiver-log').show();
+		$('#show-chat-record').html('<img src="/style/img/icon04.png"/>&nbsp;聊天记录');
+		$('#show-chat-record').css('color', '#337ab7');
+	}
+}
+
+/**
+ * 开启新的会话
+ */
+function switchNewSession(communicator) {
+	console.log(communicator);
+	if (!communicator || communicator === '') return ;
+
+	var session = app.sessions[communicator];
+
+	sessionService.updateSessionStateWhenUserSwitchNewSession(session.id, function (sessions) {
+		// 绘制会话列表
+		app.sessions = handlerSessionData(sessions || []);
+		drawSessionList(app.sessions);
+
+		// 绘制会话区域
+		if (app.currentSession) {
+			drawTitle();
+			showChatArea();
+			clearMessage();
+			handlerUnReadMessage();
+		}
+	}, function (error) {
+		console.log(error);
+	});
+}
+
+function closeChatArea() {
+	delete app.currentSession;
+
+	hideChatArea();
+	drawSessionList(app.sessions);
+}

+ 105 - 0
src/main/resources/static/app/common/data-service.js

@@ -0,0 +1,105 @@
+/**
+ * 数据服务类
+ */
+var dataService = (function (window, $) {
+
+	function getQueryString(params) {
+		if (!params || typeof params !== 'object') {
+			params = {};
+		}
+
+		var queryStrs = [];
+		for (var property in params) {
+			if (params.hasOwnProperty(property)) {
+				queryStrs.push(property + '=' + (params[property] || ''));
+			}
+		}
+		return queryStrs.join('&');
+	}
+
+	function get(url, params, data, success, error) {
+		if (!url || url === '') return;
+
+		if (!data || typeof data !== 'object') {
+			data = {};
+		}
+		var queryString = getQueryString(params);
+		if (queryString && queryString !== '') {
+			url = url + '?' + queryString;
+		}
+		$.ajax({
+			url: url,
+			method: 'GET',
+			data: data,
+			dataType: 'json',
+			success: success,
+			error: error
+		});
+	}
+
+	function post(url, params, data, success, error) {
+		if (!url || url === '') return;
+
+		if (!data || typeof data !== 'object') {
+			data = {};
+		}
+		var queryString = getQueryString(params);
+		if (queryString && queryString !== '') {
+			url = url + '?' + queryString;
+		}
+		$.ajax({
+			url: url,
+			method: 'POST',
+			contentType: 'application/json;charset=UTF-8',
+			data: JSON.stringify(data),
+			dataType: 'json',
+			success: success,
+			error: error
+		});
+	}
+
+	function put(url, params, data, success, error) {
+		if (!url || url === '') return;
+
+		if (!data || typeof data !== 'object') {
+			data = {};
+		}
+		var queryString = getQueryString(params);
+		if (queryString && queryString !== '') {
+			url = url + '?' + queryString;
+		}
+		$.ajax({
+			url: url,
+			method: 'PUT',
+			contentType: 'application/json;charset=UTF-8',
+			data: JSON.stringify(data),
+			dataType: 'json',
+			success: success,
+			error: error
+		});
+	}
+	
+	function upload(url, formData, success, error) {
+		if (!url || url === '') return;
+
+		if (!formData || typeof formData !== 'object') {
+			formData = {};
+		}
+		$.ajax({
+			url: url,
+			method: 'POST',
+			data: formData,
+			processData: false,
+			contentType: false,
+			success: success,
+			error: error
+		});
+	}
+
+	return {
+		get: get,
+		post: post,
+		put: put,
+		upload: upload
+	}
+})(window, $);

+ 31 - 0
src/main/resources/static/app/common/http-utils.js

@@ -0,0 +1,31 @@
+/**
+ * HTTP 工具类
+ */
+var httpUtils = (function (window) {
+
+	/**
+	 * 获取请求URL中的Query Params
+	 */
+	function queryParams() {
+		var params = {};
+		var url = window.location.href;
+		var index = url.indexOf('?');
+		if (index > -1) {
+			// 获取QueryString
+			var queryString = url.substring(index + 1);
+			// 获取Request Params Key Value Pair数组
+			var queryStrs = queryString.split('&');
+			for (var i = 0; i < queryStrs.length; i++) {
+				if (queryStrs[i] && queryStrs[i] !== '') {
+					var keyPair = queryStrs[i].split('=');
+					params[keyPair[0]] = keyPair[1];
+				}
+			}
+		}
+		return params;
+	}
+
+	return {
+		queryParams: queryParams
+	}
+})(window);

+ 197 - 0
src/main/resources/static/app/common/xmpp-client.js

@@ -0,0 +1,197 @@
+/**
+ * XMPP 客户端
+ */
+var Client = (function (window, Strophe, onfire, Message) {
+
+	// XMPP服务器BOSH地址
+	var BOSH_HOST = "113.105.74.140", BOSH_SERVICE = 'http://' + BOSH_HOST + ':5280';
+
+	// XMPP连接和连接状态
+	var connection = null,
+		connected = false;
+
+	/**
+	 * 获取jabber Id
+	 */
+	function getJabberId(userId) {
+		if (!userId || userId === '') {
+			return null;
+		}
+		return userId + '@' + BOSH_HOST;
+	}
+
+	/**
+	 * 连接状态改变的事件
+	 *
+	 * @param status		状态信息
+	 */
+	function onConnect(status) {
+		if (status == Strophe.Status.CONNFAIL) {
+			alert("连接失败!");
+		} else if (status == Strophe.Status.AUTHFAIL) {
+			alert("登录失败,请重新输入用户信息!");
+		} else if (status == Strophe.Status.DISCONNECTED) {
+			alert("连接断开,请刷新页面!");
+			connected = false;
+			window.location.reload();
+		} else if (status == Strophe.Status.CONNECTED) {
+			console.log("连接成功,可以开始聊天了!");
+			connected = true;
+
+			// 当接收到<message>节,调用onMessage回调函数
+			connection.addHandler(onMessage, null, 'message', 'chat');
+
+			// 首先要发送一个<presence>给服务器(initial presence)
+			connection.send($pres().tree());
+
+			if (window.debug) {
+				console.log('Login Success');
+			}
+		}
+	}
+
+	/**
+	 * 接收到<message>
+	 *
+	 * @param stanza		消息
+	 */
+	function onMessage(stanza) {
+		if (window.debug) {
+			console.log(new Date(), stanza.outerHTML);
+		}
+		var message = translateToObject(stanza);
+
+		// 设置成接受消息
+		if (message.body) {
+			message.body.style = 'RECEIVE';
+		} else {
+			message.body = {
+				style: 'RECEIVE'
+			}
+		}
+		onfire.fire('chat.message', message.body);
+
+		received(message.to, message.from, message.id);
+		return true;
+	}
+
+	function translateToObject(stanza) {
+		var message = {},
+			elements = stanza.getElementsByTagName('body');
+
+		message.from = stanza.getAttribute('from');
+		message.to = stanza.getAttribute('to');
+		message.id = stanza.getAttribute('id');
+		message.type = stanza.getAttribute('type');
+
+		if (message.type === 'chat' && elements.length > 0) {
+			message.body = $.parseJSON(elements[0].textContent);
+		}
+
+		return message;
+	}
+
+	/**
+	 * 获取Connection
+	 */
+	function getConnection() {
+		return connection;
+	}
+
+	/**
+	 * 服务器连接状态
+	 */
+	function isConnected() {
+		return connected;
+	}
+
+	/**
+	 * 创建一个<message>元素并发送
+	 *
+	 * @param message		发送的消息
+	 */
+	function sendMessage(message) {
+		if (window.debug) {
+			console.log('message info', message.sender + '-> ' + message.receiver);
+		}
+
+		var jid = getJabberId(message.sender),
+			contactJid = getJabberId(message.receiver);
+
+		if (connected) {
+
+			// 设置发送时间
+			message.timeSend = Math.round(new Date().getTime() / 1000);
+
+			var msg = $msg({
+				to: contactJid,
+				from: jid,
+				type: 'chat'
+			}).c("body", null, $.toJSON(message));
+
+			connection.send(msg.tree());
+
+			if (window.debug) {
+				console.log('' + new Date(), msg.tree().outerHTML);
+			}
+		}
+
+		return message;
+	}
+
+	/**
+	 * 发送回执,告诉发送端收到了消息
+	 *
+	 * @param fromJid		发送方UserId
+	 * @param toJid			接受方Jabber Id
+	 * @param msgId			消息ID
+	 */
+	function received(fromJid, toJid, msgId) {
+		var msg = $msg({
+			from : fromJid,
+			to : toJid
+		}).c("received", {
+			'xmlns': 'urn:xmpp:receipts',
+			'id': msgId
+		});
+		connection.send(msg.tree());
+
+		if (window.debug) {
+			console.log('reply', msg.tree().outerHTML);
+		}
+	}
+
+	/**
+	 * 连接XMPP服务器
+	 *
+	 * @param userId	用户IM ID
+	 * @param password  密码
+	 */
+	function connect(userId, password) {
+		if (!connected) {
+			var jid = getJabberId(userId);
+			connection = new Strophe.Connection(BOSH_SERVICE);
+			connection.connect(jid, password, onConnect);
+		}
+		return connection;
+	}
+
+	/**
+	 * 断开XMPP服务器连接
+	 */
+	function disconnect() {
+		if (connected) {
+			connection.disconnect();
+		}
+	}
+
+	return {
+		getConnection: getConnection,
+		getJabberId: getJabberId,
+		isConnected: isConnected,
+		connect: connect,
+		disconnect: disconnect,
+		sendMessage: sendMessage,
+		received: received
+	}
+})(window, Strophe, onfire, Message);

+ 109 - 0
src/main/resources/static/app/index.js

@@ -0,0 +1,109 @@
+// 全局对象
+var app = {};
+
+/**
+ * 验证用户信息
+ */
+function validateUserInfo() {
+	app.chatInfo.userPhone = $('#userPhone').val();
+	app.chatInfo.enterprise = $('#enterprise').val();
+	app.chatInfo.storeInfo = $('#storeInfo').val();
+
+	if (!app.chatInfo.userPhone || app.chatInfo.userPhone === '') {
+		alert('用户手机号不能为空');
+		return false;
+	}
+	if (!app.chatInfo.enterprise || app.chatInfo.enterprise === '') {
+		alert('用户单位不能为空');
+		return false;
+	}
+	return true;
+}
+
+/**
+ * 验证联系人信息
+ */
+function validateContactInfo() {
+	app.chatInfo.toPhone = $('#contactPhone').val();
+	app.chatInfo.otherEnterprise = $('#contactEnterprise').val();
+	app.chatInfo.storeInfo = $('#storeInfo').val();
+
+	if (!app.chatInfo.toPhone || app.chatInfo.toPhone === '') {
+		alert('请输入联系人手机号');
+		return false;
+	}
+	if (!app.chatInfo.otherEnterprise || app.chatInfo.otherEnterprise === '') {
+		alert('请输入联系人单位信息');
+		return false;
+	}
+	return true;
+}
+
+function goToChatPage() {
+	app.chatInfo = { type: 'CHAT' };
+
+	if (!validateUserInfo()) return;
+	if (!validateContactInfo()) return;
+
+	app.chatInfo.userType = $('#userType').val();
+	if ('ENTERPRISE' === app.chatInfo.userType) app.chatInfo.otherUserType = 'STORE';
+	if ('STORE' === app.chatInfo.userType) app.chatInfo.otherUserType = 'ENTERPRISE';
+	app.chatInfo.enterprise = JSON.parse(app.chatInfo.enterprise);
+	app.chatInfo.enterprise.storeInfo = app.chatInfo.storeInfo;
+	app.chatInfo.otherEnterprise = JSON.parse(app.chatInfo.otherEnterprise);
+	app.chatInfo.otherEnterprise.storeInfo = app.chatInfo.storeInfo;
+	app.chatInfo.topic = 'JUST FOR TEST';
+
+	dataService.post('/api/chat/infos', { condition: 'chat_info' }, app.chatInfo, handlerData, handlerError);
+
+	function handlerData(data) {
+		console.log('result data', data);
+		if (data.success) {
+			window.location.href = '/chat/visit?gid=' + data.content;
+		} else {
+			alert(data.message);
+		}
+	}
+
+	function handlerError(error) {
+		console.log(error);
+		alert('输入信息异常,请重新输入');
+	}
+}
+
+function visitSessionList() {
+	app.chatInfo = { type: 'LIST' };
+
+	if (!validateUserInfo()) return;
+
+	console.log(app.enterprise);
+	app.chatInfo.enterprise = JSON.parse(app.chatInfo.enterprise);
+	app.chatInfo.enterprise.storeInfo = app.chatInfo.storeInfo;
+
+	dataService.post('/api/chat/infos', { condition: 'chat_info' }, app.chatInfo, handlerData, handlerError);
+
+	function handlerData(data) {
+		console.log('result data', data);
+		if (data.success) {
+			window.location.href = '/chat/visit?gid=' + data.content;
+		} else {
+			alert(data.message);
+		}
+	}
+
+	function handlerError(error) {
+		console.log(error);
+		alert('输入信息异常,请重新输入');
+	}
+}
+
+function ready() {
+	$("form").on('submit', function (e) {
+		e.preventDefault();
+	});
+
+	$("#startChat").click(goToChatPage);
+	$("#visitSessionList").click(visitSessionList);
+}
+
+$(document).ready(ready);

+ 25 - 0
src/main/resources/static/app/model/message.js

@@ -0,0 +1,25 @@
+/**
+ * 消息
+ */
+var Message = (function () {
+
+	function Message() {
+		this.type = 1;		// 1 文本, 2 图片, 3 文件
+		this.senderInfo = '';
+		this.receiverInfo = '';
+		this.senderType = 'ENTERPRISE'; // ENTERPRISE 企业, STORE 店铺
+		this.sender = null;
+		this.receiver = null;
+		this.messageState = 0;
+		this.download = false;
+		this.upload = false;
+		this.timeLen = 0;
+		this.timeReceive = 0;
+		this.read = false;
+		this.timeSend = 0;
+		this.content = null;
+		this.style = 'SEND';
+	}
+
+	return Message;
+})();

+ 14 - 0
src/main/resources/static/app/service/chat-service.js

@@ -0,0 +1,14 @@
+/**
+ * 聊天信息服务
+ */
+var ChatService = (function (window, dataService) {
+
+	function queryChatInfoWhenUserVisitWebSite(id, success, error) {
+		if (!id || id === '') return ;
+		dataService.get('/api/chat/infos/' + id, {}, {}, success, error);
+	}
+
+	return {
+		queryChatInfoWhenUserVisitWebSite: queryChatInfoWhenUserVisitWebSite
+	}
+})(window, dataService);

+ 13 - 0
src/main/resources/static/app/service/file-service.js

@@ -0,0 +1,13 @@
+/**
+ * 文件上传服务
+ */
+var FileService = (function (window, dataService) {
+
+	function uploadImage(formData, success, error) {
+		dataService.upload('/api/utils/file', formData, success, error);
+	}
+
+	return {
+		uploadImage: uploadImage
+	}
+})(window, dataService);

+ 30 - 0
src/main/resources/static/app/service/message-service.js

@@ -0,0 +1,30 @@
+/**
+ * 消息服务
+ */
+var messageService = (function (window, dataService) {
+	
+	function cacheMessageWhenClientReceive(message, success, error) {
+		dataService.post('/api/chat/message', {}, message, success, error);
+	}
+
+	function loadReadableMessageWhenUserRead(owner, contact, success, error) {
+		dataService.get('/api/chat/message', { operate: 'user_read', owner: owner, contact: contact }, {}, success, error);
+	}
+
+	function loadHistoryMessage(owner, contact, max, min, success, error) {
+		var param = { operate: 'history_message', owner: owner, contact: contact };
+		if (max) {
+			param.max = max;
+		}
+		if (min) {
+			param.min = min;
+		}
+		dataService.get('/api/chat/message', param, {}, success, error);
+	}
+
+	return {
+		cacheMessageWhenClientReceive: cacheMessageWhenClientReceive,
+		loadReadableMessageWhenUserRead: loadReadableMessageWhenUserRead,
+		loadHistoryMessage: loadHistoryMessage
+	}
+})(window, dataService);

+ 14 - 0
src/main/resources/static/app/service/session-service.js

@@ -0,0 +1,14 @@
+/**
+ * 会话服务
+ */
+var sessionService = (function (window, dataService) {
+
+	function updateSessionStateWhenUserSwitchNewSession(sessionId, success, error) {
+		if (!sessionId || sessionId === '') return ;
+		dataService.put('/api/chat/session/' + sessionId, {}, {}, success, error);
+	}
+
+	return {
+		updateSessionStateWhenUserSwitchNewSession: updateSessionStateWhenUserSwitchNewSession
+	}
+})(window, dataService);

+ 165 - 0
src/main/resources/static/lib/css/jquery.emoji.css

@@ -0,0 +1,165 @@
+.emoji_btn {
+    position: absolute;
+    display: inline-block;
+    cursor: pointer;
+    width: 25px;
+    height: 25px;
+}
+
+.emoji_container * {
+    -webkit-box-sizing: border-box;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+
+.emoji_container {
+    display: none;
+    width: 544px;
+    position: absolute;
+    background-color: #fff;
+    border: 1px solid #e8e8e8;
+    /*box-shadow: 0 1px 3px rgba(0, 0, 0, 0.176);*/
+    box-shadow: 0 5px 10px rgba(0,0,0,.3);
+}
+
+.emoji_container ul {
+    list-style: none;
+    padding-left: 0;
+    margin: 0;
+}
+
+.emoji_content {
+    height: 277px;
+    overflow-y: auto;
+    padding: 5px;
+}
+
+.emoji_content ul {
+    padding-top: 1px;
+    padding-left: 1px;
+}
+
+.emoji_content ul li {
+    width: 54px;
+    height: 54px;
+    float: left;
+    border: 1px solid #e3e3e3;
+    margin-top: -1px;
+    margin-left: -1px;
+}
+
+.emoji_content ul li a {
+    display: block;
+    line-height: 54px;
+    text-align: center;
+    cursor: pointer;
+}
+
+.emoji_content ul li a img {
+    vertical-align: middle;
+    max-width: 52px;
+    max-height: 52px;
+}
+
+.emoji_content .mCSB_scrollTools {
+    width: 10px;
+}
+
+.emoji_content .mCSB_outside + .mCS-minimal-dark.mCSB_scrollTools_vertical, .emoji_content .mCSB_outside + .mCS-minimal.mCSB_scrollTools_vertical {
+    margin: 5px 0;
+}
+
+.emoji_tab {
+    background-color: #f7f7f7;
+    border-top: 1px solid #e3e3e3;
+    color: #666;
+    height: 32px;
+    position: relative;
+}
+
+.emoji_tab_prev {
+    border-top: 4px solid transparent;
+    border-bottom: 4px solid transparent;
+    border-right: 4px dashed;
+    cursor: pointer;
+    left: 8px;
+    top: 12px;
+    position: absolute;
+    display: inline-block;
+    height: 0;
+    vertical-align: middle;
+    width: 0;
+}
+
+.emoji_tab_next {
+    border-top: 4px solid transparent;
+    border-bottom: 4px solid transparent;
+    border-left: 4px dashed;
+    cursor: pointer;
+    right: 7px;
+    top: 12px;
+    position: absolute;
+    display: inline-block;
+    height: 0;
+    vertical-align: middle;
+    width: 0;
+}
+
+.emoji_tab_list {
+    left: 22px;
+    overflow: hidden;
+    position: absolute;
+    top: 0;
+    width: 500px;
+}
+
+.emoji_tab_list ul {
+    width: 1500px;
+    transition: all 0.8s ease 0s;
+}
+
+.emoji_tab_list ul li {
+    border-top: 0 none;
+    cursor: pointer;
+    float: left;
+    height: 22px;
+    line-height: 22px;
+    margin: 5px 4px 0 0;
+    font-size: 12px;
+    border-radius: 3px;
+    text-align: center;
+    width: 68px;
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}
+
+.emoji_tab_list ul li:hover {
+    background: #e5e5e5;
+}
+
+.emoji_tab_list ul li.selected {
+    color: #fff;
+    background: steelblue;
+}
+
+.emoji_preview {
+    position: absolute;
+    top: 0;
+    border: 1px solid #c8c8c8;
+    border-radius: 50%;
+    width: 65px;
+    height: 65px;
+    background: #ffffff;
+    text-align: center;
+    line-height: 65px;
+    box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.176);
+    z-index: 2;
+    display: none;
+}
+
+.emoji_preview img {
+    vertical-align: middle;
+    max-width: 42px;
+    max-height: 42px;
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
src/main/resources/static/lib/css/jquery.mCustomScrollbar.min.css


+ 381 - 0
src/main/resources/static/lib/jquery.emoji.js

@@ -0,0 +1,381 @@
+/**
+ * Created by Sky on 2015/12/11.
+ */
+(function ($, window, document) {
+
+    var PLUGIN_NAME = 'emoji',
+        VERSION = '1.1.0',
+        DEFAULTS = {
+            showTab: true,
+            animation: 'fade',
+            icons: []
+        };
+
+    window.emoji_index = 0;
+
+    function Plugin(element, options) {
+        this.$content = $(element);
+        this.options = options;
+        this.index = emoji_index;
+        switch (options.animation) {
+            case 'none':
+                this.showFunc = 'show';
+                this.hideFunc = 'hide';
+                this.toggleFunc = 'toggle';
+                break;
+            case 'slide':
+                this.showFunc = 'slideDown';
+                this.hideFunc = 'slideUp';
+                this.toggleFunc = 'slideToggle';
+                break;
+            case 'fade':
+                this.showFunc = 'fadeIn';
+                this.hideFunc = 'fadeOut';
+                this.toggleFunc = 'fadeToggle';
+                break;
+            default :
+                this.showFunc = 'fadeIn';
+                this.hideFunc = 'fadeOut';
+                this.toggleFunc = 'fadeToggle';
+                break;
+        }
+        this._init();
+    }
+
+    Plugin.prototype = {
+        _init: function () {
+            var that = this;
+            var btn = this.options.button;
+            var newBtn,
+                contentTop,
+                contentLeft,
+                btnTop,
+                btnLeft;
+            var ix = that.index;
+            if (!btn) {
+                newBtn = '<input type="image" class="emoji_btn" id="emoji_btn_' + ix + '" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAZBAMAAAA2x5hQAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAkUExURUxpcfTGAPTGAPTGAPTGAPTGAPTGAPTGAPTGAPTGAPTGAPTGAOfx6yUAAAALdFJOUwAzbVQOoYrzwdwkAoU+0gAAAM1JREFUGNN9kK0PQWEUxl8fM24iCYopwi0muuVuzGyKwATFZpJIU01RUG/RBMnHxfz+Oef9uNM84d1+23nO+zxHKVG2WWupRJkdcAwtpCK0lpbqWE01pB0QayonREMoIp7AawQrWSgGGb4pn6dSeSh68FAVXqHqy3wKrkJiDGDTg3dnp//w+WnwlwIOJauF+C7sXRVfdha4O4oIJfTbtdSxs2uqhs585A0ko8iLTMEcDE1n65A+29pYAlr72nz9dKu7GuNTcsL2fDQzB/wCPVJ69nZGb3gAAAAASUVORK5CYII="/>';
+                contentTop = this.$content.offset().top + this.$content.outerHeight() + 10;
+                contentLeft = this.$content.offset().left + 2;
+                $(newBtn).appendTo($('body'));
+                $('#emoji_btn_' + ix).css({'top': contentTop + 'px', 'left': contentLeft + 'px'});
+                btn = '#emoji_btn_' + ix;
+            }
+
+            var showTab = this.options.showTab;
+            var iconsGroup = this.options.icons;
+            var groupLength = iconsGroup.length;
+            if (groupLength === 0) {
+                alert('Missing icons config!');
+                return false;
+            }
+
+            var emoji_container = '<div class="emoji_container" id="emoji_container_' + ix + '">';
+            var emoji_content = '<div class="emoji_content">';
+            var emoji_tab = '<div class="emoji_tab" style="' + (groupLength === 1 && !showTab ? 'display:none;' : '') + '"><div class="emoji_tab_prev"></div><div class="emoji_tab_list"><ul>';
+            var panel,
+                name,
+                path,
+                maxNum,
+                excludeNums,
+                file,
+                placeholder,
+                alias,
+                title,
+                index,
+                notation;
+            for (var i = 0; i < groupLength; i++) {
+                name = iconsGroup[i].name || 'group' + (i + 1);
+                path = iconsGroup[i].path;
+                maxNum = iconsGroup[i].maxNum;
+                excludeNums = iconsGroup[i].excludeNums;
+                file = iconsGroup[i].file || '.jpg';
+                placeholder = iconsGroup[i].placeholder || '#em' + (i + 1) + '_{alias}#';
+                alias = iconsGroup[i].alias;
+                title = iconsGroup[i].title;
+                index = 0;
+                if (!path || !maxNum) {
+                    alert('The ' + i + ' index of icon groups has error config!');
+                    continue;
+                }
+                panel = '<div id="emoji' + i + '" class="emoji_icons" style="' + (i === 0 ? '' : 'display:none;') + '"><ul>';
+                for (var j = 1; j <= maxNum; j++) {
+                    if (excludeNums && excludeNums.indexOf(j) >= 0) {
+                        continue;
+                    }
+                    if (alias) {
+                        if (typeof alias !== 'object') {
+                            alert('Error config about alias!');
+                            break;
+                        }
+                        notation = placeholder.replace(new RegExp('{alias}', 'gi'), alias[j].toString());
+                    } else {
+                        notation = placeholder.replace(new RegExp('{alias}', 'gi'), j.toString());
+                    }
+                    panel += '<li><a data-emoji_code="' + notation + '" data-index="' + index + '" title="' + (title && title[j] ? title[j] : '') + '"><img src="' + path + j + file + '"/></a></li>';
+                    index++;
+                }
+                panel += '</ul></div>';
+                emoji_content += panel;
+                emoji_tab += '<li data-emoji_tab="emoji' + i + '" class="' + (i === 0 ? 'selected' : '') + '" title="' + name + '">' + name + '</li>';
+            }
+            emoji_content += '</div>';
+            emoji_tab += '</ul></div><div class="emoji_tab_next"></div></div>';
+            var emoji_preview = '<div class="emoji_preview"><img/></div>';
+            emoji_container += emoji_content;
+            emoji_container += emoji_tab;
+            emoji_container += emoji_preview;
+
+            $(emoji_container).appendTo($('body'));
+
+            btnTop = $(btn).offset().top + $(btn).outerHeight() + 5;
+            btnLeft = $(btn).offset().left;
+            $('#emoji_container_' + ix).css({'top': btnTop + 'px', 'left': btnLeft + 'px'});
+
+            $('#emoji_container_' + ix + ' .emoji_content').mCustomScrollbar({
+                theme: 'minimal-dark',
+                scrollbarPosition: 'inside',
+                mouseWheel: {
+                    scrollAmount: 275
+                }
+            });
+
+            var pageCount = groupLength % 8 === 0 ? parseInt(groupLength / 8) : parseInt(groupLength / 8) + 1;
+            var pageIndex = 1;
+            $(document).on({
+                'click': function (e) {
+                    var target = e.target;
+                    var field = that.$content[0];
+                    var code,
+                        tab,
+                        imgSrc,
+                        insertHtml;
+                    if (target === $(btn)[0]) {
+                        $('#emoji_container_' + ix)[that.toggleFunc]();
+                        that.$content.focus();
+                    } else if ($(target).parents('#emoji_container_' + ix).length > 0) {
+                        code = $(target).data('emoji_code') || $(target).parent().data('emoji_code');
+                        tab = $(target).data('emoji_tab');
+                        if (code) {
+                            if (field.nodeName === 'DIV') {
+                                imgSrc = $('#emoji_container_' + ix + ' a[data-emoji_code="' + code + '"] img').attr('src');
+                                insertHtml = '<img class="emoji_icon" src="' + imgSrc + '"/>';
+                                that._insertAtCursor(field, insertHtml, false);
+                            } else {
+                                that._insertAtCursor(field, code);
+                            }
+                            that.hide();
+                        }
+                        else if (tab) {
+                            if (!$(target).hasClass('selected')) {
+                                $('#emoji_container_' + ix + ' .emoji_icons').hide();
+                                $('#emoji_container_' + ix + ' #' + tab).show();
+                                $(target).addClass('selected').siblings().removeClass('selected');
+                            }
+                        } else if ($(target).hasClass('emoji_tab_prev')) {
+                            if (pageIndex > 1) {
+                                $('#emoji_container_' + ix + ' .emoji_tab_list ul').css('margin-left', ('-503' * (pageIndex - 2)) + 'px');
+                                pageIndex--;
+                            }
+
+                        } else if ($(target).hasClass('emoji_tab_next')) {
+                            if (pageIndex < pageCount) {
+                                $('#emoji_container_' + ix + ' .emoji_tab_list ul').css('margin-left', ('-503' * pageIndex) + 'px');
+                                pageIndex++;
+                            }
+                        }
+                        that.$content.focus();
+                    } else if ($('#emoji_container_' + ix + ':visible').length > 0) {
+                        that.hide();
+                        that.$content.focus();
+                    }
+                }
+            });
+
+            $('#emoji_container_' + ix + ' .emoji_icons a').mouseenter(function () {
+                var index = $(this).data('index');
+                if (parseInt(index / 5) % 2 === 0) {
+                    $('#emoji_container_' + ix + ' .emoji_preview').css({'left': 'auto', 'right': 0});
+                } else {
+                    $('#emoji_container_' + ix + ' .emoji_preview').css({'left': 0, 'right': 'auto'});
+                }
+                var src = $(this).find('img').attr('src');
+                $('#emoji_container_' + ix + ' .emoji_preview img').attr('src', src).parent().show();
+            });
+
+            $('#emoji_container_' + ix + ' .emoji_icons a').mouseleave(function () {
+                $('#emoji_container_' + ix + ' .emoji_preview img').removeAttr('src').parent().hide();
+            });
+        },
+
+        _insertAtCursor: function (field, value, selectPastedContent) {
+            var sel, range;
+            if (field.nodeName === 'DIV') {
+                field.focus();
+                if (window.getSelection) {
+                    sel = window.getSelection();
+                    if (sel.getRangeAt && sel.rangeCount) {
+                        range = sel.getRangeAt(0);
+                        range.deleteContents();
+                        var el = document.createElement('div');
+                        el.innerHTML = value;
+                        var frag = document.createDocumentFragment(), node, lastNode;
+                        while ((node = el.firstChild)) {
+                            lastNode = frag.appendChild(node);
+                        }
+                        var firstNode = frag.firstChild;
+                        range.insertNode(frag);
+
+                        if (lastNode) {
+                            range = range.cloneRange();
+                            range.setStartAfter(lastNode);
+                            if (selectPastedContent) {
+                                range.setStartBefore(firstNode);
+                            } else {
+                                range.collapse(true);
+                            }
+                            sel.removeAllRanges();
+                            sel.addRange(range);
+                        }
+                    }
+                } else if ((sel = document.selection) && sel.type !== 'Control') {
+                    var originalRange = sel.createRange();
+                    originalRange.collapse(true);
+                    sel.createRange().pasteHTML(html);
+                    if (selectPastedContent) {
+                        range = sel.createRange();
+                        range.setEndPoint('StartToStart', originalRange);
+                        range.select();
+                    }
+                }
+            } else {
+                if (document.selection) {
+                    field.focus();
+                    sel = document.selection.createRange();
+                    sel.text = value;
+                    sel.select();
+                }
+                else if (field.selectionStart || field.selectionStart === 0) {
+                    var startPos = field.selectionStart;
+                    var endPos = field.selectionEnd;
+                    var restoreTop = field.scrollTop;
+                    field.value = field.value.substring(0, startPos) + value + field.value.substring(endPos, field.value.length);
+                    if (restoreTop > 0) {
+                        field.scrollTop = restoreTop;
+                    }
+                    field.focus();
+                    field.selectionStart = startPos + value.length;
+                    field.selectionEnd = startPos + value.length;
+                } else {
+                    field.value += value;
+                    field.focus();
+                }
+            }
+
+        },
+
+        show: function () {
+            $('#emoji_container_' + this.index)[this.showFunc]();
+        },
+
+        hide: function () {
+            $('#emoji_container_' + this.index)[this.hideFunc]();
+        },
+
+        toggle: function () {
+            $('#emoji_container_' + this.index)[this.toggleFunc]();
+        }
+    };
+
+    function fn(option) {
+        emoji_index++;
+        return this.each(function () {
+            var $this = $(this);
+            var data = $this.data('plugin_' + PLUGIN_NAME + emoji_index);
+            var options = $.extend({}, DEFAULTS, $this.data(), typeof option === 'object' && option);
+
+            if (!data) $this.data('plugin_' + PLUGIN_NAME + emoji_index, (data = new Plugin(this, options)));
+            if (typeof option === 'string') data[option]();
+        });
+    }
+
+    $.fn[PLUGIN_NAME] = fn;
+    $.fn[PLUGIN_NAME].Constructor = Plugin;
+
+}(jQuery, window, document));
+
+(function ($, window, document) {
+
+    var PLUGIN_NAME = 'emojiParse',
+        VERSION = '1.1.0',
+        DEFAULTS = {
+            icons: []
+        };
+
+    function Plugin(element, options) {
+        this.$content = $(element);
+        this.options = options;
+        this._init();
+    }
+
+    Plugin.prototype = {
+        _init: function () {
+            var that = this;
+            var iconsGroup = this.options.icons;
+            var groupLength = iconsGroup.length;
+            var path,
+                file,
+                placeholder,
+                alias,
+                pattern,
+                regexp,
+                revertAlias = {};
+            if (groupLength > 0) {
+                for (var i = 0; i < groupLength; i++) {
+                    path = iconsGroup[i].path;
+                    file = iconsGroup[i].file || '.jpg';
+                    placeholder = iconsGroup[i].placeholder;
+                    alias = iconsGroup[i].alias;
+                    if (!path) {
+                        alert('Path not config!');
+                        continue;
+                    }
+                    if (alias) {
+                        for (var attr in alias) {
+                            if (alias.hasOwnProperty(attr)) {
+                                revertAlias[alias[attr]] = attr;
+                            }
+                        }
+                        pattern = placeholder.replace(new RegExp('{alias}', 'gi'), '([\\s\\S]+?)');
+                        regexp = new RegExp(pattern, 'gm');
+                        that.$content.html(that.$content.html().replace(regexp, function ($0, $1) {
+                            var n = revertAlias[$1];
+                            if (n) {
+                                return '<img class="emoji_icon" src="' + path + n + file + '"/>';
+                            } else {
+                                return $0;
+                            }
+                        }));
+                    } else {
+                        pattern = placeholder.replace(new RegExp('{alias}', 'gi'), '(\\d+?)');
+                        that.$content.html(that.$content.html().replace(new RegExp(pattern, 'gm'), '<img class="emoji_icon" src="' + path + '$1' + file + '"/>'));
+                    }
+                }
+            }
+        }
+    };
+
+    function fn(option) {
+        return this.each(function () {
+            var $this = $(this);
+            var data = $this.data('plugin_' + PLUGIN_NAME);
+            var options = $.extend({}, DEFAULTS, $this.data(), typeof option === 'object' && option);
+
+            if (!data) $this.data('plugin_' + PLUGIN_NAME, (data = new Plugin(this, options)));
+            if (typeof option === 'string') data[option]();
+        });
+    }
+
+    $.fn[PLUGIN_NAME] = fn;
+    $.fn[PLUGIN_NAME].Constructor = Plugin;
+
+}(jQuery, window, document));

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
src/main/resources/static/lib/jquery.emoji.min.js


+ 2 - 0
src/main/resources/static/lib/jquery.json.min.js

@@ -0,0 +1,2 @@
+/*! jQuery JSON plugin v2.5.1 | github.com/Krinkle/jquery-json */
+!function($){"use strict";var escape=/["\\\x00-\x1f\x7f-\x9f]/g,meta={"\b":"\\b","	":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},hasOwn=Object.prototype.hasOwnProperty;$.toJSON="object"==typeof JSON&&JSON.stringify?JSON.stringify:function(a){if(null===a)return"null";var b,c,d,e,f=$.type(a);if("undefined"===f)return void 0;if("number"===f||"boolean"===f)return String(a);if("string"===f)return $.quoteString(a);if("function"==typeof a.toJSON)return $.toJSON(a.toJSON());if("date"===f){var g=a.getUTCMonth()+1,h=a.getUTCDate(),i=a.getUTCFullYear(),j=a.getUTCHours(),k=a.getUTCMinutes(),l=a.getUTCSeconds(),m=a.getUTCMilliseconds();return 10>g&&(g="0"+g),10>h&&(h="0"+h),10>j&&(j="0"+j),10>k&&(k="0"+k),10>l&&(l="0"+l),100>m&&(m="0"+m),10>m&&(m="0"+m),'"'+i+"-"+g+"-"+h+"T"+j+":"+k+":"+l+"."+m+'Z"'}if(b=[],$.isArray(a)){for(c=0;c<a.length;c++)b.push($.toJSON(a[c])||"null");return"["+b.join(",")+"]"}if("object"==typeof a){for(c in a)if(hasOwn.call(a,c)){if(f=typeof c,"number"===f)d='"'+c+'"';else{if("string"!==f)continue;d=$.quoteString(c)}f=typeof a[c],"function"!==f&&"undefined"!==f&&(e=$.toJSON(a[c]),b.push(d+":"+e))}return"{"+b.join(",")+"}"}},$.evalJSON="object"==typeof JSON&&JSON.parse?JSON.parse:function(str){return eval("("+str+")")},$.secureEvalJSON="object"==typeof JSON&&JSON.parse?JSON.parse:function(str){var filtered=str.replace(/\\["\\\/bfnrtu]/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"");if(/^[\],:{}\s]*$/.test(filtered))return eval("("+str+")");throw new SyntaxError("Error parsing JSON, source is not valid.")},$.quoteString=function(a){return a.match(escape)?'"'+a.replace(escape,function(a){var b=meta[a];return"string"==typeof b?b:(b=a.charCodeAt(),"\\u00"+Math.floor(b/16).toString(16)+(b%16).toString(16))})+'"':'"'+a+'"'}}(jQuery);

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
src/main/resources/static/lib/jquery.mCustomScrollbar.min.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
src/main/resources/static/lib/jquery.min.js


+ 12 - 0
src/main/resources/static/lib/jquery.mousewheel-3.0.6.min.js

@@ -0,0 +1,12 @@
+/*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net)
+ * Licensed under the MIT License (LICENSE.txt).
+ *
+ * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers.
+ * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix.
+ * Thanks to: Seamus Leahy for adding deltaX and deltaY
+ *
+ * Version: 3.0.6
+ * 
+ * Requires: 1.2.2+
+ */
+(function(a){function d(b){var c=b||window.event,d=[].slice.call(arguments,1),e=0,f=!0,g=0,h=0;return b=a.event.fix(c),b.type="mousewheel",c.wheelDelta&&(e=c.wheelDelta/120),c.detail&&(e=-c.detail/3),h=e,c.axis!==undefined&&c.axis===c.HORIZONTAL_AXIS&&(h=0,g=-1*e),c.wheelDeltaY!==undefined&&(h=c.wheelDeltaY/120),c.wheelDeltaX!==undefined&&(g=-1*c.wheelDeltaX/120),d.unshift(b,e,g,h),(a.event.dispatch||a.event.handle).apply(this,d)}var b=["DOMMouseScroll","mousewheel"];if(a.event.fixHooks)for(var c=b.length;c;)a.event.fixHooks[b[--c]]=a.event.mouseHooks;a.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a=b.length;a;)this.addEventListener(b[--a],d,!1);else this.onmousewheel=d},teardown:function(){if(this.removeEventListener)for(var a=b.length;a;)this.removeEventListener(b[--a],d,!1);else this.onmousewheel=null}},a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery);

+ 201 - 0
src/main/resources/static/lib/md5.min.js

@@ -0,0 +1,201 @@
+/*
+ * Javascript md5() 函数 用于生成字符串对应的md5值
+ * @param string string 原始字符串
+ * @return string 加密后的32位md5字符串
+ */
+function md5(string) {
+	function md5_RotateLeft(lValue, iShiftBits) {
+		return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
+	}
+	function md5_AddUnsigned(lX, lY) {
+		var lX4, lY4, lX8, lY8, lResult;
+		lX8 = (lX & 0x80000000);
+		lY8 = (lY & 0x80000000);
+		lX4 = (lX & 0x40000000);
+		lY4 = (lY & 0x40000000);
+		lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
+		if (lX4 & lY4) {
+			return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
+		}
+		if (lX4 | lY4) {
+			if (lResult & 0x40000000) {
+				return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
+			} else {
+				return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
+			}
+		} else {
+			return (lResult ^ lX8 ^ lY8);
+		}
+	}
+	function md5_F(x, y, z) {
+		return (x & y) | ((~x) & z);
+	}
+	function md5_G(x, y, z) {
+		return (x & z) | (y & (~z));
+	}
+	function md5_H(x, y, z) {
+		return (x ^ y ^ z);
+	}
+	function md5_I(x, y, z) {
+		return (y ^ (x | (~z)));
+	}
+	function md5_FF(a, b, c, d, x, s, ac) {
+		a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_F(b, c, d), x), ac));
+		return md5_AddUnsigned(md5_RotateLeft(a, s), b);
+	}
+	;
+	function md5_GG(a, b, c, d, x, s, ac) {
+		a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_G(b, c, d), x), ac));
+		return md5_AddUnsigned(md5_RotateLeft(a, s), b);
+	}
+	;
+	function md5_HH(a, b, c, d, x, s, ac) {
+		a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_H(b, c, d), x), ac));
+		return md5_AddUnsigned(md5_RotateLeft(a, s), b);
+	}
+	;
+	function md5_II(a, b, c, d, x, s, ac) {
+		a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_I(b, c, d), x), ac));
+		return md5_AddUnsigned(md5_RotateLeft(a, s), b);
+	}
+	;
+	function md5_ConvertToWordArray(string) {
+		var lWordCount;
+		var lMessageLength = string.length;
+		var lNumberOfWords_temp1 = lMessageLength + 8;
+		var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
+		var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
+		var lWordArray = Array(lNumberOfWords - 1);
+		var lBytePosition = 0;
+		var lByteCount = 0;
+		while (lByteCount < lMessageLength) {
+			lWordCount = (lByteCount - (lByteCount % 4)) / 4;
+			lBytePosition = (lByteCount % 4) * 8;
+			lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition));
+			lByteCount++;
+		}
+		lWordCount = (lByteCount - (lByteCount % 4)) / 4;
+		lBytePosition = (lByteCount % 4) * 8;
+		lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
+		lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
+		lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
+		return lWordArray;
+	}
+	;
+	function md5_WordToHex(lValue) {
+		var WordToHexValue = "", WordToHexValue_temp = "", lByte, lCount;
+		for (lCount = 0; lCount <= 3; lCount++) {
+			lByte = (lValue >>> (lCount * 8)) & 255;
+			WordToHexValue_temp = "0" + lByte.toString(16);
+			WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2);
+		}
+		return WordToHexValue;
+	}
+	;
+	function md5_Utf8Encode(string) {
+		string = string.replace(/\r\n/g, "\n");
+		var utftext = "";
+		for (var n = 0; n < string.length; n++) {
+			var c = string.charCodeAt(n);
+			if (c < 128) {
+				utftext += String.fromCharCode(c);
+			} else if ((c > 127) && (c < 2048)) {
+				utftext += String.fromCharCode((c >> 6) | 192);
+				utftext += String.fromCharCode((c & 63) | 128);
+			} else {
+				utftext += String.fromCharCode((c >> 12) | 224);
+				utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+				utftext += String.fromCharCode((c & 63) | 128);
+			}
+		}
+		return utftext;
+	}
+	;
+	var x = Array();
+	var k, AA, BB, CC, DD, a, b, c, d;
+	var S11 = 7, S12 = 12, S13 = 17, S14 = 22;
+	var S21 = 5, S22 = 9, S23 = 14, S24 = 20;
+	var S31 = 4, S32 = 11, S33 = 16, S34 = 23;
+	var S41 = 6, S42 = 10, S43 = 15, S44 = 21;
+	string = md5_Utf8Encode(string);
+	x = md5_ConvertToWordArray(string);
+	a = 0x67452301;
+	b = 0xEFCDAB89;
+	c = 0x98BADCFE;
+	d = 0x10325476;
+	for (k = 0; k < x.length; k += 16) {
+		AA = a;
+		BB = b;
+		CC = c;
+		DD = d;
+		a = md5_FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
+		d = md5_FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
+		c = md5_FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
+		b = md5_FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
+		a = md5_FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
+		d = md5_FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
+		c = md5_FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
+		b = md5_FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
+		a = md5_FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
+		d = md5_FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
+		c = md5_FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
+		b = md5_FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
+		a = md5_FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
+		d = md5_FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
+		c = md5_FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
+		b = md5_FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
+		a = md5_GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
+		d = md5_GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
+		c = md5_GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
+		b = md5_GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
+		a = md5_GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
+		d = md5_GG(d, a, b, c, x[k + 10], S22, 0x2441453);
+		c = md5_GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
+		b = md5_GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
+		a = md5_GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
+		d = md5_GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
+		c = md5_GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
+		b = md5_GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
+		a = md5_GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
+		d = md5_GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
+		c = md5_GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
+		b = md5_GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
+		a = md5_HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
+		d = md5_HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
+		c = md5_HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
+		b = md5_HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
+		a = md5_HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
+		d = md5_HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
+		c = md5_HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
+		b = md5_HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
+		a = md5_HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
+		d = md5_HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
+		c = md5_HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
+		b = md5_HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
+		a = md5_HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
+		d = md5_HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
+		c = md5_HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
+		b = md5_HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
+		a = md5_II(a, b, c, d, x[k + 0], S41, 0xF4292244);
+		d = md5_II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
+		c = md5_II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
+		b = md5_II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
+		a = md5_II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
+		d = md5_II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
+		c = md5_II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
+		b = md5_II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
+		a = md5_II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
+		d = md5_II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
+		c = md5_II(c, d, a, b, x[k + 6], S43, 0xA3014314);
+		b = md5_II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
+		a = md5_II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
+		d = md5_II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
+		c = md5_II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
+		b = md5_II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
+		a = md5_AddUnsigned(a, AA);
+		b = md5_AddUnsigned(b, BB);
+		c = md5_AddUnsigned(c, CC);
+		d = md5_AddUnsigned(d, DD);
+	}
+	return (md5_WordToHex(a) + md5_WordToHex(b) + md5_WordToHex(c) + md5_WordToHex(d)).toLowerCase();
+}

+ 149 - 0
src/main/resources/static/lib/onfire.js

@@ -0,0 +1,149 @@
+/**
+  Copyright (c) 2016 hustcc http://www.atool.org/
+  License: MIT 
+  https://github.com/hustcc/onfire.js
+**/
+/* jshint expr: true */ 
+!function (root, factory) {
+  if (typeof module === 'object' && module.exports)
+    module.exports = factory();
+  else
+    root.onfire = factory();
+}(typeof window !== 'undefined' ? window : this, function () {
+  var __onfireEvents = {},
+   __cnt = 0, // evnet counter
+   string_str = 'string',
+   function_str = 'function',
+   hasOwnKey = Function.call.bind(Object.hasOwnProperty),
+   slice = Function.call.bind(Array.prototype.slice);
+
+  function _bind(eventName, callback, is_one, context) {
+    if (typeof eventName !== string_str || typeof callback !== function_str) {
+      throw new Error('args: '+string_str+', '+function_str+'');
+    }
+    if (! hasOwnKey(__onfireEvents, eventName)) {
+      __onfireEvents[eventName] = {};
+    }
+    __onfireEvents[eventName][++__cnt] = [callback, is_one, context];
+
+    return [eventName, __cnt];
+  }
+  function _each(obj, callback) {
+    for (var key in obj) {
+      if (hasOwnKey(obj, key)) callback(key, obj[key]);
+    }
+  }
+  /**
+   *  onfire.on( event, func, context ) -> Object
+   *  - event (String): The event name to subscribe / bind to
+   *  - func (Function): The function to call when a new event is published / triggered
+   *  Bind / subscribe the event name, and the callback function when event is triggered, will return an event Object
+  **/
+  function on(eventName, callback, context) {
+    return _bind(eventName, callback, 0, context);
+  }
+  /**
+   *  onfire.one( event, func, context ) -> Object
+   *  - event (String): The event name to subscribe / bind to
+   *  - func (Function): The function to call when a new event is published / triggered
+   *  Bind / subscribe the event name, and the callback function when event is triggered only once(can be triggered for one time), will return an event Object
+  **/
+  function one(eventName, callback, context) {
+    return _bind(eventName, callback, 1, context);
+  }
+  function _fire_func(eventName, args) {
+    if (hasOwnKey(__onfireEvents, eventName)) {
+      _each(__onfireEvents[eventName], function(key, item) {
+        item[0].apply(item[2], args); // do the function
+        if (item[1]) delete __onfireEvents[eventName][key]; // when is one, delete it after triggle
+      });
+    }
+  }
+  /**
+   *  onfire.fire( event[, data1 [,data2] ... ] )
+   *  - event (String): The event name to publish
+   *  - data...: The data to pass to subscribers / callbacks
+   *  Async Publishes / fires the the event, passing the data to it's subscribers / callbacks
+  **/
+  function fire(eventName) {
+    // fire events
+    var args = slice(arguments, 1);
+    setTimeout(function () {
+      _fire_func(eventName, args);
+    });
+  }
+  /**
+   *  onfire.fireSync( event[, data1 [,data2] ... ] )
+   *  - event (String): The event name to publish
+   *  - data...: The data to pass to subscribers / callbacks
+   *  Sync Publishes / fires the the event, passing the data to it's subscribers / callbacks
+  **/
+  function fireSync(eventName) {
+    _fire_func(eventName, slice(arguments, 1));
+  }
+  /**
+   * onfire.un( event ) -> Boolean
+   *  - event (String / Object): The message to publish
+   * When passed a event Object, removes a specific subscription.
+   * When passed event name String, removes all subscriptions for that event name(hierarchy)
+  *
+   * Unsubscribe / unbind an event or event object.
+   *
+   * Examples
+   *
+   *  // Example 1 - unsubscribing with a event object
+   *  var event_object = onfire.on('my_event', myFunc);
+   *  onfire.un(event_object);
+   *
+   *  // Example 2 - unsubscribing with a event name string
+   *  onfire.un('my_event');
+  **/
+  function un(event) {
+    var eventName, key, r = false, type = typeof event;
+    if (type === string_str) {
+      // cancel the event name if exist
+      if (hasOwnKey(__onfireEvents, event)) {
+        delete __onfireEvents[event];
+        return true;
+      }
+      return false;
+    }
+    else if (type === 'object') {
+      eventName = event[0];
+      key = event[1];
+      if (hasOwnKey(__onfireEvents, eventName) && hasOwnKey(__onfireEvents[eventName], key)) {
+        delete __onfireEvents[eventName][key];
+        return true;
+      }
+      // can not find this event, return false
+      return false;
+    }
+    else if (type === function_str) {
+      _each(__onfireEvents, function(key_1, item_1) {
+        _each(item_1, function(key_2, item_2) {
+          if (item_2[0] === event) {
+            delete __onfireEvents[key_1][key_2];
+            r = true;
+          }
+        });
+      });
+      return r;
+    }
+    return true;
+  }
+  /**
+   *  onfire.clear()
+   *  Clears all subscriptions
+  **/
+  function clear() {
+    __onfireEvents = {};
+  }
+  return {
+    on: on,
+    one: one,
+    un: un,
+    fire: fire,
+    fireSync: fireSync,
+    clear: clear
+  };
+});

+ 1 - 0
src/main/resources/static/lib/onfire.min.js

@@ -0,0 +1 @@
+!function(n,t){"object"==typeof module&&module.exports?module.exports=t():n.onfire=t()}("undefined"!=typeof window?window:this,function(){function n(n,t,e,o){if(typeof n!==p||typeof t!==a)throw new Error("args: "+p+", "+a);return y(l,n)||(l[n]={}),l[n][++d]=[t,e,o],[n,d]}function t(n,t){for(var e in n)y(n,e)&&t(e,n[e])}function e(t,e,o){return n(t,e,0,o)}function o(t,e,o){return n(t,e,1,o)}function i(n,e){y(l,n)&&t(l[n],function(t,o){o[0].apply(o[2],e),o[1]&&delete l[n][t]})}function r(n){var t=s(arguments,1);setTimeout(function(){i(n,t)})}function u(n){i(n,s(arguments,1))}function f(n){var e,o,i=!1,r=typeof n;return r===p?!!y(l,n)&&(delete l[n],!0):"object"===r?(e=n[0],o=n[1],!(!y(l,e)||!y(l[e],o))&&(delete l[e][o],!0)):r!==a||(t(l,function(e,o){t(o,function(t,o){o[0]===n&&(delete l[e][t],i=!0)})}),i)}function c(){l={}}var l={},d=0,p="string",a="function",y=Function.call.bind(Object.hasOwnProperty),s=Function.call.bind(Array.prototype.slice);return{on:e,one:o,un:f,fire:r,fireSync:u,clear:c}});

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1 - 0
src/main/resources/static/lib/strophe.min.js


+ 442 - 0
src/main/resources/static/style/css/chat.css

@@ -0,0 +1,442 @@
+/* Common */
+a, a:focus, a:hover {
+	text-decoration: none;
+	cursor: pointer;
+}
+
+body{
+	background: #f5f5f5;
+}
+/* Session List */
+#session-list {
+	padding: 0;
+	width: 250px;
+	height: 500px;
+	overflow: hidden;
+	border: 1px #ccc solid;
+	background: #519ee1;
+}
+::-webkit-scrollbar {
+	width: 8px;
+	height: 8px;
+}
+
+::-webkit-scrollbar-thumb {
+	background-color: #d8d4d4;
+	border-radius: 5px;
+	padding-left: 0 !important;
+}
+
+::-webkit-scrollbar-thumb:hover {
+	background-color: #b1abab;
+}
+
+/* Chat Area */
+#chat-area {
+	width: 650px;
+	height: 500px;
+	border: 1px #ccc solid;
+	border-left: none;
+}
+
+#chat-area #chat-title {
+	border-bottom: 1px #eee solid;
+	padding: 0 15px;
+	height: 40px;
+	line-height:40px;
+}
+#chat-area #chat-title h3{
+	margin: 0;
+	padding: 0;
+	font-size: 14px;
+	line-height:40px;
+	position: relative;
+}
+#chat-area #chat-title h3 em{
+	display: inline-block;
+	width: 8px;
+	height: 8px;
+	background: #53d769;
+	border-radius: 100%;
+	position: relative;
+	margin-left: 5px;
+}
+#chat-area #chat-receiver-log {
+	padding: 10px 5px;
+	height: 303px;
+	overflow-y: auto;
+}
+
+#chat-area #chat-receiver-log .msg {
+	padding: 5px 10px;
+	margin-bottom: 20px;
+	border-radius: 4px;
+	color: #31708f;
+	background-color: #d9edf7;
+	border: 1px solid #bce8f1;
+	display: inline-block;
+}
+
+#chat-area #chat-receiver-log .msg-mine {
+	float: right;
+	color: #8a6d3b;
+	background-color: #fcf8e3;
+	border-color: #faebcc;
+}
+
+#chat-area #chat-receiver-log .send-msg {
+	width: 100%;
+	margin: 0 auto;
+	padding: 10px 0;
+	border-bottom: #e8e8e8 1px solid;
+}
+
+#chat-area #chat-receiver-log .receive-msg {
+	width: 100%;
+	margin: 0 auto;
+	padding: 10px 0;
+	border-bottom: #e8e8e8 1px solid;
+}
+
+#chat-area #chat-receiver-log .send-msg dt {
+	width: 30px;
+	float: right;
+	height: 30px;
+	margin-top: 10px;
+}
+
+#chat-area #chat-receiver-log .receive-msg dt {
+	width: 30px;
+	float: left;
+	height: 30px;
+	margin-top: 10px;
+}
+
+#chat-area #chat-receiver-log .send-msg dd {
+	margin-left: 0;
+	margin-right: 35px;
+	position: relative;
+}
+
+#chat-area #chat-receiver-log .receive-msg dd {
+	margin-left: 35px;
+	margin-right: 0;
+	position: relative;
+}
+
+#chat-area #chat-receiver-log .send-msg dd>p {
+	font-size: 12px;
+	color: #666;
+	width: 100%;
+	display: inline-block;
+	margin-bottom: 0;
+	word-break: break-all;
+}
+
+#chat-area #chat-receiver-log .receive-msg dd>p {
+	font-size: 12px;
+	color: #666;
+	width: 100%;
+	display: inline-block;
+	margin-bottom: 0;
+	word-break: break-all;
+}
+
+#chat-area #chat-receiver-log .send-msg dd>p.name {
+	float: left;
+	text-align: right;
+	font-size: 14px;
+	color: #333;
+}
+
+#chat-area #chat-receiver-log .receive-msg dd>p.name {
+	float: left;
+	text-align: left;
+	font-size: 14px;
+	color: #333;
+}
+
+#chat-area #chat-receiver-log .send-msg dd>p.message-content {
+	text-align: right;
+}
+
+#chat-area #chat-receiver-log .receive-msg dd>p.message-content {
+	text-align: left;
+}
+
+#chat-area #chat-receiver-log .send-msg dd>p>img.content-image,
+#chat-area #chat-receiver-log .receive-msg dd>p>img.content-image {
+	max-width: 420px;
+	max-height: 100px;
+}
+
+#chat-area #chat-operations {
+	height: 25px;
+	border: 1px #ccc solid;
+	border-bottom: none;
+	background: #efefef;
+	border-left: none;
+	border-right: none;
+}
+#chat-area #chat-operations a{
+	margin: 0 5px;
+}
+#chat-area #chat-operations a:first-child{
+	margin-left: 20px;
+}
+#chat-area #chat-operations #show-emoji {
+	background: url(/style/img/icon01.png) no-repeat;
+	margin-left: 20px;
+	padding: 0;
+	height: 15px;
+	border: none;
+	width: 15px;
+}
+#chat-area #chat-operations a:last-child{
+	float: right;
+	margin-right: 10px;
+	font-size: 12px;
+	line-height: 25px;
+}
+#chat-area #chat-send-area {
+	height: 90px;
+}
+
+.container-fluid{
+	width: 900px;
+	height: 500px;
+	position: fixed;
+	top: 50%;
+	left: 43%;
+	margin: -250px 0 0 -365px;
+	background: #fff;
+	box-shadow: 0 5px 20px rgba(0,0,0,.3);
+}
+.chat-bottom{
+	height: 40px;
+	line-height: 40px;
+	border: 1px #eee solid;
+	border-bottom: none;
+
+	background: #efefef;
+	border-left: none;
+	border-right: none;
+}
+.chat-bottom button{
+	width: 80px;
+	height: 30px;
+	line-height: 30px;
+	background: #5498dd;
+	border-radius: 5px;
+	border: #2a6496 1px solid;
+	float: right;
+	padding: 0;
+	color: #fff;
+	font-size: 14px;
+	margin: 5px 10px;
+}
+.list-group{
+	overflow-y: auto;
+	max-height: 400px;
+}
+.list-group-title{
+	/*border-bottom: 1px #ccc solid;*/
+	padding: 0 15px;
+	height: 30px;
+	line-height: 30px;
+	background: #519ee1;
+}
+.list-group-title h3{
+	margin: 0;
+	padding-left: 10px;
+	font-size: 14px;
+	line-height: 30px;
+	color: #fff;
+	background: #3a83c3;
+}
+.list-group dl{
+	width: 100%;
+	height: 65px;
+	margin: 0 auto;
+	padding: 10px;
+}
+.list-group dl dt{
+	float: left;
+	width: 40px;
+	height: 40px;
+	text-align: center;
+	position: relative;
+}
+.list-group dl dt em{
+	display: inline-block;
+	width: 36px;
+	height: 16px;
+	background: #5078cb;
+	color: #fff;
+	text-align: center;
+	line-height: 16px;
+	position: absolute;
+	top: 31px;
+	left: 1px;
+	border-radius: 2px;
+	font-size: 12px;
+	font-style: normal;
+	font-weight: initial;
+	/* font-family: "宋体"; */
+}
+.list-group dl:hover,.list-group dl.active{
+	background: #498ecb;
+}
+.list-group dl dt img{
+	width: 40px;
+	height: 40px;
+	border-radius: 100%;
+}
+.list-group dl dd{
+	margin-left: 45px;
+}
+.list-group dl dd{
+	height: 22px;
+}
+.list-group dl dd span.name{
+	float: left;
+	font-size: 14px;
+	color: #333;
+}
+.list-group dl dd span.time{
+	float: right;
+	font-size: 12px;
+	color: #666;
+}
+.list-group dl dd.record{
+	font-size: 14px;
+	color: #333;
+	display: block;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+#chat-receiver-log dl{
+	width: 100%;
+	margin: 0 auto;
+	padding: 10px 0;
+	border-bottom: #e8e8e8 1px solid;
+}
+#chat-receiver-log dl dt{
+	width: 30px;
+	float: left;
+	height: 30px;
+	margin-top: 10px;
+}
+#chat-receiver-log dl.fr dt{
+	float: right;
+}
+#chat-receiver-log dl.fr dd{
+	margin-left: 0;
+}
+#chat-receiver-log dl dt img{
+	width: 30px;
+	height: 30px;
+	border-radius: 100%;
+}
+#chat-receiver-log dl dd{
+	margin-left: 35px;
+	position: relative;
+}
+#chat-receiver-log dl dd p.name{
+	float: left;
+	font-size: 14px;
+	color: #333;
+}
+#chat-receiver-log dl dd span{
+	position: absolute;
+	right: 10px;
+	top: 10px;
+	font-size: 12px;
+	color: #999;
+}
+#chat-receiver-log dl dd p{
+	font-size: 12px;
+	color: #666;
+	width: 100%;
+	display: inline-block;
+	margin-bottom: 0;
+	word-break: break-all;
+}
+
+/*
+图片发送*/
+.emoji_container{
+	top: 280px !important;
+	left: 483px !important;
+}
+#chat-area{
+	position: relative;
+}
+.chat-recored{
+	padding: 10px 5px;
+	height: 303px;
+	overflow-y: auto;
+	overflow-x: hidden;
+}
+.chat-recored ul,.chat-recored ul li{
+	width: 100%;
+	margin: 0 auto;
+	display: inline-block;
+}
+.chat-recored ul{
+	-webkit-padding-start: 0;
+	margin-left: 10px;
+}
+.chat-recored ul li{
+	float: left;
+	list-style: none;
+}
+.chat-recored ul li p{
+	font-size: 14px;
+	color: #5078cb;
+}
+.chat-recored ul li.customer p{
+	color: #53d769;
+}
+.chat-recored ul li p.content{
+	color: #333;
+	font-size: 12px;
+	padding-left: 10px;
+	padding-right: 10px;
+}
+.chat-recored .recored-menu{
+	width: 100%;
+	height: 25px;
+	line-height: 25px;
+}
+.chat-recored .recored-menu a{
+	font-size: 12px;
+	color: #333;
+	margin-left: 10px;
+}
+.member-title{
+	width: 100%;
+	height: 70px;
+	margin: 0 auto;
+	margin-bottom: 10px;
+	padding-top: 12px;
+}
+.member-title div.img {
+	width: 50px;
+	height: 50px;
+	float: left;
+	border-radius: 100%;
+	margin-left: 10px;
+}
+.member-title div.content{
+	margin-left: 70px;
+	margin-top: 6px;
+}
+.member-title div.content p{
+	color: #333;
+	font-size: 14px;
+	line-height: 20px;
+	margin: 0;
+	color: #fff;
+}

+ 185 - 0
src/main/resources/static/style/css/page.css

@@ -0,0 +1,185 @@
+body {
+	line-height: 1.6;
+	font-family: "Open Sans", "Helvetica Neue", "Hiragino Sans GB",
+		"Microsoft YaHei", "\9ED1\4F53", Arial, helvetica, verdana, sans-serif;
+	color: #222;
+	font-size: 14px;
+	width: 990px;
+	margin: 10px auto;
+	overflow: hidden;
+}
+
+.btn {
+	display: inline-block;
+	padding: 6px 12px;
+	margin-bottom: 0;
+	font-size: 14px;
+	font-weight: 400;
+	line-height: 1.42857143;
+	text-align: center;
+	white-space: nowrap;
+	vertical-align: middle;
+	-ms-touch-action: manipulation;
+	touch-action: manipulation;
+	cursor: pointer;
+	-webkit-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+	background-image: none;
+	border: 1px solid transparent;
+	border-radius: 4px;
+}
+
+.btn-block {
+	width: 100%
+}
+
+.btn-default {
+	color: #333;
+	background-color: #fff;
+	border-color: #ccc;
+}
+
+.btn-primary {
+	color: #fff;
+	background-color: #337ab7;
+	border-color: #2e6da4;
+}
+
+.btn:active {
+	outline: 0;
+	background-image: none;
+	-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+	box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+}
+
+.btn-default:active {
+	color: #333;
+	background-color: #e6e6e6;
+	border-color: #adadad;
+}
+
+.btn-primary:active {
+	color: #fff;
+	background-color: #286090;
+	border-color: #204d74;
+}
+
+.panel {
+	margin-bottom: 20px;
+	background-color: #fff;
+	border: 1px solid transparent;
+	border-radius: 4px;
+	-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
+	box-shadow: 0 1px 1px rgba(0, 0, 0, .05);
+}
+
+.panel-heading {
+	padding: 10px 15px;
+	border-bottom: 1px solid transparent;
+	border-top-left-radius: 3px;
+	border-top-right-radius: 3px;
+}
+
+.panel-title {
+	margin-top: 0;
+	margin-bottom: 0;
+	font-size: 16px;
+	color: inherit;
+}
+
+.panel-body {
+	padding: 15px;
+}
+
+.panel-info {
+	border-color: #bce8f1;
+}
+
+.panel-info>.panel-heading {
+	color: #31708f;
+	background-color: #d9edf7;
+	border-color: #bce8f1;
+}
+
+.panel-body:before,.panel-body:after {
+	display: table;
+	content: " ";
+}
+
+.form-group {
+	margin-bottom: 15px;
+}
+
+.form-control {
+	outline: 0;
+	width: 100%;
+	padding: 6px 12px;
+	border: 1px solid #ccc;
+	color: #555;
+	-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+	box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
+	-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow
+		ease-in-out .15s;
+	-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out
+		.15s;
+	transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
+	color: #555;
+}
+
+label {
+	display: inline-block;
+	max-width: 100%;
+	margin-bottom: 5px;
+	font-weight: 700;
+}
+
+#chat {
+	width: 600px;
+	margin: 30px auto
+}
+
+#msg {
+	height: 400px;
+	overflow: auto;
+}
+
+#user_wrap {
+	position: fixed;
+	top: 30px;
+	left: 50%;
+	margin-left: 320px;
+	width: 300px;
+}
+
+#user_wrap .form-control {
+	width: 243px;
+}
+
+.msg_wrap:before,.msg_wrap:after {
+	display: table;
+	content: " ";
+}
+
+.msg_wrap:after {
+	clear: both
+}
+
+.msg {
+	padding: 5px 10px;
+	margin-bottom: 20px;
+	border: 1px solid transparent;
+	border-radius: 4px;
+	color: #31708f;
+	background-color: #d9edf7;
+	border-color: #bce8f1;
+	display: inline-block;
+}
+
+.msg-mine {
+	float: right;
+	color: #8a6d3b;
+	background-color: #fcf8e3;
+	border-color: #faebcc;
+}

BIN
src/main/resources/static/style/img/face/1.gif


BIN
src/main/resources/static/style/img/face/10.gif


BIN
src/main/resources/static/style/img/face/11.gif


BIN
src/main/resources/static/style/img/face/12.gif


BIN
src/main/resources/static/style/img/face/13.gif


BIN
src/main/resources/static/style/img/face/14.gif


BIN
src/main/resources/static/style/img/face/15.gif


BIN
src/main/resources/static/style/img/face/16.gif


BIN
src/main/resources/static/style/img/face/17.gif


BIN
src/main/resources/static/style/img/face/18.gif


BIN
src/main/resources/static/style/img/face/19.gif


BIN
src/main/resources/static/style/img/face/2.gif


BIN
src/main/resources/static/style/img/face/20.gif


BIN
src/main/resources/static/style/img/face/21.gif


BIN
src/main/resources/static/style/img/face/22.gif


BIN
src/main/resources/static/style/img/face/23.gif


BIN
src/main/resources/static/style/img/face/24.gif


BIN
src/main/resources/static/style/img/face/25.gif


BIN
src/main/resources/static/style/img/face/26.gif


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio