Browse Source

init from phab

xielq 5 years ago
parent
commit
6222d0b1d8
100 changed files with 4354 additions and 0 deletions
  1. 0 0
      README.md
  2. 53 0
      build.gradle
  3. 4 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. 47 0
      src/main/java/com/uas/demo/api/ChatApiController.java
  14. 42 0
      src/main/java/com/uas/demo/api/ChatSessionController.java
  15. 56 0
      src/main/java/com/uas/demo/api/FileController.java
  16. 54 0
      src/main/java/com/uas/demo/api/MessageController.java
  17. 17 0
      src/main/java/com/uas/demo/dao/ChatSessionDao.java
  18. 15 0
      src/main/java/com/uas/demo/dao/MessageCountDao.java
  19. 29 0
      src/main/java/com/uas/demo/dao/MessageDao.java
  20. 17 0
      src/main/java/com/uas/demo/dao/UserDao.java
  21. 16 0
      src/main/java/com/uas/demo/facade/MessageFacade.java
  22. 41 0
      src/main/java/com/uas/demo/facade/impl/MessageFacadeImpl.java
  23. 148 0
      src/main/java/com/uas/demo/model/ChatSession.java
  24. 185 0
      src/main/java/com/uas/demo/model/Message.java
  25. 81 0
      src/main/java/com/uas/demo/model/MessageCount.java
  26. 6 0
      src/main/java/com/uas/demo/model/MessageType.java
  27. 93 0
      src/main/java/com/uas/demo/model/User.java
  28. 22 0
      src/main/java/com/uas/demo/service/ChatService.java
  29. 30 0
      src/main/java/com/uas/demo/service/ChatSessionService.java
  30. 13 0
      src/main/java/com/uas/demo/service/MessageCountService.java
  31. 24 0
      src/main/java/com/uas/demo/service/MessageService.java
  32. 91 0
      src/main/java/com/uas/demo/service/impl/ChatServiceImpl.java
  33. 93 0
      src/main/java/com/uas/demo/service/impl/ChatSessionServiceImpl.java
  34. 66 0
      src/main/java/com/uas/demo/service/impl/MessageCountServiceImpl.java
  35. 153 0
      src/main/java/com/uas/demo/service/impl/MessageServiceImpl.java
  36. 68 0
      src/main/java/com/uas/demo/utils/JacksonUtils.java
  37. 58 0
      src/main/java/com/uas/demo/web/ChatController.java
  38. 15 0
      src/main/java/com/uas/demo/web/HomeController.java
  39. 13 0
      src/main/java/com/uas/demo/web/LoginController.java
  40. 25 0
      src/main/java/com/uas/demo/web/UserController.java
  41. 15 0
      src/main/resources/application.yml
  42. 91 0
      src/main/resources/static/app/app.js
  43. 397 0
      src/main/resources/static/app/client.js
  44. 83 0
      src/main/resources/static/app/common/data-service.js
  45. 31 0
      src/main/resources/static/app/common/http-utils.js
  46. 173 0
      src/main/resources/static/app/common/xmpp-client.js
  47. 36 0
      src/main/resources/static/app/index.js
  48. 24 0
      src/main/resources/static/app/model/message.js
  49. 187 0
      src/main/resources/static/app/private.js
  50. 13 0
      src/main/resources/static/app/service/file-service.js
  51. 18 0
      src/main/resources/static/app/service/message-service.js
  52. 18 0
      src/main/resources/static/app/service/session-service.js
  53. 164 0
      src/main/resources/static/lib/css/jquery.emoji.css
  54. 0 0
      src/main/resources/static/lib/css/jquery.mCustomScrollbar.min.css
  55. 381 0
      src/main/resources/static/lib/jquery.emoji.js
  56. 0 0
      src/main/resources/static/lib/jquery.emoji.min.js
  57. 2 0
      src/main/resources/static/lib/jquery.json.min.js
  58. 1 0
      src/main/resources/static/lib/jquery.mCustomScrollbar.min.js
  59. 1 0
      src/main/resources/static/lib/jquery.min.js
  60. 12 0
      src/main/resources/static/lib/jquery.mousewheel-3.0.6.min.js
  61. 201 0
      src/main/resources/static/lib/md5.min.js
  62. 149 0
      src/main/resources/static/lib/onfire.js
  63. 1 0
      src/main/resources/static/lib/onfire.min.js
  64. 1 0
      src/main/resources/static/lib/strophe.min.js
  65. 43 0
      src/main/resources/static/login.html
  66. 245 0
      src/main/resources/static/style/css/chat.css
  67. 185 0
      src/main/resources/static/style/css/page.css
  68. BIN
      src/main/resources/static/style/img/face/1.gif
  69. BIN
      src/main/resources/static/style/img/face/10.gif
  70. BIN
      src/main/resources/static/style/img/face/11.gif
  71. BIN
      src/main/resources/static/style/img/face/12.gif
  72. BIN
      src/main/resources/static/style/img/face/13.gif
  73. BIN
      src/main/resources/static/style/img/face/14.gif
  74. BIN
      src/main/resources/static/style/img/face/15.gif
  75. BIN
      src/main/resources/static/style/img/face/16.gif
  76. BIN
      src/main/resources/static/style/img/face/17.gif
  77. BIN
      src/main/resources/static/style/img/face/18.gif
  78. BIN
      src/main/resources/static/style/img/face/19.gif
  79. BIN
      src/main/resources/static/style/img/face/2.gif
  80. BIN
      src/main/resources/static/style/img/face/20.gif
  81. BIN
      src/main/resources/static/style/img/face/21.gif
  82. BIN
      src/main/resources/static/style/img/face/22.gif
  83. BIN
      src/main/resources/static/style/img/face/23.gif
  84. BIN
      src/main/resources/static/style/img/face/24.gif
  85. BIN
      src/main/resources/static/style/img/face/25.gif
  86. BIN
      src/main/resources/static/style/img/face/26.gif
  87. BIN
      src/main/resources/static/style/img/face/27.gif
  88. BIN
      src/main/resources/static/style/img/face/28.gif
  89. BIN
      src/main/resources/static/style/img/face/29.gif
  90. BIN
      src/main/resources/static/style/img/face/3.gif
  91. BIN
      src/main/resources/static/style/img/face/30.gif
  92. BIN
      src/main/resources/static/style/img/face/31.gif
  93. BIN
      src/main/resources/static/style/img/face/32.gif
  94. BIN
      src/main/resources/static/style/img/face/33.gif
  95. BIN
      src/main/resources/static/style/img/face/34.gif
  96. BIN
      src/main/resources/static/style/img/face/35.gif
  97. BIN
      src/main/resources/static/style/img/face/36.gif
  98. BIN
      src/main/resources/static/style/img/face/37.gif
  99. BIN
      src/main/resources/static/style/img/face/38.gif
  100. BIN
      src/main/resources/static/style/img/face/39.gif

+ 0 - 0
README.md


+ 53 - 0
build.gradle

@@ -0,0 +1,53 @@
+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.1.9'
+
+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")
+
+	testCompile("org.springframework.boot:spring-boot-starter-test")
+}

+ 4 - 0
develop.md

