sunkean 3 жил өмнө
commit
eac320a0e2
34 өөрчлөгдсөн 2463 нэмэгдсэн , 0 устгасан
  1. 33 0
      .gitignore
  2. BIN
      .mvn/wrapper/maven-wrapper.jar
  3. 2 0
      .mvn/wrapper/maven-wrapper.properties
  4. 316 0
      mvnw
  5. 188 0
      mvnw.cmd
  6. 186 0
      pom.xml
  7. 13 0
      src/main/java/com/izouma/meta/MetaWebsocketApplication.java
  8. 16 0
      src/main/java/com/izouma/meta/config/AliyunProperties.java
  9. 22 0
      src/main/java/com/izouma/meta/config/Bucket4jConfig.java
  10. 65 0
      src/main/java/com/izouma/meta/config/CacheConfig.java
  11. 12 0
      src/main/java/com/izouma/meta/config/GeneralProperties.java
  12. 142 0
      src/main/java/com/izouma/meta/config/RedissonBasedProxyManager.java
  13. 15 0
      src/main/java/com/izouma/meta/config/WebSocketConfig.java
  14. 101 0
      src/main/java/com/izouma/meta/domain/BaseEntity.java
  15. 105 0
      src/main/java/com/izouma/meta/domain/MetaMMOLoginInfo.java
  16. 37 0
      src/main/java/com/izouma/meta/domain/PublicScreenChat.java
  17. 27 0
      src/main/java/com/izouma/meta/dto/MMOMessage.java
  18. 15 0
      src/main/java/com/izouma/meta/dto/MMOSingleMessage.java
  19. 38 0
      src/main/java/com/izouma/meta/dto/MetaServiceResult.java
  20. 25 0
      src/main/java/com/izouma/meta/dto/PublicScreenChatExceptionMsg.java
  21. 19 0
      src/main/java/com/izouma/meta/dto/PurchaseLevelDTO.java
  22. 19 0
      src/main/java/com/izouma/meta/dto/WebsocketUser.java
  23. 44 0
      src/main/java/com/izouma/meta/exception/BusinessException.java
  24. 14 0
      src/main/java/com/izouma/meta/repo/MetaMMOLoginInfoRepo.java
  25. 10 0
      src/main/java/com/izouma/meta/repo/PublicScreenChatRepo.java
  26. 91 0
      src/main/java/com/izouma/meta/service/ContentAuditService.java
  27. 27 0
      src/main/java/com/izouma/meta/utils/ApplicationContextUtil.java
  28. 16 0
      src/main/java/com/izouma/meta/utils/NullAwareBeanUtilsBean.java
  29. 35 0
      src/main/java/com/izouma/meta/utils/ObjUtils.java
  30. 182 0
      src/main/java/com/izouma/meta/websocket/PublicScreenChatWebsocket.java
  31. 396 0
      src/main/java/com/izouma/meta/websocket/WebSocket.java
  32. 82 0
      src/main/java/com/izouma/meta/websocket/WebsocketCommon.java
  33. 123 0
      src/main/resources/application.yaml
  34. 47 0
      src/test/java/com/izouma/meta/MetaWebsocketApplicationTests.java

+ 33 - 0
.gitignore