@@ -0,0 +1,4 @@
+# Develop Log
+
+* feature 002: add emoji support
+

+ 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);
+	}
+}

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

@@ -0,0 +1,47 @@
+package com.uas.demo.api;
+
+import com.uas.demo.service.ChatService;
+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.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@RestController
+@RequestMapping(value = "/api/chat/infos")
+public class ChatApiController {
+
+	private final ChatService chatService;
+
+	@Autowired
+	public ChatApiController(ChatService chatService) {
+		this.chatService = chatService;
+	}
+
+	/**
+	 * 获取聊天双方的用户信息
+	 *
+	 * @param from		信息发送者的手机号
+	 * @param to		信息接收者的手机号
+	 */
+	@ResponseBody
+	@RequestMapping(method = RequestMethod.GET, params = "condition=phone")
+	public Map<String, String> findChatUserInfo(String from, String to) {
+		return chatService.findChatUserInfo(from, to);
+	}
+
+	/**
+	 * 获取聊天双方的用户信息
+	 *
+	 * @param from		信息发送者的UserId
+	 * @param to		信息接收者的UserId
+	 */
+	@ResponseBody
+	@RequestMapping(method = RequestMethod.GET, params = "condition=userid")
+	public Map<String, String> findChatUserInfoByUserId(String from, String to) {
+		return chatService.findChatUserInfoByUserId(from, to);
+	}
+
+}

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

@@ -0,0 +1,42 @@
+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;
+import java.util.Map;
+
+@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(method = RequestMethod.GET, params = "operate=refresh")
+	public List<ChatSession> loadSessionsWhenUserRefresh(String ownId) {
+		logger.info(String.format("Load sessions when user %s refresh website", ownId));
+		return chatSessionService.loadSessionsWhenUserRefresh(ownId);
+	}
+
+	@RequestMapping(method = RequestMethod.POST)
+	public ChatSession persistSessionWhenUserSendMessage(@RequestBody ChatSession session) {
+		logger.info("Persist session when user send message " + session.toString());
+		return chatSessionService.persistSessionWhenUserSendMessage(session);
+	}
+
+	@RequestMapping(method = RequestMethod.GET, params = "operate=count_unread")
+	public Map<String, String> countUnReadSessionsWhenUserQuery(String phone) {
+		logger.info(String.format("Count unread sessions when user %s query", phone));
+		return chatSessionService.countUnReadSessionsWhenUserQuery(phone);
+	}
+}

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

@@ -0,0 +1,56 @@
+package com.uas.demo.api;
+
+import org.apache.commons.io.FilenameUtils;
+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 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);
+
+		return new ResponseEntity<>(fileUrl, headers, HttpStatus.CREATED);
+	}
+}

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

@@ -0,0 +1,54 @@
+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.MessageCountService;
+import com.uas.demo.service.MessageService;
+import org.apache.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RestController;
+
+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;
+
+	private final MessageCountService messageCountService;
+
+	@Autowired
+	public MessageController(MessageService messageService, MessageFacade messageFacade, MessageCountService messageCountService) {
+		this.messageService = messageService;
+		this.messageFacade = messageFacade;
+		this.messageCountService = messageCountService;
+	}
+
+	@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 sender, String receiver) {
+		logger.info(String.format("Load readable message when user %s read message from sender %s", receiver, sender));
+		return messageService.loadReadableMessageWhenUserRead(sender, receiver);
+	}
+
+	@RequestMapping(method = RequestMethod.GET, params = "operate=count_unread")
+	public Map<String, String> countUnReadMessageWhenUserQuery(String phone) {
+		logger.info(String.format("Count unread message when user %s query", phone));
+		return messageCountService.countUnReadMessageWhenUserQuery(phone);
+	}
+}

+ 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> findByOwn(String own);
+
+	ChatSession findByOwnAndRelateUser(String own, String relateUser);
+
+	Long countByOwnAndRead(String own, Boolean read);
+}

+ 15 - 0
src/main/java/com/uas/demo/dao/MessageCountDao.java

@@ -0,0 +1,15 @@
+package com.uas.demo.dao;
+
+import com.uas.demo.model.MessageCount;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface MessageCountDao extends MongoRepository<MessageCount, String> {
+
+	MessageCount findByOwnerIdAndSenderId(String ownerId, String senderId);
+
+	List<MessageCount> findByOwnerId(String ownerId);
+}

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

@@ -0,0 +1,29 @@
+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 sender		消息的发送者
+	 * @param receiver		消息的接受者
+	 * @param read			消息是否已读
+	 */
+	List<Message> findBySenderAndReceiverAndReadOrderByTimeSendAsc(String sender, String receiver, Boolean read);
+
+	/**
+	 * 获取当前用户的前3条消息
+	 *
+	 * @param own			消息的拥有者
+	 * @param communicator	消息的关联者
+	 * @param read			消息是否已读
+	 */
+	List<Message> findTop3ByOwnAndCommunicatorAndReadOrderByTimeSendDescStyleAsc(String own, String communicator, Boolean read);
+}

+ 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);
+}

+ 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);
+}

+ 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.loadSessionsWhenUserRefresh(message.getOwn());
+		if (CollectionUtils.isEmpty(sessions)) {
+			return Collections.emptyList();
+		}
+		return sessions;
+	}
+}

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

@@ -0,0 +1,148 @@
+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.Document;
+
+/**
+ * 聊天会话记录
+ */
+@Document
+@CompoundIndex(name = "session_unique", def = "{'own': 1, 'relateUser': 1}", unique = true)
+public class ChatSession {
+
+	/**
+	 * 主键
+	 */
+	@Id
+	private String id;
+
+	/**
+	 * 会话所属人
+	 */
+	private String own;
+
+	/**
+	 * 会话关联人
+	 */
+	private String relateUser;
+
+	/**
+	 * 会话关联人名称
+	 */
+	private String contactUserName;
+
+	/**
+	 * 消息发起者UserId
+	 */
+	private String fromUserId;
+
+	/**
+	 * 消息发送时间戳
+	 */
+	private Long timeSend;
+
+	/**
+	 * 消息类型
+	 */
+	private int type = 1;
+
+	/**
+	 * 消息内容
+	 */
+	private String content;
+
+	/**
+	 * 是否已读
+	 */
+	private Boolean read = false;
+
+	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 String getFromUserId() {
+		return fromUserId;
+	}
+
+	public void setFromUserId(String fromUserId) {
+		this.fromUserId = fromUserId;
+	}
+
+	public String getRelateUser() {
+		return relateUser;
+	}
+
+	public void setRelateUser(String relateUser) {
+		this.relateUser = relateUser;
+	}
+
+	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 getContactUserName() {
+		return contactUserName;
+	}
+
+	public void setContactUserName(String contactUserName) {
+		this.contactUserName = contactUserName;
+	}
+
+	@Override
+	public String toString() {
+		return "ChatSession{" +
+				"id='" + id + '\'' +
+				", own='" + own + '\'' +
+				", relateUser='" + relateUser + '\'' +
+				", fromUserId='" + fromUserId + '\'' +
+				", timeSend=" + timeSend +
+				", type=" + type +
+				", content='" + content + '\'' +
+				", read=" + read +
+				'}';
+	}
+}

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

@@ -0,0 +1,185 @@
+package com.uas.demo.model;
+
+import org.springframework.data.annotation.Id;
+
+/**
+ * 聊天记录-缓存
+ */
+public class Message {
+
+	/**
+	 * 主键
+	 */
+	@Id
+	private String id;
+
+	//-----------------------------------------------------
+	// 消息基础信息
+	//-----------------------------------------------------
+
+	/**
+	 * 发送时间
+	 */
+	private Long timeSend;
+
+	/**
+	 * 消息缓存拥有者[alias owner]
+	 */
+	private String own;
+
+	/**
+	 * 沟通者
+	 */
+	private String communicator;
+
+	//-----------------------------------------------------
+	// 消息内容信息
+	//-----------------------------------------------------
+
+	/**
+	 * 消息类型
+	 */
+	private String type;
+
+	/**
+	 * 发送者
+	 */
+	private String sender;
+
+	/**
+	 * 接收者
+	 */
+	private String receiver;
+
+	/**
+	 * 消息内容
+	 */
+	private String content;
+
+	/**
+	 * 是否已读
+	 */
+	private Boolean read = false;
+
+	/**
+	 * 是否自己发送
+	 */
+	@Deprecated
+	private Boolean mySend = 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() {
+		if (MessageType.RECEIVE.equals(this.style)) {
+			this.communicator = this.sender;
+		}
+		if (MessageType.SEND.equals(this.style)) {
+			this.communicator = this.receiver;
+		}
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	public String getSender() {
+		return sender;
+	}
+
+	public void setSender(String sender) {
+		this.sender = sender;
+	}
+
+	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;
+	}
+
+	@Deprecated
+	public Boolean getMySend() {
+		return mySend;
+	}
+
+	@Deprecated
+	public void setMySend(Boolean mySend) {
+		this.mySend = mySend;
+	}
+
+	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 + '\'' +
+				", sender='" + sender + '\'' +
+				", receiver='" + receiver + '\'' +
+				", content='" + content + '\'' +
+				", read=" + read +
+				", style=" + style +
+				'}';
+	}
+}

+ 81 - 0
src/main/java/com/uas/demo/model/MessageCount.java

@@ -0,0 +1,81 @@
+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.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * 统计未读消息数量
+ */
+@Document(collection = "message_count")
+@CompoundIndex(name = "own_sender_unique", def = "{'ownerId': 1, 'senderId': 1}", unique = true)
+public class MessageCount {
+
+	/**
+	 * 主键
+	 */
+	@Id
+	private String id;
+
+	/**
+	 * 用户 User Id
+	 */
+	@Indexed
+	private String ownerId;
+
+	/**
+	 * 发送者 User Id
+	 */
+	private String senderId;
+
+	/**
+	 * 统计数量
+	 */
+	private Long count = 0L;
+
+	public MessageCount() {
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getOwnerId() {
+		return ownerId;
+	}
+
+	public void setOwnerId(String ownerId) {
+		this.ownerId = ownerId;
+	}
+
+	public String getSenderId() {
+		return senderId;
+	}
+
+	public void setSenderId(String senderId) {
+		this.senderId = senderId;
+	}
+
+	public Long getCount() {
+		return count;
+	}
+
+	public void setCount(Long count) {
+		this.count = count;
+	}
+
+	@Override
+	public String toString() {
+		return "MessageCount{" +
+				"id='" + id + '\'' +
+				", ownerId='" + ownerId + '\'' +
+				", senderId='" + senderId + '\'' +
+				", count=" + count +
+				'}';
+	}
+}

+ 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, 	// 接受消息
+}

+ 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 + '\'' +
+				'}';
+	}
+}

+ 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);
+}

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

@@ -0,0 +1,30 @@
+package com.uas.demo.service;
+
+import com.uas.demo.model.ChatSession;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ChatSessionService {
+
+	/**
+	 * 用户刷新页面时加载会话记录数据
+	 *
+	 * @param ownId		用户UserId
+	 */
+	List<ChatSession> loadSessionsWhenUserRefresh(String ownId);
+
+	/**
+	 * 用户发送聊天消息时持久化会话记录
+	 *
+	 * @param session	消息发送者的会话记录
+	 */
+	ChatSession persistSessionWhenUserSendMessage(ChatSession session);
+
+	/**
+	 * 用户查询未读会话数据
+	 *
+	 * @param userPhone		用户手机号
+	 */
+	Map<String, String> countUnReadSessionsWhenUserQuery(String userPhone);
+}

+ 13 - 0
src/main/java/com/uas/demo/service/MessageCountService.java

@@ -0,0 +1,13 @@
+package com.uas.demo.service;
+
+import java.util.Map;
+
+public interface MessageCountService {
+
+	/**
+	 * 用户查询未读会话数据
+	 *
+	 * @param userPhone		用户手机号
+	 */
+	Map<String, String> countUnReadMessageWhenUserQuery(String userPhone);
+}

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

@@ -0,0 +1,24 @@
+package com.uas.demo.service;
+
+import com.uas.demo.model.Message;
+
+import java.util.List;
+
+public interface MessageService {
+
+	/**
+	 * 缓存客户端收到的消息信息
+	 *
+	 * @param message		消息
+	 */
+	Message cacheMessageWhenClientReceive(Message message);
+
+	/**
+	 * 用户读取消息时获取未读消息,并更新为已读
+	 *
+	 * @param sender		消息发送者UserId
+	 * @param receiver		消息接受者UserId
+	 */
+	List<Message> loadReadableMessageWhenUserRead(String sender, String receiver);
+
+}

+ 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;
+	}
+}

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

@@ -0,0 +1,93 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.ChatSessionDao;
+import com.uas.demo.dao.UserDao;
+import com.uas.demo.model.ChatSession;
+import com.uas.demo.model.User;
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class ChatSessionServiceImpl implements ChatSessionService {
+
+	private final UserDao userDao;
+
+	private final ChatSessionDao chatSessionDao;
+
+	@Autowired
+	public ChatSessionServiceImpl(UserDao userDao, ChatSessionDao chatSessionDao) {
+		this.userDao = userDao;
+		this.chatSessionDao = chatSessionDao;
+	}
+
+	@Override
+	public List<ChatSession> loadSessionsWhenUserRefresh(String ownId) {
+		if (StringUtils.isEmpty(ownId)) {
+			return Collections.emptyList();
+		}
+
+		List<ChatSession> sessions = chatSessionDao.findByOwn(ownId);
+		if (CollectionUtils.isEmpty(sessions)) {
+			return Collections.emptyList();
+		}
+		return sessions;
+	}
+
+	@Override
+	public ChatSession persistSessionWhenUserSendMessage(ChatSession session) {
+		if (session == null) {
+			return null;
+		}
+
+		// 如果已存在会话记录,则更新相应的会话
+		ChatSession existSession = chatSessionDao.findByOwnAndRelateUser(session.getOwn(), session.getRelateUser());
+		if (existSession != null) {
+			session.setId(existSession.getId());
+		}
+
+		User user = userDao.findByUserId(session.getRelateUser());
+		if (user != null && StringUtils.hasText(user.getName())) {
+			session.setContactUserName(user.getName());
+		}
+		session = chatSessionDao.save(session);
+
+		return session;
+	}
+
+	@Override
+	public Map<String, String> countUnReadSessionsWhenUserQuery(String userPhone) {
+		Map<String, String> map = new HashMap<>();
+		if (StringUtils.isEmpty(userPhone)) {
+			map.put("success", Boolean.toString(false));
+			map.put("message", "用户手机号不能为空");
+			return map;
+		}
+
+		User user = userDao.findByPhone(userPhone);
+		if (user == null) {
+			map.put("success", Boolean.toString(false));
+			map.put("message", String.format("用户 %s 不存在", userPhone));
+			return map;
+		} else if (StringUtils.isEmpty(user.getUserId())) {
+			map.put("success", Boolean.toString(false));
+			map.put("message", String.format("用户 %s 不存在IM ID", userPhone));
+			return map;
+		}
+
+		Long count = chatSessionDao.countByOwnAndRead(user.getUserId(), false);
+		if (count == null) {
+			count = 0L;
+		}
+		map.put("success", Boolean.toString(true));
+		map.put("count", Long.toString(count));
+		return map;
+	}
+}