@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**
+!**/src/test/**
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+
+### VS Code ###
+.vscode/
+
+.DS_Store

BIN
.mvn/wrapper/maven-wrapper.jar


+ 2 - 0
.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar

+ 316 - 0
mvnw

@@ -0,0 +1,316 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /usr/local/etc/mavenrc ] ; then
+    . /usr/local/etc/mavenrc
+  fi
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  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
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  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
+  else
+    JAVACMD="`\\unset -f command; \\command -v java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    if [ -n "$MVNW_REPOURL" ]; then
+      jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    fi
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+    if $cygwin; then
+      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+    fi
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+        fi
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+            curl -o "$wrapperJarPath" "$jarUrl" -f
+        else
+            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+        fi
+
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        # For Cygwin, switch paths to Windows format before running javac
+        if $cygwin; then
+          javaClass=`cygpath --path --windows "$javaClass"`
+        fi
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  $MAVEN_DEBUG_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" \
+  "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 188 - 0
mvnw.cmd

@@ -0,0 +1,188 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
+if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+
+FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Found %WRAPPER_JAR%
+    )
+) else (
+    if not "%MVNW_REPOURL%" == "" (
+        SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
+    )
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Couldn't find %WRAPPER_JAR%, downloading it ...
+        echo Downloading from: %DOWNLOAD_URL%
+    )
+
+    powershell -Command "&{"^
+		"$webclient = new-object System.Net.WebClient;"^
+		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+		"}"^
+		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+		"}"
+    if "%MVNW_VERBOSE%" == "true" (
+        echo Finished downloading %WRAPPER_JAR%
+    )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% ^
+  %JVM_CONFIG_MAVEN_PROPS% ^
+  %MAVEN_OPTS% ^
+  %MAVEN_DEBUG_OPTS% ^
+  -classpath %WRAPPER_JAR% ^
+  "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
+  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
+if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%"=="on" pause
+
+if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
+
+cmd /C exit /B %ERROR_CODE%

+ 186 - 0
pom.xml

@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.5</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.izouma</groupId>
+    <artifactId>meta_websocket</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>meta_websocket</name>
+    <description>meta_websocket</description>
+    <properties>
+        <java.version>1.8</java.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-envers</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>cdn20180510</artifactId>
+            <version>1.0.9</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>tea-openapi</artifactId>
+            <version>0.2.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>tea-console</artifactId>
+            <version>0.0.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>tea-util</artifactId>
+            <version>0.2.13</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>tea</artifactId>
+            <version>1.1.14</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-green</artifactId>
+            <version>3.6.5</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>2.8.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-core</artifactId>
+            <version>4.1.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-pool2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+            <version>1.9.4</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-configuration</groupId>
+            <artifactId>commons-configuration</artifactId>
+            <version>1.10</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-configuration2</artifactId>
+            <version>2.7</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.37</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>2.2.6</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+            <version>3.17.3</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.vladimir-bukhtoyarov</groupId>
+            <artifactId>bucket4j-core</artifactId>
+            <version>7.5.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-hibernate5</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>9</source>
+                    <target>9</target>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 13 - 0
src/main/java/com/izouma/meta/MetaWebsocketApplication.java

@@ -0,0 +1,13 @@
+package com.izouma.meta;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class MetaWebsocketApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(MetaWebsocketApplication.class, args);
+    }
+
+}

+ 16 - 0
src/main/java/com/izouma/meta/config/AliyunProperties.java

@@ -0,0 +1,16 @@
+package com.izouma.meta.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "aliyun")
+public class AliyunProperties {
+
+    private String accessKeyId;
+
+    private String accessKeySecret;
+
+}

+ 22 - 0
src/main/java/com/izouma/meta/config/Bucket4jConfig.java

@@ -0,0 +1,22 @@
+package com.izouma.meta.config;
+
+import io.github.bucket4j.distributed.proxy.ProxyManager;
+import org.redisson.api.RedissonClient;
+import org.redisson.command.CommandSyncService;
+import org.redisson.config.ConfigSupport;
+import org.redisson.connection.ConnectionManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+
+@Configuration
+public class Bucket4jConfig {
+
+    @Bean
+    ProxyManager<String> proxyManager(RedissonClient client) {
+        ConnectionManager connectionManager = ConfigSupport.createConnectionManager(client.getConfig());
+        CommandSyncService commandSyncService = new CommandSyncService(connectionManager, null);
+        return new RedissonBasedProxyManager(commandSyncService, Duration.ofMinutes(10));
+    }
+}

+ 65 - 0
src/main/java/com/izouma/meta/config/CacheConfig.java

@@ -0,0 +1,65 @@
+package com.izouma.meta.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.boot.autoconfigure.AutoConfigureAfter;
+import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+
+
+@Configuration
+@AutoConfigureAfter({RedisAutoConfiguration.class, CacheAutoConfiguration.class})
+@EnableRedisRepositories(basePackages = "com.izouma.meta.repo")
+public class CacheConfig {
+
+    @Bean
+    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(redisConnectionFactory);
+
+        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
+        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
+                ObjectMapper.DefaultTyping.NON_FINAL,
+                JsonTypeInfo.As.WRAPPER_ARRAY);
+        mapper.registerModule(new Hibernate5Module()
+                .enable(Hibernate5Module.Feature.FORCE_LAZY_LOADING));
+        mapper.registerModule(new JavaTimeModule());
+        mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+        SimpleModule simpleModule = new SimpleModule();
+        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
+        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
+        mapper.registerModule(simpleModule);
+
+        serializer.setObjectMapper(mapper);
+
+        template.setValueSerializer(serializer);
+        //使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+        template.afterPropertiesSet();
+        return template;
+    }
+
+}

+ 12 - 0
src/main/java/com/izouma/meta/config/GeneralProperties.java

@@ -0,0 +1,12 @@
+package com.izouma.meta.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Data
+@Component
+@ConfigurationProperties(prefix = "general")
+public class GeneralProperties {
+    private String urlPrefix;
+}

+ 142 - 0
src/main/java/com/izouma/meta/config/RedissonBasedProxyManager.java

@@ -0,0 +1,142 @@
+package com.izouma.meta.config;
+
+import io.github.bucket4j.distributed.proxy.ClientSideConfig;
+import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.AbstractCompareAndSwapBasedProxyManager;
+import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.AsyncCompareAndSwapOperation;
+import io.github.bucket4j.distributed.proxy.generic.compare_and_swap.CompareAndSwapOperation;
+import io.netty.buffer.ByteBuf;
+import org.redisson.api.RFuture;
+import org.redisson.client.codec.ByteArrayCodec;
+import org.redisson.client.protocol.RedisCommand;
+import org.redisson.client.protocol.RedisCommands;
+import org.redisson.client.protocol.convertor.BooleanNotNullReplayConvertor;
+import org.redisson.command.CommandExecutor;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+public class RedissonBasedProxyManager extends AbstractCompareAndSwapBasedProxyManager<String> {
+
+    public static RedisCommand<Boolean> SETPXNX_WORK_ARROUND = new RedisCommand<Boolean>("SET", new BooleanNotNullReplayConvertor());
+
+    private final CommandExecutor commandExecutor;
+    private final long            ttlMillis;
+
+    public RedissonBasedProxyManager(CommandExecutor commandExecutor, Duration ttl) {
+        this(commandExecutor, ClientSideConfig.getDefault(), ttl);
+    }
+
+    public RedissonBasedProxyManager(CommandExecutor commandExecutor, ClientSideConfig clientSideConfig, Duration ttl) {
+        super(clientSideConfig);
+        this.commandExecutor = Objects.requireNonNull(commandExecutor);
+        this.ttlMillis = ttl.toMillis();
+    }
+
+    @Override
+    protected CompareAndSwapOperation beginCompareAndSwapOperation(String key) {
+        List<Object> keys = Collections.singletonList(key);
+        return new CompareAndSwapOperation() {
+            @Override
+            public Optional<byte[]> getStateData() {
+                byte[] persistedState = commandExecutor.read(key, ByteArrayCodec.INSTANCE, RedisCommands.GET, key);
+                return Optional.ofNullable(persistedState);
+            }
+
+            @Override
+            public boolean compareAndSwap(byte[] originalData, byte[] newData) {
+                if (originalData == null) {
+                    // Redisson prohibits the usage null as values, so "replace" must not be used in such cases
+                    RFuture<Boolean> redissonFuture = commandExecutor.writeAsync(key, ByteArrayCodec.INSTANCE, SETPXNX_WORK_ARROUND, key, encodeByteArray(newData), "PX", ttlMillis, "NX");
+                    return commandExecutor.get(redissonFuture);
+                } else {
+                    String script =
+                            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                                    "redis.call('psetex', KEYS[1], ARGV[3], ARGV[2]); " +
+                                    "return 1; " +
+                                    "else " +
+                                    "return 0; " +
+                                    "end";
+                    Object[] params = new Object[]{originalData, newData, ttlMillis};
+                    RFuture<Boolean> redissonFuture = commandExecutor.evalWriteAsync(key, ByteArrayCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, script, keys, params);
+                    return commandExecutor.get(redissonFuture);
+                }
+            }
+        };
+    }
+
+
+    @Override
+    protected AsyncCompareAndSwapOperation beginAsyncCompareAndSwapOperation(String key) {
+        List<Object> keys = Collections.singletonList(key);
+        return new AsyncCompareAndSwapOperation() {
+            @Override
+            public CompletableFuture<Optional<byte[]>> getStateData() {
+                RFuture<byte[]> redissonFuture = commandExecutor.readAsync(key, ByteArrayCodec.INSTANCE, RedisCommands.GET, key);
+                return convertFuture(redissonFuture)
+                        .thenApply((byte[] resultBytes) -> Optional.ofNullable(resultBytes));
+            }
+
+            @Override
+            public CompletableFuture<Boolean> compareAndSwap(byte[] originalData, byte[] newData) {
+                if (originalData == null) {
+                    RFuture<Boolean> redissonFuture = commandExecutor.writeAsync(key, ByteArrayCodec.INSTANCE, SETPXNX_WORK_ARROUND, key, encodeByteArray(newData), "PX", ttlMillis, "NX");
+                    return convertFuture(redissonFuture);
+                } else {
+                    String script =
+                            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                                    "redis.call('psetex', KEYS[1], ARGV[3], ARGV[2]); " +
+                                    "return 1; " +
+                                    "else " +
+                                    "return 0; " +
+                                    "end";
+                    Object[] params = new Object[]{encodeByteArray(originalData), encodeByteArray(newData), ttlMillis};
+                    RFuture<Boolean> redissonFuture = commandExecutor.evalWriteAsync(key, ByteArrayCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, script, keys, params);
+                    return convertFuture(redissonFuture);
+                }
+            }
+        };
+    }
+
+    @Override
+    public void removeProxy(String key) {
+        RFuture<Object> future = commandExecutor.writeAsync(key, RedisCommands.DEL_VOID, key);
+        commandExecutor.get(future);
+    }
+
+    @Override
+    protected CompletableFuture<Void> removeAsync(String key) {
+        RFuture<?> redissonFuture = commandExecutor.writeAsync(key, RedisCommands.DEL_VOID, key);
+        return convertFuture(redissonFuture).thenApply(bytes -> null);
+    }
+
+    @Override
+    public boolean isAsyncModeSupported() {
+        return true;
+    }
+
+    private <T> CompletableFuture<T> convertFuture(RFuture<T> redissonFuture) {
+        CompletableFuture<T> jdkFuture = new CompletableFuture<>();
+        redissonFuture.whenComplete((result, error) -> {
+            if (error != null) {
+                jdkFuture.completeExceptionally(error);
+            } else {
+                jdkFuture.complete(result);
+            }
+        });
+        return jdkFuture;
+    }
+
+    public ByteBuf encodeByteArray(byte[] value) {
+        try {
+            return ByteArrayCodec.INSTANCE.getValueEncoder().encode(value);
+        } catch (IOException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+}

+ 15 - 0
src/main/java/com/izouma/meta/config/WebSocketConfig.java

@@ -0,0 +1,15 @@
+package com.izouma.meta.config;
+
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+
+@Configuration
+public class WebSocketConfig {
+
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+}

+ 101 - 0
src/main/java/com/izouma/meta/domain/BaseEntity.java

@@ -0,0 +1,101 @@
+package com.izouma.meta.domain;
+
+import com.alibaba.excel.annotation.ExcelIgnore;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.hibernate.envers.Audited;
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedBy;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@MappedSuperclass
+@Audited
+@EntityListeners(AuditingEntityListener.class)
+@JsonIgnoreProperties(value = {"hibernateLazyInitializer"}, ignoreUnknown = true)
+@SequenceGenerator(name = "hibernate_sequence", sequenceName = "hibernate_sequence", allocationSize = 100)
+public abstract class BaseEntity {
+    @ExcelProperty("ID")
+    @Id
+    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_sequence")
+    private Long id;
+
+    @ExcelIgnore
+    @JsonIgnore
+    @CreatedBy
+    private String createdBy;
+
+    @ExcelProperty("创建时间")
+    @JsonIgnore
+    @CreatedDate
+    private LocalDateTime createdAt;
+
+    @ExcelIgnore
+    @JsonIgnore
+    @LastModifiedBy
+    private String modifiedBy;
+
+    @ExcelIgnore
+    @JsonIgnore
+    @LastModifiedDate
+    private LocalDateTime modifiedAt;
+
+    @ExcelIgnore
+    @Column(columnDefinition = "bit(1) default 0")
+    private boolean del;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    public void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
+    @JsonProperty("createdAt")
+    public LocalDateTime getCreatedAt() {
+        return createdAt;
+    }
+
+    public void setCreatedAt(LocalDateTime createdAt) {
+        this.createdAt = createdAt;
+    }
+
+    public String getModifiedBy() {
+        return modifiedBy;
+    }
+
+    public void setModifiedBy(String modifiedBy) {
+        this.modifiedBy = modifiedBy;
+    }
+
+    public LocalDateTime getModifiedAt() {
+        return modifiedAt;
+    }
+
+    public void setModifiedAt(LocalDateTime modifiedAt) {
+        this.modifiedAt = modifiedAt;
+    }
+
+    public boolean isDel() {
+        return del;
+    }
+
+    public void setDel(boolean del) {
+        this.del = del;
+    }
+}

+ 105 - 0
src/main/java/com/izouma/meta/domain/MetaMMOLoginInfo.java

@@ -0,0 +1,105 @@
+package com.izouma.meta.domain;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Table;
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+@Table(name = "meta_mmo_login_info")
+@Builder
+public class MetaMMOLoginInfo extends BaseEntity {
+
+    private String nickname;
+
+    private Long userId;
+
+    private Long regionId;
+
+    private Long cityId;
+
+    private LocalDateTime onLineTime;
+
+    private LocalDateTime offLineTime;
+
+    private String sessionId;
+
+    private String role;
+
+    private Float axisX;
+
+    private Float axisY;
+
+    private Float axisZ;
+
+    private Float eulerX;
+
+    private Float eulerY;
+
+    private Float eulerZ;
+
+    private int top;
+
+    private int hat;
+
+    private int down;
+
+    private int shoes;
+
+    private int anim;
+
+    private int emoji;
+
+    /**
+     * 根据玩家历史登陆信息初始化本次登陆信息
+     *
+     * @param metaMMOLoginInfo 历史登陆信息
+     * @return 本次登陆默认信息
+     */
+    public static MetaMMOLoginInfo initMetaMMOLoginInfo(MetaMMOLoginInfo metaMMOLoginInfo) {
+        MetaMMOLoginInfo newMetaMMOLoginInfo = new MetaMMOLoginInfo();
+        if (Objects.isNull(metaMMOLoginInfo)) {
+            newMetaMMOLoginInfo.setRegionId(0L);
+            newMetaMMOLoginInfo.setCityId(0L);
+            newMetaMMOLoginInfo.setAxisX(0F);
+            newMetaMMOLoginInfo.setAxisY(0F);
+            newMetaMMOLoginInfo.setAxisZ(0F);
+            newMetaMMOLoginInfo.setEulerX(0F);
+            newMetaMMOLoginInfo.setEulerY(0F);
+            newMetaMMOLoginInfo.setEulerZ(0F);
+            newMetaMMOLoginInfo.setTop(0);
+            newMetaMMOLoginInfo.setHat(0);
+            newMetaMMOLoginInfo.setDown(0);
+            newMetaMMOLoginInfo.setShoes(0);
+            newMetaMMOLoginInfo.setAnim(0);
+            newMetaMMOLoginInfo.setEmoji(0);
+            return newMetaMMOLoginInfo;
+        }
+        newMetaMMOLoginInfo.setCityId(metaMMOLoginInfo.getCityId());
+        newMetaMMOLoginInfo.setRegionId(metaMMOLoginInfo.getRegionId());
+        newMetaMMOLoginInfo.setAxisX(metaMMOLoginInfo.getAxisX());
+        newMetaMMOLoginInfo.setAxisY(metaMMOLoginInfo.getAxisY());
+        newMetaMMOLoginInfo.setAxisZ(metaMMOLoginInfo.getAxisZ());
+        newMetaMMOLoginInfo.setEulerX(metaMMOLoginInfo.getEulerX());
+        newMetaMMOLoginInfo.setEulerY(metaMMOLoginInfo.getEulerY());
+        newMetaMMOLoginInfo.setEulerZ(metaMMOLoginInfo.getEulerZ());
+        newMetaMMOLoginInfo.setTop(metaMMOLoginInfo.getTop());
+        newMetaMMOLoginInfo.setHat(metaMMOLoginInfo.getHat());
+        newMetaMMOLoginInfo.setDown(metaMMOLoginInfo.getDown());
+        newMetaMMOLoginInfo.setShoes(metaMMOLoginInfo.getShoes());
+        newMetaMMOLoginInfo.setAnim(metaMMOLoginInfo.getAnim());
+        newMetaMMOLoginInfo.setEmoji(metaMMOLoginInfo.getEmoji());
+        newMetaMMOLoginInfo.setRole(metaMMOLoginInfo.getRole());
+        return newMetaMMOLoginInfo;
+    }
+
+}