+ 66 - 0
src/main/java/com/uas/demo/service/impl/MessageCountServiceImpl.java

@@ -0,0 +1,66 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.MessageCountDao;
+import com.uas.demo.dao.UserDao;
+import com.uas.demo.model.MessageCount;
+import com.uas.demo.model.User;
+import com.uas.demo.service.MessageCountService;
+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.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class MessageCountServiceImpl implements MessageCountService {
+
+	private final UserDao userDao;
+
+	private final MessageCountDao messageCountDao;
+
+	@Autowired
+	public MessageCountServiceImpl(UserDao userDao, MessageCountDao messageCountDao) {
+		this.userDao = userDao;
+		this.messageCountDao = messageCountDao;
+	}
+
+	@Override
+	public Map<String, String> countUnReadMessageWhenUserQuery(String userPhone) {
+		Map<String, String> map = new HashMap<>();
+		if (StringUtils.isEmpty(userPhone)) {
+			map.put("success", Boolean.toString(false));
+			map.put("message", "用户手机号不能为空");
+			return map;
+		}
+
+		User user = userDao.findByPhone(userPhone);
+		if (user == null) {
+			map.put("success", Boolean.toString(false));
+			map.put("message", String.format("用户 %s 不存在", userPhone));
+			return map;
+		} else if (StringUtils.isEmpty(user.getUserId())) {
+			map.put("success", Boolean.toString(false));
+			map.put("message", String.format("用户 %s 不存在IM ID", userPhone));
+			return map;
+		}
+
+		Long count = 0L;
+		List<MessageCount> counts = messageCountDao.findByOwnerId(user.getUserId());
+		if (!CollectionUtils.isEmpty(counts)) {
+			for (MessageCount messageCount : counts) {
+				if (messageCount == null || messageCount.getCount() == null || messageCount.getCount() < 0) {
+					count += 0L;
+				} else {
+					count += messageCount.getCount();
+				}
+			}
+		}
+
+		map.put("success", Boolean.toString(true));
+		map.put("count", Long.toString(count));
+		return map;
+	}
+}

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

@@ -0,0 +1,153 @@
+package com.uas.demo.service.impl;
+
+import com.uas.demo.dao.ChatSessionDao;
+import com.uas.demo.dao.MessageCountDao;
+import com.uas.demo.dao.MessageDao;
+import com.uas.demo.dao.UserDao;
+import com.uas.demo.model.ChatSession;
+import com.uas.demo.model.Message;
+import com.uas.demo.model.MessageCount;
+import com.uas.demo.model.User;
+import com.uas.demo.service.MessageService;
+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.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+@Service
+public class MessageServiceImpl implements MessageService {
+
+	private final UserDao userDao;
+
+	private final MessageDao messageDao;
+
+	private final MessageCountDao messageCountDao;
+
+	private final ChatSessionDao chatSessionDao;
+
+	@Autowired
+	public MessageServiceImpl(UserDao userDao, MessageDao messageDao, MessageCountDao messageCountDao, ChatSessionDao chatSessionDao) {
+		this.userDao = userDao;
+		this.messageDao = messageDao;
+		this.messageCountDao = messageCountDao;
+		this.chatSessionDao = chatSessionDao;
+	}
+
+	@Override
+	@Transactional
+	public Message cacheMessageWhenClientReceive(Message message) {
+		if (message == null) {
+			return null;
+		}
+
+		message.setCommunicator();
+		message = messageDao.save(message);
+
+		Boolean read = message.getRead();
+		String relateUser = message.getReceiver();
+		if (!Objects.equals(message.getOwn(), message.getSender())) {
+			relateUser = message.getSender();
+		}
+
+		ChatSession session = chatSessionDao.findByOwnAndRelateUser(message.getOwn(), relateUser);
+
+		if (session == null) {
+			session = new ChatSession();
+			session.setOwn(message.getOwn());
+			session.setRelateUser(relateUser);
+			User user = userDao.findByUserId(relateUser);
+			if (user != null && StringUtils.hasText(user.getName())) {
+				session.setContactUserName(user.getName());
+			}
+		}
+		session.setTimeSend(message.getTimeSend());
+		session.setRead(read);
+
+		session.setContent(message.getContent());
+		session.setFromUserId(message.getSender());
+		session.setType(Integer.parseInt(message.getType()));
+
+		chatSessionDao.save(session);
+
+		if (Objects.equals(message.getSender(), message.getReceiver())) {
+			return message;
+		}
+
+		MessageCount messageCount = messageCountDao.findByOwnerIdAndSenderId(message.getReceiver(), "ALL");
+		if (messageCount == null) {
+			messageCount = new MessageCount();
+			messageCount.setOwnerId(message.getReceiver());
+			messageCount.setSenderId("ALL");
+			messageCount.setCount(messageCount.getCount() + 1);
+		} else if (message.getMySend()) {
+			if (messageCount.getCount() == null || messageCount.getCount() < 0) {
+				messageCount.setCount(0L);
+			} else {
+				messageCount.setCount(messageCount.getCount() + 1);
+			}
+		} else {
+			if (message.getRead()) {
+				if (messageCount.getCount() == null || messageCount.getCount() <= 0) {
+					messageCount.setCount(0L);
+				} else {
+					messageCount.setCount(messageCount.getCount() - 1);
+				}
+			}
+		}
+
+		messageCountDao.save(messageCount);
+
+		return message;
+	}
+
+	@Override
+	public List<Message> loadReadableMessageWhenUserRead(String sender, String receiver) {
+		if (StringUtils.isEmpty(sender) || StringUtils.isEmpty(receiver)) {
+			return Collections.emptyList();
+		}
+
+		List<Message> readMessages = messageDao.findTop3ByOwnAndCommunicatorAndReadOrderByTimeSendDescStyleAsc(receiver, sender, true);
+		if (CollectionUtils.isEmpty(readMessages)) {
+			readMessages = new ArrayList<>();
+		}
+		Collections.reverse(readMessages);
+
+		// 获取未读消息列表
+		List<Message> messages = messageDao.findBySenderAndReceiverAndReadOrderByTimeSendAsc(sender, receiver, false);
+		if (CollectionUtils.isEmpty(messages)) {
+			return readMessages;
+		}
+
+		// 更新未读消息为已读消息
+		for (Message message : messages) {
+			message.setRead(true);
+			messageDao.save(message);
+		}
+
+		MessageCount messageCount = messageCountDao.findByOwnerIdAndSenderId(receiver, "ALL");
+		if (messageCount == null) {
+			messageCount = new MessageCount();
+			messageCount.setOwnerId(receiver);
+			messageCount.setSenderId("ALL");
+			messageCount.setCount(0L);
+		} else {
+			if (messageCount.getCount() == null || messageCount.getCount() < messages.size()) {
+				messageCount.setCount(0L);
+			} else {
+				messageCount.setCount(messageCount.getCount() - messages.size());
+			}
+		}
+
+		messageCountDao.save(messageCount);
+
+		readMessages.addAll(messages);
+		return readMessages;
+	}
+
+}

+ 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;
+	}
+
+}

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

@@ -0,0 +1,58 @@
+package com.uas.demo.web;
+
+import com.uas.demo.service.ChatService;
+import com.uas.demo.service.ChatSessionService;
+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 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 ChatSessionService sessionService;
+
+	@Autowired
+	public ChatController(ChatService chatService, ChatSessionService sessionService) {
+		this.chatService = chatService;
+		this.sessionService = sessionService;
+	}
+
+	/**
+	 * 跳转私聊页面
+	 *
+	 * @param from		信息发送者的手机号
+	 * @param to		信息接收者的手机号
+	 * @param model		模型属性
+	 */
+	@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 = "/sample", method = RequestMethod.GET)
+	public String sample() {
+		return "chat/Sample";
+	}
+}

+ 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

+ 91 - 0
src/main/resources/static/app/app.js

@@ -0,0 +1,91 @@
+
+// 连接状态改变的事件
+function onConnect(status) {
+	if (status == Strophe.Status.CONNFAIL) {
+		alert("连接失败!");
+	} else if (status == Strophe.Status.AUTHFAIL) {
+		alert("登录失败!");
+	} else if (status == Strophe.Status.DISCONNECTED) {
+		alert("连接断开!");
+		client.updateState(false);
+	} else if (status == Strophe.Status.CONNECTED) {
+		// alert("连接成功,可以开始聊天了!");
+		client.updateState(true);
+
+		client.getConnection().addHandler(onRosterChange, Strophe.NS.ROSTER, 'iq', 'set');
+		// getFriendsList();
+		client.getConnection().addHandler(onPresenceOn, null, 'presence');
+		// 当接收到<message>节,调用onMessage回调函数
+		client.getConnection().addHandler(onMessage, null, 'message', 'chat');
+
+		// getFriendsList();
+
+		// 首先要发送一个<presence>给服务器(initial presence)
+		client.getConnection().send($pres().tree());
+
+		console.log('Login Success');
+	}
+}
+
+function onRosterChange(stanza) {
+	console.log('监听好友列表变化', stanza);
+	var items = $(stanza).find('item');
+	items.push($('<item jid="' + jid + '" name="胡学志" presence="available"></item>'));
+	console.log(items);
+	return true;
+}
+
+function onPresenceOn(stanza) {
+	console.log('监听好友在线状态的变化', stanza);
+	return true;
+}
+
+// 接收到<message>
+function onMessage(msg) {
+	console.log('received message', msg);
+	// 解析出<message>的from、type属性,以及body子元素
+	var from = msg.getAttribute('from');
+	var type = msg.getAttribute('type');
+	var msgId = msg.getAttribute('id');
+	var elems = msg.getElementsByTagName('body');
+	if (type == "chat" && elems.length > 0) {
+		var body = elems[0], text = $.parseJSON(formatFace(body.textContent));
+		$("#chat-receiver-log").append('<div class="row"><div class="msg">' + text.content + '</div></div>');
+		$("#chat-receiver-log").animate({
+			scrollTop : $("#chat-receiver-log").height()
+		}, 300);
+	}
+	// 发送回执,告诉发送端收到了消息
+	/*received(msgId, from);*/
+	return true;
+}
+
+// 表情
+var faces = {
+	"奋斗" : "14.gif",
+	"坏笑" : "20.gif"
+};
+function formatFace(text) {
+	return text.replace(/(\[(.{2})\])/gi, function() {
+		var face = faces[arguments[2]];
+		return face ? ('<img src="../../style/img/face/' + face + '"/>') : arguments[0];
+	});
+}
+
+/**
+ * 连接XMPP服务器
+ *
+ * @param userId	用户IM ID
+ * @param password  密码
+ * @param onConnect 连接生命周期函数
+ */
+function connect(userId, password) {
+	if (!client.isConnected()) {
+		jid = userId + '@' + client.getBoshHostAddress();
+		var connection = client.getConnection();
+		console.log('jid', jid);
+		console.log('password', password);
+		console.log('onConnect', onConnect);
+		connection.connect(jid, password, onConnect);
+	}
+}

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