+ 37 - 0
src/main/java/com/izouma/meta/domain/PublicScreenChat.java

@@ -0,0 +1,37 @@
+package com.izouma.meta.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Transient;
+import java.time.LocalDateTime;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Entity
+public class PublicScreenChat extends BaseEntity {
+
+    private String nickname;
+
+    private String userId;
+
+    private int level;
+
+    private String realm;
+
+    private String title;
+
+    private String avatar;
+
+    private String messageInfo;
+
+    private LocalDateTime time;
+
+    private boolean illegal;
+
+    @Transient
+    private boolean myself;
+}

+ 27 - 0
src/main/java/com/izouma/meta/dto/MMOMessage.java

@@ -0,0 +1,27 @@
+package com.izouma.meta.dto;
+
+import com.izouma.meta.domain.MetaMMOLoginInfo;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class MMOMessage {
+
+    /**
+     * 1.地图玩家信息
+     * 2.移动
+     * 3.下线
+     * 4.玩家加入
+     * 5.自生位置信息
+     */
+    private Integer messageType;
+
+    private MetaMMOLoginInfo message;
+
+    private List<MetaMMOLoginInfo> map;
+}

+ 15 - 0
src/main/java/com/izouma/meta/dto/MMOSingleMessage.java

@@ -0,0 +1,15 @@
+package com.izouma.meta.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class MMOSingleMessage {
+
+    private Integer messageType;
+
+    private String message;
+}

+ 38 - 0
src/main/java/com/izouma/meta/dto/MetaServiceResult.java

@@ -0,0 +1,38 @@
+package com.izouma.meta.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class MetaServiceResult {
+
+    private String message;
+
+    private boolean success;
+
+    public static MetaServiceResult returnSuccess() {
+        return returnSuccess("success");
+    }
+
+    public static MetaServiceResult returnSuccess(String message) {
+        MetaServiceResult result = new MetaServiceResult();
+        result.setMessage(message);
+        result.setSuccess(true);
+        return result;
+    }
+
+    public static MetaServiceResult returnError() {
+        return returnError("error");
+    }
+
+    public static MetaServiceResult returnError(String message) {
+        MetaServiceResult result = new MetaServiceResult();
+        result.setMessage(message);
+        result.setSuccess(false);
+        return result;
+    }
+
+}

+ 25 - 0
src/main/java/com/izouma/meta/dto/PublicScreenChatExceptionMsg.java

@@ -0,0 +1,25 @@
+package com.izouma.meta.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class PublicScreenChatExceptionMsg {
+
+    /**
+     * 消息类型
+     * 1:重复登陆
+     * 2:信息保存异常
+     * 3:非法消息
+     * 4:不合法参数
+     */
+    private int type;
+
+    /**
+     * 消息体
+     */
+    private String msg;
+}

+ 19 - 0
src/main/java/com/izouma/meta/dto/PurchaseLevelDTO.java

@@ -0,0 +1,19 @@
+package com.izouma.meta.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class PurchaseLevelDTO {
+
+    private int startLevel;
+
+    private int endLevel;
+
+    private String realm;
+
+    private String title;
+}

+ 19 - 0
src/main/java/com/izouma/meta/dto/WebsocketUser.java

@@ -0,0 +1,19 @@
+package com.izouma.meta.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class WebsocketUser {
+
+    private String nickname;
+
+    private String avatar;
+
+    private int level;
+
+}
+

+ 44 - 0
src/main/java/com/izouma/meta/exception/BusinessException.java