@@ -0,0 +1,397 @@
+window.debug = true;
+
+// 记录聊天双方信息
+var dataInfo = {};
+
+// 储存聊天的会话记录信息
+var sessions = {};
+
+// 存储应用数据
+var app = {};
+
+$(document).ready(ready);
+
+// 表情
+var faces = {
+	"奋斗" : "14.gif",
+	"坏笑" : "20.gif"
+};
+function formatFace(text) {
+	return text.replace(/(\[(.{2})\])/gi, function() {
+		var face = faces[arguments[2]];
+		return face ? ('<img src="../../style/img/face/' + face + '"/>') : arguments[0];
+	});
+}
+
+function connectServer() {
+	// 连接XMPP服务器
+	Client.connect(dataInfo.userId, dataInfo.certification);
+}
+
+// 接收到<message>
+function onMessage(msg) {
+
+	console.log('subscribe received message', msg);
+
+	var message = translateToObject(msg);
+	if (message.body) {
+		var fromUserId = message.body.fromUserId;
+		// 显示接受到的消息
+		if (fromUserId === dataInfo.contactId) {
+			// 显示消息
+			drawMessage(message.body, false);
+		}
+	}
+	// 缓存消息
+	saveMessageToLocal(message.body, false);
+
+	// 发送回执,告诉发送端收到了消息
+	Client.received(dataInfo.userId, Strophe.getBareJidFromJid(message.from), message.id);
+
+	return true;
+}
+
+function activate() {
+	var params = httpUtils.queryParams();
+	params.enterprise = decodeURIComponent(params.enterprise);
+	params.condition = 'userid';
+	if (window.debug) {
+		console.log('[DEBUG]Query Params', params);
+	}
+
+	onfire.on('chat.message', onMessage);
+
+	dataService.get('/api/chat/infos', params, {}, function (data) {
+		dataInfo = data;
+		dataInfo.enterprise = params.enterprise;
+
+		if (!dataInfo.contactId || dataInfo.contactId === '') {
+			hiddenChatArea();
+		} else {
+			// 绘制标题
+			drawTitle();
+		}
+
+		// 获取会话信息
+		sessionService.loadSessionsWhenUserRefresh(dataInfo.userId, function (data) {
+
+			var sessions = handlerSessionData(data);
+
+			createNewSession(sessions);
+
+			// 绘制会话列表
+			drawSessionList(sessions);
+
+			connectServer();
+		}, function (error) {
+			console.log(error);
+
+			connectServer();
+		});
+
+		// 获取未读消息缓存
+		messageService.loadReadableMessageWhenUserRead(dataInfo.contactId, dataInfo.userId, function (messages) {
+			if (window.debug) {
+				console.log('message.unread', messages);
+			}
+
+			messages.forEach(function (message) {
+				// 显示消息
+				drawMessage(message, false);
+			});
+		}, function (error) {
+			console.log(error);
+		})
+
+	}, 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].relateUser || data[i].relateUser === '') {
+				continue;
+			}
+			if (!sessions[data[i].relateUser]) {
+				sessions[data[i].relateUser] = data[i];
+			} else {
+				if (sessions[data[i].relateUser].timeSend < data[i].timeSend) {
+					sessions[data[i].relateUser] = data[i];
+				}
+			}
+		}
+	}
+
+	return sessions;
+}
+
+function createNewSession(sessions) {
+	var session = {};
+	if (!dataInfo.contactId || dataInfo.contactId === '') return ;
+	if (sessions[dataInfo.contactId]) return ;
+
+	session.own = dataInfo.userId;
+	session.relateUser = dataInfo.contactId;
+	session.contactUserName = dataInfo.contactName;
+	session.fromUserId = dataInfo.contactId;
+	session.timeSend = new Date().getTime() / 1000;
+	session.content = '';
+	session.read = true;
+	sessions[dataInfo.contactId] = session;
+
+	sessionService.persistSessionWhenUserSendMessage(session, function (session) {
+		if (window.debug) {
+			console.log('[DEBUG]Create Session', session);
+		}
+	}, function (error) {
+		console.log('error', error);
+	});
+}
+
+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);
+	}
+
+	console.log(message);
+
+	return message;
+}
+
+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 (date.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 (date.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 hiddenChatArea() {
+	$('#chat-area').hide();
+}
+
+function drawTitle() {
+	$('#chat-title').html('<h3>' + (dataInfo.contactName && dataInfo.contactName !== '' ? dataInfo.contactName : dataInfo.contactId) + '</h3>');
+}
+
+/**
+ * 展示消息内容
+ *
+ * @param message	消息内容
+ * @param isSend	是否为发送消息
+ */
+function drawMessage(message, isSend) {
+
+	$('#chat-receiver-log').append('<dl>' +
+		'<dt><img src="/style/img/photo01.jpg"/></dt>' +
+		'<dd>' +
+		'<p class="name">' + (isSend ? dataInfo.userName : dataInfo.contactName) + '</p>' +
+		'<span class="time">' + formatFullTime(message.timeSend) + '</span>' +
+		'<p>' + 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]) {
+
+			$('#session-list .list-group').append('<dl class="' + (sessions[property].relateUser === dataInfo.contactId ? ' active' : '' ) + '">' +
+				'<a href="/chat/sample?from=' + sessions[property].own + '&to=' + sessions[property].relateUser + '">' +
+				'<dt><img src="/style/img/photo01.jpg"/></dt>' +
+				'<dd>' +
+				'<span class="name">' + (sessions[property].contactUserName && sessions[property].contactUserName !== '' ? sessions[property].contactUserName : sessions[property].relateUser) + '</span>' +
+				'<span class="time">' + formatTime(sessions[property].timeSend) + '</span>' +
+				'</dd>' +
+				'<dd class="record">' + sessions[property].content + '</dd>' +
+				'</a>' +
+				'</dl>');
+		}
+	}
+}
+
+/**
+ * 缓存消息到服务器
+ *
+ * @param stanza		消息内容
+ * @param isMySend		是否为自己发送
+ */
+function saveMessageToLocal(stanza, isMySend) {
+	console.log('message.body', stanza);
+
+	if (isMySend === null || isMySend === undefined) {
+		isMySend = stanza.sender === dataInfo.userId;
+	}
+
+	stanza.own = dataInfo.userId;
+	stanza.mySend = isMySend;
+	stanza.style = isMySend ? 'SEND' : 'RECEIVE';
+	if (isMySend || (!isMySend && stanza.sender === dataInfo.contactId)) {
+		stanza.read = true;
+	} else {
+		stanza.read = false;
+	}
+
+	// 缓存消息
+	messageService.cacheMessageWhenClientReceive(stanza, function (sessions) {
+		if (window.debug) {
+			console.log('[DEBUG]Update Sessions:', sessions);
+		}
+
+		// 处理会话数据
+		var handlerSessions = handlerSessionData(sessions);
+		// 绘制会话列表
+		drawSessionList(handlerSessions);
+	}, function (error) {
+		console.log('Cache Fail', error);
+	});
+}
+
+function ready() {
+	activate();
+
+	$('#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"
+		}]
+	});
+
+	// 发送消息
+	$("#btn-send").click(clickSendMessage);
+
+	// 关闭聊天窗口
+	$("#btn-close").click(function () {
+		window.location.href = '/chat/sample?from=' + dataInfo.userId;
+	});
+
+	// 上传图片
+	$('#upload-image-btn').click(function () {
+		// 触发文件上传的点击
+		$('#upload-image').click();
+	});
+
+	// 上传图片获取图片信息
+	$('#upload-image').change(uploadImage)
+}
+
+function sendMessage(content) {
+	if (!content || content === '') {
+		return ;
+	}
+
+	if (Client.isConnected()) {
+		if (!dataInfo.contactId || dataInfo.contactId == '') {
+			alert("请输入联系人!");
+			return;
+		}
+
+		var msg = Client.sendMessage(dataInfo.userId, dataInfo.contactId, content);
+
+		if (msg) {
+			console.log(msg.tree().outerHTML);
+			var message = translateToObject(msg.tree());
+			console.log('1-message', message.body || {});
+
+			// 显示消息
+			drawMessage(message.body || {}, true);
+
+			saveMessageToLocal(message.body, true);
+		}
+
+		$("#input-msg").text('');
+	} else {
+		alert("请先登录!");
+	}
+}
+
+/**
+ * 点击发送按钮,发送消息
+ */
+function clickSendMessage() {
+	var content = $("#input-msg").html();
+
+	if (window.debug) {
+		console.log('[DEBUG]Send Message', content);
+	}
+	sendMessage(content);
+}
+
+/**
+ * 点击图片按钮发送图片
+ */
+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) {
+			console.log(data);
+			var content = '<img src="' + data + '"/>';
+			// $("#input-msg").html(content);
+			sendMessage(content);
+			$('#upload-image')[0].files[0] = [];
+		}, function (error) {
+			console.log(error);
+			$('#upload-image')[0].files[0] = [];
+		})
+	}
+}

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

@@ -0,0 +1,83 @@
+/**
+ * 数据服务类
+ */
+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 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,
+		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);

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

@@ -0,0 +1,173 @@
+/**
+ * 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) {
+		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;
+		} 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 msg		消息
+	 */
+	function onMessage(msg) {
+		if (window.debug) {
+			console.log(new Date(), msg.outerHTML);
+		}
+		onfire.fire('chat.message', msg);
+		return true;
+	}
+
+	/**
+	 * 获取Connection
+	 */
+	function getConnection() {
+		return connection;
+	}
+
+	/**
+	 * 服务器连接状态
+	 */
+	function isConnected() {
+		return connected;
+	}
+
+	/**
+	 * 创建一个<message>元素并发送
+	 *
+	 * @param from
+	 * @param to
+	 * @param content
+	 */
+	function sendMessage(from, to, content) {
+		if (window.debug) {
+			console.log('message info', from + '-> ' + to);
+		}
+
+		var jid = getJabberId(from),
+			contactJid = getJabberId(to),
+			msg = null;
+
+		if (connected) {
+
+			var message = new Message();
+			message.fromUserId = from;
+			message.sender = from;
+			message.receiver = to;
+			message.timeSend = Math.round(new Date().getTime() / 1000);
+			message.content = content;
+
+			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 msg;
+	}
+
+	/**
+	 * 发送回执,告诉发送端收到了消息
+	 *
+	 * @param fromUserId	发送方UserId
+	 * @param toJid			接受方Jabber Id
+	 * @param msgId			消息ID
+	 */
+	function received(fromUserId, toJid, msgId) {
+		var jid = Client.getJabberId(fromUserId);
+		var msg = $msg({
+			from : jid,
+			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);

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

@@ -0,0 +1,36 @@
+
+function goToChatPage() {
+	console.log('Chat');
+
+	var userPhone = $('#userId').val();
+	var contact = $('#contactId').val();
+
+	dataService.get('/api/chat/infos', { from: userPhone, to: contact, condition: 'phone'}, {}, handlerData, handlerError);
+
+	function handlerData(data) {
+		if (!data || !data.userId || data.userId === '') {
+			alert('用户信息不存在,请重新输入');
+			return ;
+		}
+		var url = '/chat/sample?from=' + data.userId;
+		if (data.contactId && data.contactId !== '') {
+			url = url + '&to=' + data.contactId;
+		}
+		window.location.href = url;
+	}
+
+	function handlerError(error) {
+		console.log(error);
+		alert('输入信息异常,请重新输入');
+	}
+}
+
+function ready() {
+	$("form").on('submit', function (e) {
+		e.preventDefault();
+	});
+
+	$("#startChat").click(goToChatPage);
+}
+
+$(document).ready(ready);

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

@@ -0,0 +1,24 @@
+/**
+ * 消息
+ */
+var Message = (function () {
+
+	function Message() {
+		this.type = 1;
+		this.fromUserId = null;
+		this.sender = null;
+		this.receiver = null;
+		this.messageState = 0;
+		this.download = false;
+		this.upload = false;
+		this.timeLen = 0;
+		this.timeReceive = 0;
+		this.mySend = true;
+		this.read = false;
+		this.timeSend = 0;
+		this.content = null;
+		this.style = 'SEND';
+	}
+
+	return Message;
+})();

+ 187 - 0
src/main/resources/static/app/private.js

@@ -0,0 +1,187 @@
+// XMPP服务器BOSH地址
+var BOSH_HOST = "113.105.74.140", BOSH_SERVICE = 'http://' + BOSH_HOST + ':5280';
+
+// XMPP连接
+var connection = null;
+
+// 当前状态是否连接
+var connected = false;
+
+// 当前登录的JID
+var jid = "";
+
+// 连接状态改变的事件
+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;
+	} else if (status == Strophe.Status.CONNECTED) {
+		// alert("连接成功,可以开始聊天了!");
+		connected = true;
+
+		// connection.addHandler(onRosterChange, Strophe.NS.ROSTER, 'iq', 'set');
+		getFriendsList();
+		connection.addHandler(onPresenceOn, null, 'presence');
+		// 当接收到<message>节,调用onMessage回调函数
+		connection.addHandler(onMessage, null, 'message', 'chat');
+
+		// getFriendsList();
+
+		// 首先要发送一个<presence>给服务器(initial presence)
+		connection.send($pres().tree());
+
+		console.log('Login Success');
+	}
+}
+// 表情
+var faces = {
+	"奋斗" : "14.gif",
+	"坏笑" : "20.gif"
+};
+function formatFace(text) {
+	return text.replace(/(\[(.{2})\])/gi, function() {
+		var face = faces[arguments[2]];
+		return face ? ('<img src="../../style/img/face/' + face + '"/>') : arguments[0];
+	});
+}
+
+// 接收到<message>
+function onMessage(msg) {
+	console.log('received message', msg);
+	// 解析出<message>的from、type属性,以及body子元素
+	var from = msg.getAttribute('from');
+	var type = msg.getAttribute('type');
+	var msgId = msg.getAttribute('id');
+	var elems = msg.getElementsByTagName('body');
+	if (type == "chat" && elems.length > 0) {
+		var body = elems[0], text = $.parseJSON(formatFace(body.textContent));
+		$("#msg").append("<div class=\"msg_wrap\"><div class=\"msg\">" + text.content + "</div></div>");
+		$("#msg").animate({
+			scrollTop : $("#msg").height()
+		}, 300);
+	}
+	// 发送回执,告诉发送端收到了消息
+	/*received(msgId, from);*/
+	return true;
+}
+
+function onPresenceOn(stanza) {
+	console.log('监听好友在线状态的变化', stanza);
+	return true;
+}
+
+function onPresenceOff(stanza) {
+	console.log(stanza);
+	return true;
+}
+
+function onRosterChange(stanza) {
+	console.log('监听好友列表变化', stanza);
+	var items = $(stanza).find('item');
+	items.push($('<item jid="' + jid + '" name="胡学志" presence="available"></item>'));
+	console.log(items);
+	return true;
+}
+
+function received(msgId, to) {
+	var msg = $msg({
+		from : jid,
+		to : to
+	}).c("received", {
+		'xmlns': 'urn:xmpp:receipts',
+		'id': msgId
+	});
+	connection.send(msg.tree());
+}
+
+function activate() {
+	if (!connected) {
+		connection = new Strophe.Connection(BOSH_SERVICE);
+		jid = $("#input-jid").val() + "@" + BOSH_HOST;
+		console.log('jid', jid);
+		console.log('password', $("#input-certification").val());
+		connection.connect(jid, $("#input-certification").val(), onConnect);
+	}
+}
+
+function getFriendsList() {
+	var iq = $iq({ type: 'get', id: jid }).c('query', { xmlns: 'jabber:iq:roster'});
+	connection.sendIQ(iq, onRosterChange)
+}
+
+$(document).ready(
+		function() {
+
+			activate();
+
+			// 通过BOSH连接XMPP服务器
+			$('#btn-login').click(function() {
+				if (!connected) {
+					connection = new Strophe.Connection(BOSH_SERVICE);
+					connection.connect($("#input-jid").val() + "@" + BOSH_HOST, $("#input-certification").val(), onConnect);
+					jid = $("#input-jid").val() + "@" + BOSH_HOST;
+				}
+			});
+
+			$('#input-msg').bind('paste', function (e) {
+				console.log(e);
+				var clipboard = e.clipboardData;
+				console.log(clipboard);
+				for (var i = 0, len = clipboard.items.length; i < len; i++) {
+					if (clipboard.items[i].kind === 'file' || clipboard.items[i].type.indexOf('image') > -1) {
+						var imageFile = clipboard.items[i].getAsFile();
+						var form = new FormData;
+						form.append('t', 'ajax-uploadic');
+						form.append('avatar', imageFile);
+						var callback = G.uploadCallback || function (type, data) {};
+						e.preventDefault();
+					}
+				}
+			});
+
+			// 发送消息
+			$("#btn-send").click(
+					function() {
+						if (connected) {
+							if ($("#input-contacts").val() == '') {
+								alert("请输入联系人!");
+								return;
+							}
+
+							// 创建一个<message>元素并发送
+							var msg = $msg({
+								to : $("#input-contacts").val() + "@" + BOSH_HOST,
+								from : jid,
+								type : 'chat'
+							}).c("body", null, $.toJSON({
+								type : 1,
+								fromUserId : $("#input-jid").val(),
+								messageState : 0,
+								download: false,
+								upload: false,
+								timeLen: 0,
+								timeReceive: 0,
+								mySend: true,
+								read: false,
+								// 时间到分
+								timeSend : Math.round(new Date().getTime() / 1000),
+								content : $("#input-msg").val()
+							}));
+							connection.send(msg.tree());
+
+							$("#msg").append(
+									"<div class=\"msg_wrap\"><div class=\"msg msg-mine\">"
+											+ formatFace($("#input-msg").val()) + "</div></div>");
+							$("#msg").animate({
+								scrollTop : $("#msg").height()
+							}, 300);
+							$("#input-msg").val('');
+						} else {
+							alert("请先登录!");
+						}
+					});
+		});

+ 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);

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