@@ -0,0 +1,44 @@
+package com.izouma.meta.exception;
+
+import java.util.function.Supplier;
+
+public class BusinessException extends RuntimeException implements Supplier<BusinessException> {
+    private static final long serialVersionUID = 3779880207424189309L;
+
+    private Integer code = -1;
+    private String  error;
+
+    public BusinessException(String error) {
+        super(error);
+    }
+
+    public BusinessException(String error, String message) {
+        super(message);
+        this.error = error;
+    }
+
+    public BusinessException(String error, String message, Integer code) {
+        super(message);
+        this.code = code;
+        this.error = error;
+    }
+
+    public BusinessException(String error, int code) {
+        super(error);
+        this.error = error;
+        this.code = code;
+    }
+
+    @Override
+    public BusinessException get() {
+        return this;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getError() {
+        return error;
+    }
+}

+ 14 - 0
src/main/java/com/izouma/meta/repo/MetaMMOLoginInfoRepo.java

@@ -0,0 +1,14 @@
+package com.izouma.meta.repo;
+
+import com.izouma.meta.domain.MetaMMOLoginInfo;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+
+public interface MetaMMOLoginInfoRepo extends JpaRepository<MetaMMOLoginInfo, Long>, JpaSpecificationExecutor<MetaMMOLoginInfo> {
+
+    @Query(value = "select * from meta_mmo_login_info a where a.user_id = ?1 order by a.created_at desc limit 1",nativeQuery = true)
+    MetaMMOLoginInfo findLastByUserId(Long userId);
+
+    MetaMMOLoginInfo findByUserIdAndSessionIdAndDel(Long userId, String sessionId, boolean del);
+}

+ 10 - 0
src/main/java/com/izouma/meta/repo/PublicScreenChatRepo.java

@@ -0,0 +1,10 @@
+package com.izouma.meta.repo;
+
+
+import com.izouma.meta.domain.PublicScreenChat;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+public interface PublicScreenChatRepo extends JpaRepository<PublicScreenChat, Long>, JpaSpecificationExecutor<PublicScreenChat> {
+
+}

+ 91 - 0
src/main/java/com/izouma/meta/service/ContentAuditService.java

@@ -0,0 +1,91 @@
+package com.izouma.meta.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.aliyuncs.DefaultAcsClient;
+import com.aliyuncs.IAcsClient;
+import com.aliyuncs.green.model.v20180509.TextScanRequest;
+import com.aliyuncs.http.FormatType;
+import com.aliyuncs.http.HttpResponse;
+import com.aliyuncs.profile.DefaultProfile;
+import com.aliyuncs.profile.IClientProfile;
+import com.izouma.meta.config.AliyunProperties;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+public class ContentAuditService {
+    private AliyunProperties aliyunProperties;
+
+    public boolean auditText(String content) {
+        if (StringUtils.isBlank(content)) return true;
+        IClientProfile profile = DefaultProfile.getProfile("cn-shenzhen",
+                aliyunProperties.getAccessKeyId(), aliyunProperties.getAccessKeySecret());
+        DefaultProfile.addEndpoint("cn-shenzhen", "Green", "green.cn-shenzhen.aliyuncs.com");
+        IAcsClient client = new DefaultAcsClient(profile);
+        TextScanRequest textScanRequest = new TextScanRequest();
+        textScanRequest.setAcceptFormat(FormatType.JSON);
+        textScanRequest.setHttpContentType(FormatType.JSON);
+        textScanRequest.setMethod(com.aliyuncs.http.MethodType.POST);
+        textScanRequest.setEncoding("UTF-8");
+        textScanRequest.setRegionId("cn-shenzhen");
+        List<Map<String, Object>> tasks = new ArrayList<>();
+        Map<String, Object> task1 = new LinkedHashMap<>();
+        task1.put("dataId", UUID.randomUUID().toString());
+
+
+        task1.put("content", content);
+        tasks.add(task1);
+        JSONObject data = new JSONObject();
+
+        data.put("scenes", List.of("antispam"));
+        data.put("tasks", tasks);
+        textScanRequest.setHttpContent(data.toJSONString().getBytes(StandardCharsets.UTF_8), "UTF-8", FormatType.JSON);
+        textScanRequest.setConnectTimeout(3000);
+        textScanRequest.setReadTimeout(6000);
+        try {
+            HttpResponse httpResponse = client.doAction(textScanRequest);
+            if (httpResponse.isSuccess()) {
+                JSONObject scrResponse = JSON
+                        .parseObject(new String(httpResponse.getHttpContent(), StandardCharsets.UTF_8));
+                log.info(JSON.toJSONString(scrResponse, true));
+                if (200 == scrResponse.getInteger("code")) {
+                    JSONArray taskResults = scrResponse.getJSONArray("data");
+                    for (Object taskResult : taskResults) {
+                        if (200 == ((JSONObject) taskResult).getInteger("code")) {
+                            JSONArray sceneResults = ((JSONObject) taskResult).getJSONArray("results");
+                            for (Object sceneResult : sceneResults) {
+                                String scene = ((JSONObject) sceneResult).getString("scene");
+                                String suggestion = ((JSONObject) sceneResult).getString("suggestion");
+                                // 根据scene和suggetion做相关处理。
+                                // suggestion为pass表示未命中垃圾。suggestion为block表示命中了垃圾,可以通过label字段查看命中的垃圾分类。
+                                log.info("scene = [" + scene + "]");
+                                log.info("suggestion = [" + suggestion + "]");
+                                if ("block".equals(suggestion)) {
+                                    return false;
+                                }
+                            }
+                        } else {
+                            log.info("task process fail:" + ((JSONObject) taskResult).getInteger("code"));
+                        }
+                    }
+                } else {
+                    log.info("detect not success. code:" + scrResponse.getInteger("code"));
+                }
+            } else {
+                log.info("response not success. status:" + httpResponse.getStatus());
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return true;
+    }
+}

+ 27 - 0
src/main/java/com/izouma/meta/utils/ApplicationContextUtil.java

@@ -0,0 +1,27 @@
+package com.izouma.meta.utils;
+
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ApplicationContextUtil implements ApplicationContextAware {
+
+    private static ApplicationContext context;
+
+    @Override
+    public void setApplicationContext(ApplicationContext context) throws BeansException {
+        ApplicationContextUtil.context = context;
+    }
+
+    public static ApplicationContext getContext() {
+        return context;
+    }
+
+    public static Object getBean(String beanName) {
+        return context.getBean(beanName);
+    }
+
+}

+ 16 - 0
src/main/java/com/izouma/meta/utils/NullAwareBeanUtilsBean.java

@@ -0,0 +1,16 @@
+package com.izouma.meta.utils;
+
+import org.apache.commons.beanutils.BeanUtilsBean;
+
+import java.lang.reflect.InvocationTargetException;
+
+public class NullAwareBeanUtilsBean extends BeanUtilsBean {
+
+    @Override
+    public void copyProperty(Object dest, String name, Object value)
+            throws IllegalAccessException, InvocationTargetException {
+        if (value == null) return;
+        super.copyProperty(dest, name, value);
+    }
+
+}

+ 35 - 0
src/main/java/com/izouma/meta/utils/ObjUtils.java

@@ -0,0 +1,35 @@
+package com.izouma.meta.utils;
+
+
+import org.apache.commons.beanutils.BeanUtilsBean;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Objects;
+
+public class ObjUtils {
+    public static void merge(Object dst, Object src) {
+        Objects.requireNonNull(src);
+        Objects.requireNonNull(dst);
+        if (!dst.getClass().equals(src.getClass())) {
+            throw new RuntimeException("cannot merge different class");
+        }
+        BeanUtilsBean notNull = new NullAwareBeanUtilsBean();
+        try {
+            notNull.copyProperties(dst, src);
+        } catch (IllegalAccessException | InvocationTargetException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static String[] getFields(Class clazz) {
+        Field[] fields = clazz.getDeclaredFields();
+        String[] fieldNames = new String[fields.length];
+        for (int i = 0; i < fields.length; i++) {
+            fieldNames[i] = fields[i].getName();
+        }
+        return fieldNames;
+    }
+
+
+}

+ 182 - 0
src/main/java/com/izouma/meta/websocket/PublicScreenChatWebsocket.java

@@ -0,0 +1,182 @@
+package com.izouma.meta.websocket;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.izouma.meta.domain.PublicScreenChat;
+import com.izouma.meta.dto.PublicScreenChatExceptionMsg;
+import com.izouma.meta.dto.PurchaseLevelDTO;
+import com.izouma.meta.dto.WebsocketUser;
+import com.izouma.meta.exception.BusinessException;
+import com.izouma.meta.repo.PublicScreenChatRepo;
+import com.izouma.meta.service.ContentAuditService;
+import com.izouma.meta.utils.ApplicationContextUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.stereotype.Service;
+
+import javax.websocket.*;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+@ServerEndpoint(value = "/websocket/public/screen/{nickName}/{userId}")
+@Slf4j
+public class PublicScreenChatWebsocket {
+
+    /**
+     * 当前在线的客户端map
+     */
+    private static final Map<String, Session> clients = new ConcurrentHashMap();
+
+    private PublicScreenChatRepo publicScreenChatRepo;
+
+    private final String PREFIX = "meta-chat:";
+
+    private ContentAuditService contentAuditService;
+
+    private WebsocketCommon websocketCommon;
+
+    private void init() {
+        if (Objects.isNull(publicScreenChatRepo)) {
+            publicScreenChatRepo = (PublicScreenChatRepo) ApplicationContextUtil.getBean("publicScreenChatRepo");
+        }
+        if (Objects.isNull(contentAuditService)) {
+            contentAuditService = (ContentAuditService) ApplicationContextUtil.getBean("contentAuditService");
+        }
+        if (Objects.isNull(websocketCommon)) {
+            websocketCommon = (WebsocketCommon) ApplicationContextUtil.getBean("websocketCommon");
+        }
+    }
+
+    @OnOpen
+    public void onOpen(@PathParam("nickName") String nickName, @PathParam("userId") String userId, Session session) {
+        init();
+        // 判断当前玩家是否在其他地方登陆
+        if (clients.containsKey(PREFIX.concat(userId))) {
+            String msg = String.format("已在别处登陆,sessionId为[%S],正在为您关闭本连接", session.getId());
+            exceptionHandle(userId, new PublicScreenChatExceptionMsg(1, msg));
+            try {
+                log.info("关闭session连接");
+                clients.get(PREFIX.concat(userId)).close();
+            } catch (Exception e) {
+                exceptionHandle(userId, new PublicScreenChatExceptionMsg(1, String.format("session close throw exception[%S]", e)));
+                return;
+            }
+        }
+        log.info("现在来连接的sessionId:" + session.getId() + "玩家id:" + userId + "玩家昵称" + nickName);
+        clients.put(PREFIX.concat(userId), session);
+        String format = String.format("玩家[%S][%S]进入大厅", userId, nickName);
+        PublicScreenChat publicScreenChat;
+        try {
+            publicScreenChat = savePublicScreenChat(userId, format, true);
+        } catch (Exception e) {
+            String errMsg = String.format("玩家进入大厅,保存信息发生异常[%S]", e);
+            exceptionHandle(userId, new PublicScreenChatExceptionMsg(2, errMsg));
+            return;
+        }
+        websocketCommon.sendMessageToOther(clients, JSON.toJSONString(publicScreenChat), PREFIX.concat(userId));
+    }
+
+    @OnError
+    public void onError(Session session, Throwable error) {
+        // 异常处理
+        log.error(String.format("sessionId[%S]的服务端发生了错误:[%S]", session.getId(), error.getMessage()));
+    }
+
+    @OnClose
+    public void onClose(@PathParam("nickName") String nickName, @PathParam("userId") String userId, Session session) {
+        init();
+        log.info(String.format("sessionId:[%S] userId:[%S] is closed", session.getId(), userId));
+        String format = String.format("玩家[%S][%S]离开大厅", userId, nickName);
+        PublicScreenChat publicScreenChat;
+        try {
+            publicScreenChat = savePublicScreenChat(userId, format, true);
+        } catch (Exception e) {
+            String errMsg = String.format("玩家离开大厅,保存信息发生异常[%S]", e);
+            exceptionHandle(userId, new PublicScreenChatExceptionMsg(2, errMsg));
+            return;
+        }
+        websocketCommon.sendMessageToOther(clients, JSON.toJSONString(publicScreenChat), PREFIX.concat(userId));
+        clients.remove(PREFIX.concat(userId));
+    }
+
+    @OnMessage
+    public void onMessage(@PathParam("userId") String userId, String message) {
+        init();
+        if (StringUtils.isBlank(message)) {
+            String errMsg = "Illegal parameter : message can not be null";
+            exceptionHandle(userId, new PublicScreenChatExceptionMsg(4, errMsg));
+            return;
+        }
+        if (!contentAuditService.auditText(message)) {
+            savePublicScreenChat(userId, message, false);
+            exceptionHandle(userId, new PublicScreenChatExceptionMsg(3, "消息包含非法内容"));
+            return;
+        }
+        PublicScreenChat publicScreenChat;
+        try {
+            publicScreenChat = savePublicScreenChat(userId, message, true);
+        } catch (Exception e) {
+            String errMsg = String.format("玩家发送消息,保存信息发生异常[%S]", e);
+            exceptionHandle(userId, new PublicScreenChatExceptionMsg(2, errMsg));
+            return;
+        }
+        sendMessageToAll(clients, JSON.toJSONString(publicScreenChat), PREFIX.concat(userId));
+    }
+
+    /**
+     * 给所有用户发消息
+     *
+     * @param clients 在线客户端
+     * @param message 消息
+     */
+    public void sendMessageToAll(Map<String, Session> clients, String message, String userId) {
+        PublicScreenChat publicScreenChat = JSON.parseObject(message, PublicScreenChat.class);
+        Set<String> userIds = clients.keySet();
+        userIds.forEach(id -> {
+            try {
+                publicScreenChat.setMyself(id.equals(userId));
+                log.info(String.format("服务器给所有在线用户发送消息,当前在线人员为[%S]。消息:[%S]", id, JSON.toJSONString(publicScreenChat)));
+                clients.get(id).getBasicRemote().sendText(JSON.toJSONString(publicScreenChat));
+            } catch (Exception e) {
+                log.error(String.format("send message [%S] to [%S] throw exception [%S]:", JSON.toJSONString(publicScreenChat), id, e));
+            }
+        });
+    }
+
+    private PublicScreenChat savePublicScreenChat(String userId, String messageInfo, boolean illegal) {
+        String resultForWebsocketUser = websocketCommon.getRequest("/user/websocket/".concat(userId));
+        WebsocketUser websocketUser = JSONObject.parseObject(resultForWebsocketUser, WebsocketUser.class);
+        if (Objects.isNull(websocketUser)) {
+            throw new BusinessException("用户信息不存在");
+        }
+        PublicScreenChat publicScreenChat = new PublicScreenChat();
+        publicScreenChat.setUserId(userId);
+        publicScreenChat.setAvatar(websocketUser.getAvatar());
+        publicScreenChat.setNickname(websocketUser.getNickname());
+        publicScreenChat.setLevel(websocketUser.getLevel());
+        String resultForPurchaseLevel = websocketCommon.getRequest("/purchaseLevel/websocket/".concat(String.valueOf(websocketUser.getLevel())));
+        PurchaseLevelDTO purchaseLevelDTO = JSONObject.parseObject(resultForPurchaseLevel, PurchaseLevelDTO.class);
+        if (Objects.isNull(purchaseLevelDTO)) {
+            throw new BusinessException("用户等级称号信息不存在");
+        }
+        publicScreenChat.setRealm(purchaseLevelDTO.getRealm());
+        publicScreenChat.setTitle(purchaseLevelDTO.getTitle());
+        publicScreenChat.setMessageInfo(messageInfo);
+        publicScreenChat.setTime(LocalDateTime.now());
+        publicScreenChat.setIllegal(illegal);
+        return publicScreenChatRepo.save(publicScreenChat);
+    }
+
+    private void exceptionHandle(String userId, PublicScreenChatExceptionMsg msg) {
+        log.error(JSON.toJSONString(msg));
+        // 推送消息给该玩家
+        websocketCommon.sendMessageTo(clients, JSON.toJSONString(msg), PREFIX.concat(userId));
+    }
+}
+

+ 396 - 0
src/main/java/com/izouma/meta/websocket/WebSocket.java

@@ -0,0 +1,396 @@
+package com.izouma.meta.websocket;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.izouma.meta.domain.MetaMMOLoginInfo;
+import com.izouma.meta.dto.MMOMessage;
+import com.izouma.meta.dto.MMOSingleMessage;
+import com.izouma.meta.dto.MetaServiceResult;
+import com.izouma.meta.repo.MetaMMOLoginInfoRepo;
+import com.izouma.meta.utils.ApplicationContextUtil;
+import com.izouma.meta.utils.ObjUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang.StringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+
+import javax.websocket.*;
+import javax.websocket.server.PathParam;
+import javax.websocket.server.ServerEndpoint;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+@Service
+@ServerEndpoint(value = "/websocket/mmo/{nickName}/{userId}")
+@Slf4j
+public class WebSocket {
+
+    /**
+     * 当前在线的客户端map
+     */
+    private static final Map<String, Session> clients = new ConcurrentHashMap();
+
+    private final String REDIS_PREFIX = "meta:";
+
+    private RedisTemplate redisTemplate;
+
+    private MetaMMOLoginInfoRepo metaMMOLoginInfoRepo;
+
+    private WebsocketCommon websocketCommon;
+
+    private void init() {
+        if (Objects.isNull(redisTemplate)) {
+            redisTemplate = (RedisTemplate) ApplicationContextUtil.getBean("redisTemplate");
+        }
+        if (Objects.isNull(metaMMOLoginInfoRepo)) {
+            metaMMOLoginInfoRepo = (MetaMMOLoginInfoRepo) ApplicationContextUtil.getBean("metaMMOLoginInfoRepo");
+        }
+        if (Objects.isNull(websocketCommon)) {
+            websocketCommon = (WebsocketCommon) ApplicationContextUtil.getBean("websocketCommon");
+        }
+    }
+
+    @OnOpen
+    public void onOpen(@PathParam("nickName") String nickName, @PathParam("userId") String userId, Session session) {
+        init();
+        // 判断当前玩家是否在其他地方登陆
+        if (clients.containsKey(REDIS_PREFIX.concat(userId))) {
+            log.info(String.format("当前玩家[%S]已经在别处登陆,sessionId为[%S]", userId, session.getId()));
+            // 关闭连接
+            MMOSingleMessage mmoSingleMessage = new MMOSingleMessage();
+            mmoSingleMessage.setMessageType(6);
+            mmoSingleMessage.setMessage("您已在其他地方登录,已为您关闭上次登陆信息");
+            websocketCommon.sendMessageTo(clients, JSON.toJSONString(mmoSingleMessage), REDIS_PREFIX.concat(userId));
+            try {
+                log.info("关闭上次登陆的session连接");
+                clients.get(REDIS_PREFIX.concat(userId)).close();
+            } catch (Exception e) {
+                log.error("session close throw exception:", e);
+            }
+        }
+        log.info("现在来连接的sessionId:" + session.getId() + "玩家id:" + userId + "玩家昵称" + nickName);
+        clients.put(REDIS_PREFIX.concat(userId), session);
+        // 获取上次登录的信息
+        MetaMMOLoginInfo dbMetaMMOLoginInfo = metaMMOLoginInfoRepo.findLastByUserId(Long.parseLong(userId));
+        // 玩家登陆信息入库
+        MetaMMOLoginInfo metaMMOLoginInfo = MetaMMOLoginInfo.initMetaMMOLoginInfo(dbMetaMMOLoginInfo);
+        metaMMOLoginInfo.setNickname(nickName);
+        metaMMOLoginInfo.setUserId(Long.parseLong(userId));
+        metaMMOLoginInfo.setOnLineTime(LocalDateTime.now());
+        metaMMOLoginInfo.setSessionId(session.getId());
+        MetaMMOLoginInfo save = metaMMOLoginInfoRepo.save(metaMMOLoginInfo);
+        MMOMessage mmoMessage = new MMOMessage();
+        mmoMessage.setMessageType(5);
+        mmoMessage.setMessage(save);
+        log.info(String.format("通知玩家[%S],本次登陆信息[%S]", userId, JSON.toJSONString(mmoMessage)));
+        websocketCommon.sendMessageTo(clients, JSON.toJSONString(mmoMessage), REDIS_PREFIX.concat(userId));
+    }
+
+    @OnError
+    public void onError(Session session, Throwable error) {
+        // 异常处理
+        log.error(String.format("sessionId[%S]的服务端发生了错误:[%S]", session.getId(), error.getMessage()));
+    }
+
+    @OnClose
+    public void onClose(@PathParam("userId") String userId) {
+        init();
+        // 查询地图中玩家信息
+        MetaMMOLoginInfo metaMMOLoginInfo = (MetaMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId));
+        if (Objects.isNull(metaMMOLoginInfo)) {
+            // 如果缓存中玩家信息为空,根据userId和sessionId查询数据库
+            MetaMMOLoginInfo dbMetaMMOLoginInfo = metaMMOLoginInfoRepo.findByUserIdAndSessionIdAndDel(Long.parseLong(userId), clients.get(REDIS_PREFIX.concat(userId)).getId(), false);
+            dbMetaMMOLoginInfo.setOffLineTime(LocalDateTime.now());
+            // 更新离线时间
+            metaMMOLoginInfoRepo.save(dbMetaMMOLoginInfo);
+            clients.remove(REDIS_PREFIX.concat(userId));
+            return;
+        }
+        String key = String.valueOf(metaMMOLoginInfo.getCityId()).concat(":").concat(String.valueOf(metaMMOLoginInfo.getRegionId()));
+        List<String> otherUserIds = remove(key, userId);
+        if (CollectionUtils.isNotEmpty(otherUserIds)) {
+            // 分发下线消息给区域内其他玩家
+            buildMessageForSendingToAllOther(otherUserIds, 3, metaMMOLoginInfo);
+        }
+        MetaMMOLoginInfo dbMetaMMOLoginInfo = metaMMOLoginInfoRepo.findById(metaMMOLoginInfo.getId()).orElse(null);
+        if (Objects.isNull(dbMetaMMOLoginInfo)) {
+            log.error(String.format("数据库中不存在id[%S]的记录", metaMMOLoginInfo.getId()));
+            return;
+        }
+        ObjUtils.merge(dbMetaMMOLoginInfo, metaMMOLoginInfo);
+        dbMetaMMOLoginInfo.setOffLineTime(LocalDateTime.now());
+        metaMMOLoginInfoRepo.save(dbMetaMMOLoginInfo);
+        clients.remove(REDIS_PREFIX.concat(userId));
+        // 删除redis中自己的信息
+        redisTemplate.delete(REDIS_PREFIX.concat(userId));
+        // 移除地图中自己的信息
+        redisTemplate.opsForList().remove(REDIS_PREFIX.concat(key), 0, REDIS_PREFIX.concat(userId));
+    }
+
+    @OnMessage
+    public void onMessage(@PathParam("nickName") String nickName, @PathParam("userId") String userId, String message, Session session) {
+        init();
+        if (StringUtils.isBlank(message)) {
+            log.error("Illegal parameter : message can not be null");
+            return;
+        }
+        JSONObject jsonObject = JSON.parseObject(message);
+        MetaServiceResult result = checkParams(jsonObject);
+        if (!result.isSuccess()) {
+            log.error(result.getMessage());
+            return;
+        }
+        log.info("来自客户端消息:" + message + "客户端的id是:" + session.getId());
+        int type = Integer.parseInt(jsonObject.getString("type"));
+        String cityId = jsonObject.getString("cityId");
+        String regionId = jsonObject.getString("regionId");
+        String key = cityId.concat(":").concat(regionId);
+        List<String> otherUserIds = remove(key, userId);
+        MetaMMOLoginInfo metaMMOLoginInfo;
+        List<MetaMMOLoginInfo> metaMMOLoginInfos = redisTemplate.opsForValue().multiGet(otherUserIds);
+        switch (type) {
+            case 1:
+                log.info("当前操作类型为1 -> 玩家进入地图");
+                metaMMOLoginInfo = buildMetaMMOLoginInfo(jsonObject, Long.parseLong(cityId), Long.parseLong(regionId), nickName, userId);
+                if (CollectionUtils.isNotEmpty(otherUserIds)) {
+                    if (CollectionUtils.isNotEmpty(metaMMOLoginInfos)) {
+                        // 分发区域内其他玩家信息给自己
+                        buildMessageForSendingToUser(REDIS_PREFIX.concat(userId), 1, metaMMOLoginInfos);
+                        // 分发自己信息给区域内其他玩家
+                        buildMessageForSendingToAllOther(otherUserIds, 4, metaMMOLoginInfo);
+                    }
+                }
+                // 将自己信息存到redis中
+                redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo);
+                redisTemplate.opsForList().leftPush(REDIS_PREFIX.concat(key), REDIS_PREFIX.concat(userId));
+                break;
+            case 2:
+                log.info(String.format("当前操作类型为[%S] -> 玩家切换区域", type));
+                metaMMOLoginInfo = buildMetaMMOLoginInfo(jsonObject, Long.parseLong(cityId), Long.parseLong(regionId), nickName, userId);
+                if (CollectionUtils.isNotEmpty(otherUserIds)) {
+                    // 分发自己信息给区域内其他玩家
+                    buildMessageForSendingToAllOther(otherUserIds, 4, metaMMOLoginInfo);
+                    // 分发区域内其他玩家信息给自己
+                    buildMessageForSendingToUser(REDIS_PREFIX.concat(userId), 1, metaMMOLoginInfos);
+                }
+                MetaMMOLoginInfo oldMetaMMOLoginInfo = (MetaMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId));
+                if (Objects.isNull(oldMetaMMOLoginInfo)) {
+                    log.error("缺失玩家上次区域的地图缓存信息");
+                    break;
+                }
+                String oldKey = String.valueOf(oldMetaMMOLoginInfo.getCityId()).concat(":").concat(String.valueOf(oldMetaMMOLoginInfo.getRegionId()));
+                List<String> oldUserIds = redisTemplate.opsForList().range(REDIS_PREFIX.concat(oldKey), 0, -1);
+                if (CollectionUtils.isEmpty(oldUserIds)) {
+                    log.error("查询不到上次所进入的区域的玩家信息");
+                    break;
+                }
+                // 分发消息给之前区域的玩家
+                buildMessageForSendingToAllOther(oldUserIds, 3, oldMetaMMOLoginInfo);
+                // 清除玩家上次缓存的地图信息
+                redisTemplate.opsForList().remove(REDIS_PREFIX.concat(oldKey), 0, REDIS_PREFIX.concat(userId));
+                // 缓存玩家新的地图信息
+                redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo);
+                redisTemplate.opsForList().leftPush(REDIS_PREFIX.concat(key), REDIS_PREFIX.concat(userId));
+                break;
+            case 3:
+                log.info(String.format("当前操作类型为[%S] -> 玩家在地图内移动", type));
+                metaMMOLoginInfo = (MetaMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId));
+                if (Objects.isNull(metaMMOLoginInfo)) {
+                    log.error("缓存中不存在本玩家信息");
+                    break;
+                }
+                // 更新玩家位置信息
+                buildCommonProperty(metaMMOLoginInfo, jsonObject);
+                // 分发玩家信息给区域内其他玩家
+                buildMessageForSendingToAllOther(otherUserIds, 2, metaMMOLoginInfo);
+                redisTemplate.opsForValue().set(REDIS_PREFIX.concat(userId), metaMMOLoginInfo);
+                break;
+            case 4:
+                log.info(String.format("当前操作类型为[%S] -> 玩家进入大厅", type));
+                metaMMOLoginInfo = (MetaMMOLoginInfo) redisTemplate.opsForValue().get(REDIS_PREFIX.concat(userId));
+                if (Objects.isNull(metaMMOLoginInfo)) {
+                    log.error("缓存中不存在本玩家信息");
+                    break;
+                }
+                // 分发玩家信息给区域内其他玩家
+                buildMessageForSendingToAllOther(otherUserIds, 3, metaMMOLoginInfo);
+                // 更新库中玩家位置信息
+                MetaMMOLoginInfo dbMetaMMOLoginInfo = metaMMOLoginInfoRepo.findById(metaMMOLoginInfo.getId()).orElse(null);
+                if (Objects.isNull(dbMetaMMOLoginInfo)) {
+                    log.error(String.format("数据库不存在id[%S]的记录", metaMMOLoginInfo.getId()));
+                    break;
+                }
+                dbMetaMMOLoginInfo.setAxisX(metaMMOLoginInfo.getAxisX());
+                dbMetaMMOLoginInfo.setAxisY(metaMMOLoginInfo.getAxisY());
+                dbMetaMMOLoginInfo.setAxisZ(metaMMOLoginInfo.getAxisZ());
+                dbMetaMMOLoginInfo.setEulerX(metaMMOLoginInfo.getEulerX());
+                dbMetaMMOLoginInfo.setEulerY(metaMMOLoginInfo.getEulerY());
+                dbMetaMMOLoginInfo.setEulerZ(metaMMOLoginInfo.getEulerZ());
+                dbMetaMMOLoginInfo.setCityId(metaMMOLoginInfo.getCityId());
+                dbMetaMMOLoginInfo.setRegionId(metaMMOLoginInfo.getRegionId());
+                dbMetaMMOLoginInfo.setTop(metaMMOLoginInfo.getTop());
+                dbMetaMMOLoginInfo.setHat(metaMMOLoginInfo.getHat());
+                dbMetaMMOLoginInfo.setShoes(metaMMOLoginInfo.getShoes());
+                dbMetaMMOLoginInfo.setDown(metaMMOLoginInfo.getDown());
+                dbMetaMMOLoginInfo.setEmoji(metaMMOLoginInfo.getEmoji());
+                dbMetaMMOLoginInfo.setAnim(metaMMOLoginInfo.getAnim());
+                dbMetaMMOLoginInfo.setRole(metaMMOLoginInfo.getRole());
+                metaMMOLoginInfoRepo.save(dbMetaMMOLoginInfo);
+                break;
+            default:
+                log.error(String.format("不存在的操作类型[%S]", type));
+        }
+
+    }
+
+    /**
+     * 分发玩家信息给区域内其他玩家
+     *
+     * @param userIds          玩家id集合
+     * @param messageType      消息类型
+     * @param metaMMOLoginInfo 消息体
+     */
+    private void buildMessageForSendingToAllOther(List<String> userIds, int messageType, MetaMMOLoginInfo metaMMOLoginInfo) {
+        MMOMessage mmoMessage = new MMOMessage();
+        mmoMessage.setMessageType(messageType);
+        mmoMessage.setMessage(metaMMOLoginInfo);
+        if (!clients.containsKey(REDIS_PREFIX.concat(String.valueOf(metaMMOLoginInfo.getUserId())))) {
+            log.error("session信息不存在");
+            return;
+        }
+        userIds.forEach(id -> {
+            try {
+                log.info(String.format("服务器给所有当前区域内在线用户发送消息,当前在线人员为[%S]。消息:[%S]", id, JSON.toJSONString(mmoMessage)));
+                clients.get(id).getBasicRemote().sendText(JSON.toJSONString(mmoMessage));
+            } catch (Exception e) {
+                log.error(String.format("send message [%S] to [%S] throw exception [%S]:", JSON.toJSONString(mmoMessage), id, e));
+            }
+        });
+    }
+
+    /**
+     * 分发区域内其他玩家信息给自己
+     *
+     * @param userId            玩家id
+     * @param messageType       消息类型
+     * @param metaMMOLoginInfos 消息体(其他玩家信息)
+     */
+    private void buildMessageForSendingToUser(String userId, int messageType, List<MetaMMOLoginInfo> metaMMOLoginInfos) {
+        MMOMessage mmoMessage = new MMOMessage();
+        mmoMessage.setMessageType(messageType);
+        mmoMessage.setMap(metaMMOLoginInfos);
+        websocketCommon.sendMessageTo(clients, JSON.toJSONString(mmoMessage), userId);
+    }
+
+    /**
+     * 构建玩家登陆信息
+     *
+     * @param jsonObject 玩家位置等信息json对象
+     * @param cityId     城市id
+     * @param regionId   区域id
+     * @return 玩家位置信息
+     */
+    private MetaMMOLoginInfo buildMetaMMOLoginInfo(JSONObject jsonObject, Long cityId, Long regionId, String nickName, String userId) {
+        // 获取到进入地图时自己的信息
+        MetaMMOLoginInfo metaMMOLoginInfo = new MetaMMOLoginInfo();
+        buildCommonProperty(metaMMOLoginInfo, jsonObject);
+        metaMMOLoginInfo.setCityId(cityId);
+        metaMMOLoginInfo.setRegionId(regionId);
+        metaMMOLoginInfo.setUserId(Long.parseLong(userId));
+        metaMMOLoginInfo.setNickname(nickName);
+        if (Objects.nonNull(jsonObject.getString("role"))) {
+            metaMMOLoginInfo.setRole(jsonObject.getString("role"));
+        }
+        if (Objects.nonNull(jsonObject.getString("id"))) {
+            metaMMOLoginInfo.setId(Long.parseLong(jsonObject.getString("id")));
+        }
+        metaMMOLoginInfo.setCityId(cityId);
+        return metaMMOLoginInfo;
+    }
+
+    /**
+     * 根据jsonObject构建玩家位置信息
+     *
+     * @param metaMMOLoginInfo 玩家登陆信息
+     * @param jsonObject       玩家位置信息json对象
+     */
+    private void buildCommonProperty(MetaMMOLoginInfo metaMMOLoginInfo, JSONObject jsonObject) {
+        if (Objects.nonNull(jsonObject.getString("axisX"))) {
+            metaMMOLoginInfo.setAxisX(Float.parseFloat(jsonObject.getString("axisX")));
+        }
+        if (Objects.nonNull(jsonObject.getString("axisY"))) {
+            metaMMOLoginInfo.setAxisY(Float.parseFloat(jsonObject.getString("axisY")));
+        }
+        if (Objects.nonNull(jsonObject.getString("axisZ"))) {
+            metaMMOLoginInfo.setAxisZ(Float.parseFloat(jsonObject.getString("axisZ")));
+        }
+        if (Objects.nonNull(jsonObject.getString("eulerX"))) {
+            metaMMOLoginInfo.setEulerX(Float.parseFloat(jsonObject.getString("eulerX")));
+        }
+        if (Objects.nonNull(jsonObject.getString("eulerY"))) {
+            metaMMOLoginInfo.setEulerY(Float.parseFloat(jsonObject.getString("eulerY")));
+        }
+        if (Objects.nonNull(jsonObject.getString("eulerZ"))) {
+            metaMMOLoginInfo.setEulerZ(Float.parseFloat(jsonObject.getString("eulerZ")));
+        }
+        if (Objects.nonNull(jsonObject.getString("top"))) {
+            metaMMOLoginInfo.setTop(Integer.parseInt(jsonObject.getString("top")));
+        }
+        if (Objects.nonNull(jsonObject.getString("hat"))) {
+            metaMMOLoginInfo.setHat(Integer.parseInt(jsonObject.getString("hat")));
+        }
+        if (Objects.nonNull(jsonObject.getString("down"))) {
+            metaMMOLoginInfo.setDown(Integer.parseInt(jsonObject.getString("down")));
+        }
+        if (Objects.nonNull(jsonObject.getString("shoes"))) {
+            metaMMOLoginInfo.setShoes(Integer.parseInt(jsonObject.getString("shoes")));
+        }
+        if (Objects.nonNull(jsonObject.getString("anim"))) {
+            metaMMOLoginInfo.setAnim(Integer.parseInt(jsonObject.getString("anim")));
+        }
+        if (Objects.nonNull(jsonObject.getString("emoji"))) {
+            metaMMOLoginInfo.setEmoji(Integer.parseInt(jsonObject.getString("emoji")));
+        }
+    }
+
+    /**
+     * 校验参数
+     *
+     * @param jsonObject 参数
+     * @return 校验结果
+     */
+    private MetaServiceResult checkParams(JSONObject jsonObject) {
+        if (Objects.isNull(jsonObject)) {
+            return MetaServiceResult.returnError("Illegal parameter : jsonObject can not be null");
+        }
+        if (Objects.isNull(jsonObject.getString("type"))) {
+            return MetaServiceResult.returnError("Illegal parameter : type can not be null");
+        }
+        if (Objects.isNull(jsonObject.getString("cityId"))) {
+            return MetaServiceResult.returnError("Illegal parameter : cityId can not be null");
+        }
+        if (Objects.isNull(jsonObject.getString("regionId"))) {
+            return MetaServiceResult.returnError("Illegal parameter : regionId can not be null");
+        }
+        if (Objects.isNull(jsonObject.getString("id"))) {
+            return MetaServiceResult.returnError("Illegal parameter : id can not be null");
+        }
+        return MetaServiceResult.returnSuccess("check success");
+    }
+
+    private List<String> remove(String key, String userId) {
+        List<String> userIds = redisTemplate.opsForList().range(REDIS_PREFIX.concat(key), 0, -1);
+        //  去除当前玩家id
+        List<String> otherUserIds = new ArrayList<>();
+        if (CollectionUtils.isNotEmpty(userIds)) {
+            otherUserIds = userIds.stream().filter(id -> !Objects.equals(id, REDIS_PREFIX.concat(userId))).collect(Collectors.toList());
+        }
+        return otherUserIds;
+    }
+}

+ 82 - 0
src/main/java/com/izouma/meta/websocket/WebsocketCommon.java

@@ -0,0 +1,82 @@
+package com.izouma.meta.websocket;
+
+
+import com.izouma.meta.config.GeneralProperties;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.springframework.stereotype.Service;
+
+import javax.websocket.Session;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+@Slf4j
+@AllArgsConstructor
+public class WebsocketCommon {
+    private GeneralProperties generalProperties;
+
+    /**
+     * 给指定用户分发消息
+     *
+     * @param message  消息体
+     * @param toUserId 用户id
+     */
+    public void sendMessageTo(Map<String, Session> clients, String message, String toUserId) {
+        if (!clients.containsKey(toUserId)) {
+            log.error("session信息不存在");
+            return;
+        }
+        log.info(String.format("给指定的在线用户发送消息,当前在线人员为[%S]。消息:[%S]", toUserId, message));
+        try {
+            clients.get(toUserId).getBasicRemote().sendText(message);
+        } catch (Exception e) {
+            log.error(String.format("send message [%S] to [%S] throw exception [%S]:", message, toUserId, e));
+        }
+    }
+
+    /**
+     * 给所有用户发消息
+     *
+     * @param clients 在线客户端
+     * @param message 消息
+     */
+    public void sendMessageToOther(Map<String, Session> clients, String message, String userId) {
+        Set<String> userIds = clients.keySet();
+        userIds.forEach(id -> {
+            try {
+                if (!id.equals(userId)) {
+                    log.info(String.format("服务器给所有在线用户发送消息,当前在线人员为[%S]。消息:[%S]", id, message));
+                    clients.get(id).getBasicRemote().sendText(message);
+                }
+            } catch (Exception e) {
+                log.error(String.format("send message [%S] to [%S] throw exception [%S]:", message, id, e));
+            }
+        });
+    }
+
+    public String getRequest(String url) {
+        HttpClient client = HttpClients.createDefault();
+        HttpGet get = new HttpGet(generalProperties.getUrlPrefix().concat(url));
+        String obj;
+        try {
+            log.info(String.format("execute get request url : %S ", generalProperties.getUrlPrefix().concat(url)));
+            HttpResponse res = client.execute(get);
+            if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+                obj = EntityUtils.toString(res.getEntity());
+            } else {
+                throw new RuntimeException(String.format(" request [%S] returned error ", url));
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(String.format(" client failed to execute request for url: %S ", url));
+        }
+        return obj;
+    }
+
+}