@@ -0,0 +1,18 @@
+/**
+ * 消息服务
+ */
+var messageService = (function (window, dataService) {
+	
+	function cacheMessageWhenClientReceive(message, success, error) {
+		dataService.post('/api/chat/message', {}, message, success, error);
+	}
+
+	function loadReadableMessageWhenUserRead(sender, receiver, success, error) {
+		dataService.get('/api/chat/message', { operate: 'user_read', sender: sender, receiver: receiver }, {}, success, error);
+	}
+
+	return {
+		cacheMessageWhenClientReceive: cacheMessageWhenClientReceive,
+		loadReadableMessageWhenUserRead: loadReadableMessageWhenUserRead
+	}
+})(window, dataService);

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

@@ -0,0 +1,18 @@
+/**
+ * 会话服务
+ */
+var sessionService = (function (window, dataService) {
+
+	function loadSessionsWhenUserRefresh(ownId, success, error) {
+		dataService.get('/api/chat/session', { operate: 'refresh', ownId: ownId }, {}, success, error);
+	}
+
+	function persistSessionWhenUserSendMessage(session, success, error) {
+		dataService.post('/api/chat/session', {}, session, success, error);
+	}
+
+	return {
+		loadSessionsWhenUserRefresh: loadSessionsWhenUserRefresh,
+		persistSessionWhenUserSendMessage: persistSessionWhenUserSendMessage
+	}
+})(window, dataService);

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

@@ -0,0 +1,164 @@
+.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 #bfbfbf;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.176);
+}
+
+.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;
+}

File diff suppressed because it is too large
+ 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));

File diff suppressed because it is too large
+ 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);

File diff suppressed because it is too large
+ 1 - 0
src/main/resources/static/lib/jquery.mCustomScrollbar.min.js


File diff suppressed because it is too large
+ 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}});

File diff suppressed because it is too large
+ 1 - 0
src/main/resources/static/lib/strophe.min.js


+ 43 - 0
src/main/resources/static/login.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="UTF-8">
+	<title>Login Page</title>
+	<link rel="stylesheet" href="/webjars/bootstrap/css/bootstrap.min.css">
+	<script src="/webjars/jquery/jquery.min.js"></script>
+</head>
+<body>
+	<div class="container">
+		<div>
+			<form class="form-horizontal" style="margin: 110px auto 0;width: 600px;">
+				<div class="form-group">
+					<label for="inputEmail3" class="col-sm-2 control-label">Email</label>
+					<div class="col-sm-10">
+						<input type="email" class="form-control" id="inputEmail3" placeholder="Email">
+					</div>
+				</div>
+				<div class="form-group">
+					<label for="inputPassword3" class="col-sm-2 control-label">Password</label>
+					<div class="col-sm-10">
+						<input type="password" class="form-control" id="inputPassword3" placeholder="Password">
+					</div>
+				</div>
+				<div class="form-group">
+					<div class="col-sm-offset-2 col-sm-10">
+						<div class="checkbox">
+							<label>
+								<input type="checkbox"> Remember me
+							</label>
+						</div>
+					</div>
+				</div>
+				<div class="form-group">
+					<div class="col-sm-offset-2 col-sm-10">
+						<button type="submit" class="btn btn-default">Sign in</button>
+					</div>
+				</div>
+			</form>
+		</div>
+	</div>
+</body>
+</html>

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

@@ -0,0 +1,245 @@
+/* Common */
+a, a:focus, a:hover {
+	text-decoration: none;
+	cursor: pointer;
+}
+
+body{
+	background: #f5f5f5;
+}
+/* Session List */
+#session-list {
+	padding: 0;
+	height: 413px;
+	overflow: hidden;
+	border: 1px #ccc solid;
+}
+::-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 {
+	border: 1px #ccc solid;
+	border-left: none;
+}
+
+#chat-area #chat-title {
+	border-bottom: 1px #ccc solid;
+	padding: 0 15px;
+	height: 30px;
+	line-height:30px;
+	background: #efefef;
+}
+#chat-area #chat-title h3{
+	margin: 0;
+	padding: 0;
+	font-size: 14px;
+	line-height:30px;
+}
+#chat-area #chat-receiver-log {
+	padding: 10px 5px;
+	height: 215px;
+	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-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: 100px;
+}
+
+.container-fluid{
+	width: 730px;
+	margin-top: 100px;
+	height: 413px;
+	position: fixed;
+	top: 50%;
+	left: 50%;
+	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 #ccc 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: #efefef;
+}
+.list-group-title h3{
+	margin: 0;
+	padding-left: 10px;
+	font-size: 14px;
+	line-height: 30px;
+}
+.list-group dl{
+	width: 100%;
+	height: 60px;
+	margin: 0 auto;
+	padding: 10px;
+}
+.list-group dl dt{
+	float: left;
+	width: 40px;
+	height: 40px;
+	text-align: center;
+}
+.list-group dl:hover,.list-group dl.active{
+	background: #ebeef3;
+}
+.list-group dl dt img{
+	width: 40px;
+	height: 40px;
+	border-radius: 100%;
+}
+.list-group dl dd{
+	margin-left: 45px;
+}
+.list-group dl dd{
+	height: 25px;
+}
+.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: 12px;
+	color: #666;
+	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;
+}

+ 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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


Some files were not shown because too many files changed in this diff