+ 123 - 0
src/main/resources/application.yaml

@@ -0,0 +1,123 @@
+server:
+  port: 8088
+  servlet:
+    context_path: /
+  compression:
+    enabled: true
+    mime-types: application/json,application/xml,text/html,text/xml,text/plain
+  error:
+    whitelabel:
+      enabled: false
+  tomcat:
+    accept-count: 10000
+    threads:
+      max: 3000
+    max-http-form-post-size: 100MB
+spring:
+  redis:
+    host: 120.78.171.194
+    database: 0
+#    database: 1
+    password: jV%93RtjUx82Tp
+    timeout: 60s
+    lettuce:
+      pool:
+        max_active: 50
+        max_idle: 50
+        min_idle: 0
+  datasource:
+    url: jdbc:mysql://rm-wz9q65wzuf8c56647ro.mysql.rds.aliyuncs.com/raex_test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8
+    username: raex_test
+#    url: jdbc:mysql://rm-wz9sc79f5255780opqo.mysql.rds.aliyuncs.com/raex?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8
+#    username: raex_server
+    password: tetQsjw!u4!c5$URduo7BH
+    hikari:
+      minimum-idle: 20
+      maximum-pool-size: 3000
+      auto-commit: true
+      idle-timeout: 7200
+      max-lifetime: 1800000
+      connection-timeout: 5000
+      connection-test-query: SELECT 1
+    druid:
+      # ????????
+      # ???????????
+      initial-size: 5
+      min-idle: 20
+      maxActive: 3000
+      # ?????????????
+      maxWait: 60000
+      # ??????????
+      use-unfair-lock: true
+      # ???????????????????????????????
+      timeBetweenEvictionRunsMillis: 30000
+      # ??????????????????????
+      minEvictableIdleTimeMillis: 300000
+      validationQuery: SELECT 1
+      testWhileIdle: true
+      testOnBorrow: false
+      testOnReturn: false
+      # ??PSCache??????????PSCache???
+      poolPreparedStatements: true
+      maxPoolPreparedStatementPerConnectionSize: 20
+      query-timeout: 300000
+      # ?????????filters????????sql?????'wall'?????
+      filters: stat,wall,slf4j
+      # ??connectProperties?????mergeSql????SQL??
+      connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
+      remove-abandoned: true
+      remove-abandoned-timeout: 1800
+      log-abandoned: true
+      # ????
+      keep-alive: true
+      # ??????
+      keep-alive-between-time-millis: 60000
+      # ??DruidStatFilter
+      web-stat-filter:
+        enabled: true
+        url-pattern: "/*"
+        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
+      # ??DruidStatViewServlet
+      stat-view-servlet:
+        enabled: true
+        url-pattern: "/druid/*"
+        login-username: admin
+        login-password: 3edc#EDC
+      filter:
+        wall:
+          enabled: true
+          config:
+            condition-and-alway-false-allow: true
+            condition-and-alway-true-allow: true
+            select-where-alway-true-check: false
+  jpa:
+    database: MySQL
+    database-platform: org.hibernate.dialect.MySQL8Dialect
+    hibernate:
+      ddl_auto: update
+    properties:
+      hibernate:
+        dialect: org.hibernate.dialect.MySQL8Dialect
+        storage_engine: innodb
+        enable_lazy_load_no_trans: true
+        order_inserts: true
+        order_updates: true
+        fetch_size: 400
+        jdbc:
+          batch_size: 30
+    open-in-view: true
+  servlet:
+    multipart:
+      max_file_size: 100MB
+      max_request_size: 100MB
+  freemarker:
+    settings:
+      number_format: 0
+  cache:
+    type: redis
+aliyun:
+  access-key-id: LTAI5tPoBCiEMSDaS1Q4HKr9
+  access-key-secret: F8ZNiqdH35T7gikBkn6Fq8tgbvdY88
+general:
+  url-prefix: https://test.raex.vip
+#  url-prefix: https://www.raex.vip

+ 47 - 0
src/test/java/com/izouma/meta/MetaWebsocketApplicationTests.java

@@ -0,0 +1,47 @@
+package com.izouma.meta;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.izouma.meta.dto.PurchaseLevelDTO;
+import com.izouma.meta.dto.WebsocketUser;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpHeaders;
+
+import java.util.HashMap;
+
+
+@SpringBootTest
+@Slf4j
+class MetaWebsocketApplicationTests {
+
+    @Test
+    void userQuery() {
+        HashMap<String, Object> map = new HashMap<>();
+        HttpClient client = HttpClients.createDefault();
+        // 要调用的接口方法
+        String url = "https://test.raex.vip/user/websocket/7961941"; //请求对方的路径地址
+        HttpGet get = new HttpGet(url);
+        try {
+            get.setHeader(HttpHeaders.AUTHORIZATION, "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI3OTYxOTQxIiwiZXhwIjoxNjY5NTM3MzM4LCJpYXQiOjE2NjY5NDUzMzh9.KMAMg9-JJC8s8amDN6mW8VkbsJm9v-slMPiREXeB3Jk");
+            HttpResponse res = client.execute(get);
+            if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+                String result = EntityUtils.toString(res.getEntity());// 返回json格式:
+                WebsocketUser websocketUser = JSONObject.parseObject(result, WebsocketUser.class);
+                log.info(JSON.toJSONString(websocketUser));
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}