Explorar o código

新系统第一次提交(完成登录)

25057 hai 6 meses
pai
achega
2556f7b6dc
Modificáronse 100 ficheiros con 11873 adicións e 0 borrados
  1. 33 0
      .gitignore
  2. 310 0
      mvnw
  3. 182 0
      mvnw.cmd
  4. BIN=BIN
      out/artifacts/maintenance_jar/maintenance.jar
  5. 278 0
      pom.xml
  6. 24 0
      src/main/java/com/loan/system/LoanSystemApplication.java
  7. 22 0
      src/main/java/com/loan/system/annotation/ExcelColumn.java
  8. 25 0
      src/main/java/com/loan/system/annotation/OperLog.java
  9. 49 0
      src/main/java/com/loan/system/config/RedisConfig.java
  10. 116 0
      src/main/java/com/loan/system/config/WebMvcConfiguration.java
  11. 14 0
      src/main/java/com/loan/system/constant/AutoFillConstant.java
  12. 11 0
      src/main/java/com/loan/system/constant/JwtClaimsConstant.java
  13. 28 0
      src/main/java/com/loan/system/constant/MessageConstant.java
  14. 10 0
      src/main/java/com/loan/system/constant/PasswordConstant.java
  15. 13 0
      src/main/java/com/loan/system/constant/StatusConstant.java
  16. 23 0
      src/main/java/com/loan/system/context/BaseContext.java
  17. 99 0
      src/main/java/com/loan/system/controller/wechat/UploadController.java
  18. 57 0
      src/main/java/com/loan/system/controller/wechat/UserController.java
  19. 1412 0
      src/main/java/com/loan/system/domain/ContractExample.java
  20. 50 0
      src/main/java/com/loan/system/domain/ContractType.java
  21. 460 0
      src/main/java/com/loan/system/domain/ContractTypeExample.java
  22. 202 0
      src/main/java/com/loan/system/domain/Contracts.java
  23. 10 0
      src/main/java/com/loan/system/domain/dto/UserLoginDTO.java
  24. 44 0
      src/main/java/com/loan/system/domain/entity/ApprovalRecord.java
  25. 39 0
      src/main/java/com/loan/system/domain/entity/BaseEntity.java
  26. 65 0
      src/main/java/com/loan/system/domain/entity/BizRecommender.java
  27. 52 0
      src/main/java/com/loan/system/domain/entity/Collateral.java
  28. 67 0
      src/main/java/com/loan/system/domain/entity/Contract.java
  29. 52 0
      src/main/java/com/loan/system/domain/entity/Customer.java
  30. 37 0
      src/main/java/com/loan/system/domain/entity/CustomersOther.java
  31. 38 0
      src/main/java/com/loan/system/domain/entity/DictAttribute.java
  32. 38 0
      src/main/java/com/loan/system/domain/entity/DictBusinessType.java
  33. 39 0
      src/main/java/com/loan/system/domain/entity/DictChannel.java
  34. 38 0
      src/main/java/com/loan/system/domain/entity/DictLocation.java
  35. 38 0
      src/main/java/com/loan/system/domain/entity/DictMessage.java
  36. 41 0
      src/main/java/com/loan/system/domain/entity/DictStep.java
  37. 38 0
      src/main/java/com/loan/system/domain/entity/DictType.java
  38. 64 0
      src/main/java/com/loan/system/domain/entity/Disbursement.java
  39. 32 0
      src/main/java/com/loan/system/domain/entity/DisbursementRecord.java
  40. 48 0
      src/main/java/com/loan/system/domain/entity/Document.java
  41. 131 0
      src/main/java/com/loan/system/domain/entity/ExceptionLog.java
  42. 58 0
      src/main/java/com/loan/system/domain/entity/LoanCase.java
  43. 72 0
      src/main/java/com/loan/system/domain/entity/Repayment.java
  44. 31 0
      src/main/java/com/loan/system/domain/entity/RepaymentRecord.java
  45. 28 0
      src/main/java/com/loan/system/domain/entity/Role.java
  46. 45 0
      src/main/java/com/loan/system/domain/entity/StatBusinessSnapshot.java
  47. 38 0
      src/main/java/com/loan/system/domain/entity/StatFundEfficiency.java
  48. 55 0
      src/main/java/com/loan/system/domain/entity/Step.java
  49. 44 0
      src/main/java/com/loan/system/domain/entity/SysMessage.java
  50. 59 0
      src/main/java/com/loan/system/domain/entity/User.java
  51. 53 0
      src/main/java/com/loan/system/domain/enums/AuthEnum.java
  52. 28 0
      src/main/java/com/loan/system/domain/enums/ContractEnum.java
  53. 132 0
      src/main/java/com/loan/system/domain/enums/ExceptionEnum.java
  54. 38 0
      src/main/java/com/loan/system/domain/enums/LogEnum.java
  55. 27 0
      src/main/java/com/loan/system/domain/enums/RemindTypeEnum.java
  56. 32 0
      src/main/java/com/loan/system/domain/enums/RoleEnum.java
  57. 27 0
      src/main/java/com/loan/system/domain/enums/StepEnum.java
  58. 36 0
      src/main/java/com/loan/system/domain/enums/StepPropertyEnum.java
  59. 61 0
      src/main/java/com/loan/system/domain/pojo/FileResult.java
  60. 53 0
      src/main/java/com/loan/system/domain/pojo/MultipartFileUpload.java
  61. 63 0
      src/main/java/com/loan/system/domain/pojo/Result.java
  62. 20 0
      src/main/java/com/loan/system/domain/vo/UserLoginVO.java
  63. 35 0
      src/main/java/com/loan/system/exception/DescribeException.java
  64. 40 0
      src/main/java/com/loan/system/exception/FileException.java
  65. 46 0
      src/main/java/com/loan/system/exception/GlobalExceptionHandler.java
  66. 32 0
      src/main/java/com/loan/system/exception/LoginException.java
  67. 54 0
      src/main/java/com/loan/system/interceptor/JwtTokenUserInterceptor.java
  68. 52 0
      src/main/java/com/loan/system/json/JacksonObjectMapper.java
  69. 368 0
      src/main/java/com/loan/system/loan_system.sql
  70. 26 0
      src/main/java/com/loan/system/properties/JwtProperties.java
  71. 22 0
      src/main/java/com/loan/system/properties/WeChatProperties.java
  72. 14 0
      src/main/java/com/loan/system/repository/ExceptionLogDao.java
  73. 19 0
      src/main/java/com/loan/system/repository/UserRepository.java
  74. 14 0
      src/main/java/com/loan/system/service/ExceptionLogService.java
  75. 40 0
      src/main/java/com/loan/system/service/Impl/ExceptionServiceImpl.java
  76. 289 0
      src/main/java/com/loan/system/service/Impl/MediaUploadService.java
  77. 57 0
      src/main/java/com/loan/system/service/Impl/PermissionService.java
  78. 23 0
      src/main/java/com/loan/system/service/Impl/UserServiceImpl.java
  79. 8 0
      src/main/java/com/loan/system/service/UserService.java
  80. 55 0
      src/main/java/com/loan/system/utils/AuthUtil.java
  81. 376 0
      src/main/java/com/loan/system/utils/ExcelUtil.java
  82. 489 0
      src/main/java/com/loan/system/utils/FileUploadUtil.java
  83. 95 0
      src/main/java/com/loan/system/utils/JpaUtil.java
  84. 135 0
      src/main/java/com/loan/system/utils/JsonUtil.java
  85. 107 0
      src/main/java/com/loan/system/utils/JwtTokenUtil.java
  86. 59 0
      src/main/java/com/loan/system/utils/JwtUtil.java
  87. 50 0
      src/main/java/com/loan/system/utils/PoiWordUtil.java
  88. 3273 0
      src/main/java/com/loan/system/utils/RedisUtil.java
  89. 45 0
      src/main/java/com/loan/system/utils/ResultUtil.java
  90. 45 0
      src/main/java/com/loan/system/utils/TreeUtil.java
  91. 122 0
      src/main/java/com/loan/system/utils/WeChatUtil.java
  92. 70 0
      src/main/java/com/loan/system/utils/logUtils/IPUtils.java
  93. 108 0
      src/main/java/com/loan/system/utils/logUtils/OperLogAspect.java
  94. 42 0
      src/main/java/com/loan/system/utils/logUtils/ServiceHelper.java
  95. 75 0
      src/main/resources/application-dev.yaml
  96. 57 0
      src/main/resources/application-prod.yaml
  97. 61 0
      src/main/resources/application-test.yaml
  98. 46 0
      src/main/resources/application.yaml
  99. 95 0
      src/main/resources/logback.xml
  100. 0 0
      src/main/resources/rebel.xml

+ 33 - 0
.gitignore

@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### 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/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/

+ 310 - 0
mvnw

@@ -0,0 +1,310 @@
+#!/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 /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="`which 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/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+    else
+      jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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"
+        else
+            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$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 \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 182 - 0
mvnw.cmd

@@ -0,0 +1,182 @@
+@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 "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\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/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "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%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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 "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\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%
+
+exit /B %ERROR_CODE%

BIN=BIN
out/artifacts/maintenance_jar/maintenance.jar


+ 278 - 0
pom.xml

@@ -0,0 +1,278 @@
+<?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.3.3.RELEASE</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.loan</groupId>
+    <artifactId>system</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>system</name>
+    <description>Demo project for Spring Boot</description>
+
+    <properties>
+<!--        1.8-->
+        <java.version>8</java.version>
+        <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
+        <org.lombok.version>1.18.20</org.lombok.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>jsr250-api</artifactId>
+            <version>1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+            <version>2.7.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.0.1</version>
+        </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-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>${org.lombok.version}</version>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.vintage</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-aspects</artifactId>
+            <version>5.3.23</version>
+<!--            <scope>test</scope>-->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.62</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+            <version>${org.mapstruct.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.5</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-fileupload</groupId>
+            <artifactId>commons-fileupload</artifactId>
+            <version>1.3.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.10</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+        </dependency>
+        <dependency>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+            <version>0.4.12</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+        <!--集成redis-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-redis</artifactId>
+            <version>1.4.1.RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <!-- 操作 Excel2007以后 -->
+        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
+<!--        <dependency>-->
+<!--            <groupId>org.apache.poi</groupId>-->
+<!--            <artifactId>poi-ooxml</artifactId>-->
+<!--            <version>4.1.0</version>-->
+<!--        </dependency>-->
+        <!-- 操作 Excel2003-->
+        <!-- https://mvnrepository.com/artifact/org.apache.poi/poi -->
+<!--        <dependency>-->
+<!--            <groupId>org.apache.poi</groupId>-->
+<!--            <artifactId>poi</artifactId>-->
+<!--            <version>4.1.0</version>-->
+<!--        </dependency>-->
+        <!-- 生成 WORD 文档所需, 说明文档网址为:http://deepoove.com/poi-tl/-->
+        <dependency>
+            <groupId>com.deepoove</groupId>
+            <artifactId>poi-tl</artifactId>
+            <version>1.9.1</version>
+        </dependency>
+
+        <!--        WechatUtils 需要-->
+        <!-- https://mvnrepository.com/artifact/org.codehaus.xfire/xfire-all -->
+        <dependency>
+            <groupId>org.codehaus.xfire</groupId>
+            <artifactId>xfire-all</artifactId>
+            <version>1.2.5</version>
+        </dependency>
+
+
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-spring-boot-starter</artifactId>
+            <version>3.0.2</version>
+        </dependency>
+
+        <!-- swagger -->
+<!--        <dependency>-->
+<!--            <groupId>io.springfox</groupId>-->
+<!--            <artifactId>springfox-swagger-ui</artifactId>-->
+<!--            <version>2.9.2</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>io.springfox</groupId>-->
+<!--            <artifactId>springfox-swagger2</artifactId>-->
+<!--            <version>2.9.2</version>-->
+<!--        </dependency>-->
+<!--        &lt;!&ndash;  解决 Illegal DefaultValue null for parameter type integer    异常  &ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>io.swagger</groupId>-->
+<!--            <artifactId>swagger-annotations</artifactId>-->
+<!--            <version>1.5.21</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>io.swagger</groupId>-->
+<!--            <artifactId>swagger-models</artifactId>-->
+<!--            <version>1.5.21</version>-->
+<!--        </dependency>-->
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis</artifactId>
+            <version>3.5.10</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper</artifactId>
+            <version>5.2.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis</groupId>
+            <artifactId>mybatis-spring</artifactId>
+            <version>2.0.7</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.activation</groupId>
+            <artifactId>activation</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/javax.activation/activation -->
+<!--        <dependency>-->
+<!--            <groupId>javax.activation</groupId>-->
+<!--            <artifactId>activation</artifactId>-->
+<!--            <version>1.0.2</version>-->
+<!--        </dependency>-->
+
+
+
+
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                    <groupId>org.mybatis.generator</groupId>
+                    <artifactId>mybatis-generator-maven-plugin</artifactId>
+                    <version>1.3.6</version>
+                    <configuration>
+                        <configurationFile>GeneratorMapper.xml</configurationFile>
+                        <verbose>true</verbose>
+                        <overwrite>true</overwrite>
+                    </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <annotationProcessorPaths>
+                        <path>
+                            <groupId>org.mapstruct</groupId>
+                            <artifactId>mapstruct-processor</artifactId>
+                            <version>${org.mapstruct.version}</version>
+                        </path>
+                        <path>
+                            <groupId>org.projectlombok</groupId>
+                            <artifactId>lombok</artifactId>
+                            <version>${org.lombok.version}</version>
+                        </path>
+                    </annotationProcessorPaths>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 24 - 0
src/main/java/com/loan/system/LoanSystemApplication.java

@@ -0,0 +1,24 @@
+package com.loan.system;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.ApplicationContext;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+//@MapperScan(value = {"com/hdu/maintenance/mapper"})
+@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
+@EnableCaching
+@EnableJpaAuditing
+@Slf4j
+public class LoanSystemApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(LoanSystemApplication.class, args);
+
+    }
+
+}

+ 22 - 0
src/main/java/com/loan/system/annotation/ExcelColumn.java

@@ -0,0 +1,22 @@
+package com.loan.system.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * @author : EdwinXu
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ExcelColumn {
+    /**
+     * Excel title
+     */
+    String value() default "";
+
+    /**
+     * Excel 从左往右排列位置
+     */
+    int col() default 0;
+
+}

+ 25 - 0
src/main/java/com/loan/system/annotation/OperLog.java

@@ -0,0 +1,25 @@
+package com.loan.system.annotation;
+
+
+import com.loan.system.domain.enums.LogEnum;
+
+import java.lang.annotation.*;
+
+
+@Target(ElementType.METHOD) //方法级别
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface OperLog {
+    // 模块:该操作作用在的模块
+    LogEnum operModul();
+
+    // 类型:{delete,insert,update}
+    LogEnum operType();
+
+    // 描述:对该操作描述信息
+    String operDesc();
+
+    // 反射:对于更新和删除操作需要的实例
+    String operService() default "";
+
+}

+ 49 - 0
src/main/java/com/loan/system/config/RedisConfig.java

@@ -0,0 +1,49 @@
+package com.loan.system.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+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.core.StringRedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+
+import java.net.UnknownHostException;
+@Configuration
+public class RedisConfig {
+
+    @Bean
+    @ConditionalOnMissingBean(name = "redisTemplate")
+    public RedisTemplate<String, Object> redisTemplate(
+            RedisConnectionFactory redisConnectionFactory)
+            throws UnknownHostException {
+
+        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
+        ObjectMapper om = new ObjectMapper();
+        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
+        jackson2JsonRedisSerializer.setObjectMapper(om);
+
+        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
+        template.setConnectionFactory(redisConnectionFactory);
+        template.setKeySerializer(jackson2JsonRedisSerializer);
+        template.setValueSerializer(jackson2JsonRedisSerializer);
+        template.setHashKeySerializer(jackson2JsonRedisSerializer);
+        template.setHashValueSerializer(jackson2JsonRedisSerializer);
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @ConditionalOnMissingBean(StringRedisTemplate.class)
+    public StringRedisTemplate stringRedisTemplate(
+            RedisConnectionFactory redisConnectionFactory)
+            throws UnknownHostException {
+        StringRedisTemplate template = new StringRedisTemplate();
+        template.setConnectionFactory(redisConnectionFactory);
+        return template;
+    }
+}

+ 116 - 0
src/main/java/com/loan/system/config/WebMvcConfiguration.java

@@ -0,0 +1,116 @@
+package com.loan.system.config;
+
+import com.loan.system.interceptor.JwtTokenUserInterceptor;
+import com.loan.system.json.JacksonObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+
+import java.util.List;
+
+/**
+ * 配置类,注册web层相关组件
+ */
+@Configuration//配置文件类会在程序运行时自动加载
+@Slf4j
+public class WebMvcConfiguration extends WebMvcConfigurationSupport {
+
+//    @Autowired
+//    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
+    @Autowired
+    private JwtTokenUserInterceptor jwtTokenUserInterceptor;
+
+    /*
+     * 注册自定义拦截器
+     * @param registry
+     */
+    protected void addInterceptors(InterceptorRegistry registry) {
+        log.info("开始注册自定义拦截器...");
+//        registry.addInterceptor(jwtTokenAdminInterceptor)
+//                .addPathPatterns("/admin/**")
+//                .excludePathPatterns("/admin/employee/login");
+
+        registry.addInterceptor(jwtTokenUserInterceptor)
+                .addPathPatterns("/wechat/**")
+                .excludePathPatterns("/wechat/login");
+    }
+
+    /*
+     * 通过knife4j生成接口文档
+     * 1.导入 knife4j 的依赖
+     * 2.在配置类中加入 knife4j 相关配置
+     * 3.设置静态资源映射,否则接口文档页面无法访问
+     * @return
+     */
+//    @Bean
+//    public Docket docket1() {
+//        log.info("准备生成接口文件");
+//        ApiInfo apiInfo = new ApiInfoBuilder()
+//                .title("苍穹外卖项目接口文档")
+//                .version("2.0")
+//                .description("苍穹外卖项目接口文档")
+//                .build();
+//        Docket docket = new Docket(DocumentationType.SWAGGER_2)
+//                .groupName("管理端接口")
+//                .apiInfo(apiInfo)
+//                .select()
+//                //最重要:确定要扫描的包
+//                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
+//                .paths(PathSelectors.any())
+//                .build();
+//        return docket;
+//    }
+
+    @Bean
+    public Docket docket2() {
+        log.info("准备生成接口文件");
+        ApiInfo apiInfo = new ApiInfoBuilder()
+                .title("典当项目接口文档")
+                .version("2.0")
+                .description("典当项目接口文档")
+                .build();
+        Docket docket = new Docket(DocumentationType.SWAGGER_2)
+                .groupName("小程序端端接口")
+                .apiInfo(apiInfo)
+                .select()
+                //最重要:确定要扫描的包
+                .apis(RequestHandlerSelectors.basePackage("com.loan.system.controller.wechat"))
+                .paths(PathSelectors.any())
+                .build();
+        return docket;
+    }
+
+    /*
+     * 设置静态资源映射:显示/doc.html接口文档
+     * @param registry
+     */
+    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
+        log.info("设置静态资源映射");
+        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
+        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
+    }
+
+    /*
+    扩展SpringMVC框架的消息转化器
+     */
+    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){
+        //创建一个消息转化器
+        MappingJackson2HttpMessageConverter converter=new MappingJackson2HttpMessageConverter();
+        //需要为消息转化器设置一个对象转换器,对象转换器可以将java对象序列化为json数据
+        converter.setObjectMapper(new JacksonObjectMapper());
+        //将自己的消息转换器加入容器中--排在容器中的最后,故使用插入使它排在第一个
+        converters.add(0,converter);
+    }
+}

+ 14 - 0
src/main/java/com/loan/system/constant/AutoFillConstant.java

@@ -0,0 +1,14 @@
+package com.loan.system.constant;
+
+/**
+ * 公共字段自动填充相关常量
+ */
+public class AutoFillConstant {
+    /**
+     * 实体类中的方法名称
+     */
+    public static final String SET_CREATE_TIME = "setCreateTime";
+    public static final String SET_UPDATE_TIME = "setUpdateTime";
+    public static final String SET_CREATE_USER = "setCreateUser";
+    public static final String SET_UPDATE_USER = "setUpdateUser";
+}

+ 11 - 0
src/main/java/com/loan/system/constant/JwtClaimsConstant.java

@@ -0,0 +1,11 @@
+package com.loan.system.constant;
+
+public class JwtClaimsConstant {
+
+    public static final String EMP_ID = "empId";
+    public static final String USER_ID = "userId";
+    public static final String PHONE = "phone";
+    public static final String USERNAME = "username";
+    public static final String NAME = "name";
+
+}

+ 28 - 0
src/main/java/com/loan/system/constant/MessageConstant.java

@@ -0,0 +1,28 @@
+package com.loan.system.constant;
+
+/**
+ * 信息提示常量类
+ */
+public class MessageConstant {
+
+    public static final String PASSWORD_ERROR = "密码错误";
+    public static final String ACCOUNT_NOT_FOUND = "账号不存在";
+    public static final String ACCOUNT_LOCKED = "账号被锁定";
+    public  static final  String ALREADY_EXISTS="已存在";
+    public static final String UNKNOWN_ERROR = "未知错误";
+    public static final String USER_NOT_LOGIN = "用户未登录";
+    public static final String CATEGORY_BE_RELATED_BY_SETMEAL = "当前分类关联了套餐,不能删除";
+    public static final String CATEGORY_BE_RELATED_BY_DISH = "当前分类关联了菜品,不能删除";
+    public static final String SHOPPING_CART_IS_NULL = "购物车数据为空,不能下单";
+    public static final String ADDRESS_BOOK_IS_NULL = "用户地址为空,不能下单";
+    public static final String LOGIN_FAILED = "登录失败";
+    public static final String UPLOAD_FAILED = "文件上传失败";
+    public static final String SETMEAL_ENABLE_FAILED = "套餐内包含未启售菜品,无法启售";
+    public static final String PASSWORD_EDIT_FAILED = "密码修改失败";
+    public static final String DISH_ON_SALE = "起售中的菜品不能删除";
+    public static final String SETMEAL_ON_SALE = "起售中的套餐不能删除";
+    public static final String DISH_BE_RELATED_BY_SETMEAL = "当前菜品关联了套餐,不能删除";
+    public static final String ORDER_STATUS_ERROR = "订单状态错误";
+    public static final String ORDER_NOT_FOUND = "订单不存在";
+
+}

+ 10 - 0
src/main/java/com/loan/system/constant/PasswordConstant.java

@@ -0,0 +1,10 @@
+package com.loan.system.constant;
+
+/**
+ * 密码常量
+ */
+public class PasswordConstant {
+
+    public static final String DEFAULT_PASSWORD = "123456";
+
+}

+ 13 - 0
src/main/java/com/loan/system/constant/StatusConstant.java

@@ -0,0 +1,13 @@
+package com.loan.system.constant;
+
+/**
+ * 状态常量,启用或者禁用
+ */
+public class StatusConstant {
+
+    //启用
+    public static final Integer ENABLE = 1;
+
+    //禁用
+    public static final Integer DISABLE = 0;
+}

+ 23 - 0
src/main/java/com/loan/system/context/BaseContext.java

@@ -0,0 +1,23 @@
+package com.loan.system.context;
+
+/*
+ * ThreadLocal:线程的id----每次运行都是一次新的线程,即客户端的每一次运行都是一个新的线程,每次运行中的
+ * Controller,Service,Interceptor都拥有同一个线程id,用于保存当前用户的id
+ */
+public class BaseContext {
+
+    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
+
+    public static void setCurrentId(Long id) {
+        threadLocal.set(id);
+    }
+
+    public static Long getCurrentId() {
+        return threadLocal.get();
+    }
+
+    public static void removeCurrentId() {
+        threadLocal.remove();
+    }
+
+}

+ 99 - 0
src/main/java/com/loan/system/controller/wechat/UploadController.java

@@ -0,0 +1,99 @@
+package com.loan.system.controller.wechat;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+import com.loan.system.domain.pojo.Result;
+import com.loan.system.service.Impl.MediaUploadService;
+import com.loan.system.utils.FileUploadUtil;
+import com.loan.system.utils.ResultUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+
+/**
+ * @author : EdwinXu
+ * @date : Created in 2020/12/16 15:34
+ */
+@RestController
+public class UploadController {
+
+//    @Autowired
+//    MediaUploadService uploadService;
+//
+//    @PostMapping("/file/verify")
+//    public Result verifyExist(@RequestParam("fileMd5") String fileMd5,
+//                              @RequestParam("fileName") String fileName,
+//                              @RequestParam("fileSize") Long fileSize,
+//                              @RequestParam("mimetype") String mimetype,
+//                              @RequestParam("fileExt") String fileExt) throws IOException {
+//        System.out.println("=========register==========> ");
+//        System.out.println(fileMd5);
+//        System.out.println(fileName);
+//        System.out.println(fileSize);
+//        System.out.println(mimetype);
+//        System.out.println(fileExt);
+//        boolean register = FileUploadUtil.register(fileMd5, "/project-1/contracts/contract-1", fileExt);
+//        if (register){
+//            return ResultUtil.success();
+//        } else {
+//            return ResultUtil.error(ExceptionEnum.FILE_NOT_EXIST);
+//        }
+////        return uploadService.register(fileMd5,fileName,fileSize,mimetype,fileExt);
+//    }
+//    //校验文件块
+//
+//    @PostMapping("/checkChunk")
+//    public Result checkChunk(@RequestParam("fileMd5") String fileMd5,
+//                             @RequestParam("chunk") Integer chunk,
+//                             @RequestParam("chunkSize") Integer chunkSize) {
+//        System.out.println("=========checkChunk==========> ");
+//        boolean aBoolean = FileUploadUtil.checkChunk("/project-1/contracts/contract-1", chunk);
+//        if (aBoolean){
+//            return ResultUtil.success();
+//        } else {
+//            return ResultUtil.error(ExceptionEnum.FILE_NOT_EXIST);
+//        }
+////        return uploadService.checkChunk(fileMd5,chunk,chunkSize);
+//    }
+//
+//    //上传文件块
+//
+//    @PostMapping("/uploadChunk")
+//    public Result uploadChunk(@RequestParam("file") MultipartFile file,
+//                              @RequestParam("fileMd5") String fileMd5,
+//                              @RequestParam("chunk") Integer chunk) {
+//        System.out.println("=========uploadChunk==========> ");
+//        System.out.println(file);
+//        System.out.println(fileMd5);
+//        System.out.println(chunk);
+//        boolean uploadChunk = FileUploadUtil.uploadChunk(file, "/project-1/contracts/contract-1", chunk);
+//        if (uploadChunk){
+//            return ResultUtil.success();
+//        } else {
+//            return ResultUtil.error(ExceptionEnum.FILE_NOT_EXIST);
+//        }
+//        return uploadService.uploadChunk(file,fileMd5,chunk);
+//    }
+
+    //合并文件块
+/*
+    @PostMapping("/mergeChunks")
+    public Result mergeChunks(@RequestParam("fileMd5") String fileMd5,
+                              @RequestParam("fileName") String fileName,
+                              @RequestParam("fileSize") Long fileSize,
+                              @RequestParam("mimetype") String mimetype,
+                              @RequestParam("fileExt") String fileExt) {
+        System.out.println("=========mergeChunks==========> ");
+        System.out.println(fileMd5);
+        System.out.println(fileName);
+        System.out.println(fileSize);
+        System.out.println(mimetype);
+        System.out.println(fileExt);
+//        return uploadService.mergeChunks(fileMd5,fileMd5,fileSize,mimetype,fileExt);
+//        FileUploadUtil.mergeChunks(fileMd5, fileName, "/project-1/contracts/contract-1", fileSize, mimetype, fileExt);
+
+    }*/
+}

+ 57 - 0
src/main/java/com/loan/system/controller/wechat/UserController.java

@@ -0,0 +1,57 @@
+package com.loan.system.controller.wechat;
+
+import com.loan.system.constant.JwtClaimsConstant;
+import com.loan.system.domain.dto.UserLoginDTO;
+import com.loan.system.domain.entity.User;
+import com.loan.system.domain.pojo.Result;
+import com.loan.system.domain.vo.UserLoginVO;
+import com.loan.system.properties.JwtProperties;
+import com.loan.system.service.UserService;
+import com.loan.system.utils.JwtUtil;
+import com.loan.system.utils.ResultUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Edwin
+ * @date 2020/9/2 - 19:11
+ */
+@RestController
+@RequestMapping("/wechat")
+@Api(tags = "微信用户接口")
+public class UserController {
+    @Autowired
+    private UserService userService;
+    @Autowired
+    private JwtProperties jwtProperties;
+
+    @PostMapping("/login")
+    @ApiOperation("微信登陆")
+    public Result login(@RequestBody UserLoginDTO userLoginDTO){
+        //微信登录
+        User user = userService.wxLogin(userLoginDTO);
+        System.out.println(userLoginDTO.getTel());
+
+        if (ObjectUtils.isEmpty(user))
+            throw new IllegalArgumentException("User object is null");
+
+        //为微信用户生成jwt令牌
+        Map<String ,Object> claims=new HashMap<>();
+        claims.put(JwtClaimsConstant.USER_ID,user.getId());
+        String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);
+
+        UserLoginVO userLoginVO = UserLoginVO.builder()
+                .id(user.getId())
+                .openid(user.getOpenid())
+                .token(token)
+                .build();
+
+        return ResultUtil.success("success", userLoginVO);
+    }
+}

+ 1412 - 0
src/main/java/com/loan/system/domain/ContractExample.java

@@ -0,0 +1,1412 @@
+package com.loan.system.domain;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+public class ContractExample {
+    protected String orderByClause;
+
+    protected boolean distinct;
+
+    protected List<Criteria> oredCriteria;
+
+    public ContractExample() {
+        oredCriteria = new ArrayList<Criteria>();
+    }
+
+    public void setOrderByClause(String orderByClause) {
+        this.orderByClause = orderByClause;
+    }
+
+    public String getOrderByClause() {
+        return orderByClause;
+    }
+
+    public void setDistinct(boolean distinct) {
+        this.distinct = distinct;
+    }
+
+    public boolean isDistinct() {
+        return distinct;
+    }
+
+    public List<Criteria> getOredCriteria() {
+        return oredCriteria;
+    }
+
+    public void or(Criteria criteria) {
+        oredCriteria.add(criteria);
+    }
+
+    public Criteria or() {
+        Criteria criteria = createCriteriaInternal();
+        oredCriteria.add(criteria);
+        return criteria;
+    }
+
+    public Criteria createCriteria() {
+        Criteria criteria = createCriteriaInternal();
+        if (oredCriteria.size() == 0) {
+            oredCriteria.add(criteria);
+        }
+        return criteria;
+    }
+
+    protected Criteria createCriteriaInternal() {
+        Criteria criteria = new Criteria();
+        return criteria;
+    }
+
+    public void clear() {
+        oredCriteria.clear();
+        orderByClause = null;
+        distinct = false;
+    }
+
+    protected abstract static class GeneratedCriteria {
+        protected List<Criterion> criteria;
+
+        protected GeneratedCriteria() {
+            super();
+            criteria = new ArrayList<Criterion>();
+        }
+
+        public boolean isValid() {
+            return criteria.size() > 0;
+        }
+
+        public List<Criterion> getAllCriteria() {
+            return criteria;
+        }
+
+        public List<Criterion> getCriteria() {
+            return criteria;
+        }
+
+        protected void addCriterion(String condition) {
+            if (condition == null) {
+                throw new RuntimeException("Value for condition cannot be null");
+            }
+            criteria.add(new Criterion(condition));
+        }
+
+        protected void addCriterion(String condition, Object value, String property) {
+            if (value == null) {
+                throw new RuntimeException("Value for " + property + " cannot be null");
+            }
+            criteria.add(new Criterion(condition, value));
+        }
+
+        protected void addCriterion(String condition, Object value1, Object value2, String property) {
+            if (value1 == null || value2 == null) {
+                throw new RuntimeException("Between values for " + property + " cannot be null");
+            }
+            criteria.add(new Criterion(condition, value1, value2));
+        }
+
+        public Criteria andIdIsNull() {
+            addCriterion("id is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdIsNotNull() {
+            addCriterion("id is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdEqualTo(Long value) {
+            addCriterion("id =", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotEqualTo(Long value) {
+            addCriterion("id <>", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdGreaterThan(Long value) {
+            addCriterion("id >", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdGreaterThanOrEqualTo(Long value) {
+            addCriterion("id >=", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdLessThan(Long value) {
+            addCriterion("id <", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdLessThanOrEqualTo(Long value) {
+            addCriterion("id <=", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdIn(List<Long> values) {
+            addCriterion("id in", values, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotIn(List<Long> values) {
+            addCriterion("id not in", values, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdBetween(Long value1, Long value2) {
+            addCriterion("id between", value1, value2, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotBetween(Long value1, Long value2) {
+            addCriterion("id not between", value1, value2, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeIsNull() {
+            addCriterion("begin_time is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeIsNotNull() {
+            addCriterion("begin_time is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeEqualTo(Date value) {
+            addCriterion("begin_time =", value, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeNotEqualTo(Date value) {
+            addCriterion("begin_time <>", value, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeGreaterThan(Date value) {
+            addCriterion("begin_time >", value, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeGreaterThanOrEqualTo(Date value) {
+            addCriterion("begin_time >=", value, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeLessThan(Date value) {
+            addCriterion("begin_time <", value, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeLessThanOrEqualTo(Date value) {
+            addCriterion("begin_time <=", value, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeIn(List<Date> values) {
+            addCriterion("begin_time in", values, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeNotIn(List<Date> values) {
+            addCriterion("begin_time not in", values, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeBetween(Date value1, Date value2) {
+            addCriterion("begin_time between", value1, value2, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andBeginTimeNotBetween(Date value1, Date value2) {
+            addCriterion("begin_time not between", value1, value2, "beginTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationIsNull() {
+            addCriterion("confirmation is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationIsNotNull() {
+            addCriterion("confirmation is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationEqualTo(String value) {
+            addCriterion("confirmation =", value, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationNotEqualTo(String value) {
+            addCriterion("confirmation <>", value, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationGreaterThan(String value) {
+            addCriterion("confirmation >", value, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationGreaterThanOrEqualTo(String value) {
+            addCriterion("confirmation >=", value, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationLessThan(String value) {
+            addCriterion("confirmation <", value, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationLessThanOrEqualTo(String value) {
+            addCriterion("confirmation <=", value, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationLike(String value) {
+            addCriterion("confirmation like", value, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationNotLike(String value) {
+            addCriterion("confirmation not like", value, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationIn(List<String> values) {
+            addCriterion("confirmation in", values, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationNotIn(List<String> values) {
+            addCriterion("confirmation not in", values, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationBetween(String value1, String value2) {
+            addCriterion("confirmation between", value1, value2, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andConfirmationNotBetween(String value1, String value2) {
+            addCriterion("confirmation not between", value1, value2, "confirmation");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledIsNull() {
+            addCriterion("enabled is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledIsNotNull() {
+            addCriterion("enabled is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledEqualTo(Boolean value) {
+            addCriterion("enabled =", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledNotEqualTo(Boolean value) {
+            addCriterion("enabled <>", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledGreaterThan(Boolean value) {
+            addCriterion("enabled >", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledGreaterThanOrEqualTo(Boolean value) {
+            addCriterion("enabled >=", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledLessThan(Boolean value) {
+            addCriterion("enabled <", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledLessThanOrEqualTo(Boolean value) {
+            addCriterion("enabled <=", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledIn(List<Boolean> values) {
+            addCriterion("enabled in", values, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledNotIn(List<Boolean> values) {
+            addCriterion("enabled not in", values, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledBetween(Boolean value1, Boolean value2) {
+            addCriterion("enabled between", value1, value2, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledNotBetween(Boolean value1, Boolean value2) {
+            addCriterion("enabled not between", value1, value2, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeIsNull() {
+            addCriterion("end_time is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeIsNotNull() {
+            addCriterion("end_time is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeEqualTo(Date value) {
+            addCriterion("end_time =", value, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeNotEqualTo(Date value) {
+            addCriterion("end_time <>", value, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeGreaterThan(Date value) {
+            addCriterion("end_time >", value, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeGreaterThanOrEqualTo(Date value) {
+            addCriterion("end_time >=", value, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeLessThan(Date value) {
+            addCriterion("end_time <", value, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeLessThanOrEqualTo(Date value) {
+            addCriterion("end_time <=", value, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeIn(List<Date> values) {
+            addCriterion("end_time in", values, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeNotIn(List<Date> values) {
+            addCriterion("end_time not in", values, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeBetween(Date value1, Date value2) {
+            addCriterion("end_time between", value1, value2, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andEndTimeNotBetween(Date value1, Date value2) {
+            addCriterion("end_time not between", value1, value2, "endTime");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyIsNull() {
+            addCriterion("money is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyIsNotNull() {
+            addCriterion("money is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyEqualTo(BigDecimal value) {
+            addCriterion("money =", value, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyNotEqualTo(BigDecimal value) {
+            addCriterion("money <>", value, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyGreaterThan(BigDecimal value) {
+            addCriterion("money >", value, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyGreaterThanOrEqualTo(BigDecimal value) {
+            addCriterion("money >=", value, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyLessThan(BigDecimal value) {
+            addCriterion("money <", value, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyLessThanOrEqualTo(BigDecimal value) {
+            addCriterion("money <=", value, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyIn(List<BigDecimal> values) {
+            addCriterion("money in", values, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyNotIn(List<BigDecimal> values) {
+            addCriterion("money not in", values, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyBetween(BigDecimal value1, BigDecimal value2) {
+            addCriterion("money between", value1, value2, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andMoneyNotBetween(BigDecimal value1, BigDecimal value2) {
+            addCriterion("money not between", value1, value2, "money");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameIsNull() {
+            addCriterion("name is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameIsNotNull() {
+            addCriterion("name is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameEqualTo(String value) {
+            addCriterion("name =", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameNotEqualTo(String value) {
+            addCriterion("name <>", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameGreaterThan(String value) {
+            addCriterion("name >", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameGreaterThanOrEqualTo(String value) {
+            addCriterion("name >=", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameLessThan(String value) {
+            addCriterion("name <", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameLessThanOrEqualTo(String value) {
+            addCriterion("name <=", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameLike(String value) {
+            addCriterion("name like", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameNotLike(String value) {
+            addCriterion("name not like", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameIn(List<String> values) {
+            addCriterion("name in", values, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameNotIn(List<String> values) {
+            addCriterion("name not in", values, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameBetween(String value1, String value2) {
+            addCriterion("name between", value1, value2, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameNotBetween(String value1, String value2) {
+            addCriterion("name not between", value1, value2, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateIsNull() {
+            addCriterion("notify_date is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateIsNotNull() {
+            addCriterion("notify_date is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateEqualTo(Date value) {
+            addCriterion("notify_date =", value, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateNotEqualTo(Date value) {
+            addCriterion("notify_date <>", value, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateGreaterThan(Date value) {
+            addCriterion("notify_date >", value, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateGreaterThanOrEqualTo(Date value) {
+            addCriterion("notify_date >=", value, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateLessThan(Date value) {
+            addCriterion("notify_date <", value, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateLessThanOrEqualTo(Date value) {
+            addCriterion("notify_date <=", value, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateIn(List<Date> values) {
+            addCriterion("notify_date in", values, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateNotIn(List<Date> values) {
+            addCriterion("notify_date not in", values, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateBetween(Date value1, Date value2) {
+            addCriterion("notify_date between", value1, value2, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNotifyDateNotBetween(Date value1, Date value2) {
+            addCriterion("notify_date not between", value1, value2, "notifyDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberIsNull() {
+            addCriterion("number is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberIsNotNull() {
+            addCriterion("number is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberEqualTo(String value) {
+            addCriterion("number =", value, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberNotEqualTo(String value) {
+            addCriterion("number <>", value, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberGreaterThan(String value) {
+            addCriterion("number >", value, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberGreaterThanOrEqualTo(String value) {
+            addCriterion("number >=", value, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberLessThan(String value) {
+            addCriterion("number <", value, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberLessThanOrEqualTo(String value) {
+            addCriterion("number <=", value, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberLike(String value) {
+            addCriterion("number like", value, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberNotLike(String value) {
+            addCriterion("number not like", value, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberIn(List<String> values) {
+            addCriterion("number in", values, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberNotIn(List<String> values) {
+            addCriterion("number not in", values, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberBetween(String value1, String value2) {
+            addCriterion("number between", value1, value2, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andNumberNotBetween(String value1, String value2) {
+            addCriterion("number not between", value1, value2, "number");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationIsNull() {
+            addCriterion("organization is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationIsNotNull() {
+            addCriterion("organization is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationEqualTo(String value) {
+            addCriterion("organization =", value, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationNotEqualTo(String value) {
+            addCriterion("organization <>", value, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationGreaterThan(String value) {
+            addCriterion("organization >", value, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationGreaterThanOrEqualTo(String value) {
+            addCriterion("organization >=", value, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationLessThan(String value) {
+            addCriterion("organization <", value, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationLessThanOrEqualTo(String value) {
+            addCriterion("organization <=", value, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationLike(String value) {
+            addCriterion("organization like", value, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationNotLike(String value) {
+            addCriterion("organization not like", value, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationIn(List<String> values) {
+            addCriterion("organization in", values, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationNotIn(List<String> values) {
+            addCriterion("organization not in", values, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationBetween(String value1, String value2) {
+            addCriterion("organization between", value1, value2, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andOrganizationNotBetween(String value1, String value2) {
+            addCriterion("organization not between", value1, value2, "organization");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidIsNull() {
+            addCriterion("paid is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidIsNotNull() {
+            addCriterion("paid is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidEqualTo(BigDecimal value) {
+            addCriterion("paid =", value, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidNotEqualTo(BigDecimal value) {
+            addCriterion("paid <>", value, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidGreaterThan(BigDecimal value) {
+            addCriterion("paid >", value, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidGreaterThanOrEqualTo(BigDecimal value) {
+            addCriterion("paid >=", value, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidLessThan(BigDecimal value) {
+            addCriterion("paid <", value, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidLessThanOrEqualTo(BigDecimal value) {
+            addCriterion("paid <=", value, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidIn(List<BigDecimal> values) {
+            addCriterion("paid in", values, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidNotIn(List<BigDecimal> values) {
+            addCriterion("paid not in", values, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidBetween(BigDecimal value1, BigDecimal value2) {
+            addCriterion("paid between", value1, value2, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPaidNotBetween(BigDecimal value1, BigDecimal value2) {
+            addCriterion("paid not between", value1, value2, "paid");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBIsNull() {
+            addCriterion("party_b is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBIsNotNull() {
+            addCriterion("party_b is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBEqualTo(String value) {
+            addCriterion("party_b =", value, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBNotEqualTo(String value) {
+            addCriterion("party_b <>", value, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBGreaterThan(String value) {
+            addCriterion("party_b >", value, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBGreaterThanOrEqualTo(String value) {
+            addCriterion("party_b >=", value, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBLessThan(String value) {
+            addCriterion("party_b <", value, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBLessThanOrEqualTo(String value) {
+            addCriterion("party_b <=", value, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBLike(String value) {
+            addCriterion("party_b like", value, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBNotLike(String value) {
+            addCriterion("party_b not like", value, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBIn(List<String> values) {
+            addCriterion("party_b in", values, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBNotIn(List<String> values) {
+            addCriterion("party_b not in", values, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBBetween(String value1, String value2) {
+            addCriterion("party_b between", value1, value2, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andPartyBNotBetween(String value1, String value2) {
+            addCriterion("party_b not between", value1, value2, "partyB");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdIsNull() {
+            addCriterion("project_id is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdIsNotNull() {
+            addCriterion("project_id is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdEqualTo(Long value) {
+            addCriterion("project_id =", value, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdNotEqualTo(Long value) {
+            addCriterion("project_id <>", value, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdGreaterThan(Long value) {
+            addCriterion("project_id >", value, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdGreaterThanOrEqualTo(Long value) {
+            addCriterion("project_id >=", value, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdLessThan(Long value) {
+            addCriterion("project_id <", value, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdLessThanOrEqualTo(Long value) {
+            addCriterion("project_id <=", value, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdIn(List<Long> values) {
+            addCriterion("project_id in", values, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdNotIn(List<Long> values) {
+            addCriterion("project_id not in", values, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdBetween(Long value1, Long value2) {
+            addCriterion("project_id between", value1, value2, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andProjectIdNotBetween(Long value1, Long value2) {
+            addCriterion("project_id not between", value1, value2, "projectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateIsNull() {
+            addCriterion("sign_date is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateIsNotNull() {
+            addCriterion("sign_date is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateEqualTo(Date value) {
+            addCriterion("sign_date =", value, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateNotEqualTo(Date value) {
+            addCriterion("sign_date <>", value, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateGreaterThan(Date value) {
+            addCriterion("sign_date >", value, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateGreaterThanOrEqualTo(Date value) {
+            addCriterion("sign_date >=", value, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateLessThan(Date value) {
+            addCriterion("sign_date <", value, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateLessThanOrEqualTo(Date value) {
+            addCriterion("sign_date <=", value, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateIn(List<Date> values) {
+            addCriterion("sign_date in", values, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateNotIn(List<Date> values) {
+            addCriterion("sign_date not in", values, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateBetween(Date value1, Date value2) {
+            addCriterion("sign_date between", value1, value2, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andSignDateNotBetween(Date value1, Date value2) {
+            addCriterion("sign_date not between", value1, value2, "signDate");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusIsNull() {
+            addCriterion("status is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusIsNotNull() {
+            addCriterion("status is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusEqualTo(String value) {
+            addCriterion("status =", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusNotEqualTo(String value) {
+            addCriterion("status <>", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusGreaterThan(String value) {
+            addCriterion("status >", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusGreaterThanOrEqualTo(String value) {
+            addCriterion("status >=", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusLessThan(String value) {
+            addCriterion("status <", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusLessThanOrEqualTo(String value) {
+            addCriterion("status <=", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusLike(String value) {
+            addCriterion("status like", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusNotLike(String value) {
+            addCriterion("status not like", value, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusIn(List<String> values) {
+            addCriterion("status in", values, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusNotIn(List<String> values) {
+            addCriterion("status not in", values, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusBetween(String value1, String value2) {
+            addCriterion("status between", value1, value2, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andStatusNotBetween(String value1, String value2) {
+            addCriterion("status not between", value1, value2, "status");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidIsNull() {
+            addCriterion("unpaid is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidIsNotNull() {
+            addCriterion("unpaid is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidEqualTo(BigDecimal value) {
+            addCriterion("unpaid =", value, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidNotEqualTo(BigDecimal value) {
+            addCriterion("unpaid <>", value, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidGreaterThan(BigDecimal value) {
+            addCriterion("unpaid >", value, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidGreaterThanOrEqualTo(BigDecimal value) {
+            addCriterion("unpaid >=", value, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidLessThan(BigDecimal value) {
+            addCriterion("unpaid <", value, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidLessThanOrEqualTo(BigDecimal value) {
+            addCriterion("unpaid <=", value, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidIn(List<BigDecimal> values) {
+            addCriterion("unpaid in", values, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidNotIn(List<BigDecimal> values) {
+            addCriterion("unpaid not in", values, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidBetween(BigDecimal value1, BigDecimal value2) {
+            addCriterion("unpaid between", value1, value2, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andUnpaidNotBetween(BigDecimal value1, BigDecimal value2) {
+            addCriterion("unpaid not between", value1, value2, "unpaid");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdIsNull() {
+            addCriterion("contract_select_id is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdIsNotNull() {
+            addCriterion("contract_select_id is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdEqualTo(Long value) {
+            addCriterion("contract_select_id =", value, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdNotEqualTo(Long value) {
+            addCriterion("contract_select_id <>", value, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdGreaterThan(Long value) {
+            addCriterion("contract_select_id >", value, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdGreaterThanOrEqualTo(Long value) {
+            addCriterion("contract_select_id >=", value, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdLessThan(Long value) {
+            addCriterion("contract_select_id <", value, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdLessThanOrEqualTo(Long value) {
+            addCriterion("contract_select_id <=", value, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdIn(List<Long> values) {
+            addCriterion("contract_select_id in", values, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdNotIn(List<Long> values) {
+            addCriterion("contract_select_id not in", values, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdBetween(Long value1, Long value2) {
+            addCriterion("contract_select_id between", value1, value2, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andContractSelectIdNotBetween(Long value1, Long value2) {
+            addCriterion("contract_select_id not between", value1, value2, "contractSelectId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdIsNull() {
+            addCriterion("step_id is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdIsNotNull() {
+            addCriterion("step_id is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdEqualTo(Long value) {
+            addCriterion("step_id =", value, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdNotEqualTo(Long value) {
+            addCriterion("step_id <>", value, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdGreaterThan(Long value) {
+            addCriterion("step_id >", value, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdGreaterThanOrEqualTo(Long value) {
+            addCriterion("step_id >=", value, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdLessThan(Long value) {
+            addCriterion("step_id <", value, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdLessThanOrEqualTo(Long value) {
+            addCriterion("step_id <=", value, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdIn(List<Long> values) {
+            addCriterion("step_id in", values, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdNotIn(List<Long> values) {
+            addCriterion("step_id not in", values, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdBetween(Long value1, Long value2) {
+            addCriterion("step_id between", value1, value2, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andStepIdNotBetween(Long value1, Long value2) {
+            addCriterion("step_id not between", value1, value2, "stepId");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsIsNull() {
+            addCriterion("nonfunds is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsIsNotNull() {
+            addCriterion("nonfunds is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsEqualTo(String value) {
+            addCriterion("nonfunds =", value, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsNotEqualTo(String value) {
+            addCriterion("nonfunds <>", value, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsGreaterThan(String value) {
+            addCriterion("nonfunds >", value, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsGreaterThanOrEqualTo(String value) {
+            addCriterion("nonfunds >=", value, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsLessThan(String value) {
+            addCriterion("nonfunds <", value, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsLessThanOrEqualTo(String value) {
+            addCriterion("nonfunds <=", value, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsLike(String value) {
+            addCriterion("nonfunds like", value, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsNotLike(String value) {
+            addCriterion("nonfunds not like", value, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsIn(List<String> values) {
+            addCriterion("nonfunds in", values, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsNotIn(List<String> values) {
+            addCriterion("nonfunds not in", values, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsBetween(String value1, String value2) {
+            addCriterion("nonfunds between", value1, value2, "nonfunds");
+            return (Criteria) this;
+        }
+
+        public Criteria andNonfundsNotBetween(String value1, String value2) {
+            addCriterion("nonfunds not between", value1, value2, "nonfunds");
+            return (Criteria) this;
+        }
+    }
+
+    public static class Criteria extends GeneratedCriteria {
+
+        protected Criteria() {
+            super();
+        }
+    }
+
+    public static class Criterion {
+        private String condition;
+
+        private Object value;
+
+        private Object secondValue;
+
+        private boolean noValue;
+
+        private boolean singleValue;
+
+        private boolean betweenValue;
+
+        private boolean listValue;
+
+        private String typeHandler;
+
+        public String getCondition() {
+            return condition;
+        }
+
+        public Object getValue() {
+            return value;
+        }
+
+        public Object getSecondValue() {
+            return secondValue;
+        }
+
+        public boolean isNoValue() {
+            return noValue;
+        }
+
+        public boolean isSingleValue() {
+            return singleValue;
+        }
+
+        public boolean isBetweenValue() {
+            return betweenValue;
+        }
+
+        public boolean isListValue() {
+            return listValue;
+        }
+
+        public String getTypeHandler() {
+            return typeHandler;
+        }
+
+        protected Criterion(String condition) {
+            super();
+            this.condition = condition;
+            this.typeHandler = null;
+            this.noValue = true;
+        }
+
+        protected Criterion(String condition, Object value, String typeHandler) {
+            super();
+            this.condition = condition;
+            this.value = value;
+            this.typeHandler = typeHandler;
+            if (value instanceof List<?>) {
+                this.listValue = true;
+            } else {
+                this.singleValue = true;
+            }
+        }
+
+        protected Criterion(String condition, Object value) {
+            this(condition, value, null);
+        }
+
+        protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
+            super();
+            this.condition = condition;
+            this.value = value;
+            this.secondValue = secondValue;
+            this.typeHandler = typeHandler;
+            this.betweenValue = true;
+        }
+
+        protected Criterion(String condition, Object value, Object secondValue) {
+            this(condition, value, secondValue, null);
+        }
+    }
+}

+ 50 - 0
src/main/java/com/loan/system/domain/ContractType.java

@@ -0,0 +1,50 @@
+package com.loan.system.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ContractType {
+    private Long id;
+
+    private Boolean enabled;
+
+    private String name;
+
+    private String type;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Boolean getEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name == null ? null : name.trim();
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type == null ? null : type.trim();
+    }
+}

+ 460 - 0
src/main/java/com/loan/system/domain/ContractTypeExample.java

@@ -0,0 +1,460 @@
+package com.loan.system.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ContractTypeExample {
+    protected String orderByClause;
+
+    protected boolean distinct;
+
+    protected List<Criteria> oredCriteria;
+
+    public ContractTypeExample() {
+        oredCriteria = new ArrayList<Criteria>();
+    }
+
+    public void setOrderByClause(String orderByClause) {
+        this.orderByClause = orderByClause;
+    }
+
+    public String getOrderByClause() {
+        return orderByClause;
+    }
+
+    public void setDistinct(boolean distinct) {
+        this.distinct = distinct;
+    }
+
+    public boolean isDistinct() {
+        return distinct;
+    }
+
+    public List<Criteria> getOredCriteria() {
+        return oredCriteria;
+    }
+
+    public void or(Criteria criteria) {
+        oredCriteria.add(criteria);
+    }
+
+    public Criteria or() {
+        Criteria criteria = createCriteriaInternal();
+        oredCriteria.add(criteria);
+        return criteria;
+    }
+
+    public Criteria createCriteria() {
+        Criteria criteria = createCriteriaInternal();
+        if (oredCriteria.size() == 0) {
+            oredCriteria.add(criteria);
+        }
+        return criteria;
+    }
+
+    protected Criteria createCriteriaInternal() {
+        Criteria criteria = new Criteria();
+        return criteria;
+    }
+
+    public void clear() {
+        oredCriteria.clear();
+        orderByClause = null;
+        distinct = false;
+    }
+
+    protected abstract static class GeneratedCriteria {
+        protected List<Criterion> criteria;
+
+        protected GeneratedCriteria() {
+            super();
+            criteria = new ArrayList<Criterion>();
+        }
+
+        public boolean isValid() {
+            return criteria.size() > 0;
+        }
+
+        public List<Criterion> getAllCriteria() {
+            return criteria;
+        }
+
+        public List<Criterion> getCriteria() {
+            return criteria;
+        }
+
+        protected void addCriterion(String condition) {
+            if (condition == null) {
+                throw new RuntimeException("Value for condition cannot be null");
+            }
+            criteria.add(new Criterion(condition));
+        }
+
+        protected void addCriterion(String condition, Object value, String property) {
+            if (value == null) {
+                throw new RuntimeException("Value for " + property + " cannot be null");
+            }
+            criteria.add(new Criterion(condition, value));
+        }
+
+        protected void addCriterion(String condition, Object value1, Object value2, String property) {
+            if (value1 == null || value2 == null) {
+                throw new RuntimeException("Between values for " + property + " cannot be null");
+            }
+            criteria.add(new Criterion(condition, value1, value2));
+        }
+
+        public Criteria andIdIsNull() {
+            addCriterion("id is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdIsNotNull() {
+            addCriterion("id is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdEqualTo(Long value) {
+            addCriterion("id =", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotEqualTo(Long value) {
+            addCriterion("id <>", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdGreaterThan(Long value) {
+            addCriterion("id >", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdGreaterThanOrEqualTo(Long value) {
+            addCriterion("id >=", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdLessThan(Long value) {
+            addCriterion("id <", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdLessThanOrEqualTo(Long value) {
+            addCriterion("id <=", value, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdIn(List<Long> values) {
+            addCriterion("id in", values, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotIn(List<Long> values) {
+            addCriterion("id not in", values, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdBetween(Long value1, Long value2) {
+            addCriterion("id between", value1, value2, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andIdNotBetween(Long value1, Long value2) {
+            addCriterion("id not between", value1, value2, "id");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledIsNull() {
+            addCriterion("enabled is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledIsNotNull() {
+            addCriterion("enabled is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledEqualTo(Boolean value) {
+            addCriterion("enabled =", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledNotEqualTo(Boolean value) {
+            addCriterion("enabled <>", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledGreaterThan(Boolean value) {
+            addCriterion("enabled >", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledGreaterThanOrEqualTo(Boolean value) {
+            addCriterion("enabled >=", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledLessThan(Boolean value) {
+            addCriterion("enabled <", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledLessThanOrEqualTo(Boolean value) {
+            addCriterion("enabled <=", value, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledIn(List<Boolean> values) {
+            addCriterion("enabled in", values, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledNotIn(List<Boolean> values) {
+            addCriterion("enabled not in", values, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledBetween(Boolean value1, Boolean value2) {
+            addCriterion("enabled between", value1, value2, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andEnabledNotBetween(Boolean value1, Boolean value2) {
+            addCriterion("enabled not between", value1, value2, "enabled");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameIsNull() {
+            addCriterion("name is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameIsNotNull() {
+            addCriterion("name is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameEqualTo(String value) {
+            addCriterion("name =", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameNotEqualTo(String value) {
+            addCriterion("name <>", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameGreaterThan(String value) {
+            addCriterion("name >", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameGreaterThanOrEqualTo(String value) {
+            addCriterion("name >=", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameLessThan(String value) {
+            addCriterion("name <", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameLessThanOrEqualTo(String value) {
+            addCriterion("name <=", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameLike(String value) {
+            addCriterion("name like", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameNotLike(String value) {
+            addCriterion("name not like", value, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameIn(List<String> values) {
+            addCriterion("name in", values, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameNotIn(List<String> values) {
+            addCriterion("name not in", values, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameBetween(String value1, String value2) {
+            addCriterion("name between", value1, value2, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andNameNotBetween(String value1, String value2) {
+            addCriterion("name not between", value1, value2, "name");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeIsNull() {
+            addCriterion("type is null");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeIsNotNull() {
+            addCriterion("type is not null");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeEqualTo(String value) {
+            addCriterion("type =", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeNotEqualTo(String value) {
+            addCriterion("type <>", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeGreaterThan(String value) {
+            addCriterion("type >", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeGreaterThanOrEqualTo(String value) {
+            addCriterion("type >=", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeLessThan(String value) {
+            addCriterion("type <", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeLessThanOrEqualTo(String value) {
+            addCriterion("type <=", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeLike(String value) {
+            addCriterion("type like", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeNotLike(String value) {
+            addCriterion("type not like", value, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeIn(List<String> values) {
+            addCriterion("type in", values, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeNotIn(List<String> values) {
+            addCriterion("type not in", values, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeBetween(String value1, String value2) {
+            addCriterion("type between", value1, value2, "type");
+            return (Criteria) this;
+        }
+
+        public Criteria andTypeNotBetween(String value1, String value2) {
+            addCriterion("type not between", value1, value2, "type");
+            return (Criteria) this;
+        }
+    }
+
+    public static class Criteria extends GeneratedCriteria {
+
+        protected Criteria() {
+            super();
+        }
+    }
+
+    public static class Criterion {
+        private String condition;
+
+        private Object value;
+
+        private Object secondValue;
+
+        private boolean noValue;
+
+        private boolean singleValue;
+
+        private boolean betweenValue;
+
+        private boolean listValue;
+
+        private String typeHandler;
+
+        public String getCondition() {
+            return condition;
+        }
+
+        public Object getValue() {
+            return value;
+        }
+
+        public Object getSecondValue() {
+            return secondValue;
+        }
+
+        public boolean isNoValue() {
+            return noValue;
+        }
+
+        public boolean isSingleValue() {
+            return singleValue;
+        }
+
+        public boolean isBetweenValue() {
+            return betweenValue;
+        }
+
+        public boolean isListValue() {
+            return listValue;
+        }
+
+        public String getTypeHandler() {
+            return typeHandler;
+        }
+
+        protected Criterion(String condition) {
+            super();
+            this.condition = condition;
+            this.typeHandler = null;
+            this.noValue = true;
+        }
+
+        protected Criterion(String condition, Object value, String typeHandler) {
+            super();
+            this.condition = condition;
+            this.value = value;
+            this.typeHandler = typeHandler;
+            if (value instanceof List<?>) {
+                this.listValue = true;
+            } else {
+                this.singleValue = true;
+            }
+        }
+
+        protected Criterion(String condition, Object value) {
+            this(condition, value, null);
+        }
+
+        protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
+            super();
+            this.condition = condition;
+            this.value = value;
+            this.secondValue = secondValue;
+            this.typeHandler = typeHandler;
+            this.betweenValue = true;
+        }
+
+        protected Criterion(String condition, Object value, Object secondValue) {
+            this(condition, value, secondValue, null);
+        }
+    }
+}

+ 202 - 0
src/main/java/com/loan/system/domain/Contracts.java

@@ -0,0 +1,202 @@
+package com.loan.system.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.Date;
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Contracts {
+    private Long id;
+
+    private Date beginTime;
+
+    private String confirmation;
+
+    private Boolean enabled;
+
+    private Date endTime;
+
+    private BigDecimal money;
+
+    private String name;
+
+    private Date notifyDate;
+
+    private String number;
+
+    private String organization;
+
+    private BigDecimal paid;
+
+    private String partyB;
+
+    private Long projectId;
+
+    private Date signDate;
+
+    private String status;
+
+    private BigDecimal unpaid;
+
+    private Long contractSelectId;
+
+    private Long stepId;
+
+    private String nonfunds;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Date getBeginTime() {
+        return beginTime;
+    }
+
+    public void setBeginTime(Date beginTime) {
+        this.beginTime = beginTime;
+    }
+
+    public String getConfirmation() {
+        return confirmation;
+    }
+
+    public void setConfirmation(String confirmation) {
+        this.confirmation = confirmation == null ? null : confirmation.trim();
+    }
+
+    public Boolean getEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public Date getEndTime() {
+        return endTime;
+    }
+
+    public void setEndTime(Date endTime) {
+        this.endTime = endTime;
+    }
+
+    public BigDecimal getMoney() {
+        return money;
+    }
+
+    public void setMoney(BigDecimal money) {
+        this.money = money;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name == null ? null : name.trim();
+    }
+
+    public Date getNotifyDate() {
+        return notifyDate;
+    }
+
+    public void setNotifyDate(Date notifyDate) {
+        this.notifyDate = notifyDate;
+    }
+
+    public String getNumber() {
+        return number;
+    }
+
+    public void setNumber(String number) {
+        this.number = number == null ? null : number.trim();
+    }
+
+    public String getOrganization() {
+        return organization;
+    }
+
+    public void setOrganization(String organization) {
+        this.organization = organization == null ? null : organization.trim();
+    }
+
+    public BigDecimal getPaid() {
+        return paid;
+    }
+
+    public void setPaid(BigDecimal paid) {
+        this.paid = paid;
+    }
+
+    public String getPartyB() {
+        return partyB;
+    }
+
+    public void setPartyB(String partyB) {
+        this.partyB = partyB == null ? null : partyB.trim();
+    }
+
+    public Long getProjectId() {
+        return projectId;
+    }
+
+    public void setProjectId(Long projectId) {
+        this.projectId = projectId;
+    }
+
+    public Date getSignDate() {
+        return signDate;
+    }
+
+    public void setSignDate(Date signDate) {
+        this.signDate = signDate;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status == null ? null : status.trim();
+    }
+
+    public BigDecimal getUnpaid() {
+        return unpaid;
+    }
+
+    public void setUnpaid(BigDecimal unpaid) {
+        this.unpaid = unpaid;
+    }
+
+    public Long getContractSelectId() {
+        return contractSelectId;
+    }
+
+    public void setContractSelectId(Long contractSelectId) {
+        this.contractSelectId = contractSelectId;
+    }
+
+    public Long getStepId() {
+        return stepId;
+    }
+
+    public void setStepId(Long stepId) {
+        this.stepId = stepId;
+    }
+
+    public String getNonfunds() {
+        return nonfunds;
+    }
+
+    public void setNonfunds(String nonfunds) {
+        this.nonfunds = nonfunds == null ? null : nonfunds.trim();
+    }
+}

+ 10 - 0
src/main/java/com/loan/system/domain/dto/UserLoginDTO.java

@@ -0,0 +1,10 @@
+package com.loan.system.domain.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class UserLoginDTO implements Serializable {
+    String tel;
+}

+ 44 - 0
src/main/java/com/loan/system/domain/entity/ApprovalRecord.java

@@ -0,0 +1,44 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+
+@Entity
+@Table(name = "approval_record", indexes = {
+        @Index(name = "idx_case_id", columnList = "case_id")
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ApprovalRecord extends BaseEntity{
+    private static final long serialVersionUID = 1L;
+
+    @Column(name = "case_id")
+    private Long caseId;
+
+    @Column(name = "step_name", length = 100)
+    private String stepName;
+
+    @Column(name = "approver_id")
+    private Long approverId;
+
+    @Column(name = "decision", length = 20)
+    private String decision;
+
+    @Lob
+    @Column(name = "comments")
+    private String comments;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 39 - 0
src/main/java/com/loan/system/domain/entity/BaseEntity.java

@@ -0,0 +1,39 @@
+package com.loan.system.domain.entity;
+
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+
+import javax.persistence.*;
+import java.io.Serializable;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/14 - 10:28
+ */
+@MappedSuperclass
+@JsonIgnoreProperties(value = {"handler","hibernateLazyInitializer","fieldHandler"})
+@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
+public class BaseEntity implements Serializable {
+    private static final long serialVersionUID = 7410732905451820274L;
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    @Column(name = "id")
+    private Long id;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    @Override
+    public String toString() {
+        return "BaseEntity{" +
+                "id=" + id +
+                '}';
+    }
+}

+ 65 - 0
src/main/java/com/loan/system/domain/entity/BizRecommender.java

@@ -0,0 +1,65 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Entity
+@Table(name = "biz_recommender", uniqueConstraints = {
+        @UniqueConstraint(name = "recommender_code", columnNames = {"recommender_code"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class BizRecommender extends BaseEntity{
+    private static final long serialVersionUID = 2L;
+
+    @Column(name = "recommender_code", length = 50)
+    private String recommenderCode;
+
+    @Column(name = "recommender_name", length = 100)
+    private String recommenderName;
+
+    @Column(name = "recommender_type", length = 50)
+    private String recommenderType;
+
+    @Column(name = "phone", length = 20)
+    private String phone;
+
+    @Column(name = "channel", length = 50)
+    private String channel;
+
+    @Column(name = "id_card", length = 20)
+    private String idCard;
+
+    @Column(name = "bank_account", length = 50)
+    private String bankAccount;
+
+    @Column(name = "commission_rate", precision = 5, scale = 4)
+    private BigDecimal commissionRate;
+
+    @Column(name = "company", length = 50)
+    private String company;
+
+    @Lob
+    @Column(name = "remark")
+    private String remark;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "create_user_id")
+    private Long createUserId;
+
+    @Column(name = "update_user_id")
+    private Long updateUserId;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 52 - 0
src/main/java/com/loan/system/domain/entity/Collateral.java

@@ -0,0 +1,52 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Entity
+@Table(name = "collateral")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Collateral extends BaseEntity{
+    private static final long serialVersionUID = 3L;
+    @Column(name = "case_id")
+    private Long caseId;
+
+    @Column(name = "collateral_type", length = 30)
+    private String collateralType;
+
+    @Column(name = "owner_customer_id")
+    private Long ownerCustomerId;
+
+    @Column(name = "allocated_amount", precision = 18, scale = 2)
+    private BigDecimal allocatedAmount;
+
+    @Column(name = "address", length = 500)
+    private String address;
+
+    @Column(name = "eval_price", precision = 18, scale = 2)
+    private BigDecimal evalPrice;
+
+    @Column(name = "is_involved_in_litigation")
+    private Boolean isInvolvedInLitigation;
+
+    @Column(name = "staus", length = 20)
+    private String staus;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 67 - 0
src/main/java/com/loan/system/domain/entity/Contract.java

@@ -0,0 +1,67 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Entity
+@Table(name = "contract", indexes = {
+        @Index(name = "idx_case_id", columnList = "case_id")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "contract_no", columnNames = {"contract_no"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Contract extends BaseEntity{
+    private static final long serialVersionUID = 4L;
+
+    @Column(name = "business_attr", length = 200)
+    private String businessAttr;
+
+    @Column(name = "case_id")
+    private Long caseId;
+
+    @Column(name = "contract_no", length = 50)
+    private String contractNo;
+
+    @Column(name = "contract_name", length = 50)
+    private String contractName;
+
+    @Column(name = "contract_version")
+    private Integer contractVersion;
+
+    @Column(name = "contract_amount", precision = 15, scale = 2)
+    private BigDecimal contractAmount;
+
+    @Column(name = "interest_rate", precision = 5, scale = 4)
+    private BigDecimal interestRate;
+
+    @Column(name = "loan_period")
+    private Integer loanPeriod;
+
+    @Lob
+    @Column(name = "content")
+    private String content;
+
+    @Column(name = "signed_by_customer")
+    private Boolean signedByCustomer;
+
+    @Column(name = "sifned_id")
+    private Long sifnedId;
+
+    @Column(name = "signed_time")
+    private Instant signedTime;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 52 - 0
src/main/java/com/loan/system/domain/entity/Customer.java

@@ -0,0 +1,52 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "customers", indexes = {
+        @Index(name = "idx_id_number", columnList = "id_number"),
+        @Index(name = "idx_mobile", columnList = "mobile")
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Customer extends BaseEntity{
+    private static final long serialVersionUID = 5L;
+
+    @Column(name = "openid", length = 128)
+    private String openid;
+
+    @Column(name = "name", length = 100)
+    private String name;
+
+    @Column(name = "sex", length = 10)
+    private String sex;
+
+    @Column(name = "id_number", length = 18)
+    private String idNumber;
+
+    @Column(name = "mobile", length = 11)
+    private String mobile;
+
+    @Column(name = "register_source", length = 50)
+    private String registerSource;
+
+    @Column(name = "bank_account", length = 64)
+    private String bankAccount;
+
+    @Column(name = "face_auth")
+    private Boolean faceAuth;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 37 - 0
src/main/java/com/loan/system/domain/entity/CustomersOther.java

@@ -0,0 +1,37 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.time.Instant;
+
+@Entity
+@Table(name = "customers_other")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class CustomersOther extends BaseEntity{
+    private static final long serialVersionUID = 6L;
+
+    @Column(name = "name", length = 100)
+    private String name;
+
+    @Column(name = "id_number", length = 18)
+    private String idNumber;
+
+    @Column(name = "mobile", length = 11)
+    private String mobile;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 38 - 0
src/main/java/com/loan/system/domain/entity/DictAttribute.java

@@ -0,0 +1,38 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "dict_attribute", indexes = {
+        @Index(name = "idx_enabled", columnList = "enabled")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "code", columnNames = {"code"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DictAttribute extends BaseEntity{
+    private static final long serialVersionUID = 7L;
+
+    @Column(name = "code", length = 50)
+    private String code;
+
+    @Column(name = "name", length = 100)
+    private String name;
+
+    @Column(name = "enabled")
+    private Boolean enabled;
+
+    @Column(name = "sort_order")
+    private Integer sortOrder;
+
+    @Column(name = "created_at")
+    private Instant createdAt;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 38 - 0
src/main/java/com/loan/system/domain/entity/DictBusinessType.java

@@ -0,0 +1,38 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "dict_business_type", indexes = {
+        @Index(name = "idx_enabled", columnList = "enabled")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "code", columnNames = {"code"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DictBusinessType extends BaseEntity{
+    private static final long serialVersionUID = 8L;
+
+    @Column(name = "code", length = 50)
+    private String code;
+
+    @Column(name = "name", length = 100)
+    private String name;
+
+    @Column(name = "enabled")
+    private Boolean enabled;
+
+    @Column(name = "sort_order")
+    private Integer sortOrder;
+
+    @Column(name = "created_at")
+    private Instant createdAt;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 39 - 0
src/main/java/com/loan/system/domain/entity/DictChannel.java

@@ -0,0 +1,39 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+
+@Entity
+@Table(name = "dict_channel", indexes = {
+        @Index(name = "idx_enabled", columnList = "enabled")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "code", columnNames = {"code"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DictChannel extends BaseEntity{
+    private static final long serialVersionUID = 9L;
+
+    @Column(name = "code", length = 50)
+    private String code;
+
+    @Column(name = "name", length = 100)
+    private String name;
+
+    @Column(name = "enabled")
+    private Boolean enabled;
+
+    @Column(name = "sort_order")
+    private Integer sortOrder;
+
+    @Column(name = "created_at")
+    private Instant createdAt;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 38 - 0
src/main/java/com/loan/system/domain/entity/DictLocation.java

@@ -0,0 +1,38 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "dict_location", indexes = {
+        @Index(name = "idx_enabled", columnList = "enabled")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "code", columnNames = {"code"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DictLocation extends BaseEntity{
+    private static final long serialVersionUID = 10L;
+
+    @Column(name = "code", length = 50)
+    private String code;
+
+    @Column(name = "name", length = 100)
+    private String name;
+
+    @Column(name = "enabled")
+    private Boolean enabled;
+
+    @Column(name = "sort_order")
+    private Integer sortOrder;
+
+    @Column(name = "created_at")
+    private Instant createdAt;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 38 - 0
src/main/java/com/loan/system/domain/entity/DictMessage.java

@@ -0,0 +1,38 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "dict_message", indexes = {
+        @Index(name = "idx_enabled", columnList = "enabled")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "code", columnNames = {"code"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DictMessage extends BaseEntity{
+    private static final long serialVersionUID = 11L;
+
+    @Column(name = "code")
+    private Integer code;
+
+    @Column(name = "name", length = 100)
+    private String name;
+
+    @Column(name = "enabled")
+    private Boolean enabled;
+
+    @Column(name = "sort_order")
+    private Integer sortOrder;
+
+    @Column(name = "created_at")
+    private Instant createdAt;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 41 - 0
src/main/java/com/loan/system/domain/entity/DictStep.java

@@ -0,0 +1,41 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "dict_step", indexes = {
+        @Index(name = "idx_enabled", columnList = "enabled")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "code", columnNames = {"code"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DictStep extends BaseEntity{
+    private static final long serialVersionUID = 12L;
+
+    @Column(name = "code")
+    private Integer code;
+
+    @Column(name = "name", length = 50)
+    private String name;
+
+    @Column(name = "is_parent")
+    private Long isParent;
+
+    @Column(name = "enabled")
+    private Boolean enabled;
+
+    @Column(name = "sort_order")
+    private Integer sortOrder;
+
+    @Column(name = "created_at")
+    private Instant createdAt;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 38 - 0
src/main/java/com/loan/system/domain/entity/DictType.java

@@ -0,0 +1,38 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "dict_type", indexes = {
+        @Index(name = "idx_enabled", columnList = "enabled")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "code", columnNames = {"code"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DictType extends BaseEntity{
+    private static final long serialVersionUID = 13L;
+
+    @Column(name = "code")
+    private Integer code;
+
+    @Column(name = "name", length = 100)
+    private String name;
+
+    @Column(name = "enabled")
+    private Boolean enabled;
+
+    @Column(name = "sort_order")
+    private Integer sortOrder;
+
+    @Column(name = "created_at")
+    private Instant createdAt;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 64 - 0
src/main/java/com/loan/system/domain/entity/Disbursement.java

@@ -0,0 +1,64 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "disbursement", indexes = {
+        @Index(name = "idx_case_id", columnList = "case_id"),
+        @Index(name = "idx_disbursement_status", columnList = "disbursement_status")
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Disbursement extends BaseEntity{
+    private static final long serialVersionUID = 14L;
+
+    @Column(name = "case_id")
+    private Long caseId;
+
+    @Column(name = "payout_approve_user_id")
+    private Long payoutApproveUserId;
+
+    @Column(name = "payout_operator_user_id")
+    private Long payoutOperatorUserId;
+
+    @Column(name = "apply_by")
+    private Long applyBy;
+
+    @Column(name = "apply_at")
+    private Instant applyAt;
+
+    @Column(name = "disbursement_type", length = 10)
+    private String disbursementType;
+
+    @Column(name = "planned_amount", precision = 18, scale = 2)
+    private BigDecimal plannedAmount;
+
+    @Column(name = "planned_location")
+    private String plannedLocation;
+
+    @Column(name = "disbursement_status", length = 30)
+    private String disbursementStatus;
+
+    @Column(name = "contract_id")
+    private Long contractId;
+
+    @Column(name = "payout_time")
+    private Instant payoutTime;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 32 - 0
src/main/java/com/loan/system/domain/entity/DisbursementRecord.java

@@ -0,0 +1,32 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Entity
+@Table(name = "disbursement_record")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class DisbursementRecord extends BaseEntity{
+    private static final long serialVersionUID = 15L;
+
+    @Column(name = "disbursement_id")
+    private Long disbursementId;
+
+    @Column(name = "amount", precision = 18, scale = 2)
+    private BigDecimal amount;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 48 - 0
src/main/java/com/loan/system/domain/entity/Document.java

@@ -0,0 +1,48 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "documents", indexes = {
+        @Index(name = "idx_owner_id", columnList = "owner_id")
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Document extends BaseEntity{
+    private static final long serialVersionUID = 16L;
+
+    @Column(name = "case_id")
+    private Long caseId;
+
+    @Column(name = "owner_id")
+    private Long ownerId;
+
+    @Column(name = "type_id")
+    private Long typeId;
+
+    @Column(name = "doc_type", length = 50)
+    private String docType;
+
+    @Column(name = "file_path", length = 500)
+    private String filePath;
+
+    @Column(name = "file_name")
+    private String fileName;
+
+    @Column(name = "is_current")
+    private Boolean isCurrent;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 131 - 0
src/main/java/com/loan/system/domain/entity/ExceptionLog.java

@@ -0,0 +1,131 @@
+package com.loan.system.domain.entity;
+
+import org.hibernate.annotations.Proxy;
+
+import javax.persistence.*;
+import java.util.Date;
+
+@Entity
+@Table(name = "exception_log")
+@Proxy(lazy = false)
+public class ExceptionLog {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private int id;
+
+    @Lob
+    @Column(name = "exception_message")
+    private String exceptionMessage;
+    @Column(name = "exception_name")
+    private String exceptionName;
+    @Column(name = "exception_method")
+    private String exceptionMethod; //异常方法
+    @Column(name = "oper_user_name", length = 50)
+    private String operUserName; //用户名
+    @Column(name = "oper_ip", length = 50)
+    private String operIp; //ip地址
+    @Column(name = "oper_uri")
+    private String operUri; //请求地址
+    @Column(name = "oper_time")
+    @Temporal(TemporalType.TIMESTAMP)
+    private Date operTime; //操作时间
+    @Lob
+    @Column(name="method_param")
+    private String methodParam;
+
+    @Override
+    public String toString() {
+        return "ExceptionLog{" +
+                "id=" + id +
+                ", exceptionMessage='" + exceptionMessage + '\'' +
+                ", exceptionName='" + exceptionName + '\'' +
+                ", exceptionMethod='" + exceptionMethod + '\'' +
+                ", operUserName='" + operUserName + '\'' +
+                ", operIp='" + operIp + '\'' +
+                ", operUri='" + operUri + '\'' +
+                ", operTime=" + operTime +
+                ", methodParam='" + methodParam + '\'' +
+                '}';
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public ExceptionLog setId(int id) {
+        this.id = id;
+        return this;
+    }
+
+    public String getExceptionMessage() {
+        return exceptionMessage;
+    }
+
+    public ExceptionLog setExceptionMessage(String exceptionMessage) {
+        this.exceptionMessage = exceptionMessage;
+        return this;
+    }
+
+    public String getExceptionName() {
+        return exceptionName;
+    }
+
+    public ExceptionLog setExceptionName(String exceptionName) {
+        this.exceptionName = exceptionName;
+        return this;
+    }
+
+    public String getExceptionMethod() {
+        return exceptionMethod;
+    }
+
+    public ExceptionLog setExceptionMethod(String exceptionMethod) {
+        this.exceptionMethod = exceptionMethod;
+        return this;
+    }
+
+    public String getOperUserName() {
+        return operUserName;
+    }
+
+    public ExceptionLog setOperUserName(String operUserName) {
+        this.operUserName = operUserName;
+        return this;
+    }
+
+    public String getOperIp() {
+        return operIp;
+    }
+
+    public ExceptionLog setOperIp(String operIp) {
+        this.operIp = operIp;
+        return this;
+    }
+
+    public String getOperUri() {
+        return operUri;
+    }
+
+    public ExceptionLog setOperUri(String operUri) {
+        this.operUri = operUri;
+        return this;
+    }
+
+    public Date getOperTime() {
+        return operTime;
+    }
+
+    public ExceptionLog setOperTime(Date operTime) {
+        this.operTime = operTime;
+        return this;
+    }
+
+    public String getMethodParam() {
+        return methodParam;
+    }
+
+    public ExceptionLog setMethodParam(String methodParam) {
+        this.methodParam = methodParam;
+        return this;
+    }
+}

+ 58 - 0
src/main/java/com/loan/system/domain/entity/LoanCase.java

@@ -0,0 +1,58 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Entity
+@Table(name = "loan_case", uniqueConstraints = {
+        @UniqueConstraint(name = "case_no", columnNames = {"case_no"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class LoanCase extends BaseEntity{
+    private static final long serialVersionUID = 17L;
+
+    @Column(name = "case_no", length = 50)
+    private String caseNo;
+
+    @Column(name = "customer_id")
+    private Long customerId;
+
+    @Column(name = "business_type_id")
+    private Long businessTypeId;
+
+    @Column(name = "business_attrs", length = 200)
+    private String businessAttrs;
+
+    @Column(name = "channel_id")
+    private Long channelId;
+
+    @Column(name = "remark_id")
+    private Long remarkId;
+
+    @Column(name = "custom1_id")
+    private Long custom1Id;
+
+    @Column(name = "custom2_id")
+    private Long custom2Id;
+
+    @Column(name = "requested_amount", precision = 18, scale = 2)
+    private BigDecimal requestedAmount;
+
+    @Column(name = "total_loan_amount", precision = 18, scale = 2)
+    private BigDecimal totalLoanAmount;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 72 - 0
src/main/java/com/loan/system/domain/entity/Repayment.java

@@ -0,0 +1,72 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Entity
+@Table(name = "repayment", indexes = {
+        @Index(name = "idx_case_id", columnList = "case_id"),
+        @Index(name = "idx_repay_at", columnList = "repay_at"),
+        @Index(name = "idx_is_cleared", columnList = "is_cleared")
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Repayment extends BaseEntity{
+    private static final long serialVersionUID = 18L;
+
+    @Column(name = "case_id")
+    private Long caseId;
+
+    @Column(name = "contract_id")
+    private Long contractId;
+
+    @Column(name = "repay_by_customer_id")
+    private Long repayByCustomerId;
+
+    @Column(name = "repayment_plan_user_id")
+    private Long repaymentPlanUserId;
+
+    @Column(name = "repayment_operator_user_id")
+    private Long repaymentOperatorUserId;
+
+    @Column(name = "repayment_type", length = 10)
+    private String repaymentType;
+
+    @Column(name = "repay_amount", precision = 18, scale = 2)
+    private BigDecimal repayAmount;
+
+    @Column(name = "repay_at")
+    private Instant repayAt;
+
+    @Column(name = "repay_bank", length = 2000)
+    private String repayBank;
+
+    @Column(name = "repay_record_id")
+    private Long repayRecordId;
+
+    @Column(name = "confirmed_by")
+    private Long confirmedBy;
+
+    @Column(name = "confirmed_at")
+    private Instant confirmedAt;
+
+    @Column(name = "interest", precision = 18, scale = 2)
+    private BigDecimal interest;
+
+    @Column(name = "is_cleared")
+    private Boolean isCleared;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 31 - 0
src/main/java/com/loan/system/domain/entity/RepaymentRecord.java

@@ -0,0 +1,31 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Entity
+@Table(name = "repayment_record")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class RepaymentRecord extends BaseEntity{
+    private static final long serialVersionUID = 19L;
+    @Column(name = "repayment_id")
+    private Long repaymentId;
+
+    @Column(name = "amount", precision = 18, scale = 2)
+    private BigDecimal amount;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 28 - 0
src/main/java/com/loan/system/domain/entity/Role.java

@@ -0,0 +1,28 @@
+package com.loan.system.domain.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.persistence.*;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "roles", uniqueConstraints = {
+        @UniqueConstraint(name = "role_name", columnNames = {"role_name"})
+})
+public class Role {
+    @Id
+    @Column(name = "id", nullable = false)
+    private Long id;
+
+    @Column(name = "role_name", length = 64)
+    private String roleName;
+
+    @Column(name = "description")
+    private String description;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 45 - 0
src/main/java/com/loan/system/domain/entity/StatBusinessSnapshot.java

@@ -0,0 +1,45 @@
+package com.loan.system.domain.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.LocalDate;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "stat_business_snapshot")
+public class StatBusinessSnapshot extends BaseEntity{
+    private static final long serialVersionUID = 20L;
+
+    @Column(name = "stat_date")
+    private LocalDate statDate;
+
+    @Column(name = "channel", length = 50)
+    private String channel;
+
+    @Column(name = "biz_type", length = 50)
+    private String bizType;
+
+    @Column(name = "application_count")
+    private Integer applicationCount;
+
+    @Column(name = "total_loan_amount", precision = 15, scale = 2)
+    private BigDecimal totalLoanAmount;
+
+    @Column(name = "user_id")
+    private Long userId;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 38 - 0
src/main/java/com/loan/system/domain/entity/StatFundEfficiency.java

@@ -0,0 +1,38 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.math.BigDecimal;
+import java.time.Instant;
+
+@Entity
+@Table(name = "stat_fund_efficiency")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class StatFundEfficiency extends BaseEntity{
+    private static final long serialVersionUID = 21L;
+
+    @Column(name = "stat_month", length = 7)
+    private String statMonth;
+
+    @Column(name = "daily_loan_balance", precision = 15, scale = 2)
+    private BigDecimal dailyLoanBalance;
+
+    @Column(name = "actual_days")
+    private Integer actualDays;
+
+    @Column(name = "interest_income", precision = 15, scale = 2)
+    private BigDecimal interestIncome;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 55 - 0
src/main/java/com/loan/system/domain/entity/Step.java

@@ -0,0 +1,55 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.time.Instant;
+
+@Entity
+@Table(name = "steps")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Step extends BaseEntity{
+    private static final long serialVersionUID = 22L;
+
+    @Column(name = "step_name", length = 50)
+    private String stepName;
+
+    @Column(name = "case_id")
+    private Long caseId;
+
+    @Column(name = "status", length = 50)
+    private String status;
+
+    @Column(name = "user_id1")
+    private Long userId1;
+
+    @Column(name = "user_id2")
+    private Long userId2;
+
+    @Column(name = "begin_time")
+    private Instant beginTime;
+
+    @Column(name = "parent_id")
+    private Long parentId;
+
+    @Column(name = "pre_id")
+    private Long preId;
+
+    @Column(name = "next_id")
+    private Long nextId;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "update_time")
+    private Instant updateTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 44 - 0
src/main/java/com/loan/system/domain/entity/SysMessage.java

@@ -0,0 +1,44 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "sys_message")
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class SysMessage extends BaseEntity{
+    private static final long serialVersionUID = 23L;
+
+    @Column(name = "user_id")
+    private Long userId;
+
+    @Column(name = "message_title", length = 200)
+    private String messageTitle;
+
+    @Lob
+    @Column(name = "message_content")
+    private String messageContent;
+
+    @Column(name = "message_type", length = 50)
+    private String messageType;
+
+    @Column(name = "read_status")
+    private Byte readStatus;
+
+    @Column(name = "related_id")
+    private Long relatedId;
+
+    @Column(name = "related_type", length = 50)
+    private String relatedType;
+
+    @Column(name = "create_time")
+    private Instant createTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 59 - 0
src/main/java/com/loan/system/domain/entity/User.java

@@ -0,0 +1,59 @@
+package com.loan.system.domain.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.Instant;
+
+@Entity
+@Table(name = "users", indexes = {
+        @Index(name = "idx_mobile", columnList = "mobile")
+}, uniqueConstraints = {
+        @UniqueConstraint(name = "username", columnNames = {"username"})
+})
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class User extends BaseEntity{
+    private static final long serialVersionUID = 24L;
+
+    @Column(name = "openid", length = 128)
+    private String openid;
+
+    @Column(name = "username", length = 64)
+    private String username;
+
+    @Column(name = "password")
+    private String password;
+
+    @Column(name = "real_name", length = 100)
+    private String realName;
+
+    @Column(name = "sex", length = 10)
+    private String sex;
+
+    @Column(name = "mobile", length = 11)
+    private String mobile;
+
+    @Column(name = "role")
+    private String role;
+
+    @Column(name = "ext_role_type", length = 50)
+    private String extRoleType;
+
+    @Column(name = "dept", length = 100)
+    private String dept;
+
+    @Column(name = "status")
+    private Byte status;
+
+    @Column(name = "created_time")
+    private Instant createdTime;
+
+    @Column(name = "updated_time")
+    private Instant updatedTime;
+
+    @Column(name = "is_delete")
+    private Boolean isDelete;
+
+}

+ 53 - 0
src/main/java/com/loan/system/domain/enums/AuthEnum.java

@@ -0,0 +1,53 @@
+package com.loan.system.domain.enums;
+
+/**
+ * @author : EdwinXu
+ * @date : Created in 2021/3/2 15:49
+ */
+public enum AuthEnum {
+    PROJECT_CREATE("PROJECT_CREATE","项目创建"),
+    PROJECT_MODIFY("PROJECT_MODIFY","项目修改"),
+    PROJECT_REMOVE("PROJECT_REMOVE","项目删除"),
+    PROJECT_CONFIG("PROJECT_CONFIG","项目配置"),
+
+    STEP_CREATE("STEP_CREATE","环节创建"),
+    STEP_MODIFY("STEP_MODIFY","环节修改"),
+    STEP_REMOVE("STEP_REMOVE","环节删除"),
+
+    CONTRACT_CREATE("CONTRACT_CREATE","合同创建"),
+    CONTRACT_MODIFY("CONTRACT_MODIFY","合同修改"),
+    CONTRACT_REMOVE("CONTRACT_REMOVE","合同删除"),
+
+    CONTRACT_MANAGEMENT("CONTRACT_MANAGEMENT","合同管理"),
+
+    FUND_CREATE("FUND_CREATE","经费创建"),
+    FUND_MODIFY("FUND_MODIFY","经费修改"),
+    FUND_REMOVE("FUND_REMOVE","经费删除"),
+
+    USER_CREATE("USER_CREATE","用户创建"),
+    USER_MODIFY("USER_MODIFY","用户修改"),
+    USER_REMOVE("USER_REMOVE","用户删除"),
+
+    CONSTRUCTION_LOG("CONSTRUCTION_LOG","施工日志"),
+
+
+
+    PROJECT_TYPE("PROJECT_TYPE","项目类型");
+
+
+    private final String auth;
+    private final String msg;
+
+    public String getAuth() {
+        return auth;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    AuthEnum(String auth, String msg) {
+        this.auth = auth;
+        this.msg = msg;
+    }
+}

+ 28 - 0
src/main/java/com/loan/system/domain/enums/ContractEnum.java

@@ -0,0 +1,28 @@
+package com.loan.system.domain.enums;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/30 - 19:40
+ */
+public enum ContractEnum {
+
+    UNSTART("UNSTART","未开始"),
+    PROCESS("PROCESS","进行中"),
+    COMPLETED("COMPLETED","已完成");
+
+    private final String status;
+    private final String msg;
+
+    public String getStatus() {
+        return status;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    ContractEnum(String status, String msg) {
+        this.status = status;
+        this.msg = msg;
+    }
+}

+ 132 - 0
src/main/java/com/loan/system/domain/enums/ExceptionEnum.java

@@ -0,0 +1,132 @@
+package com.loan.system.domain.enums;
+
+
+/**
+ * @author xuhong
+ * @date 2020/9/2 - 20:05
+ * @Description 错误枚举类
+ */
+public enum ExceptionEnum {
+    SUCCESS(200, "成功"),
+    Forbidden(403, "禁止访问"),
+    /**
+     *      CODE        错误模块
+     * -1   ~   -99     一般常见
+     * -100 ~   -149    登录
+     * -150 ~   -199    用户管理
+     * -200 ~   -249    项目管理
+     * -250 ~   -299    合同管理
+     * -300 ~   -349    模块管理
+     * -350 ~   -399    环节管理
+     * -400 ~   -449    支付管理
+     * -450 ~   -499    系统管理
+     */
+
+    UNKNOWN_ERROR(-10, "未知错误"),
+    INPUT_ERROR(-11, "输入格式错误"),
+    SAVE_ERROR(-12,"保存数据出错"),
+    DELETE_ERROR(-13, "删除数据出错"),
+    UPDATE_ERROR(-14, "更新数据失败"),
+    NullPointerException(-20, "空指针错误"),
+    UNAUTHORIZED(-21,"未授权"),
+    PERMISSION_DENIED(-22,"没有操作权限"),
+
+    DIRECTORY_CREATE_ERROR(-30,"目录创建失败"),
+    DIRECTORY_NOT_EXIST(-31,"目录不存在"),
+    DIRECTORY_DELETE_ERROR(-32,"目录删除失败"),
+
+    FILE_CREATE_ERROR(-40,"文件创建失败"),
+    FILE_NOT_EXIST(-41,"文件不存在"),
+    FILE_ALREADY_EXIST(-42,"文件已存在"),
+    FILE_DELETE_ERROR(-43,"文件删除失败"),
+    FILE_UPLOAD_OBJECT_NOT_EXIST(-44,"上传文件对象不存在"),
+    FILE_UPLOAD_TYPE_NOT_DEFINED(-45,"上传文件类型未定义"),
+    FILE_IS_EMPTY(-46,"文件为空"),
+    FILE_BLOCK_UPLOAD_FAILED(-47,"文件块上传失败"),
+    FILE_MERGE_IS_EMPTY(-48,"合并文件为空"),
+    FILE_MERGE_FAILED(-49,"文件合并失败"),
+    FILE_CHECK_FAILED(-50,"文件校验错误"),
+    IO_CLOSE_ERROR(-51,"读写错误"),
+    FILE_NOT_IMAGE(-52,"文件不是图片类型"),
+
+    // -45 文件类型限制
+
+    TOKEN_INVALID(-100,"Invalid Token"),
+    TOKEN_EXPIRED(-101,"Token Expired"),
+    WITHOUT_TOKEN(-102,"WITHOUT TOKEN"),
+    LOGIN_EXPIRED(-103,"登录过期"),
+
+    USER_NOT_EXIST(-110, "账户不存在"),
+    USER_EXIST(-111, "账户已存在"),
+    USERNAME_IS_NULL(-112, "账户不能为空"),
+    WRONG_PASSWORD(-113, "密码错误,若密码连续错误3次以上账户将被锁定"),
+    USER_MUST_BE_UNAUTHORIZED(-114, "被授权的用户必须是未被授权的"),
+    USER_NUMBER_EXIST(-114, "工号已存在"),
+
+    PROJECT_EXIST(-200,"项目已存在"),
+    PROJECT_NOT_EXIST(-201,"项目不存在"),
+    PROJECT_LABEL_EXIST(-202,"项目标签已存在"),
+    PROJECT_LABEL_NOT_EXIST(-203,"项目标签不存在"),
+    PROJECT_TYPE_EXIST(-204,"项目类型已存在"),
+    PROJECT_TYPE_NOT_EXIST(-205,"项目类型不存在"),
+    PROJECT_TYPE_IS_NECESSARY(-206,"项目类型是必须的"),
+    PROJECT_LABEL_HAS_NOT_EXIST(-207,"存在项目标签不存在"),
+    PROJECT_TASK_BOOK_EXIST(-208,"项目任务书已存在"),
+    PROJECT_TASK_BOOK_NOT_EXIST(-209,"项目任务书不存在"),
+    PROJECT_TASK_BOOK_MATERIAL_NOT_EXIST(-210,"项目任务书材料不存在"),
+
+
+    CONTRACT_EXIST(-250, "合同已存在"),
+    CONTRACT_NOT_EXIST(-251, "合同不存在"),
+    CONTRACT_MATERIAL_EXIST(-255, "合同材料已存在"),
+    CONTRACT_MATERIAL_NOT_EXIST(-256, "合同材料不存在"),
+    CONTRACT_MATERIAL_HAS_NOT_EXIST(-257,"存在材料不存在"),
+    CONTRACT_TYPE_EXIST(-260, "合同类型已存在"),
+    CONTRACT_TYPE_NOT_EXIST(-261, "合同类型不存在"),
+    CONTRACT_TYPE_ERROR(-263,"合同类型超出限制"),
+    CONTRACT_SELECT_EXIST(-265, "合同采购方式已存在"),
+    CONTRACT_SELECT_NOT_EXIST(-266, "合同采购方式不存在"),
+    CONTRACT_PAYMENT_EXIST(-270,"付款已存在"),
+    CONTRACT_PAYMENT_NOT_EXIST(-271,"付款不存在"),
+    CONTRACT_PAYMENT_HAS_NOT_EXIST(-272,"存在付款不存在"),
+    CONTRACT_HAS_NOT_COMPELETED(-280,"合同未完成所有支付"),
+
+
+    MODULE_EXIST(-300,"已存在"),
+    MODULE_NOT_EXIST(-301,"模块不存在"),
+
+
+    STEP_EXIST(-350, "项目环节已存在"),
+    STEP_NOT_EXIST(-351, "项目环节不存在"),
+    STEP_MATERIAL_EXIST(-352,"环节材料已存在"),
+    STEP_MATERIAL_NOT_EXIST(-353,"环节材料不存在"),
+    STEP_TEMPLATE_NOT_EXIST(-354,"环节模板不存在"),
+    STEP_TEMPLATE_READ_ERROR(-355,"环节模板读取失败"),
+    STEP_TEMPLATE_LIST_NOT_EXIST(-356,"环节模板边不存在"),
+    STEP_TEMPLATE_NODE_NOT_EXIST(-357,"环节模板节点读取失败"),
+
+    FUND_EXIST(-400,"经费已存在"),
+    FUND_NOT_EXIST(-401,"经费不存在"),
+    FUND_OUTPUT_EXIST(-402,"支出表已存在"),
+    FUND_OUTPUT_NOT_EXIST(-403,"支出表不存在"),
+    FUND_OUTPUT_HAS_NOT_EXIST(-404,"存在支出表不存在"),
+    FUND_SHOULD_NOT_BE_NULL(-405,"经费来源不能为空"),
+    INSUFFICIENT_BALANCE_OF_FUND(-410,"经费余额不足"),
+    FUND_OUTPUT_EXCESSIVE(-411,"经费使用超出所需"), ACCOUNT_IS_LOCK(-412, "账户已经被锁定请15分钟后再试"), WEAK_PASSWORD_ACCOUNT_IS_LOCK(-413, "账户因弱密码被锁定请联系管理员解锁"), WRONG_PASSWORD_ACCOUNT(-414, "密码错误次数已达上线,账户将被锁定请15分钟后再试");
+
+    private Integer code;
+    private String msg;
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    ExceptionEnum(Integer code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+}

+ 38 - 0
src/main/java/com/loan/system/domain/enums/LogEnum.java

@@ -0,0 +1,38 @@
+package com.loan.system.domain.enums;
+
+/**
+ * @author chen
+ * @date 2020/9/2 - 15:58
+ */
+public enum LogEnum {
+
+    LOG_DEFAULE(0,null),
+
+    LOG_OPER_TYPE_INSERT(100,"insert"),
+    LOG_OPER_TYPE_UPDATE(101,"update"),
+    LOG_OPER_TYPE_DELETE(102,"delete"),
+    //模快
+    LOG_MODULE_USER(200,"用户管理"),
+    LOG_MODULE_PROJECT_TYPE(300,"项目类型管理"),
+    LOG_MODULE_CONTRACT(400,"合同管理"),
+    LOG_MODULE_PROJECT(500,"项目管理"),
+    LOG_MODULE_FUND(600,"经费管理"),
+    LOG_MODULE_CONTRACT_TYPE(700,"合同类管理"),
+    LOG_MODULE_STEP(800,"环节管理");
+
+    private Integer code;
+    private String msg;
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    LogEnum(Integer code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+}

+ 27 - 0
src/main/java/com/loan/system/domain/enums/RemindTypeEnum.java

@@ -0,0 +1,27 @@
+package com.loan.system.domain.enums;
+
+/**
+ * @author : EdwinXu
+ * @date : Created in 2022/5/6 19:46
+ */
+public enum RemindTypeEnum {
+    TUI_ZHI_BAO_JIN_REMIND("TUI_ZHI_BAO_JIN_REMIND","退质保金提醒"),
+    JUN_GONG_MATERIAL_APPROVE_REMIND("JUN_GONG_MATERIAL_APPROVE_REMIND","竣工材料到期送审提醒")
+    ;
+
+    private String type;
+    private String msg;
+
+    public String getType() {
+        return type;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    RemindTypeEnum(String type, String msg) {
+        this.type = type;
+        this.msg = msg;
+    }
+}

+ 32 - 0
src/main/java/com/loan/system/domain/enums/RoleEnum.java

@@ -0,0 +1,32 @@
+package com.loan.system.domain.enums;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/2 - 15:58
+ */
+public enum RoleEnum {
+    // 这里似乎不需要三个角色
+    SYSTEM_ADMIN(1, "系统管理员"),
+    APPROVER(2, "审批人员"),
+    LEAD_SALES(3, "主办业务员"),
+    ASSIST_SALES(4, "辅办业务员"),
+    FINANCE(5, "财务人员"),
+    BACK_OFFICE(6, "综合内勤"),
+    EXTERNAL(7, "外部人员");
+
+    private Integer code;
+    private String msg;
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    RoleEnum(Integer code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+}

+ 27 - 0
src/main/java/com/loan/system/domain/enums/StepEnum.java

@@ -0,0 +1,27 @@
+package com.loan.system.domain.enums;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/30 - 19:40
+ */
+public enum StepEnum {
+    UNSTART("UNSTART","还未开始"),
+    PROCESS("PROCESS","进行中"),
+    COMPLETED("COMPLETED","已完成");
+
+    private final String status;
+    private final String msg;
+
+    public String getStatus() {
+        return status;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    StepEnum(String status, String msg) {
+        this.status = status;
+        this.msg = msg;
+    }
+}

+ 36 - 0
src/main/java/com/loan/system/domain/enums/StepPropertyEnum.java

@@ -0,0 +1,36 @@
+package com.loan.system.domain.enums;
+
+/**
+ * 针对特殊环节的标识设置
+ * @author : EdwinXu
+ * @date : Created in 2021/1/25 16:57
+ */
+public enum StepPropertyEnum {
+    BLANK("BLANK","空白"),
+    UNIVERSAL("UNIVERSAL","通用"),
+    SKIP("SKIP","跳过"),
+    APPROVE("APPROVE","审批"),
+    PERMITION("PERMITION","许可"),
+    CONSTRUCTIONLOD("CONSTRUCTIONLOD","施工日志"),
+    PAYMENT("PAYMENT","支付"),
+
+    APPROVE_SUB("APPROVE_SUB","审批子环节"),
+    PERMITION_SUB("PERMITION_SUB","许可子环节"),
+    CONSTRUCTIONLOD_SUB("CONSTRUCTIONLOD_SUB","施工日志子环节"),
+    PAYMENT_SUB("PAYMENT_SUB","支付子环节");
+    private final String property;
+    private final String desc;
+
+    public String getProperty() {
+        return property;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    StepPropertyEnum(String property, String desc) {
+        this.property = property;
+        this.desc = desc;
+    }
+}

+ 61 - 0
src/main/java/com/loan/system/domain/pojo/FileResult.java

@@ -0,0 +1,61 @@
+package com.loan.system.domain.pojo;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/25 - 14:48
+ * @Description
+ */
+public class FileResult {
+    // 扩展名
+
+    private String fileName;
+    // 文件大小,字节
+
+    private String extName;
+    // 文件存储在服务器的相对地址
+
+    private Long fileSize;
+    private String serverPath;
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public String getExtName() {
+        return extName;
+    }
+
+    public void setExtName(String extName) {
+        this.extName = extName;
+    }
+
+    public Long getFileSize() {
+        return fileSize;
+    }
+
+    public void setFileSize(Long fileSize) {
+        this.fileSize = fileSize;
+    }
+
+    public String getServerPath() {
+        return serverPath;
+    }
+
+    public void setServerPath(String serverPath) {
+        this.serverPath = serverPath;
+    }
+
+    @Override
+    public String toString() {
+        return "FileResult{" +
+                "fileName='" + fileName + '\'' +
+                ", extName='" + extName + '\'' +
+                ", fileSize=" + fileSize +
+                ", serverPath='" + serverPath + '\'' +
+                '}';
+    }
+}

+ 53 - 0
src/main/java/com/loan/system/domain/pojo/MultipartFileUpload.java

@@ -0,0 +1,53 @@
+package com.loan.system.domain.pojo;
+
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * @author : EdwinXu
+ * @date : Created in 2020/12/18 09:05
+ */
+public class MultipartFileUpload implements MultipartFile {
+    @Override
+    public String getName() {
+        return null;
+    }
+
+    @Override
+    public String getOriginalFilename() {
+        return null;
+    }
+
+    @Override
+    public String getContentType() {
+        return null;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    @Override
+    public long getSize() {
+        return 0;
+    }
+
+    @Override
+    public byte[] getBytes() throws IOException {
+        return new byte[0];
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        return null;
+    }
+
+    @Override
+    public void transferTo(File dest) throws IOException, IllegalStateException {
+
+    }
+}

+ 63 - 0
src/main/java/com/loan/system/domain/pojo/Result.java

@@ -0,0 +1,63 @@
+package com.loan.system.domain.pojo;
+
+import java.io.Serializable;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/2 - 19:55
+ */
+public class Result implements Serializable {
+
+    private static final long serialVersionUID = -6737213892547362600L;
+    private Integer code;
+    private String msg;
+    private Object data;
+    private static Result result;
+
+    public static Result getInstance(){
+        if (result == null){
+            return new Result();
+        }
+        return result;
+    }
+
+    public static long getSerialVersionUID() {
+        return serialVersionUID;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public Result setCode(Integer code) {
+        this.code = code;
+        return this;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public Result setMsg(String msg) {
+        this.msg = msg;
+        return this;
+    }
+
+    public Object getData() {
+        return data;
+    }
+
+    public Result setData(Object data) {
+        this.data = data;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "Result{" +
+                "code=" + code +
+                ", msg='" + msg + '\'' +
+                ", data=" + data +
+                '}';
+    }
+}

+ 20 - 0
src/main/java/com/loan/system/domain/vo/UserLoginVO.java

@@ -0,0 +1,20 @@
+package com.loan.system.domain.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserLoginVO implements Serializable {
+
+    private Long id;
+    private String openid;
+    private String token;
+
+}

+ 35 - 0
src/main/java/com/loan/system/exception/DescribeException.java

@@ -0,0 +1,35 @@
+package com.loan.system.exception;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/2 - 20:28
+ */
+public class DescribeException extends RuntimeException {
+    private static final long serialVersionUID = 1721399907506190029L;
+    private Integer code;
+
+    public DescribeException(Integer code ,String msg) {
+        super(msg);
+        this.code = code;
+    }
+
+    public DescribeException(ExceptionEnum exceptionEnum){
+        super(exceptionEnum.getMsg());
+        this.code = exceptionEnum.getCode();
+    }
+
+    Integer getCode() {
+        return code;
+    }
+
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+
+    @Override
+    public String toString() {
+        return "DescribeException { " +"code = " + code + " msg = " + this.getMessage() +" }";
+    }
+}

+ 40 - 0
src/main/java/com/loan/system/exception/FileException.java

@@ -0,0 +1,40 @@
+package com.loan.system.exception;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+
+import java.io.IOException;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/26 - 21:38
+ * @Description
+ */
+public class FileException extends IOException {
+    private Integer code;
+
+    public FileException(Integer code, String msg){
+        super(msg);
+        this.code = code;
+    }
+
+    public FileException(ExceptionEnum exceptionEnum) {
+        super(exceptionEnum.getMsg());
+        this.code = code;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+
+    @Override
+    public String toString() {
+        return "FileException{" +
+                "code=" + code +
+                " msg = " + this.getMessage() +
+                '}';
+    }
+}

+ 46 - 0
src/main/java/com/loan/system/exception/GlobalExceptionHandler.java

@@ -0,0 +1,46 @@
+package com.loan.system.exception;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+import com.loan.system.domain.pojo.Result;
+import com.loan.system.utils.ResultUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.nio.file.AccessDeniedException;
+
+/**
+ *  统一异常处理
+ * @author EdwinXu
+ * @date 2020/9/2 - 20:28
+ */
+@ControllerAdvice
+@ResponseBody
+public class GlobalExceptionHandler {
+    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+    @ExceptionHandler(value = Exception.class)
+    public Result exceptionGet(Exception e) {
+        if (e instanceof DescribeException) {
+            DescribeException myException = (DescribeException) e;
+            logger.error("[ 异常 >> "+e.toString()+" ]");
+            return ResultUtil.error(myException.getCode(), myException.getMessage());
+        } else if(e instanceof FileException){
+            FileException myException = (FileException) e;
+            logger.error("[ 异常 >> "+e.toString()+" ]");
+            return ResultUtil.error(myException.getCode(), myException.getMessage());
+        } else if (e instanceof AccessDeniedException){
+            logger.error("[ 异常 >> "+e.toString()+" ]");
+            return ResultUtil.error(ExceptionEnum.PERMISSION_DENIED);
+        } else if (e.toString().split("\\.")[e.toString().split("\\.").length - 1].equals(
+                ExceptionEnum.NullPointerException.name())) {
+            logger.error("[ 异常 >>  ]", e);
+            return ResultUtil.error(ExceptionEnum.NullPointerException);
+        } else {
+            logger.error("[ 异常 >>  ]", e);
+            return ResultUtil.error(ExceptionEnum.UNKNOWN_ERROR);
+        }
+    }
+}

+ 32 - 0
src/main/java/com/loan/system/exception/LoginException.java

@@ -0,0 +1,32 @@
+package com.loan.system.exception;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * @author xuhong
+ * @date 2020/9/8 - 12:49
+ * @Description 登录错误类
+ */
+public class LoginException extends AuthenticationException {
+    private static final long serialVersionUID = 716346130825379873L;
+    private Integer code;
+
+    public LoginException(Integer code, String msg) {
+        super(msg);
+        this.code = code;
+    }
+    public LoginException(ExceptionEnum exceptionEnum){
+        super(exceptionEnum.getMsg());
+        this.code = exceptionEnum.getCode();
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public LoginException setCode(Integer code) {
+        this.code = code;
+        return this;
+    }
+}

+ 54 - 0
src/main/java/com/loan/system/interceptor/JwtTokenUserInterceptor.java

@@ -0,0 +1,54 @@
+package com.loan.system.interceptor;
+
+import com.loan.system.constant.JwtClaimsConstant;
+import com.loan.system.context.BaseContext;
+import com.loan.system.properties.JwtProperties;
+import com.loan.system.utils.JwtUtil;
+import io.jsonwebtoken.Claims;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * jwt令牌校验的拦截器
+ */
+@Component
+@Slf4j
+public class JwtTokenUserInterceptor implements HandlerInterceptor {
+
+    @Autowired
+    private JwtProperties jwtProperties;
+
+    /**
+     * 在拦截的请求前校验jwt
+     */
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        //判断当前拦截到的是Controller的方法还是其他资源
+        if (!(handler instanceof HandlerMethod)) {
+            //当前拦截到的不是动态方法,直接放行
+            return true;
+        }
+
+        //1、从请求头中获取令牌
+        String token = request.getHeader(jwtProperties.getUserTokenName());
+
+        //2、校验令牌
+        try {
+            log.info("jwt校验:{}", token);
+            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
+            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
+            BaseContext.setCurrentId(userId);
+            //3、通过,放行
+            return true;
+        } catch (Exception ex) {
+            //4、不通过,响应401状态码
+            response.setStatus(401);
+            return false;
+        }
+    }
+}

+ 52 - 0
src/main/java/com/loan/system/json/JacksonObjectMapper.java

@@ -0,0 +1,52 @@
+package com.loan.system.json;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
+
+/*
+ * 序列化与反序列化的过程
+ * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
+ * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
+ * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
+ */
+public class JacksonObjectMapper extends ObjectMapper {
+
+    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
+    //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
+    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
+
+    public JacksonObjectMapper() {
+        super();
+        //收到未知属性时不报异常
+        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        //反序列化时,属性不存在的兼容处理
+        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+
+        SimpleModule simpleModule = new SimpleModule()
+                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
+                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
+                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
+                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
+                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
+                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
+
+        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
+        this.registerModule(simpleModule);
+    }
+}

+ 368 - 0
src/main/java/com/loan/system/loan_system.sql

@@ -0,0 +1,368 @@
+-- 创建数据库
+CREATE DATABASE IF NOT EXISTS `loan_system` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+USE `loan_system`;
+
+-- 1. roles 表(角色表)
+CREATE TABLE `roles` (
+                         `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                         `role_name` VARCHAR(64) UNIQUE,
+                         `description` VARCHAR(255),
+                         `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 2. users 表(用户表)
+CREATE TABLE `users` (
+                         `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                         `username` VARCHAR(64) UNIQUE,
+                         `password_hash` VARCHAR(255),
+                         `real_name` VARCHAR(100),
+                         `sex` VARCHAR(10),
+                         `mobile` CHAR(11),
+                         `role` VARCHAR(255),
+                         `ext_role_type` VARCHAR(50),
+                         `dept` VARCHAR(100),
+                         `status` TINYINT,
+                         `created_time` DATETIME,
+                         `updated_time` DATETIME,
+                         `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                         INDEX `idx_mobile` (`mobile`)
+);
+
+-- 3. customers 表(客户表)
+CREATE TABLE `customers` (
+                             `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                             `openid` VARCHAR(128),
+                             `name` VARCHAR(100),
+                             `sex` VARCHAR(10),
+                             `id_number` CHAR(18),
+                             `mobile` CHAR(11),
+                             `register_source` VARCHAR(50),
+                             `bank_account` VARCHAR(64),
+                             `face_auth` BOOLEAN,
+                             `create_time` DATETIME,
+                             `update_time` DATETIME,
+                             `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                             INDEX `idx_id_number` (`id_number`),
+                             INDEX `idx_mobile` (`mobile`)
+);
+
+-- 4. customers_other 表(其他客户表)
+CREATE TABLE `customers_other` (
+                                   `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                   `name` VARCHAR(100),
+                                   `id_number` CHAR(18),
+                                   `mobile` CHAR(11),
+                                   `create_time` DATETIME,
+                                   `update_time` DATETIME,
+                                   `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 5. collateral 表(押品表)
+CREATE TABLE `collateral` (
+                              `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                              `case_id` BIGINT,
+                              `collateral_type` VARCHAR(30),
+                              `owner_customer_id` BIGINT,
+                              `allocated_amount` DECIMAL(18,2),
+                              `address` VARCHAR(500),
+                              `eval_price` DECIMAL(18,2),
+                              `is_involved_in_litigation` BOOLEAN,
+                              `staus` VARCHAR(20),
+                              `create_time` DATETIME,
+                              `update_time` DATETIME,
+                              `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 6. loan_case 表(业务单表)
+CREATE TABLE `loan_case` (
+                             `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                             `case_no` VARCHAR(50) UNIQUE,
+                             `customer_id` BIGINT,
+                             `business_type_id` BIGINT,
+                             `business_attrs` VARCHAR(200),
+                             `channel_id` BIGINT,
+                             `remark_id` BIGINT,
+                             `custom1_id` BIGINT,
+                             `custom2_id` BIGINT,
+                             `requested_amount` DECIMAL(18,2),
+                             `total_loan_amount` DECIMAL(18,2),
+                             `create_time` DATETIME,
+                             `update_time` DATETIME,
+                             `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 8. documents 表(附件表)
+CREATE TABLE `documents` (
+                             `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                             `case_id` BIGINT,
+                             `owner_id` BIGINT,
+                             `type_id` BIGINT,
+                             `doc_type` VARCHAR(50),
+                             `file_path` VARCHAR(500),
+                             `file_name` VARCHAR(255),
+                             `is_current` BOOLEAN,
+                             `create_time` DATETIME,
+                             `update_time` DATETIME,
+                             `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                             INDEX `idx_owner_id` (`owner_id`)
+);
+
+-- 9. approval_record 表(审批记录表)
+CREATE TABLE `approval_record` (
+                                   `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                   `case_id` BIGINT,
+                                   `step_name` VARCHAR(100),
+                                   `approver_id` BIGINT,
+                                   `decision` VARCHAR(20),
+                                   `comments` TEXT,
+                                   `create_time` DATETIME,
+                                   `update_time` DATETIME,
+                                   `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                                   INDEX `idx_case_id` (`case_id`)
+);
+
+-- 10. contract 表(合同表)
+CREATE TABLE `contract` (
+                            `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                            `business_attr` VARCHAR(200),
+                            `case_id` BIGINT,
+                            `contract_no` VARCHAR(50) UNIQUE,
+                            `contract_name` VARCHAR(50),
+                            `contract_version` INT,
+                            `contract_amount` DECIMAL(15,2),
+                            `interest_rate` DECIMAL(5,4),
+                            `loan_period` INT,
+                            `content` TEXT,
+                            `signed_by_customer` BOOLEAN,
+                            `sifned_id` BIGINT,
+                            `signed_time` DATETIME,
+                            `create_time` DATETIME,
+                            `update_time` DATETIME,
+                            `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                            INDEX `idx_case_id` (`case_id`)
+);
+
+-- 11. disbursement 表(出款表)
+CREATE TABLE `disbursement` (
+                                `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                `case_id` BIGINT,
+                                `payout_approve_user_id` BIGINT,
+                                `payout_operator_user_id` BIGINT,
+                                `apply_by` BIGINT,
+                                `apply_at` DATETIME,
+                                `disbursement_type` VARCHAR(10),
+                                `planned_amount` DECIMAL(18,2),
+                                `planned_location` VARCHAR(255),
+                                `disbursement_status` VARCHAR(30),
+                                `contract_id` BIGINT,
+                                `payout_time` DATETIME,
+                                `create_time` DATETIME,
+                                `update_time` DATETIME,
+                                `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                                INDEX `idx_case_id` (`case_id`),
+                                INDEX `idx_disbursement_status` (`disbursement_status`)
+);
+
+-- disbursement_record 表(出款记录表)
+CREATE TABLE `disbursement_record` (
+                                       `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                       `disbursement_id` BIGINT,
+                                       `amount` DECIMAL(18,2),
+                                       `create_time` DATETIME,
+                                       `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 12. repayment 表(回款表)
+CREATE TABLE `repayment` (
+                             `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                             `case_id` BIGINT,
+                             `contract_id` BIGINT,
+                             `repay_by_customer_id` BIGINT,
+                             `repayment_plan_user_id` BIGINT,
+                             `repayment_operator_user_id` BIGINT,
+                             `repayment_type` VARCHAR(10),
+                             `repay_amount` DECIMAL(18,2),
+                             `repay_at` DATETIME,
+                             `repay_bank` VARCHAR(2000),
+                             `repay_record_id` BIGINT,
+                             `confirmed_by` BIGINT,
+                             `confirmed_at` DATETIME,
+                             `interest` DECIMAL(18,2),
+                             `is_cleared` BOOLEAN,
+                             `create_time` DATETIME,
+                             `update_time` DATETIME,
+                             `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                             INDEX `idx_case_id` (`case_id`),
+                             INDEX `idx_repay_at` (`repay_at`),
+                             INDEX `idx_is_cleared` (`is_cleared`)
+);
+
+-- repayment_record 表(回款记录表)
+CREATE TABLE `repayment_record` (
+                                    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                    `repayment_id` BIGINT,
+                                    `amount` DECIMAL(18,2),
+                                    `create_time` DATETIME,
+                                    `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 14. biz_recommender 表(推荐人表)
+CREATE TABLE `biz_recommender` (
+                                   `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                   `recommender_code` VARCHAR(50) UNIQUE,
+                                   `recommender_name` VARCHAR(100),
+                                   `recommender_type` VARCHAR(50),
+                                   `phone` VARCHAR(20),
+                                   `channel` VARCHAR(50),
+                                   `id_card` VARCHAR(20),
+                                   `bank_account` VARCHAR(50),
+                                   `commission_rate` DECIMAL(5,4),
+                                   `company` VARCHAR(50),
+                                   `remark` TEXT,
+                                   `create_time` DATETIME,
+                                   `update_time` DATETIME,
+                                   `create_user_id` BIGINT,
+                                   `update_user_id` BIGINT,
+                                   `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 15. sys_message 表(消息表)
+CREATE TABLE `sys_message` (
+                               `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                               `user_id` BIGINT,
+                               `message_title` VARCHAR(200),
+                               `message_content` TEXT,
+                               `message_type` VARCHAR(50),
+                               `read_status` TINYINT,
+                               `related_id` BIGINT,
+                               `related_type` VARCHAR(50),
+                               `create_time` DATETIME,
+                               `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 16. stat_business_snapshot 表(业务统计快照表)
+CREATE TABLE `stat_business_snapshot` (
+                                          `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                          `stat_date` DATE,
+                                          `channel` VARCHAR(50),
+                                          `biz_type` VARCHAR(50),
+                                          `application_count` INT,
+                                          `total_loan_amount` DECIMAL(15,2),
+                                          `user_id` BIGINT,
+                                          `create_time` DATETIME,
+                                          `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 17. stat_fund_efficiency 表(资金效率统计表)
+CREATE TABLE `stat_fund_efficiency` (
+                                        `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                        `stat_month` VARCHAR(7),
+                                        `daily_loan_balance` DECIMAL(15,2),
+                                        `actual_days` INT,
+                                        `interest_income` DECIMAL(15,2),
+                                        `create_time` DATETIME,
+                                        `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);
+
+-- 18. dict_business_type 表(业务类型字典)
+CREATE TABLE `dict_business_type` (
+                                      `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                      `code` VARCHAR(50) UNIQUE,
+                                      `name` VARCHAR(100),
+                                      `enabled` BOOLEAN,
+                                      `sort_order` INT,
+                                      `created_at` DATETIME,
+                                      `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                                      INDEX `idx_enabled` (`enabled`)
+);
+
+-- 19. dict_channel 表(渠道字典)
+CREATE TABLE `dict_channel` (
+                                `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                `code` VARCHAR(50) UNIQUE,
+                                `name` VARCHAR(100),
+                                `enabled` BOOLEAN,
+                                `sort_order` INT,
+                                `created_at` DATETIME,
+                                `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                                INDEX `idx_enabled` (`enabled`)
+);
+
+-- 20. dict_attribute 表(业务属性字典)
+CREATE TABLE `dict_attribute` (
+                                  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                  `code` VARCHAR(50) UNIQUE,
+                                  `name` VARCHAR(100),
+                                  `enabled` BOOLEAN,
+                                  `sort_order` INT,
+                                  `created_at` DATETIME,
+                                  `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                                  INDEX `idx_enabled` (`enabled`)
+);
+
+-- 21. dict_location 表(位置字典)
+CREATE TABLE `dict_location` (
+                                 `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                 `code` VARCHAR(50) UNIQUE,
+                                 `name` VARCHAR(100),
+                                 `enabled` BOOLEAN,
+                                 `sort_order` INT,
+                                 `created_at` DATETIME,
+                                 `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                                 INDEX `idx_enabled` (`enabled`)
+);
+
+-- 22. dict_message 表(消息类型字典)
+CREATE TABLE `dict_message` (
+                                `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                                `code` INT UNIQUE,
+                                `name` VARCHAR(100),
+                                `enabled` BOOLEAN,
+                                `sort_order` INT,
+                                `created_at` DATETIME,
+                                `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                                INDEX `idx_enabled` (`enabled`)
+);
+
+-- 23. dict_type 表(资料类型表)
+CREATE TABLE `dict_type` (
+                             `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                             `code` INT UNIQUE,
+                             `name` VARCHAR(100),
+                             `enabled` BOOLEAN,
+                             `sort_order` INT,
+                             `created_at` DATETIME,
+                             `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                             INDEX `idx_enabled` (`enabled`)
+);
+
+-- 25. dict_step 表(环节类型)
+CREATE TABLE `dict_step` (
+                             `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
+                             `code` INT UNIQUE,
+                             `name` VARCHAR(50),
+                             `is_parent` BIGINT,
+                             `enabled` BOOLEAN,
+                             `sort_order` INT,
+                             `created_at` DATETIME,
+                             `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
+                             INDEX `idx_enabled` (`enabled`)
+);
+
+-- 26. steps 表
+CREATE TABLE `steps` (
+                         `id` BIGINT PRIMARY KEY  AUTO_INCREMENT,
+                         `step_name` VARCHAR(50),
+                         `case_id` BIGINT,
+                         `status` VARCHAR(50),
+                         `user_id1` BIGINT,
+                         `user_id2` BIGINT,
+                         `begin_time` DATETIME,
+                         `parent_id` BIGINT,
+                         `pre_id` BIGINT,
+                         `next_id` BIGINT,
+                         `create_time` DATETIME,
+                         `update_time` DATETIME,
+                         `is_delete` TINYINT(1) DEFAULT 0 COMMENT '是否删除'
+);

+ 26 - 0
src/main/java/com/loan/system/properties/JwtProperties.java

@@ -0,0 +1,26 @@
+package com.loan.system.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "system.jwt")
+@Data
+public class JwtProperties {
+
+    /**
+     * 管理端员工生成jwt令牌相关配置
+     */
+    private String adminSecretKey;
+    private long adminTtl;
+    private String adminTokenName;
+
+    /**
+     * 用户端微信用户生成jwt令牌相关配置
+     */
+    private String userSecretKey;
+    private long userTtl;
+    private String userTokenName;
+
+}

+ 22 - 0
src/main/java/com/loan/system/properties/WeChatProperties.java

@@ -0,0 +1,22 @@
+package com.loan.system.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "system.wechat")
+@Data
+public class WeChatProperties {
+
+    private String appid; //小程序的appid
+    private String secret; //小程序的秘钥
+//    private String mchid; //商户号
+//    private String mchSerialNo; //商户API证书的证书序列号
+//    private String privateKeyFilePath; //商户私钥文件
+//    private String apiV3Key; //证书解密的密钥
+//    private String weChatPayCertFilePath; //平台证书
+//    private String notifyUrl; //支付成功的回调地址
+//    private String refundNotifyUrl; //退款成功的回调地址
+
+}

+ 14 - 0
src/main/java/com/loan/system/repository/ExceptionLogDao.java

@@ -0,0 +1,14 @@
+package com.loan.system.repository;
+
+
+import com.loan.system.domain.entity.ExceptionLog;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+
+public interface ExceptionLogDao extends JpaRepository<ExceptionLog, Integer>, JpaSpecificationExecutor<ExceptionLog> {
+    @Query("SELECT e FROM ExceptionLog e")
+    public Page<ExceptionLog> getExceptionLogByPage(Pageable pageable);
+}

+ 19 - 0
src/main/java/com/loan/system/repository/UserRepository.java

@@ -0,0 +1,19 @@
+package com.loan.system.repository;
+
+import com.loan.system.domain.entity.User;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.Set;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/2 - 15:35
+ * @Description
+ */
+public interface UserRepository extends JpaRepository<User,Long> {
+    User findByMobile(String mobile);
+}

+ 14 - 0
src/main/java/com/loan/system/service/ExceptionLogService.java

@@ -0,0 +1,14 @@
+package com.loan.system.service;
+
+import com.loan.system.domain.entity.ExceptionLog;
+import org.springframework.data.domain.Page;
+
+public interface ExceptionLogService {
+
+    public void insertExceptionLog(ExceptionLog e);
+
+    public ExceptionLog getOne(int id);
+
+    public Page<ExceptionLog> getExceptionLogByPage(int page,int size);
+
+}

+ 40 - 0
src/main/java/com/loan/system/service/Impl/ExceptionServiceImpl.java

@@ -0,0 +1,40 @@
+package com.loan.system.service.Impl;
+
+import com.loan.system.domain.entity.ExceptionLog;
+import com.loan.system.repository.ExceptionLogDao;
+import com.loan.system.service.ExceptionLogService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ExceptionServiceImpl implements ExceptionLogService {
+
+    ExceptionLogDao exceptionLogDao;
+
+    @Autowired
+    public ExceptionServiceImpl(ExceptionLogDao exceptionLogDao) {
+        this.exceptionLogDao = exceptionLogDao;
+    }
+
+    @Override
+    public void insertExceptionLog(ExceptionLog e) {
+            exceptionLogDao.save(e);
+    }
+
+    @Override
+    public ExceptionLog getOne(int id) {
+        return exceptionLogDao.getOne(id);
+    }
+
+    @Override
+    public Page<ExceptionLog> getExceptionLogByPage(int page, int size) {
+        Sort sort = Sort.by(Sort.Direction.DESC, "operTime");
+        Pageable pageable = PageRequest.of(page, size, sort);
+        return exceptionLogDao.getExceptionLogByPage(pageable);
+
+    }
+}

+ 289 - 0
src/main/java/com/loan/system/service/Impl/MediaUploadService.java

@@ -0,0 +1,289 @@
+package com.loan.system.service.Impl;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+import com.loan.system.domain.pojo.Result;
+import com.loan.system.exception.DescribeException;
+import com.loan.system.utils.ResultUtil;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * @author : EdwinXu
+ * @date : Created in 2020/12/17 10:08
+ */
+@Service
+public class MediaUploadService {
+
+    //上传文件的根目录
+    @Value("${upload.location}")
+
+    String uploadPath;
+    /**
+     * 根据文件md5得到文件路径
+     * 规则:
+     * 一级目录:md5的第一个字符
+     * 二级目录:md5的第二个字符
+     * 三级目录:md5
+     * 文件名:md5+文件扩展名
+     *
+     * @param fileMd5 文件md5值
+     * @param fileExt 文件扩展名
+     * @return 文件路径
+     */
+    private String getFilePath(String fileMd5, String fileExt) {
+        return getFileFolderPath(fileMd5) + fileMd5 + "." + fileExt;
+    }
+
+    // 得到文件所在目录
+
+    private String getFileFolderPath(String fileMd5) {
+        return uploadPath + getFileFolderRelativePath(fileMd5);
+    }
+
+    //得到文件目录相对路径,路径中去掉根目录
+
+    private String getFileFolderRelativePath(String fileMd5) {
+        return fileMd5.substring(0, 1) + "/"
+        + fileMd5.substring(1, 2) + "/"
+        + fileMd5 + "/";
+    }
+
+
+    // 创建文件目录
+
+    private boolean createFileFold(String fileMd5) {
+        // 创建上传文件,目录
+        String fileFolderPath = getFileFolderPath(fileMd5);
+        File fileFolder = new File(fileFolderPath);
+        if (!fileFolder.exists()) {
+            //创建文件夹
+            return fileFolder.mkdirs();
+        }
+        return true;
+    }
+
+    public Result register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
+        // 检查文件是否上传
+        // 1.获取文件的路径
+        String filePath = getFilePath(fileMd5, fileExt);
+        System.out.println("register filePath = "+filePath);
+        File file = new File(filePath);
+        // 2.查询数据库文件是否存在
+        if (file.exists()){
+            throw new DescribeException(ExceptionEnum.FILE_NOT_EXIST);
+        }
+        // 文件未上传处理
+        boolean fileFold = createFileFold(fileMd5);
+        if (!fileFold){
+            throw new DescribeException(ExceptionEnum.FILE_CREATE_ERROR);
+        }
+        return ResultUtil.success();
+    }
+
+    //   得到块文件所在目录
+
+    private String getChunkFileFolderPath(String fileMd5) {
+        return getFileFolderPath(fileMd5) + "chunks" + "/";
+    }
+
+    //检查块文件
+
+    public Result checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
+        //得到块文件所在路径
+        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
+        System.out.println("=> chunkFileFolderPath "+chunkFileFolderPath);
+        //块文件的文件名称以1,2,3..序号命名,没有扩展名
+        File chunkFile = new File(chunkFileFolderPath + chunk);
+        System.out.println("==》 chunkFile "+chunkFile);
+        if (chunkFile.exists()) {
+            return ResultUtil.success("true",true);
+        } else {
+            return  ResultUtil.success("false",false);
+        }
+    }
+
+    private boolean createChunkFileFolder(String fileMd5) {
+        //创建上传文件目录
+        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
+        File chunkFileFolder = new File(chunkFileFolderPath);
+        if (!chunkFileFolder.exists()) {
+            //创建文件夹
+            return chunkFileFolder.mkdirs();
+        }
+        return true;
+    }
+
+    //块文件上传
+
+    public Result uploadChunk(MultipartFile file, String fileMd5, Integer chunk) {
+        if (file == null) {
+            throw new DescribeException(ExceptionEnum.FILE_IS_EMPTY);
+        }
+        //创建块文件目录
+        boolean fileFold = createChunkFileFolder(fileMd5);
+        //块文件
+        File chunkFile = new File(getChunkFileFolderPath(fileMd5) + chunk);
+        System.out.println(" uploadChunk chunkFile "+getChunkFileFolderPath(fileMd5) + chunk);
+        //上传的块文件
+        InputStream inputStream = null;
+        FileOutputStream outputStream = null;
+        try {
+            inputStream = file.getInputStream();
+            outputStream = new FileOutputStream(chunkFile);
+            IOUtils.copy(inputStream, outputStream);
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new DescribeException(ExceptionEnum.FILE_BLOCK_UPLOAD_FAILED);
+        } finally {
+            try {
+                assert inputStream != null;
+                inputStream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+            try {
+                assert outputStream != null;
+                outputStream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return ResultUtil.success();
+    }
+
+    // 合并块文件
+
+    public Result mergeChunks(String fileMd5, String fileName, Long fileSize, String mimeType, String fileExt) {
+        //获取块文件的路径
+        
+        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
+        System.out.println("mergeChunks chunkFileFolderPath "+chunkFileFolderPath);
+        File chunkFileFolder = new File(chunkFileFolderPath);
+        if (!chunkFileFolder.exists()) {
+            chunkFileFolder.mkdirs();
+        }
+        //合并文件路径
+        File mergeFile = new File(getFilePath(fileMd5, fileExt));
+        //创建合并文件
+        //合并文件存在先删除再创建
+        if (mergeFile.exists()) {
+            mergeFile.delete();
+        }
+        boolean newFile = false;
+        try {
+            newFile = mergeFile.createNewFile();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        if (!newFile) {
+            throw new DescribeException(ExceptionEnum.FILE_MERGE_IS_EMPTY);
+        }
+        //获取块文件,此列表是已经排好序的列表
+        List<File> chunkFiles = getChunkFiles(chunkFileFolder);
+        //合并文件
+        mergeFile = mergeFile(mergeFile, chunkFiles);
+        if (mergeFile == null) {
+            throw new DescribeException(ExceptionEnum.FILE_MERGE_FAILED);
+        }
+        //校验文件
+        boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
+        if (!checkResult) {
+            throw new DescribeException(ExceptionEnum.FILE_CHECK_FAILED);
+        }
+        //将文件信息保存到数据库
+        /*MediaFile mediaFile = new MediaFile();
+        mediaFile.setFileId(fileMd5);
+        mediaFile.setFileName(fileMd5 + "." + fileExt);
+        mediaFile.setFileOriginalName(fileName);
+        //文件路径保存相对路径
+        mediaFile.setFilePath(getFileFolderRelativePath(fileMd5));
+        mediaFile.setFileSize(fileSize);
+        mediaFile.setUploadTime(new Date());
+        mediaFile.setMimeType(mimeType);
+        mediaFile.setFileType(fileExt);
+        //状态为上传成功
+        mediaFile.setFileStatus("301002");
+        MediaFile save = mediaFileRepository.save(mediaFile);*/
+        return ResultUtil.success();
+    }
+
+    // 校验文件的md5值
+
+    private boolean checkFileMd5(File mergeFile, String md5) {
+        if (mergeFile == null || StringUtils.isEmpty(md5)) {
+            return false;
+        }
+        //进行md5校验
+        FileInputStream mergeFileInputstream = null;
+        try {
+            mergeFileInputstream = new FileInputStream(mergeFile);
+            //得到文件的md5
+            String mergeFileMd5 = DigestUtils.md5Hex(mergeFileInputstream);
+            //比较md5
+            if (md5.equalsIgnoreCase(mergeFileMd5)) {
+                return true;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                mergeFileInputstream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        return false;
+    }
+
+    //获取所有块文件
+
+    private List<File> getChunkFiles(File chunkFileFolder) {
+        //获取路径下的所有块文件
+        File[] chunkFiles = chunkFileFolder.listFiles();
+        //将文件数组转成list,并排序
+        List<File> chunkFileList = new ArrayList<File>();
+        chunkFileList.addAll(Arrays.asList(chunkFiles));
+        //排序
+        Collections.sort(chunkFileList, (o1, o2) -> {
+            if (Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName())) {
+                return 1;
+            }
+            return -1;
+        });
+        return chunkFileList;
+    }
+
+    //合并文件
+
+    private File mergeFile(File mergeFile, List<File> chunkFiles) {
+        try {
+            //创建写文件对象
+            RandomAccessFile raf_write = new RandomAccessFile(mergeFile, "rw");
+            //遍历分块文件开始合并
+            //读取文件缓冲区
+            byte[] b = new byte[1024];
+            for (File chunkFile : chunkFiles) {
+                RandomAccessFile raf_read = new RandomAccessFile(chunkFile, "r");
+                int len = -1;
+                //读取分块文件
+                while ((len = raf_read.read(b)) != -1) {
+                    //向合并文件中写数据
+                    raf_write.write(b, 0, len);
+                }
+                raf_read.close();
+            }
+            raf_write.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+        return mergeFile;
+    }
+}

+ 57 - 0
src/main/java/com/loan/system/service/Impl/PermissionService.java

@@ -0,0 +1,57 @@
+package com.loan.system.service.Impl;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+import com.loan.system.exception.DescribeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Component("pms")
+public class PermissionService {
+    private static final Logger logger = LoggerFactory.getLogger(PermissionService.class);
+
+    public boolean hasRole(String role, String auth){
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null) {
+            logger.error("PermissionService | method = hasRole() | 登录过期");
+            throw new DescribeException(ExceptionEnum.LOGIN_EXPIRED);
+        }
+        List<String> authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
+        String roles = authorities.get(0);
+        boolean contains = roles.contains(role) || roles.contains(auth);
+        if (contains){
+            return true;
+        } else {
+            logger.error("PermissionService | method = hasRole() | 没有操作权限");
+            throw new DescribeException(ExceptionEnum.PERMISSION_DENIED);
+        }
+    }
+
+    public boolean hasAnyRoles(String...roles){
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null) {
+            logger.error("PermissionService | method = hasRole() | 登录过期");
+            throw new DescribeException(ExceptionEnum.LOGIN_EXPIRED);
+        }
+        boolean contains = false;
+        List<String> strings = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
+        for(String role:roles){
+            if(strings.contains(role)){
+                contains = true;
+                break;
+            }
+        }
+        if (contains){
+            return true;
+        } else {
+            logger.error("PermissionService | method = hasRole() | 没有操作权限");
+            throw new DescribeException(ExceptionEnum.PERMISSION_DENIED);
+        }
+    }
+}

+ 23 - 0
src/main/java/com/loan/system/service/Impl/UserServiceImpl.java

@@ -0,0 +1,23 @@
+package com.loan.system.service.Impl;
+
+import com.loan.system.domain.dto.UserLoginDTO;
+import com.loan.system.domain.entity.User;
+import com.loan.system.repository.UserRepository;
+import com.loan.system.service.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserServiceImpl implements UserService {
+    UserRepository userRepository;
+
+    @Autowired
+    public UserServiceImpl(UserRepository userRepository) {
+        this.userRepository = userRepository;
+    }
+    @Override
+    public User wxLogin(UserLoginDTO userLoginDTO) {
+        return userRepository.findByMobile(userLoginDTO.getTel());
+    }
+}

+ 8 - 0
src/main/java/com/loan/system/service/UserService.java

@@ -0,0 +1,8 @@
+package com.loan.system.service;
+
+import com.loan.system.domain.dto.UserLoginDTO;
+import com.loan.system.domain.entity.User;
+
+public interface UserService {
+    User wxLogin(UserLoginDTO userLoginDTO);
+}

+ 55 - 0
src/main/java/com/loan/system/utils/AuthUtil.java

@@ -0,0 +1,55 @@
+package com.loan.system.utils;
+
+import com.loan.system.domain.enums.AuthEnum;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @author : EdwinXu
+ * @date : Created in 2021/3/2 16:28
+ */
+public class AuthUtil {
+    /**
+     * 判断额外权限
+     * @param authEnum  操作对应的权限
+     * @param roles     除去第一个的额外权限
+     * @return          true or false
+     */
+    public static Boolean hasAuth(AuthEnum authEnum, String[] roles){
+        for (int i = 1; i < roles.length; i++) {
+            if (authEnum.equals(AuthEnum.valueOf(roles[i]))){
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static boolean checkRequestIsFromMobile(HttpServletRequest request){
+        boolean isMoblie = false;
+        String[] mobileAgents = { "iphone", "android","ipad", "phone", "mobile", "wap", "netfront", "java", "opera mobi",
+                "opera mini", "ucweb", "windows ce", "symbian", "series", "webos", "sony", "blackberry", "dopod",
+                "nokia", "samsung", "palmsource", "xda", "pieplus", "meizu", "midp", "cldc", "motorola", "foma",
+                "docomo", "up.browser", "up.link", "blazer", "helio", "hosin", "huawei", "novarra", "coolpad", "webos",
+                "techfaith", "palmsource", "alcatel", "amoi", "ktouch", "nexian", "ericsson", "philips", "sagem",
+                "wellcom", "bunjalloo", "maui", "smartphone", "iemobile", "spice", "bird", "zte-", "longcos",
+                "pantech", "gionee", "portalmmm", "jig browser", "hiptop", "benq", "haier", "^lct", "320x320",
+                "240x320", "176x220", "w3c ", "acs-", "alav", "alca", "amoi", "audi", "avan", "benq", "bird", "blac",
+                "blaz", "brew", "cell", "cldc", "cmd-", "dang", "doco", "eric", "hipt", "inno", "ipaq", "java", "jigs",
+                "kddi", "keji", "leno", "lg-c", "lg-d", "lg-g", "lge-", "maui", "maxo", "midp", "mits", "mmef", "mobi",
+                "mot-", "moto", "mwbp", "nec-", "newt", "noki", "oper", "palm", "pana", "pant", "phil", "play", "port",
+                "prox", "qwap", "sage", "sams", "sany", "sch-", "sec-", "send", "seri", "sgh-", "shar", "sie-", "siem",
+                "smal", "smar", "sony", "sph-", "symb", "t-mo", "teli", "tim-", "tosh", "tsm-", "upg1", "upsi", "vk-v",
+                "voda", "wap-", "wapa", "wapi", "wapp", "wapr", "webc", "winw", "winw", "xda", "xda-",
+                "Googlebot-Mobile" };
+        if (request.getHeader("User-Agent") != null) {
+            String agent = request.getHeader("User-Agent");
+            for (String mobileAgent : mobileAgents) {
+                if (agent.toLowerCase().contains(mobileAgent) && agent.toLowerCase().indexOf("windows nt")<=0 &&agent.toLowerCase().indexOf("macintosh")<=0) {
+                    isMoblie = true;
+                    break;
+                }
+            }
+        }
+        return isMoblie;
+    }
+}

+ 376 - 0
src/main/java/com/loan/system/utils/ExcelUtil.java

@@ -0,0 +1,376 @@
+package com.loan.system.utils;
+
+import com.loan.system.annotation.ExcelColumn;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.CharUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.poi.hssf.usermodel.HSSFDateUtil;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.net.URLEncoder;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author : EdwinXu
+ * @date : Created in 2020/11/25 21:53
+ */
+public class ExcelUtil {
+    private final static Logger logger = LoggerFactory.getLogger(ExcelUtil.class);
+    private final static String EXCEL2003 = "xls";
+    private final static String EXCEL2007 = "xlsx";
+
+    public static <T> List<T> readExcel(String path, Class<T> cls, MultipartFile file){
+        String fileName = file.getOriginalFilename();
+        if (!fileName.matches("^.+\\.(?i)(xls)$") && !fileName.matches("^.+\\.(?i)(xlsx)$")) {
+            logger.error("上传文件格式不正确");
+        }
+        List<T> dataList = new ArrayList<>();
+        Workbook workbook = null;
+        try {
+            InputStream is = file.getInputStream();
+            if (fileName.endsWith(EXCEL2007)) {
+//                FileInputStream is = new FileInputStream(new File(path));
+                workbook = new XSSFWorkbook(is);
+            }
+            if (fileName.endsWith(EXCEL2003)) {
+//                FileInputStream is = new FileInputStream(new File(path));
+                workbook = new HSSFWorkbook(is);
+            }
+            if (workbook != null) {
+                //类映射  注解 value-->bean columns
+                Map<String, List<Field>> classMap = new HashMap<>();
+                List<Field> fields = Stream.of(cls.getDeclaredFields()).collect(Collectors.toList());
+                fields.forEach(
+                        field -> {
+                            ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
+                            if (annotation != null) {
+                                String value = annotation.value();
+                                if (StringUtils.isBlank(value)) {
+                                    return;//return起到的作用和continue是相同的 语法
+                                }
+                                if (!classMap.containsKey(value)) {
+                                    classMap.put(value, new ArrayList<>());
+                                }
+                                field.setAccessible(true);
+                                classMap.get(value).add(field);
+                            }
+                        }
+                );
+                //索引-->columns
+                Map<Integer, List<Field>> reflectionMap = new HashMap<>(16);
+                //默认读取第一个sheet
+                Sheet sheet = workbook.getSheetAt(0);
+
+                boolean firstRow = true;
+                for (int i = sheet.getFirstRowNum(); i <= sheet.getLastRowNum(); i++) {
+                    Row row = sheet.getRow(i);
+                    //首行  提取注解
+                    if (firstRow) {
+                        for (int j = row.getFirstCellNum(); j <= row.getLastCellNum(); j++) {
+                            Cell cell = row.getCell(j);
+                            String cellValue = getCellValue(cell);
+                            if (classMap.containsKey(cellValue)) {
+                                reflectionMap.put(j, classMap.get(cellValue));
+                            }
+                        }
+                        firstRow = false;
+                    } else {
+                        //忽略空白行
+                        if (row == null) {
+                            continue;
+                        }
+                        try {
+                            T t = cls.newInstance();
+                            //判断是否为空白行
+                            boolean allBlank = true;
+                            for (int j = row.getFirstCellNum(); j <= row.getLastCellNum(); j++) {
+                                if (reflectionMap.containsKey(j)) {
+                                    Cell cell = row.getCell(j);
+                                    String cellValue = getCellValue(cell);
+                                    if (StringUtils.isNotBlank(cellValue)) {
+                                        allBlank = false;
+                                    }
+                                    List<Field> fieldList = reflectionMap.get(j);
+                                    fieldList.forEach(
+                                            x -> {
+                                                try {
+                                                    handleField(t, cellValue, x);
+                                                } catch (Exception e) {
+                                                    logger.error(String.format("reflect field:%s value:%s exception!", x.getName(), cellValue), e);
+                                                }
+                                            }
+                                    );
+                                }
+                            }
+                            if (!allBlank) {
+                                dataList.add(t);
+                            } else {
+                                logger.warn(String.format("row:%s is blank ignore!", i));
+                            }
+                        } catch (Exception e) {
+                            logger.error(String.format("parse row:%s exception!", i), e);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            logger.error("parse excel exception!", e);
+        } finally {
+            if (workbook != null) {
+                try {
+                    workbook.close();
+                } catch (Exception e) {
+                    logger.error("parse excel exception!", e);
+                }
+            }
+        }
+        return dataList;
+    }
+
+    private static <T> void handleField(T t, String value, Field field) throws Exception {
+        Class<?> type = field.getType();
+        if (type == void.class || StringUtils.isBlank(value)) {
+            return;
+        }
+        if (type == Object.class) {
+            field.set(t, value);
+            //数字类型
+        } else if (type.getSuperclass() == null || type.getSuperclass() == Number.class) {
+            if (type == int.class || type == Integer.class) {
+                field.set(t, NumberUtils.toInt(value));
+            } else if (type == long.class || type == Long.class) {
+                field.set(t, NumberUtils.toLong(value));
+            } else if (type == byte.class || type == Byte.class) {
+                field.set(t, NumberUtils.toByte(value));
+            } else if (type == short.class || type == Short.class) {
+                field.set(t, NumberUtils.toShort(value));
+            } else if (type == double.class || type == Double.class) {
+                field.set(t, NumberUtils.toDouble(value));
+            } else if (type == float.class || type == Float.class) {
+                field.set(t, NumberUtils.toFloat(value));
+            } else if (type == char.class || type == Character.class) {
+                field.set(t, CharUtils.toChar(value));
+            } else if (type == boolean.class) {
+                field.set(t, BooleanUtils.toBoolean(value));
+            } else if (type == BigDecimal.class) {
+                field.set(t, new BigDecimal(value));
+            }
+        } else if (type == Boolean.class) {
+            field.set(t, BooleanUtils.toBoolean(value));
+        } else if (type == Date.class) {
+            //
+            field.set(t, value);
+        } else if (type == String.class) {
+            field.set(t, value);
+        } else {
+            Constructor<?> constructor = type.getConstructor(String.class);
+            field.set(t, constructor.newInstance(value));
+        }
+    }
+
+    private static String getCellValue(Cell cell) {
+        if (cell == null) {
+            return "";
+        }
+        if (cell.getCellType() == CellType.NUMERIC) {
+            if (HSSFDateUtil.isCellDateFormatted(cell)) {
+                return HSSFDateUtil.getJavaDate(cell.getNumericCellValue()).toString();
+            } else {
+                return BigDecimal.valueOf(cell.getNumericCellValue()).toString();
+            }
+        } else if (cell.getCellType() == CellType.STRING) {
+            return StringUtils.trimToEmpty(cell.getStringCellValue());
+        } else if (cell.getCellType() == CellType.FORMULA) {
+            return StringUtils.trimToEmpty(cell.getCellFormula());
+        } else if (cell.getCellType() == CellType.BLANK) {
+            return "";
+        } else if (cell.getCellType() == CellType.BOOLEAN) {
+            return String.valueOf(cell.getBooleanCellValue());
+        } else if (cell.getCellType() == CellType.ERROR) {
+            return "ERROR";
+        } else {
+            return cell.toString().trim();
+        }
+
+    }
+
+    public static <T> void writeExcel(HttpServletResponse response, List<T> dataList, Class<T> cls){
+        Field[] fields = cls.getDeclaredFields();
+        // 获得行标题
+        List<Field> fieldList = Arrays.stream(fields)
+                .filter(field -> {
+                    ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
+                    if (annotation != null && annotation.col() > 0) {
+                        field.setAccessible(true);
+                        return true;
+                    }
+                    return false;
+                }).collect(Collectors.toList());
+        // 这个排序没有必要
+//        .sorted(Comparator.comparing(field -> {
+//                    int col = 0;
+//                    ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
+//                    if (annotation != null) {
+//                        col = annotation.col();
+//                    }
+//                    return col;
+//                }))
+
+        // 创建工作簿
+        Workbook wb = new XSSFWorkbook();
+        // 创建工作表
+        Sheet sheet = wb.createSheet("Sheet1");
+        AtomicInteger ai = new AtomicInteger();
+        {
+            // 行
+            Row row = sheet.createRow(ai.getAndIncrement());
+            AtomicInteger aj = new AtomicInteger();
+            //写入头部
+            fieldList.forEach(field -> {
+                ExcelColumn annotation = field.getAnnotation(ExcelColumn.class);
+                String columnName = "";
+                if (annotation != null) {
+                    columnName = annotation.value();
+                }
+//                int i = fieldList.indexOf(field);
+//                sheet.setColumnWidth(i,annotation.col());
+                Cell cell = row.createCell(aj.getAndIncrement());
+                CellStyle cellStyle = wb.createCellStyle();
+//                cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); // 设置背景色
+//                cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); //
+                cellStyle.setAlignment(HorizontalAlignment.LEFT); // 居中
+                cellStyle.setBorderBottom(BorderStyle.THIN); //下边框
+                cellStyle.setBorderLeft(BorderStyle.THIN);//左边框
+                cellStyle.setBorderTop(BorderStyle.THIN);//上边框
+                cellStyle.setBorderRight(BorderStyle.THIN);//右边框
+                Font font = wb.createFont();
+                font.setFontName("宋体");
+                cellStyle.setFont(font);
+                cell.setCellStyle(cellStyle);
+                cell.setCellValue(columnName);
+            });
+        }
+        // 写入内容
+        if (CollectionUtils.isNotEmpty(dataList)) {
+            dataList.forEach(t -> {
+                Row row1 = sheet.createRow(ai.getAndIncrement());
+                AtomicInteger aj = new AtomicInteger();
+                fieldList.forEach(field -> {
+//                    Class<?> type = field.getType();
+                    Object value = "";
+                    try {
+                        value = field.get(t);
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                    Cell cell = row1.createCell(aj.getAndIncrement());
+                    CellStyle cellStyle = wb.createCellStyle();
+
+                    cellStyle.setBorderBottom(BorderStyle.THIN); //下边框
+                    cellStyle.setBorderLeft(BorderStyle.THIN);//左边框
+                    cellStyle.setBorderTop(BorderStyle.THIN);//上边框
+                    cellStyle.setBorderRight(BorderStyle.THIN);//右边框
+
+                    Font font = wb.createFont();
+                    font.setFontName("宋体");
+                    cellStyle.setFont(font);
+                    cell.setCellStyle(cellStyle);
+
+                    if (value != null) {
+                        cell.setCellValue(value.toString());
+                    }
+                });
+            });
+        }
+        for (int i = 0; i < fieldList.size(); i++) {
+            sheet.autoSizeColumn((short)i);
+        }
+        Sheet sheetAt = wb.getSheetAt(0);
+        int physicalNumberOfRows = sheetAt.getPhysicalNumberOfRows();
+        int col = 0;
+        if (physicalNumberOfRows > 1 && sheet.getRow(0) != null) {
+            col = sheet.getRow(0).getPhysicalNumberOfCells();
+        }
+        for(int i = 0;i<col;i++){
+            Row row = sheetAt.getRow(0);
+            Cell cell = row.getCell(i);
+            if(cell != null&& cell.getStringCellValue().contains("金额") ){
+                for(int j = 1;j<physicalNumberOfRows;j++){
+                    Row row1 = sheetAt.getRow(j);
+                    Cell cell1 = row1.getCell(i);
+                    if(cell1 != null) {
+                        if(StringUtils.isNotEmpty(cell1.getStringCellValue()))
+                            row1.getCell(i).setCellValue(Double.parseDouble(cell1.getStringCellValue()));
+                    }
+                    else{
+                        continue;
+                    }
+                }
+            }
+
+        }
+
+        //冻结窗格
+//        wb.getSheet("Sheet1").createFreezePane(0, 1, 0, 1);
+        //浏览器下载excel
+        buildExcelDocument(UUID.randomUUID().toString()+".xlsx",wb,response);
+        //生成excel文件
+//        buildExcelFile(".\\default.xlsx",wb);
+    }
+
+    /**
+     * 浏览器下载excel
+     * @param fileName
+     * @param wb
+     * @param response
+     */
+
+    private static void buildExcelDocument(String fileName, Workbook wb, HttpServletResponse response){
+        try {
+            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+            response.setHeader("Content-Disposition", "attachment;filename="+ URLEncoder.encode(fileName, "utf-8"));
+            response.flushBuffer();
+            wb.write(response.getOutputStream());
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 生成excel文件
+     * @param path 生成excel路径
+     * @param wb
+     */
+    private static void buildExcelFile(String path, Workbook wb){
+
+        File file = new File(path);
+        if (file.exists()) {
+            file.delete();
+        }
+        try {
+            wb.write(new FileOutputStream(file));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+}

+ 489 - 0
src/main/java/com/loan/system/utils/FileUploadUtil.java

@@ -0,0 +1,489 @@
+package com.loan.system.utils;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+import com.loan.system.exception.DescribeException;
+import com.loan.system.exception.FileException;
+import net.coobird.thumbnailator.Thumbnails;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/26 - 20:52
+ * 使用前请指定 path 路径,直接设置即可
+ * 图片缩略图未具体实现
+ * 文件目录样例
+ *  |maintenance|uploads|project-1|contracts|contract-1|xxxxxxxxxxxx.pdf
+ *  ---------------------------------------------------|xxxxxxxxxxxx.jpg
+ *  ---------------------------------------------------|chunks |tempFile
+ *  ------------------------------|steps
+ *  ------------------------------|taskBook
+ */
+@Component
+public class FileUploadUtil {
+//    private static final Logger logger = LoggerFactory.getLogger(FileUploadUtil.class);
+//
+//    private static final String PNG = "png";
+//    private static String path;
+//    private static String host;
+//    private static String extensions;
+//
+//    @Value("${upload.location}")
+//    public void setPath(String path) {
+//        FileUploadUtil.path = path;
+//    }
+//    @Value("${upload.host}")
+//    public void setHost(String host) {
+//        FileUploadUtil.host = host;
+//    }
+//    @Value("${upload.extensions}")
+//    public void setExtensions(String extensions) {
+//        FileUploadUtil.extensions = extensions;
+//    }
+//
+//    /**
+//     * 上传文件
+//     * @param files 上传的文件对象(必须)
+//     * @param childFile     上传父目录,为空则直接上传到指定目录
+//     */
+//    public static <T extends Material> List<T> saveFile(MultipartFile[] files, String childFile, Class<T> tClass) throws IOException {
+//        // 关闭上传文件限制
+//        // final Boolean aBoolean = checkType(files, extensions);
+//        List<T> list = new ArrayList<>();
+//        for (MultipartFile file : files){
+//            // 验证是否为图片类型,可能需要额外处理
+//            Boolean isImage = checkIsImage(file, "JPG,jpg,bmp,BMP,gif,GIF,bmp,BMP,png,PNG,jpeg,JPEG,svg");
+//            T t = saveFile(file, childFile, isImage, tClass);
+//            list.add(t);
+//        }
+//        return list;
+//    }
+//
+//    /**
+//     * 创建文件
+//     * @param path 创建文件的路径
+//     */
+//    public static File createFile(String path) throws IOException {
+//        // 获取文件的完整目录 getFullPath() 获取的是文件的目录路径
+//        String fullPath = FilenameUtils.getFullPath(path);
+//        // 判断目录是否存在,不存在就创建一个目录
+//        File file = new File(fullPath);
+//        if (!file.isDirectory()){
+//            if (!file.mkdirs()){
+//                throw new FileException(ExceptionEnum.DIRECTORY_CREATE_ERROR);
+//            }
+//        }
+//        // 判断文件是否存在,不存在就创建
+//        file = new File(path);
+//        if (!file.exists()){
+//            if (!file.createNewFile()){
+//                throw new FileException(ExceptionEnum.FILE_CREATE_ERROR);
+//            }
+//        }
+//        return file;
+//    }
+//
+//    public static boolean createChunksFileFolder(String path) {
+//        // 获取文件的完整目录 getFullPath() 获取的是文件的目录路径
+//        String fullPath = FilenameUtils.getFullPath(path);
+//        // 判断目录是否存在,不存在就创建一个目录
+//        File file = new File(fullPath);
+//        if (!file.exists()){
+//            //创建文件夹
+//            return file.mkdirs();
+//        }
+//        return true;
+//    }
+//
+//    /**
+//     * 上传文件具体操作方法
+//     * @param file         上传的文件对象(必须)
+//     * @param childFile     上传父目录,为空则直接上传到指定目录
+//     * @param isImage       上传文件是否为图片
+//     */
+//    private static <T extends Material> T saveFile(MultipartFile file, String childFile, Boolean isImage, Class<T> tClass) throws IOException {
+//        // 文件名
+//        String fileName = file.getOriginalFilename();
+//        // 文件后缀名
+//        String extension = FilenameUtils.getExtension(fileName);
+//        // 创建文件的文件名
+//        String newFileName = getUniqueName(extension);
+//        // 创建文件保存路径
+//        String savePath = getServerPath(path, childFile+ '/' + newFileName);
+//        // 创建文件访问链接
+//        String link = getServerPath(host, childFile+ '/' + newFileName);
+//        // 创建文件
+//        File file1 = createFile(savePath);
+//        // 保存文件(未生成缩略图)
+//        InputStream is = file.getInputStream();
+//        FileOutputStream fos = new FileOutputStream(file1);
+//        BufferedOutputStream bos = new BufferedOutputStream(fos);
+//        byte[] buffer = new byte[1024];
+//        int length;
+//        try {
+//            while ((length = is.read(buffer)) > -1) {
+//                bos.write(buffer, 0, length);
+//                bos.flush();
+//            }
+//        } catch (IOException e) {
+//            e.printStackTrace();
+//        } finally {
+//            bos.close();
+//            fos.close();
+//            is.close();
+//        }
+//        // 拼装返回的数据
+//        try {
+//            T t = tClass.newInstance();
+//            t.setUnique_name(newFileName).setOriginal_name(fileName).setType(extension).setLink(link).setSize(file.getSize());
+//            return t;
+//        } catch (InstantiationException | IllegalAccessException e) {
+//            throw new DescribeException(-100,"内部错误");
+//        }
+//    }
+//
+//    /**
+//     * 压缩图片
+//     * @param originalPath  原始文件路径
+//     * @param thumbPath     压缩后文件路径
+//     * @param extension     文件后缀名
+//     */
+//    private static String thumbnails(String originalPath, String thumbPath, String extension) throws IOException {
+//        // outputFormat:文件输出后缀名
+//        // Thumbnails 如果用来压缩 png 格式的文件,会越压越大,
+//        // 得把 png 格式的图片转换为 jpg 格式
+//        if (PNG.equalsIgnoreCase(extension)) {
+//            String removeExtensionFilePath = FilenameUtils.removeExtension(thumbPath);
+//            // 由于 outputFormat 会自动在路径后加上后缀名,所以移除以前的后缀名
+//            Thumbnails.of(originalPath).scale(1f)
+//                    .outputQuality(0.5f)
+//                    .outputFormat("jpg")
+//                    .toFile(removeExtensionFilePath);
+//        } else {
+//            Thumbnails.of(originalPath).scale(1f).outputQuality(0.5f).toFile(thumbPath);
+//        }
+//        // 删除被压缩的文件
+////        FileUtils.forceDelete(new File(thumbPath));
+//        return thumbPath.replace(path, host);
+//    }
+//
+//    /**
+//     * 检查所有文件类型是否符合要求
+//     * @param files 所有文件
+//     * @param extensions 允许上传的文件类型
+//     */
+//    public static Boolean checkType(MultipartFile[] files, String extensions){
+//        Boolean aBoolean = true;
+//        for (MultipartFile file : files){
+//            if(ObjectUtils.isEmpty(file) || file.isEmpty()){
+//                throw new DescribeException(ExceptionEnum.FILE_UPLOAD_OBJECT_NOT_EXIST);
+//            }
+//            // 文件名
+//            String fileName = file.getOriginalFilename();
+//            // 文件后缀名
+//            String extension = FilenameUtils.getExtension(fileName);
+//            if (StringUtils.isEmpty(extension)) {
+//                throw new DescribeException(ExceptionEnum.FILE_UPLOAD_TYPE_NOT_DEFINED);
+//            }
+////            aBoolean = isContains(extensions, extension);
+////            if (BooleanUtils.isFalse(aBoolean)){
+////                break;
+////            }
+//        }
+//        return aBoolean;
+//    }
+//
+//    public static Boolean checkIsImage(MultipartFile file, String extensions){
+//        // 文件名
+//        String fileName = file.getOriginalFilename();
+//        // 文件后缀名
+//        String extension = FilenameUtils.getExtension(fileName);
+//        return isContains(extensions,extension);
+//    }
+//
+//    /**
+//     * 判断 extension 中是否存在 extensionName
+//     * @param extension         使用逗号隔开的字符串,精确匹配例如:txt,jpg,png,zip
+//     * @param extensionName     文件的后缀名
+//     */
+//    public static Boolean isContains(String extension, String extensionName){
+//       if (StringUtils.isNotEmpty(extension)){
+//           String[] extensions = StringUtils.split(extension, ",");
+//           if (ArrayUtils.isNotEmpty(extensions)){
+//               List<String> extensionsList = Arrays.asList(extensions);
+//               return extensionsList.contains(extensionName);
+//           }else {
+//                // 判断文件的后缀名是否为 extension
+//               return extension.equalsIgnoreCase(extensionName);
+//           }
+//       }
+//       return true;
+//    }
+//
+//    /**
+//     * 生成文件文件名
+//     * @param extension      上传文件后缀名
+//     */
+//    private static String getUniqueName(String extension){
+//        return UUID.randomUUID().toString() + "."+extension;
+//    }
+//
+//    /**
+//     * 生成文件在的实际的路径
+//     * @param destPath 文件的相对路径
+//     */
+//    public static String getServerPath(String path, String destPath){
+//        // 文件分隔符转化为当前系统的格式
+//        return FilenameUtils.separatorsToSystem(path + destPath);
+//    }
+//
+//    public static String getChunkFileFolderPath(String childFile){
+//        return getServerPath(path,childFile+ "/chunks/");
+//    }
+//
+//    /******************** 以下是为处理分片上传文件新添加的代码 2020-12-18 EdwinXu *******************/
+//
+//    /**
+//     * 检查文件是否上传,非临时文件,文件上传中间生成的临时文件在上传完后删除
+//     * @param fileMd5   文件唯一值
+//     * @param fileExt   文件后缀名
+//     */
+//    public static boolean register(String fileMd5, String childFile, String fileExt) throws IOException {
+//        // 关闭上传限制
+//        // Boolean aBoolean = isContains(extensions,fileExt);
+//        // 检查文件是否上传
+//        // 1.获取文件的路径
+//        String fileName = fileMd5 + "." + fileExt;
+//        String serverPath = getServerPath(path, childFile+ '/' + fileName);
+//        File file = createFile(serverPath);
+//        return ObjectUtils.isNotEmpty(file);
+//    }
+//        // 验证文件格式是否允许上传
+//
+//    /**
+//     * 检查块文件 应该去对应文件的临时文件夹下(chunk)
+//     * @param childFile     文件相对位置
+//     * @param chunk         块序号
+//     */
+//    public static boolean checkChunk(String childFile, Integer chunk) {
+//        //得到块文件所在路径
+//        String chunkFileFolderPath = getChunkFileFolderPath(childFile);
+//        //块文件的文件名称以1,2,3..序号命名,没有扩展名
+//        File chunkFile = new File(chunkFileFolderPath +chunk);
+//        return chunkFile.exists();
+//    }
+//
+//    /**
+//     * 块文件上传
+//     * @param file          文件
+//     * @param childFile     文件具体目录
+//     * @param chunk         文件块号
+//     */
+//    public static boolean uploadChunk(MultipartFile file, String childFile, Integer chunk) {
+//        if (ObjectUtils.isEmpty(file)) {
+//            throw new DescribeException(ExceptionEnum.FILE_IS_EMPTY);
+//        }
+//        TreeMap<Integer,Integer> treeMap = new TreeMap<>();
+//        int[] count = new int[40];
+//        Arrays.sort(count);
+//        //创建块文件目录
+//        boolean chunksFileFolder = createChunksFileFolder(getChunkFileFolderPath(childFile));
+//        //块文件
+//        File chunkFile = new File(getChunkFileFolderPath(childFile) + chunk);
+//        //上传的块文件
+//        InputStream inputStream = null;
+//        FileOutputStream outputStream = null;
+//        try {
+//            inputStream = file.getInputStream();
+//            outputStream = new FileOutputStream(chunkFile);
+//            IOUtils.copy(inputStream, outputStream);
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//            throw new DescribeException(ExceptionEnum.FILE_BLOCK_UPLOAD_FAILED);
+//        } finally {
+//            try {
+//                if (inputStream!=null){
+//                    inputStream.close();
+//                }
+//                if (outputStream!=null){
+//                    outputStream.close();
+//                }
+//            } catch (IOException e) {
+//                e.printStackTrace();
+//            }
+//        }
+//        return true;
+//    }
+//
+//    // 合并块文件
+//
+//    public static <T extends Material> T mergeChunks(String fileMd5, String fileName, String childFile, Long fileSize, String mimeType, String fileExt, Class<T> nClass) {
+//        //获取块文件的路径
+//        String chunkFileFolderPath = getChunkFileFolderPath(childFile);
+//        File chunkFileFolder = new File(chunkFileFolderPath);
+//        String uniqueName = fileMd5+"."+fileExt;
+//        if (!chunkFileFolder.exists()) {
+//            boolean mkdir = chunkFileFolder.mkdirs();
+//            if (!mkdir){
+//                throw new DescribeException(ExceptionEnum.FILE_MERGE_FAILED);
+//            }
+//        }
+//        //合并文件路径
+//        String originalPath  = getServerPath(path,childFile+"/"+uniqueName);
+//        String thumbPath = getServerPath(path,childFile+"/"+"thumb-"+uniqueName);
+//        File mergeFile = new File(originalPath);
+//        //创建合并文件
+//        //合并文件存在先删除再创建
+//        if (mergeFile.exists()) {
+//            boolean delete = mergeFile.delete();
+//            if (!delete){
+//                throw new DescribeException(ExceptionEnum.FILE_MERGE_FAILED);
+//            }
+//        }
+//        boolean newFile = false;
+//        try {
+//            newFile = mergeFile.createNewFile();
+//        } catch (IOException e) {
+//            e.printStackTrace();
+//        }
+//        if (!newFile) {
+//            throw new DescribeException(ExceptionEnum.FILE_MERGE_IS_EMPTY);
+//        }
+//        //获取块文件,此列表是已经排好序的列表
+//        List<File> chunkFiles = getChunkFiles(chunkFileFolder);
+//        //合并文件
+//        mergeFile = mergeFile(mergeFile, chunkFiles);
+//        if (mergeFile == null) {
+//            throw new DescribeException(ExceptionEnum.FILE_MERGE_FAILED);
+//        }
+//        boolean checkResult = checkFileMd5(mergeFile, fileMd5);
+//        if (!checkResult) {
+//            throw new DescribeException(ExceptionEnum.FILE_CHECK_FAILED);
+//        }
+//        try {
+//            // 删除临时文件下的文件块,防止后面继续上传
+//            deleteChunks(childFile);
+//            T t = nClass.newInstance();
+//            String link = getServerPath(host,childFile+'/'+uniqueName);
+//            uniqueName = fileMd5+childFile+"."+fileExt;
+//            t.setUnique_name(uniqueName)
+//                    .setOriginal_name(fileName)
+//                    .setType(fileExt)
+//                    .setLink(link)
+//                    .setSize(fileSize);
+//            // 如果是图片,则生成缩略图
+//            if (isContains("JPG,jpg,bmp,BMP,gif,GIF,bmp,BMP,png,PNG,jpeg,JPEG,svg",fileExt)){
+//                String thumbnails = thumbnails(originalPath, thumbPath, fileExt);
+//                if(PNG.equalsIgnoreCase(fileExt)){
+//                    thumbnails = thumbnails.substring(0,thumbnails.length()-3)+"jpg";
+//                }
+//                t.setThumbnails(thumbnails);
+//            }
+//            return t;
+//        } catch (InstantiationException | IllegalAccessException | IOException e) {
+//            e.printStackTrace();
+//            throw new DescribeException(ExceptionEnum.FILE_MERGE_FAILED);
+//        }
+//    }
+//
+//    // 校验文件的md5值
+//
+//    private static boolean checkFileMd5(File mergeFile, String md5) {
+//        if ( ObjectUtils.isEmpty(mergeFile) || StringUtils.isEmpty(md5)) {
+//            return false;
+//        }
+//        //进行md5校验
+//        FileInputStream mergeFileInputStream = null;
+//        try {
+//            mergeFileInputStream = new FileInputStream(mergeFile);
+//            //得到文件的md5
+//            String mergeFileMd5 = DigestUtils.md5Hex(mergeFileInputStream);
+//            logger.info("md5:--------->" + md5);
+//            logger.info("md5:--------->" + mergeFileMd5);
+//            //比较md5
+//            if (md5.equalsIgnoreCase(mergeFileMd5)) {
+//                return true;
+//            }
+//
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//        } finally {
+//            try {
+//                assert mergeFileInputStream != null;
+//                mergeFileInputStream.close();
+//            } catch (IOException e) {
+//                e.printStackTrace();
+//            }
+//        }
+//        return false;
+//    }
+//
+//    //获取所有块文件
+//
+//    private static List<File> getChunkFiles(File chunkFileFolder) {
+//        //获取路径下的所有块文件
+//        File[] chunkFiles = chunkFileFolder.listFiles();
+//        //将文件数组转成list,并排序
+//        assert chunkFiles != null;
+//        List<File> chunkFileList = new ArrayList<>(Arrays.asList(chunkFiles));
+//        //排序
+//        chunkFileList.sort((o1, o2) -> {
+//            int a = Integer.parseInt(o1.getName());
+//            int b = Integer.parseInt(o2.getName());
+//            if ( a > b) {
+//                return 1;
+//            }
+//            return a == b ? 0 : -1;
+//        });
+//        return chunkFileList;
+//    }
+//
+//    //合并文件
+//
+//    private static File mergeFile(File mergeFile, List<File> chunkFiles) {
+//        try {
+//            //创建写文件对象
+//            RandomAccessFile rafWrite = new RandomAccessFile(mergeFile, "rw");
+//            //遍历分块文件开始合并
+//            //读取文件缓冲区
+//            byte[] b = new byte[1024];
+//            for (File chunkFile : chunkFiles) {
+//                RandomAccessFile rafRead = new RandomAccessFile(chunkFile, "r");
+//                int len = -1;
+//                //读取分块文件
+//                while ((len = rafRead.read(b)) != -1) {
+//                    //向合并文件中写数据
+//                    rafWrite.write(b, 0, len);
+//                }
+//                rafRead.close();
+//            }
+//            rafWrite.close();
+//        } catch (Exception e) {
+//            e.printStackTrace();
+//            return null;
+//        }
+//        return mergeFile;
+//    }
+//
+//    public static void deleteChunks(String childFile){
+//        File file = new File(path + childFile + "/chunks");
+//        File[] files = file.listFiles();
+//        assert files != null;
+//        for (File file1:files){
+//            file1.delete();
+//        }
+//        file.delete();
+//    }
+}

+ 95 - 0
src/main/java/com/loan/system/utils/JpaUtil.java

@@ -0,0 +1,95 @@
+package com.loan.system.utils;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class JpaUtil {
+
+
+    /**
+     * @param src
+     * @param target
+     * @name 排除指定字段
+     */
+    public static void copyNotNullPropertiesExclude(Object src, Object target, String[] Field) {
+        BeanUtils.copyProperties(src, target, getNullPropertyNames(src, Field, true));
+    }
+
+    /**
+     * @param src
+     * @param target
+     * @name 允许指定字段
+     */
+    public static void copyNotNullPropertiesAllow(Object src, Object target, String[] excludeField) {
+        BeanUtils.copyProperties(src, target, getNullPropertyNames(src, excludeField, false));
+    }
+
+
+    /**
+     * @param src
+     * @param target
+     * @name 允许指定字段
+     */
+    public static void copyPropertiesAllow(Object src, Object target, String[] excludeField) {
+        BeanUtils.copyProperties(src, target, getPropertyNames(src, excludeField, false));
+    }
+
+    /**
+     * @param source
+     * @return
+     * @name 筛选忽略字段
+     */
+    public static String[] getNullPropertyNames(Object source, String[] excludeField, boolean tag) {
+        String[] result = null;
+        final BeanWrapper src = new BeanWrapperImpl(source);
+        java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();
+        Set<String> emptyNames = new HashSet<String>();
+        for (java.beans.PropertyDescriptor pd : pds) {
+            if (pd.getName().equals("baseData")) {
+                continue;
+            } else {
+                Object srcValue = src.getPropertyValue(pd.getName());
+                //pd.getName() , 存在 Allow ,Exclude
+                boolean contains = Arrays.asList(excludeField).contains(pd.getName());
+                if (srcValue == null || contains == tag) {
+                    emptyNames.add(pd.getName());
+                }
+            }
+        }
+        result = new String[emptyNames.size()];
+        return emptyNames.toArray(result);
+    }
+
+
+
+    /**
+     * @param source
+     * @return
+     * @name 筛选忽略字段 , 不去掉null
+     */
+    public static String[] getPropertyNames(Object source, String[] excludeField, boolean tag) {
+        String[] result = null;
+        final BeanWrapper src = new BeanWrapperImpl(source);
+        java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();
+        Set<String> emptyNames = new HashSet<String>();
+        for (java.beans.PropertyDescriptor pd : pds) {
+            if (pd.getName().equals("baseData")) {
+                continue;
+            } else {
+                Object srcValue = src.getPropertyValue(pd.getName());
+                //pd.getName() , 存在 Allow ,Exclude
+                boolean contains = Arrays.asList(excludeField).contains(pd.getName());
+                if (contains == tag) {
+                    emptyNames.add(pd.getName());
+                }
+            }
+        }
+        result = new String[emptyNames.size()];
+        return emptyNames.toArray(result);
+    }
+}

+ 135 - 0
src/main/java/com/loan/system/utils/JsonUtil.java

@@ -0,0 +1,135 @@
+package com.loan.system.utils;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializeConfig;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.alibaba.fastjson.serializer.SimpleDateFormatSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/15 - 19:23
+ * @Description 基于 fastjson 封装的 json 工具类
+ */
+public final class JsonUtil {
+
+    private final static Logger logger = LoggerFactory.getLogger(JsonUtil.class);
+    private static final SerializeConfig SERIALIZE_CONFIG;
+    private static final SerializerFeature[] SERIALIZER_FEATURES  = {
+            // 输出空置字段
+            SerializerFeature.WriteMapNullValue,
+            // list字段如果为null,输出为[],而不是null
+            SerializerFeature.WriteNullListAsEmpty,
+            // 数值字段如果为null,输出为0,而不是null
+            SerializerFeature.WriteNullNumberAsZero,
+            // Boolean字段如果为null,输出为false,而不是null
+            SerializerFeature.WriteNullBooleanAsFalse,
+            // 字符类型字段如果为null,输出为"",而不是null
+            SerializerFeature.WriteNullStringAsEmpty
+    };
+    static {
+        SERIALIZE_CONFIG = new SerializeConfig();
+        SERIALIZE_CONFIG.put(Date.class, new SimpleDateFormatSerializer("yyyy-MM-dd HH:mm:ss"));
+    }
+    /**
+     * Object to Json String 字符串输出
+     * @param object
+     * @return: java.lang.String
+     */
+    public static String toJson(Object object){
+        try {
+            return JSON.toJSONString(object);
+        } catch (Exception e) {
+            logger.error("JsonUtil | method=toJSON() | 对象转为Json字符串 Error!" + e.getMessage(), e);
+        }
+        return null;
+    }
+
+    /**
+     * Object TO Json String Json-lib 兼容的日期输出格式
+     * @param object
+     * @return: java.lang.String
+     */
+    public static String toJSONLib(Object object) {
+        try {
+            return JSON.toJSONString(object, SERIALIZE_CONFIG, SERIALIZER_FEATURES);
+        } catch (Exception e) {
+            logger.error("JsonUtil | method=toJSONLib() | 对象转为 Json 字符串 Json-lib 兼容的日期输出格式   Error!" + e.getMessage(), e);
+        }
+        return null;
+    }
+
+   /**
+    * 转换为数组 Object
+    * @param text
+    * @return: java.lang.Object[]
+    */
+    public static Object[] toArray(String text) {
+        try {
+            return toArray(text, null);
+        } catch (Exception e) {
+            logger.error("JsonUtil | method=toArray() | 将json格式的数据转换为数组 Object  Error!" + e.getMessage(), e);
+        }
+        return null;
+    }
+
+    /***
+     * 转换为数组 (可指定类型)
+     * @param text
+     * @param clazz
+     * @return: java.lang.Object[]
+     */
+    public static <T> Object[] toArray(String text, Class<T> clazz) {
+        try {
+            return JSON.parseArray(text, clazz).toArray();
+        } catch (Exception e) {
+            logger.error("JsonUtil | method=toArray() | 将json格式的数据转换为数组 (可指定类型)   Error!" + e.getMessage(), e);
+        }
+        return null;
+    }
+
+    /***
+     * Json 转为 Java Bean
+     * @param text
+     * @param clazz
+     * @return: T
+     */
+    public static <T> T toBean(String text, Class<T> clazz) {
+        try {
+            return JSON.parseObject(text, clazz);
+        } catch (Exception e) {
+            logger.error("JsonUtil | method=toBean() | Json 转为  Java Bean  Error!" + e.getMessage(), e);
+        }
+        return null;
+    }
+
+    /** Json 转为 Map */
+    public static Map<?, ?> toMap(String json) {
+        try {
+            return JSON.parseObject(json);
+        } catch (Exception e) {
+            logger.error("JsonUtil | method=toMap() | Json 转为   Map {},{}" + e.getMessage(), e);
+        }
+        return null;
+    }
+
+    /***
+     * Json 转 List,Class 集合中泛型的类型,非集合本身,可json-lib兼容的日期格式
+     * @param text
+     * @param clazz
+     * @return: java.util.List<T>
+     */
+    public static <T> List<T> toList(String text, Class<T> clazz) {
+        try {
+            return JSON.parseArray(text, clazz);
+        } catch (Exception e) {
+            logger.error("JsonUtil | method=toList() | Json 转为   List {},{}" + e.getMessage(), e);
+        }
+        return null;
+    }
+}

+ 107 - 0
src/main/java/com/loan/system/utils/JwtTokenUtil.java

@@ -0,0 +1,107 @@
+package com.loan.system.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/2 - 20:46
+ */
+@Component
+public class JwtTokenUtil implements Serializable {
+    private static final long serialVersionUID = 8998885548318234112L;
+
+    private static String secret;
+    private static Long expiration;
+    private static String header;
+    private static String token_prefix;
+    private static String issuer;
+
+    @Value("${JWT.SECRET}")
+    public void setSecret(String secret) {
+        JwtTokenUtil.secret = secret;
+    }
+    @Value("${JWT.EXPIRATION}")
+    public void setExpiration(Long expiration) {
+        JwtTokenUtil.expiration = expiration;
+    }
+    @Value("${JWT.HEADER}")
+    public void setHeader(String header) {
+        JwtTokenUtil.header = header;
+    }
+    @Value("${JWT.TOKEN_PREFIX}")
+    public void setTokenPrefix(String tokenPrefix) {
+        JwtTokenUtil.token_prefix = tokenPrefix;
+    }
+    @Value("${JWT.ISSUER}")
+    public void setIssuer(String issuer) {
+        JwtTokenUtil.issuer = issuer;
+    }
+    /**
+     * 生成 token
+     * @param username  用户
+     * @param roles     角色
+     */
+    public static String generateToken(String number, String username, List<String> roles){
+        // token 签发时间
+        final Date createdDate = new Date();
+        // token 过期时间
+        final Date expirationDate = new Date(createdDate.getTime() + expiration * 1000);
+        final HashMap<String, Object> claims = new HashMap<>(2);
+        claims.put("number",number);
+        claims.put("username",username);
+        claims.put("role",String.join(",",roles));
+        return token_prefix+Jwts.builder()
+                .setClaims(claims)
+                // token 签发者
+                .setIssuer(issuer)
+                .signWith(SignatureAlgorithm.HS256 , secret)
+                .setIssuedAt(createdDate)
+                .setExpiration(expirationDate)
+                // token 面向对象
+                .setSubject(username)
+                .compact();
+    }
+
+    public static Claims getClaimsFromToken(String token) {
+        Claims claims;
+        try {
+            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
+        } catch (ExpiredJwtException e) {
+            claims = e.getClaims();
+        }
+        return claims;
+    }
+
+    public static String getUsernameFromToken(String token) {
+        return getClaimsFromToken(token).getSubject();
+    }
+
+    public static String getNumberFromToken(String token){
+        return (String) getClaimsFromToken(token).get("number");
+    }
+
+    public static Boolean validateToken(String token, UserDetails userDetails) {
+        final String username = getUsernameFromToken(token);
+        return username.equals(userDetails.getUsername());
+    }
+
+    public static Boolean isTokenExpired(String token) {
+        final Date expiration = getExpirationDateFromToken(token);
+        return expiration.before(new Date());
+    }
+
+    public static Date getExpirationDateFromToken(String token) {
+        return getClaimsFromToken(token).getExpiration();
+    }
+}

+ 59 - 0
src/main/java/com/loan/system/utils/JwtUtil.java

@@ -0,0 +1,59 @@
+package com.loan.system.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.JwtBuilder;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Map;
+
+public class JwtUtil {
+    /**
+     * 生成jwt
+     * 使用Hs256算法, 私匙使用固定秘钥
+     *
+     * @param secretKey jwt秘钥
+     * @param ttlMillis jwt过期时间(毫秒)
+     * @param claims    设置的信息
+     * @return
+     */
+    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
+        // 指定签名的时候使用的签名算法,也就是header那部分
+        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
+
+        // 生成JWT的时间
+        long expMillis = System.currentTimeMillis() + ttlMillis;
+        Date exp = new Date(expMillis);
+
+        // 设置jwt的body
+        JwtBuilder builder = Jwts.builder()
+                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
+                .setClaims(claims)
+                // 设置签名使用的签名算法和签名使用的秘钥
+                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
+                // 设置过期时间
+                .setExpiration(exp);
+
+        return builder.compact();
+    }
+
+    /**
+     * Token解密
+     *
+     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
+     * @param token     加密后的token
+     * @return
+     */
+    public static Claims parseJWT(String secretKey, String token) {
+        // 得到DefaultJwtParser
+        Claims claims = Jwts.parser()
+                // 设置签名的秘钥
+                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
+                // 设置需要解析的jwt
+                .parseClaimsJws(token).getBody();
+        return claims;
+    }
+
+}

+ 50 - 0
src/main/java/com/loan/system/utils/PoiWordUtil.java

@@ -0,0 +1,50 @@
+package com.loan.system.utils;
+
+import com.deepoove.poi.XWPFTemplate;
+import com.deepoove.poi.util.PoitlIOUtils;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.HashMap;
+
+/**
+ * @author : EdwinXu
+ * @date : Created in 2021/1/26 16:15
+ */
+public class PoiWordUtil {
+//    public static void writeApprove(HttpServletResponse response, String path, Approve approveById, String downloadName){
+//        File file = new File(path);
+//        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+//        XWPFTemplate template = XWPFTemplate.compile(file).render(
+//                new HashMap<String, Object>(){{
+//                    put("projectName", approveById.getProjectName());
+//                    put("copies", approveById.getCopies());
+//                    put("submittingDepartment", approveById.getSubmittingDepartment());
+//                    put("submittingDate", format.format(approveById.getSubmittingDate()));
+//                    put("submitter", approveById.getSubmitter());
+//                    put("acceptDate", format.format(approveById.getAcceptDate()));
+//                    put("accepter", approveById.getAccepter());
+//                    put("logisticsServicesReviewComments", approveById.getLogisticsServicesReviewComments());
+//                    put("publicAffairsReviewComments", approveById.getPublicAffairsReviewComments());
+//                    put("leadersInChargeReviewComments", approveById.getLeadersInChargeReviewComments());
+//                    put("contractCommissionSignedOptions", approveById.getContractCommissionSignedOptions());
+//                    put("contractNumber", approveById.getContractNumber());
+//                }});
+//        try {
+//            response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=UTF-8");
+//            response.setHeader("Content-Disposition","attachment;filename=\""+downloadName+".docx"+"\"");
+//            OutputStream out = response.getOutputStream();
+//            BufferedOutputStream bos = new BufferedOutputStream(out);
+//            template.write(bos);
+//            bos.flush();
+//            out.flush();
+//            PoitlIOUtils.closeQuietlyMulti(template, bos, out);
+//        } catch (IOException e) {
+//            e.printStackTrace();
+//        }
+//    }
+}

+ 3273 - 0
src/main/java/com/loan/system/utils/RedisUtil.java

@@ -0,0 +1,3273 @@
+package com.loan.system.utils;
+
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.data.redis.RedisSystemException;
+import org.springframework.data.redis.connection.DataType;
+import org.springframework.data.redis.connection.RedisConnection;
+import org.springframework.data.redis.connection.RedisStringCommands;
+import org.springframework.data.redis.connection.ReturnType;
+import org.springframework.data.redis.connection.jedis.JedisConnection;
+import org.springframework.data.redis.connection.lettuce.LettuceConnection;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
+import org.springframework.data.redis.core.types.Expiration;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis工具类
+ *
+ * 声明: 此工具只简单包装了redisTemplate的大部分常用的api, 没有包装redisTemplate所有的api。
+ *      如果对此工具类中的功能不太满意, 或对StringRedisTemplate提供的api不太满意,
+ *      那么可自行实现相应的{@link StringRedisTemplate}类中的对应execute方法, 以达
+ *      到自己想要的效果; 至于如何实现,则可参考源码或{@link LockOps}中的方法。
+ *
+ * 注: 此工具类依赖spring-boot-starter-data-redis类库、以及可选的lombok、fastjson
+ * 注: 更多javadoc细节,可详见{@link RedisOperations}
+ *
+ * 统一说明一: 方法中的key、 value都不能为null。
+ * 统一说明二: 不能跨数据类型进行操作, 否者会操作失败/操作报错。
+ *            如: 向一个String类型的做Hash操作,会失败/报错......等等
+ *
+ */
+@Slf4j
+@Component
+@SuppressWarnings("unused")
+public class RedisUtil implements ApplicationContextAware {
+
+    /** 使用StringRedisTemplate(,其是RedisTemplate的定制化升级) */
+    private static StringRedisTemplate redisTemplate;
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        RedisUtil.redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
+    }
+
+    /**
+     * key相关操作
+     *
+     * @author JustryDeng
+     * @date 2020/3/7 16:54:25
+     */
+    public static class KeyOps {
+
+        /**
+         * 根据key, 删除redis中的对应key-value
+         *
+         *  注: 若删除失败, 则返回false。
+         *
+         *      若redis中,不存在该key, 那么返回的也是false。
+         *      所以,不能因为返回了false,就认为redis中一定还存
+         *      在该key对应的key-value。
+         *
+         * @param key
+         *            要删除的key
+         * @return  删除是否成功
+         * @date 2020/3/7 17:15:02
+         */
+        public static boolean delete(String key) {
+            log.info("delete(...) => key -> {}", key);
+            // 返回值只可能为true/false, 不可能为null
+            Boolean result = redisTemplate.delete(key);
+            log.info("delete(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 根据keys, 批量删除key-value
+         *
+         * 注: 若redis中,不存在对应的key, 那么计数不会加1, 即:
+         *     redis中存在的key-value里,有名为a1、a2的key,
+         *     删除时,传的集合是a1、a2、a3,那么返回结果为2。
+         *
+         * @param keys
+         *            要删除的key集合
+         * @return  删除了的key-value个数
+         * @date 2020/3/7 17:48:04
+         */
+        public static long delete(Collection<String> keys) {
+            log.info("delete(...) => keys -> {}", keys);
+            Long count = redisTemplate.delete(keys);
+            log.info("delete(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 将key对应的value值进行序列化,并返回序列化后的value值。
+         *
+         * 注: 若不存在对应的key, 则返回null。
+         * 注: dump时,并不会删除redis中的对应key-value。
+         * 注: dump功能与restore相反。
+         *
+         * @param key
+         *            要序列化的value的key
+         * @return  序列化后的value值
+         * @date 2020/3/8 11:34:13
+         */
+        public static byte[] dump(String key) {
+            log.info("dump(...) =>key -> {}", key);
+            byte[] result = redisTemplate.dump(key);
+            log.info("dump(...) => result -> {}", result);
+            return result;
+        }
+
+        /**
+         * 将给定的value值,反序列化到redis中, 形成新的key-value。
+         *
+         * @param key
+         *            value对应的key
+         * @param value
+         *            要反序列的value值。
+         *            注: 这个值可以由{@link this#dump(String)}获得
+         * @param timeToLive
+         *            反序列化后的key-value的存活时长
+         * @param unit
+         *            timeToLive的单位
+         *
+         * @throws RedisSystemException
+         *             如果redis中已存在同样的key时,抛出此异常
+         * @date 2020/3/8 11:36:45
+         */
+        public static void restore(String key, byte[] value, long timeToLive, TimeUnit unit) {
+            restore(key, value, timeToLive, unit, false);
+        }
+
+        /**
+         * 将给定的value值,反序列化到redis中, 形成新的key-value。
+         *
+         * @param key
+         *            value对应的key
+         * @param value
+         *            要反序列的value值。
+         *            注: 这个值可以由{@link this#dump(String)}获得
+         * @param timeout
+         *            反序列化后的key-value的存活时长
+         * @param unit
+         *            timeout的单位
+         * @param replace
+         *            若redis中已经存在了相同的key, 是否替代原来的key-value
+         *
+         * @throws RedisSystemException
+         *             如果redis中已存在同样的key, 且replace为false时,抛出此异常
+         * @date 2020/3/8 11:36:45
+         */
+        public static void restore(String key, byte[] value, long timeout, TimeUnit unit, boolean replace) {
+            log.info("restore(...) => key -> {}, value -> {}, timeout -> {}, unit -> {}, replace -> {}",
+                    key, value, timeout, unit, replace);
+            redisTemplate.restore(key, value, timeout, unit, replace);
+        }
+
+        /**
+         * redis中是否存在,指定key的key-value
+         *
+         * @param key
+         *            指定的key
+         * @return  是否存在对应的key-value
+         * @date 2020/3/8 12:16:46
+         */
+        public static boolean hasKey(String key) {
+            log.info("hasKey(...) => key -> {}", key);
+            Boolean result = redisTemplate.hasKey(key);
+            log.info("hasKey(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 给指定的key对应的key-value设置: 多久过时
+         *
+         * 注:过时后,redis会自动删除对应的key-value。
+         * 注:若key不存在,那么也会返回false。
+         *
+         * @param key
+         *            指定的key
+         * @param timeout
+         *            过时时间
+         * @param unit
+         *            timeout的单位
+         * @return  操作是否成功
+         * @date 2020/3/8 12:18:58
+         */
+        public static boolean expire(String key, long timeout, TimeUnit unit) {
+            log.info("expire(...) => key -> {}, timeout -> {}, unit -> {}", key, timeout, unit);
+            Boolean result = redisTemplate.expire(key, timeout, unit);
+            log.info("expire(...) => result is -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 给指定的key对应的key-value设置: 什么时候过时
+         *
+         * 注:过时后,redis会自动删除对应的key-value。
+         * 注:若key不存在,那么也会返回false。
+         *
+         * @param key
+         *            指定的key
+         * @param date
+         *            啥时候过时
+         *
+         * @return  操作是否成功
+         * @date 2020/3/8 12:19:29
+         */
+        public static boolean expireAt(String key, Date date) {
+            log.info("expireAt(...) => key -> {}, date -> {}", key, date);
+            Boolean result = redisTemplate.expireAt(key, date);
+            log.info("expireAt(...) => result is -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 找到所有匹配pattern的key,并返回该key的结合.
+         *
+         * 提示:若redis中键值对较多,此方法耗时相对较长,慎用!慎用!慎用!
+         *
+         * @param pattern
+         *            匹配模板。
+         *            注: 常用的通配符有:
+         *                 ?    有且只有一个;
+         *                 *     >=0哥;
+         *
+         * @return  匹配pattern的key的集合。 可能为null。
+         * @date 2020/3/8 12:38:38
+         */
+        public static Set<String> keys(String pattern) {
+            log.info("keys(...) => pattern -> {}", pattern);
+            Set<String> keys = redisTemplate.keys(pattern);
+            log.info("keys(...) => keys -> {}", keys);
+            return keys;
+        }
+
+        /**
+         * 将当前数据库中的key对应的key-value,移动到对应位置的数据库中。
+         *
+         * 注:单机版的redis,默认将存储分为16个db, index为0 到 15。
+         * 注:同一个db下,key唯一; 但是在不同db中,key可以相同。
+         * 注:若目标db下,已存在相同的key, 那么move会失败,返回false。
+         *
+         * @param key
+         *            定位要移动的key-value的key
+         * @param dbIndex
+         *            要移动到哪个db
+         * @return 移动是否成功。
+         *         注: 若目标db下,已存在相同的key, 那么move会失败,返回false。
+         * @date 2020/3/8 13:01:00
+         */
+        public static boolean move(String key, int dbIndex) {
+            log.info("move(...) => key  -> {}, dbIndex -> {}", key, dbIndex);
+            Boolean result = redisTemplate.move(key, dbIndex);
+            log.info("move(...) =>result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 移除key对应的key-value的过期时间, 使该key-value一直存在
+         *
+         * 注: 若key对应的key-value,本身就是一直存在(无过期时间的), 那么persist方法会返回false;
+         *    若没有key对应的key-value存在,本那么persist方法会返回false;
+         *
+         * @param key
+         *            定位key-value的key
+         * @return 操作是否成功
+         * @date 2020/3/8 13:10:02
+         */
+        public static boolean persist(String key) {
+            log.info("persist(...) => key -> {}", key);
+            Boolean result = redisTemplate.persist(key);
+            log.info("persist(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 获取key对应的key-value的过期时间
+         *
+         * 注: 若key-value永不过期, 那么返回的为-1。
+         * 注: 若不存在key对应的key-value, 那么返回的为-2
+         * 注:若存在零碎时间不足1 SECONDS,则(大体上)四舍五入到SECONDS级别。
+         *
+         * @param key
+         *            定位key-value的key
+         * @return  过期时间(单位s)
+         * @date 2020/3/8 13:17:35
+         */
+        public static long getExpire(String key) {
+            Long result = getExpire(key, TimeUnit.SECONDS);
+            return result;
+        }
+
+        /**
+         * 获取key对应的key-value的过期时间
+         *
+         * 注: 若key-value永不过期, 那么返回的为-1。
+         * 注: 若不存在key对应的key-value, 那么返回的为-2
+         * 注:若存在零碎时间不足1 unit,则(大体上)四舍五入到unit别。
+         *
+         * @param key
+         *            定位key-value的key
+         * @return  过期时间(单位unit)
+         * @date 2020/3/8 13:17:35
+         */
+        public static long getExpire(String key, TimeUnit unit) {
+            log.info("getExpire(...) =>key -> {}, unit is -> {}", key, unit);
+            Long result = redisTemplate.getExpire(key, unit);
+            log.info("getExpire(...) => result ->  {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 从redis的所有key中,随机获取一个key
+         *
+         * 注: 若redis中不存在任何key-value, 那么这里返回null
+         *
+         * @return  随机获取到的一个key
+         * @date 2020/3/8 14:11:43
+         */
+        public static String randomKey() {
+            String result = redisTemplate.randomKey();
+            log.info("randomKey(...) => result is -> {}", result);
+            return result;
+        }
+
+        /**
+         * 重命名对应的oldKey为新的newKey
+         *
+         * 注: 若oldKey不存在, 则会抛出异常.
+         * 注: 若redis中已存在与newKey一样的key,
+         *     那么原key-value会被丢弃,
+         *     只留下新的key,以及原来的value
+         *     示例说明: 假设redis中已有 (keyAlpha, valueAlpha) 和 (keyBeta, valueBeat),
+         *              在使用rename(keyAlpha, keyBeta)替换后, redis中只会剩下(keyBeta, valueAlpha)
+         *
+         * @param oldKey
+         *            旧的key
+         * @param newKey
+         *            新的key
+         * @throws RedisSystemException
+         *             若oldKey不存在时, 抛出此异常
+         * @date 2020/3/8 14:14:17
+         */
+        public static void rename(String oldKey, String newKey) {
+            log.info("rename(...) => oldKey -> {}, newKey -> {}", oldKey, newKey);
+            redisTemplate.rename(oldKey, newKey);
+        }
+
+        /**
+         * 当redis中不存在newKey时, 重命名对应的oldKey为新的newKey。
+         * 否者不进行重命名操作。
+         *
+         * 注: 若oldKey不存在, 则会抛出异常.
+         *
+         * @param oldKey
+         *            旧的key
+         * @param newKey
+         *            新的key
+         * @throws RedisSystemException
+         *             若oldKey不存在时, 抛出此异常
+         * @date 2020/3/8 14:14:17
+         */
+        public static boolean renameIfAbsent(String oldKey, String newKey) {
+            log.info("renameIfAbsent(...) => oldKey -> {}, newKey -> {}", oldKey, newKey);
+            Boolean result = redisTemplate.renameIfAbsent(oldKey, newKey);
+            log.info("renameIfAbsent(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 获取key对应的value的数据类型
+         *
+         * 注: 若redis中不存在该key对应的key-value, 那么这里返回NONE。
+         *
+         * @param key
+         *            用于定位的key
+         * @return  key对应的value的数据类型
+         * @date 2020/3/8 14:40:16
+         */
+        public static DataType type(String key) {
+            log.info("type(...) => key -> {}", key);
+            DataType result = redisTemplate.type(key);
+            log.info("type(...) => result -> {}", result);
+            return result;
+        }
+    }
+
+    /**
+     * string相关操作
+     *
+     * 提示: redis中String的数据结构可参考resources/data-structure/String(字符串)的数据结构(示例一).png
+     *      redis中String的数据结构可参考resources/data-structure/String(字符串)的数据结构(示例二).png
+     *
+     * @author JustryDeng
+     * @date 2020/3/7 16:54:25
+     */
+    public static class StringOps {
+
+        /**
+         * 设置key-value
+         *
+         * 注: 若已存在相同的key, 那么原来的key-value会被丢弃。
+         *
+         * @param key
+         *            key
+         * @param value
+         *            key对应的value
+         * @date 2020/3/8 15:40:59
+         */
+        public static void set(String key, String value) {
+            log.info("set(...) => key -> {}, value -> {}", key, value);
+            redisTemplate.opsForValue().set(key, value);
+        }
+
+        /**
+         * 处理redis中key对应的value值, 将第offset位的值, 设置为1或0。
+         *
+         * 说明: 在redis中,存储的字符串都是以二级制的进行存在的; 如存储的key-value里,值为abc,实际上,
+         *       在redis里面存储的是011000010110001001100011,前8为对应a,中间8为对应b,后面8位对应c。
+         *       示例:这里如果setBit(key, 6, true)的话,就是将索引位置6的那个数,设置值为1,值就变成
+         *            了011000110110001001100011
+         *       追注:offset即index,从0开始。
+         *
+         * 注: 参数value为true, 则设置为1;参数value为false, 则设置为0。
+         *
+         * 注: 若redis中不存在对应的key,那么会自动创建新的。
+         * 注: offset可以超过value在二进制下的索引长度。
+         *
+         * @param key
+         *            定位value的key
+         * @param offset
+         *            要改变的bit的索引
+         * @param value
+         *            改为1或0, true - 改为1, false - 改为0
+         *
+         * @return set是否成功
+         * @date 2020/3/8 16:30:37
+         */
+        public static boolean setBit(String key, long offset, boolean value) {
+            log.info("setBit(...) => key -> {}, offset -> {}, value -> {}", key, offset, value);
+            Boolean result = redisTemplate.opsForValue().setBit(key, offset, value);
+            log.info("setBit(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 设置key-value
+         *
+         * 注: 若已存在相同的key, 那么原来的key-value会被丢弃
+         *
+         * @param key
+         *            key
+         * @param value
+         *            key对应的value
+         * @param timeout
+         *            过时时长
+         * @param unit
+         *            timeout的单位
+         * @date 2020/3/8 15:40:59
+         */
+        public static void setEx(String key, String value, long timeout, TimeUnit unit) {
+            log.info("setEx(...) => key -> {}, value -> {}, timeout -> {}, unit -> {}",
+                    key, value, timeout, unit);
+            redisTemplate.opsForValue().set(key, value, timeout, unit);
+        }
+
+        /**
+         * 若不存在key时, 向redis中添加key-value, 返回成功/失败。
+         * 若存在,则不作任何操作, 返回false。
+         *
+         * @param key
+         *            key
+         * @param value
+         *            key对应的value
+         *
+         * @return set是否成功
+         * @date 2020/3/8 16:51:36
+         */
+        public static boolean setIfAbsent(String key, String value) {
+            log.info("setIfAbsent(...) => key -> {}, value -> {}", key, value);
+            Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value);
+            log.info("setIfAbsent(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 若不存在key时, 向redis中添加一个(具有超时时长的)key-value, 返回成功/失败。
+         * 若存在,则不作任何操作, 返回false。
+         *
+         * @param key
+         *            key
+         * @param value
+         *            key对应的value
+         * @param timeout
+         *            超时时长
+         * @param unit
+         *            timeout的单位
+         *
+         * @return set是否成功
+         * @date 2020/3/8 16:51:36
+         */
+        public static boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {
+            log.info("setIfAbsent(...) => key -> {}, value -> {}, key -> {}, value -> {}", key, value, timeout, unit);
+            Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
+            log.info("setIfAbsent(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 从(redis中key对应的)value的offset位置起(包含该位置),用replaceValue替换对应长度的值。
+         *
+         * 举例说明:
+         *       1.假设redis中存在key-value ("ds", "0123456789"); 调
+         *         用setRange("ds", "abcdefghijk", 3)后, redis中该value值就变为了[012abcdefghijk]
+         *
+         *       2.假设redis中存在key-value ("jd", "0123456789");调
+         * 		   用setRange("jd", "xyz", 3)后, redis中该value值就变为了[012xyz6789]
+         *
+         *       3.假设redis中存在key-value ("ey", "0123456789");调
+         * 		   用setRange("ey", "qwer", 15)后, redis中该value值就变为了[0123456789     qwer]
+         *       注:case3比较特殊,offset超过了原value的长度了, 中间就会有一些空格来填充,但是如果在程序
+         *          中直接输出的话,中间那部分空格可能会出现乱码。
+         *
+         * @param key
+         *            定位key-value的key
+         * @param replaceValue
+         *            要替换的值
+         * @param offset
+         *            起始位置
+         * @date 2020/3/8 17:04:31
+         */
+        public static void setRange(String key, String replaceValue, long offset) {
+            log.info("setRange(...) => key -> {}, replaceValue -> {}, offset -> {}", key, replaceValue, offset);
+            redisTemplate.opsForValue().set(key, replaceValue, offset);
+        }
+
+        /**
+         * 获取到key对应的value的长度。
+         *
+         * 注: 长度等于{@link String#length}。
+         * 注: 若redis中不存在对应的key-value, 则返回值为0.
+         *
+         * @param key
+         *            定位value的key
+         * @return  value的长度
+         * @date 2020/3/8 17:14:30
+         */
+        public static long size(String key) {
+            log.info("size(...) => key -> {}", key);
+            Long result = redisTemplate.opsForValue().size(key);
+            log.info("size(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 批量设置 key-value
+         *
+         * 注: 若存在相同的key, 则原来的key-value会被丢弃。
+         *
+         * @param maps
+         *            key-value 集
+         * @date 2020/3/8 17:21:19
+         */
+        public static void multiSet(Map<String, String> maps) {
+            log.info("multiSet(...) => maps -> {}", maps);
+            redisTemplate.opsForValue().multiSet(maps);
+        }
+
+        /**
+         * 当redis中,不存在任何一个keys时, 才批量设置 key-value, 并返回成功/失败.
+         * 否者,不进行任何操作, 并返回false。
+         *
+         * 即: 假设调用此方法时传入的参数map是这样的: {k1=v1, k2=v2, k3=v3}
+         *     那么redis中, k1、k2、k3都不存在时,才会批量设置key-value;
+         *     否则不会设置任何key-value。
+         *
+         * 注: 若存在相同的key, 则原来的key-value会被丢弃。
+         *
+         * 注:
+         *
+         * @param maps
+         *            key-value 集
+         *
+         * @return 操作是否成功
+         * @date 2020/3/8 17:21:19
+         */
+        public static boolean multiSetIfAbsent(Map<String, String> maps) {
+            log.info("multiSetIfAbsent(...) => maps -> {}", maps);
+            Boolean result = redisTemplate.opsForValue().multiSetIfAbsent(maps);
+            log.info("multiSetIfAbsent(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 增/减 整数
+         *
+         * 注: 负数则为减。
+         * 注: 若key对应的value值不支持增/减操作(即: value不是数字), 那么会
+         *     抛出org.springframework.data.redis.RedisSystemException
+         *
+         * @param key
+         *            用于定位value的key
+         * @param increment
+         *            增加多少
+         * @return  增加后的总值。
+         * @throws RedisSystemException key对应的value值不支持增/减操作时
+         * @date 2020/3/8 17:45:51
+         */
+        public static long incrBy(String key, long increment) {
+            log.info("incrBy(...) => key -> {}, increment -> {}", key, increment);
+            Long result = redisTemplate.opsForValue().increment(key, increment);
+            log.info("incrBy(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 增/减 浮点数
+         *
+         * 注: 慎用浮点数,会有精度问题。
+         *     如: 先 RedisUtil.StringOps.set("ds", "123");
+         *         然后再RedisUtil.StringOps.incrByFloat("ds", 100.6);
+         *         就会看到精度问题。
+         * 注: 负数则为减。
+         * 注: 若key对应的value值不支持增/减操作(即: value不是数字), 那么会
+         *     抛出org.springframework.data.redis.RedisSystemException
+         *
+         * @param key
+         *            用于定位value的key
+         * @param increment
+         *            增加多少
+         * @return  增加后的总值。
+         * @throws RedisSystemException key对应的value值不支持增/减操作时
+         * @date 2020/3/8 17:45:51
+         */
+        public static double incrByFloat(String key, double increment) {
+            log.info("incrByFloat(...) => key -> {}, increment -> {}", key, increment);
+            Double result = redisTemplate.opsForValue().increment(key, increment);
+            log.info("incrByFloat(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 追加值到末尾
+         *
+         * 注: 当redis中原本不存在key时,那么(从效果上来看)此方法就等价于{@link this#set(String, String)}
+         *
+         * @param key
+         *            定位value的key
+         * @param value
+         *            要追加的value值
+         * @return 追加后, 整个value的长度
+         * @date 2020/3/8 17:59:21
+         */
+        public static int append(String key, String value) {
+            log.info("append(...) => key -> {}, value -> {}", key, value);
+            Integer result = redisTemplate.opsForValue().append(key, value);
+            log.info("append(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 根据key,获取到对应的value值
+         *
+         * @param key
+         *            key-value对应的key
+         * @return  该key对应的值。
+         *          注: 若key不存在, 则返回null。
+         *
+         * @date 2020/3/8 16:27:41
+         */
+        public static String get(String key) {
+            log.info("get(...) => key -> {}", key);
+            String result = redisTemplate.opsForValue().get(key);
+            log.info("get(...) => result -> {} ", result);
+            return result;
+        }
+
+        /**
+         * 对(key对应的)value进行截取, 截取范围为[start, end]
+         *
+         * 注: 若[start, end]的范围不在value的范围中,那么返回的是空字符串 ""
+         * 注: 若value只有一部分在[start, end]的范围中,那么返回的是value对应部分的内容(即:不足的地方,并不会以空来填充)
+         *
+         * @param key
+         *            定位value的key
+         * @param start
+         *            起始位置 (从0开始)
+         * @param end
+         *            结尾位置 (从0开始)
+         * @return  截取后的字符串
+         * @date 2020/3/8 18:08:45
+         */
+        public static String getRange(String key, long start, long end) {
+            log.info("getRange(...) => kry -> {}", key);
+            String result = redisTemplate.opsForValue().get(key, start, end);
+            log.info("getRange(...) => result -> {} ", result);
+            return result;
+        }
+
+        /**
+         * 给指定key设置新的value, 并返回旧的value
+         *
+         * 注: 若redis中不存在key, 那么此操作仍然可以成功, 不过返回的旧值是null
+         *
+         * @param key
+         *            定位value的key
+         * @param newValue
+         *            要为该key设置的新的value值
+         * @return  旧的value值
+         * @date 2020/3/8 18:14:24
+         */
+        public static String getAndSet(String key, String newValue) {
+            log.info("getAndSet(...) => key -> {}, value -> {}", key, newValue);
+            String oldValue = redisTemplate.opsForValue().getAndSet(key, newValue);
+            log.info("getAndSet(...) => oldValue -> {}", oldValue);
+            return oldValue;
+        }
+
+        /**
+         * 获取(key对应的)value在二进制下,offset位置的bit值。
+         *
+         * 注: 当offset的值在(二进制下的value的)索引范围外时, 返回的也是false。
+         *
+         * 示例:
+         *      RedisUtil.StringOps.set("akey", "a");
+         *      字符串a, 转换为二进制为01100001
+         *      那么getBit("akey", 6)获取到的结果为false。
+         *
+         * @param key
+         *            定位value的key
+         * @param offset
+         *            定位bit的索引
+         * @return  offset位置对应的bit的值(true - 1, false - 0)
+         * @date 2020/3/8 18:21:10
+         */
+        public static boolean getBit(String key, long offset) {
+            log.info("getBit(...) => key -> {}, offset -> {}", key, offset);
+            Boolean result = redisTemplate.opsForValue().getBit(key, offset);
+            log.info("getBit(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 批量获取value值
+         *
+         * 注: 若redis中,对应的key不存在,那么该key对应的返回的value值为null
+         *
+         * @param keys
+         *            key集
+         * @return  value值集合
+         * @date 2020/3/8 18:26:33
+         */
+        public static List<String> multiGet(Collection<String> keys) {
+            log.info("multiGet(...) => keys -> {}", keys);
+            List<String> result = redisTemplate.opsForValue().multiGet(keys);
+            log.info("multiGet(...) => result -> {}", result);
+            return result;
+        }
+    }
+
+    /**
+     * hash相关操作
+     *
+     * 提示: 简单的,可以将redis中hash的数据结构看作是 Map<String, Map<HK, HV>>
+     * 提示: redis中String的数据结构可参考resources/data-structure/Hash(散列)的数据结构(示例一).png
+     *      redis中String的数据结构可参考resources/data-structure/Hash(散列)的数据结构(示例二).png
+     *
+     * @author JustryDeng
+     * @date 2020/3/8 23:39:26
+     */
+    public static class HashOps {
+
+        /**
+         * 向key对应的hash中,增加一个键值对entryKey-entryValue
+         *
+         * 注: 同一个hash里面,若已存在相同的entryKey, 那么此操作将丢弃原来的entryKey-entryValue,
+         *     而使用新的entryKey-entryValue。
+         *
+         *
+         * @param key
+         *            定位hash的key
+         * @param entryKey
+         *            要向hash中增加的键值对里的 键
+         * @param entryValue
+         *            要向hash中增加的键值对里的 值
+         * @date 2020/3/8 23:49:52
+         */
+        public static void hPut(String key, String entryKey, String entryValue) {
+            log.info("hPut(...) => key -> {}, entryKey -> {}, entryValue -> {}", key, entryKey, entryValue);
+            redisTemplate.opsForHash().put(key, entryKey, entryValue);
+        }
+
+        /**
+         * 向key对应的hash中,增加maps(即: 批量增加entry集)
+         *
+         * 注: 同一个hash里面,若已存在相同的entryKey, 那么此操作将丢弃原来的entryKey-entryValue,
+         *     而使用新的entryKey-entryValue
+         *
+         * @param key
+         *            定位hash的key
+         * @param maps
+         *            要向hash中增加的键值对集
+         * @date 2020/3/8 23:49:52
+         */
+        public static void hPutAll(String key, Map<String, String> maps) {
+            log.info("hPutAll(...) => key -> {}, maps -> {}", key, maps);
+            redisTemplate.opsForHash().putAll(key, maps);
+        }
+
+        /**
+         * 当key对应的hash中,不存在entryKey时,才(向key对应的hash中,)增加entryKey-entryValue
+         * 否者,不进行任何操作
+         *
+         * @param key
+         *            定位hash的key
+         * @param entryKey
+         *            要向hash中增加的键值对里的 键
+         * @param entryValue
+         *            要向hash中增加的键值对里的 值
+         *
+         * @return 操作是否成功。
+         * @date 2020/3/8 23:49:52
+         */
+        public static boolean hPutIfAbsent(String key, String entryKey, String entryValue) {
+            log.info("hPutIfAbsent(...) => key -> {}, entryKey -> {}, entryValue -> {}",
+                    key, entryKey, entryValue);
+            Boolean result = redisTemplate.opsForHash().putIfAbsent(key, entryKey, entryValue);
+            log.info("hPutIfAbsent(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 获取到key对应的hash里面的对应字段的值
+         *
+         * 注: 若redis中不存在对应的key, 则返回null。
+         *     若key对应的hash中不存在对应的entryKey, 也会返回null。
+         *
+         * @param key
+         *            定位hash的key
+         * @param entryKey
+         *            定位hash里面的entryValue的entryKey
+         *
+         * @return  key对应的hash里的entryKey对应的entryValue值
+         * @date 2020/3/9 9:09:30
+         */
+        public static Object hGet(String key, String entryKey) {
+            log.info("hGet(...) => key -> {}, entryKey -> {}", key, entryKey);
+            Object entryValue = redisTemplate.opsForHash().get(key, entryKey);
+            log.info("hGet(...) => entryValue -> {}", entryValue);
+            return entryValue;
+        }
+
+        /**
+         * 获取到key对应的hash(即: 获取到key对应的Map<HK, HV>)
+         *
+         * 注: 若redis中不存在对应的key, 则返回一个没有任何entry的空的Map(,而不是返回null)。
+         *
+         * @param key
+         *            定位hash的key
+         *
+         * @return  key对应的hash。
+         * @date 2020/3/9 9:09:30
+         */
+        public static Map<Object, Object> hGetAll(String key) {
+            log.info("hGetAll(...) => key -> {}",  key);
+            Map<Object, Object> result = redisTemplate.opsForHash().entries(key);
+            log.info("hGetAll(...) => result -> {}", result);
+            return result;
+        }
+
+        /**
+         * 批量获取(key对应的)hash中的entryKey的entryValue
+         *
+         * 注: 若hash中对应的entryKey不存在,那么返回的对应的entryValue值为null
+         * 注: redis中key不存在,那么返回的List中,每个元素都为null。
+         *     追注: 这个List本身不为null, size也不为0, 只是每个list中的每个元素为null而已。
+         *
+         * @param key
+         *            定位hash的key
+         * @param entryKeys
+         *            需要获取的hash中的字段集
+         * @return  hash中对应entryKeys的对应entryValue集
+         * @date 2020/3/9 9:25:38
+         */
+        public static List<Object> hMultiGet(String key, Collection<Object> entryKeys) {
+            log.info("hMultiGet(...) => key -> {}, entryKeys -> {}", key, entryKeys);
+            List<Object> entryValues = redisTemplate.opsForHash().multiGet(key, entryKeys);
+            log.info("hMultiGet(...) => entryValues -> {}", entryValues);
+            return entryValues;
+        }
+
+        /**
+         * (批量)删除(key对应的)hash中的对应entryKey-entryValue
+         *
+         * 注: 1、若redis中不存在对应的key, 则返回0;
+         *     2、若要删除的entryKey,在key对应的hash中不存在,在count不会+1, 如:
+         *                 RedisUtil.HashOps.hPut("ds", "name", "邓沙利文");
+         *                 RedisUtil.HashOps.hPut("ds", "birthday", "1994-02-05");
+         *                 RedisUtil.HashOps.hPut("ds", "hobby", "女");
+         *                 则调用RedisUtil.HashOps.hDelete("ds", "name", "birthday", "hobby", "non-exist-entryKey")
+         *                 的返回结果为3
+         * 注: 若(key对应的)hash中的所有entry都被删除了,那么该key也会被删除
+         *
+         * @param key
+         *            定位hash的key
+         * @param entryKeys
+         *            定位要删除的entryKey-entryValue的entryKey
+         *
+         * @return 删除了对应hash中多少个entry
+         * @date 2020/3/9 9:37:47
+         */
+        public static long hDelete(String key, Object... entryKeys) {
+            log.info("hDelete(...) => key -> {}, entryKeys -> {}", key, entryKeys);
+            Long count = redisTemplate.opsForHash().delete(key, entryKeys);
+            log.info("hDelete(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 查看(key对应的)hash中,是否存在entryKey对应的entry
+         *
+         * 注: 若redis中不存在key,则返回false。
+         * 注: 若key对应的hash中不存在对应的entryKey, 也会返回false。
+         *
+         * @param key
+         *            定位hash的key
+         * @param entryKey
+         *            定位hash中entry的entryKey
+         *
+         * @return  hash中是否存在entryKey对应的entry.
+         * @date 2020/3/9 9:51:55
+         */
+        public static boolean hExists(String key, String entryKey) {
+            log.info("hDelete(...) => key -> {}, entryKeys -> {}", key, entryKey);
+            Boolean exist = redisTemplate.opsForHash().hasKey(key, entryKey);
+            log.info("hDelete(...) => exist -> {}", exist);
+            return exist;
+        }
+
+        /**
+         * 增/减(hash中的某个entryValue值) 整数
+         *
+         * 注: 负数则为减。
+         * 注: 若key不存在,那么会自动创建对应的hash,并创建对应的entryKey、entryValue,entryValue的初始值为increment。
+         * 注: 若entryKey不存在,那么会自动创建对应的entryValue,entryValue的初始值为increment。
+         * 注: 若key对应的value值不支持增/减操作(即: value不是数字), 那么会
+         *     抛出org.springframework.data.redis.RedisSystemException
+         *
+         * @param key
+         *            用于定位hash的key
+         * @param entryKey
+         *            用于定位entryValue的entryKey
+         * @param increment
+         *            增加多少
+         * @return  增加后的总值。
+         * @throws RedisSystemException key对应的value值不支持增/减操作时
+         * @date 2020/3/9 10:09:28
+         */
+        public static long hIncrBy(String key, Object entryKey, long increment) {
+            log.info("hIncrBy(...) => key -> {}, entryKey -> {}, increment -> {}",
+                    key, entryKey, increment);
+            Long result = redisTemplate.opsForHash().increment(key, entryKey, increment);
+            log.info("hIncrBy(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 增/减(hash中的某个entryValue值) 浮点数
+         *
+         * 注: 负数则为减。
+         * 注: 若key不存在,那么会自动创建对应的hash,并创建对应的entryKey、entryValue,entryValue的初始值为increment。
+         * 注: 若entryKey不存在,那么会自动创建对应的entryValue,entryValue的初始值为increment。
+         * 注: 若key对应的value值不支持增/减操作(即: value不是数字), 那么会
+         *     抛出org.springframework.data.redis.RedisSystemException
+         * 注: 因为是浮点数, 所以可能会和{@link StringOps#incrByFloat(String, double)}一样, 出现精度问题。
+         *     追注: 本人简单测试了几组数据,暂未出现精度问题。
+         *
+         * @param key
+         *            用于定位hash的key
+         * @param entryKey
+         *            用于定位entryValue的entryKey
+         * @param increment
+         *            增加多少
+         * @return  增加后的总值。
+         * @throws RedisSystemException key对应的value值不支持增/减操作时
+         * @date 2020/3/9 10:09:28
+         */
+        public static double hIncrByFloat(String key, Object entryKey, double increment) {
+            log.info("hIncrByFloat(...) => key -> {}, entryKey -> {}, increment -> {}",
+                    key, entryKey, increment);
+            Double result = redisTemplate.opsForHash().increment(key, entryKey, increment);
+            log.info("hIncrByFloat(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 获取(key对应的)hash中的所有entryKey
+         *
+         * 注: 若key不存在,则返回的是一个空的Set(,而不是返回null)
+         *
+         * @param key
+         *            定位hash的key
+         *
+         * @return  hash中的所有entryKey
+         * @date 2020/3/9 10:30:13
+         */
+        public static Set<Object> hKeys(String key) {
+            log.info("hKeys(...) => key -> {}", key);
+            Set<Object> entryKeys = redisTemplate.opsForHash().keys(key);
+            log.info("hKeys(...) => entryKeys -> {}", entryKeys);
+            return entryKeys;
+        }
+
+        /**
+         * 获取(key对应的)hash中的所有entryValue
+         *
+         * 注: 若key不存在,则返回的是一个空的List(,而不是返回null)
+         *
+         * @param key
+         *            定位hash的key
+         *
+         * @return  hash中的所有entryValue
+         * @date 2020/3/9 10:30:13
+         */
+        public static List<Object> hValues(String key) {
+            log.info("hValues(...) => key -> {}", key);
+            List<Object> entryValues = redisTemplate.opsForHash().values(key);
+            log.info("hValues(...) => entryValues -> {}", entryValues);
+            return entryValues;
+        }
+
+        /**
+         * 获取(key对应的)hash中的所有entry的数量
+         *
+         * 注: 若redis中不存在对应的key, 则返回值为0
+         *
+         * @param key
+         *            定位hash的key
+         *
+         * @return  (key对应的)hash中,entry的个数
+         * @date 2020/3/9 10:41:01
+         */
+        public static long hSize(String key) {
+            log.info("hSize(...) => key -> {}", key);
+            Long count = redisTemplate.opsForHash().size(key);
+            log.info("hSize(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 根据options匹配到(key对应的)hash中的对应的entryKey, 并返回对应的entry集
+         *
+         *
+         * 注: ScanOptions实例的创建方式举例:
+         *     1、ScanOptions.NONE
+         *     2、ScanOptions.scanOptions().match("n??e").build()
+         *
+         * @param key
+         *            定位hash的key
+         * @param options
+         *            匹配entryKey的条件
+         *            注: ScanOptions.NONE表示全部匹配。
+         *            注: ScanOptions.scanOptions().match(pattern).build()表示按照pattern匹配,
+         *                其中pattern中可以使用通配符 * ? 等,
+         *                * 表示>=0个字符
+         *                ? 表示有且只有一个字符
+         *                此处的匹配规则与{@link KeyOps#keys(String)}处的一样。
+         *
+         * @return  匹配到的(key对应的)hash中的entry
+         * @date 2020/3/9 10:49:27
+         */
+        public static Cursor<Entry<Object, Object>> hScan(String key, ScanOptions options) {
+            log.info("hScan(...) => key -> {}, options -> {}", key, JSON.toJSONString(options));
+            Cursor<Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(key, options);
+            log.info("hScan(...) => cursor -> {}", JSON.toJSONString(cursor));
+            return cursor;
+        }
+    }
+
+    /**
+     * list相关操作
+     *
+     * 提示: 列表中的元素,可以重复。
+     *
+     * 提示: list是有序的。
+     *
+     * 提示: redis中的list中的索引,可分为两类,这两类都可以用来定位list中元素:
+     *      类别一: 从left到right, 是从0开始依次增大:   0,  1,  2,  3...
+     *      类别二: 从right到left, 是从-1开始依次减小: -1, -2, -3, -4...
+     *
+     * 提示: redis中String的数据结构可参考resources/data-structure/List(列表)的数据结构(示例一).png
+     *      redis中String的数据结构可参考resources/data-structure/List(列表)的数据结构(示例二).png
+     *
+     * @author JustryDeng
+     * @date 2020/3/9 11:30:48
+     */
+    public static class ListOps {
+
+        /**
+         * 从左端推入元素进列表
+         *
+         * 注: 若redis中不存在对应的key, 那么会自动创建
+         *
+         * @param key
+         *            定位list的key
+         * @param item
+         *            要推入list的元素
+         *
+         * @return 推入后,(key对应的)list的size
+         * @date 2020/3/9 11:56:05
+         */
+        public static long lLeftPush(String key, String item) {
+            log.info("lLeftPush(...) => key -> {}, item -> {}", key, item);
+            Long size = redisTemplate.opsForList().leftPush(key, item);
+            log.info("lLeftPush(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 从左端批量推入元素进列表
+         *
+         * 注: 若redis中不存在对应的key, 那么会自动创建
+         * 注: 这一批item中,先push左侧的, 后push右侧的
+         *
+         * @param key
+         *            定位list的key
+         * @param items
+         *            要批量推入list的元素集
+         *
+         * @return 推入后,(key对应的)list的size
+         * @date 2020/3/9 11:56:05
+         */
+        public static long lLeftPushAll(String key, String... items) {
+            log.info("lLeftPushAll(...) => key -> {}, items -> {}", key, items);
+            Long size = redisTemplate.opsForList().leftPushAll(key, items);
+            log.info("lLeftPushAll(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 从左端批量推入元素进列表
+         *
+         * 注: 若redis中不存在对应的key, 那么会自动创建
+         * 注: 这一批item中,那个item先从Collection取出来,就先push哪个
+         *
+         * @param key
+         *            定位list的key
+         * @param items
+         *            要批量推入list的元素集
+         *
+         * @return 推入后,(key对应的)list的size
+         * @date 2020/3/9 11:56:05
+         */
+        public static long lLeftPushAll(String key, Collection<String> items) {
+            log.info("lLeftPushAll(...) => key -> {}, items -> {}", key, items);
+            Long size = redisTemplate.opsForList().leftPushAll(key, items);
+            log.info("lLeftPushAll(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 如果redis中存在key, 则从左端批量推入元素进列表;
+         * 否则,不进行任何操作
+         *
+         * @param key
+         *            定位list的key
+         * @param item
+         *            要推入list的项
+         *
+         * @return  推入后,(key对应的)list的size
+         * @date 2020/3/9 13:40:08
+         */
+        public static long lLeftPushIfPresent(String key, String item) {
+            log.info("lLeftPushIfPresent(...) => key -> {}, item -> {}", key, item);
+            Long size = redisTemplate.opsForList().leftPushIfPresent(key, item);
+            log.info("lLeftPushIfPresent(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 若key对应的list中存在pivot项, 那么将item放入第一个pivot项前(即:放在第一个pivot项左边);
+         * 若key对应的list中不存在pivot项, 那么不做任何操作, 直接返回-1。
+         *
+         * 注: 若redis中不存在对应的key, 那么会自动创建
+         *
+         * @param key
+         *            定位list的key
+         * @param item
+         *            要推入list的元素
+         *
+         * @return 推入后,(key对应的)list的size
+         * @date 2020/3/9 11:56:05
+         */
+        public static long lLeftPush(String key, String pivot, String item) {
+            log.info("lLeftPush(...) => key -> {}, pivot -> {}, item -> {}", key, pivot, item);
+            Long size = redisTemplate.opsForList().leftPush(key, pivot, item);
+            log.info("lLeftPush(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 与{@link ListOps#lLeftPush(String, String)}类比即可, 不过是从list右侧推入元素
+         */
+        public static long lRightPush(String key, String item) {
+            log.info("lRightPush(...) => key -> {}, item -> {}", key, item);
+            Long size = redisTemplate.opsForList().rightPush(key, item);
+            log.info("lRightPush(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 与{@link ListOps#lLeftPushAll(String, String...)}类比即可, 不过是从list右侧推入元素
+         */
+        public static long lRightPushAll(String key, String... items) {
+            log.info("lRightPushAll(...) => key -> {}, items -> {}", key, items);
+            Long size = redisTemplate.opsForList().rightPushAll(key, items);
+            log.info("lRightPushAll(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 与{@link ListOps#lLeftPushAll(String, Collection<String>)}类比即可, 不过是从list右侧推入元素
+         */
+        public static long lRightPushAll(String key, Collection<String> items) {
+            log.info("lRightPushAll(...) => key -> {}, items -> {}", key, items);
+            Long size = redisTemplate.opsForList().rightPushAll(key, items);
+            log.info("lRightPushAll(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 与{@link ListOps#lLeftPushIfPresent(String, String)}类比即可, 不过是从list右侧推入元素
+         */
+        public static long lRightPushIfPresent(String key, String item) {
+            log.info("lRightPushIfPresent(...) => key -> {}, item -> {}", key, item);
+            Long size = redisTemplate.opsForList().rightPushIfPresent(key, item);
+            log.info("lRightPushIfPresent(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 与{@link ListOps#lLeftPush(String, String, String)}类比即可, 不过是从list右侧推入元素
+         */
+        public static long lRightPush(String key, String pivot, String item) {
+            log.info("lLeftPush(...) => key -> {}, pivot -> {}, item -> {}", key, pivot, item);
+            Long size = redisTemplate.opsForList().rightPush(key, pivot, item);
+            log.info("lLeftPush(...) => size -> {}",  size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 【非阻塞队列】 从左侧移出(key对应的)list中的第一个元素, 并将该元素返回
+         *
+         * 注: 此方法是非阻塞的, 即: 若(key对应的)list中的所有元素都被pop移出了,此时,再进行pop的话,会立即返回null
+         * 注: 此方法是非阻塞的, 即: 若redis中不存在对应的key,那么会立即返回null
+         * 注: 若将(key对应的)list中的所有元素都pop完了,那么该key会被删除
+         *
+         * @param key
+         *            定位list的key
+         * @return  移出的那个元素
+         * @date 2020/3/9 14:33:56
+         */
+        public static String lLeftPop(String key) {
+            log.info("lLeftPop(...) => key -> {}", key);
+            String item = redisTemplate.opsForList().leftPop(key);
+            log.info("lLeftPop(...) => item -> {}", item);
+            return item;
+        }
+
+        /**
+         * 【阻塞队列】 从左侧移出(key对应的)list中的第一个元素, 并将该元素返回
+         *
+         * 注: 此方法是阻塞的, 即: 若(key对应的)list中的所有元素都被pop移出了,此时,再进行pop的话,
+         *     会阻塞timeout这么久,然后返回null
+         * 注: 此方法是阻塞的, 即: 若redis中不存在对应的key,那么会阻塞timeout这么久,然后返回null
+         * 注: 若将(key对应的)list中的所有元素都pop完了,那么该key会被删除
+         *
+         * 提示: 若阻塞过程中, 目标key-list出现了,且里面有item了,那么会立马停止阻塞, 进行元素移出并返回
+         *
+         * @param key
+         *            定位list的key
+         * @param timeout
+         *            超时时间
+         * @param unit
+         *            timeout的单位
+         * @return  移出的那个元素
+         * @date 2020/3/9 14:33:56
+         */
+        public static String lLeftPop(String key, long timeout, TimeUnit unit) {
+            log.info("lLeftPop(...) => key -> {}, timeout -> {}, unit -> {}", key, timeout, unit);
+            String item = redisTemplate.opsForList().leftPop(key, timeout, unit);
+            log.info("lLeftPop(...) => item -> {}", item);
+            return item;
+        }
+
+        /**
+         * 与{@link ListOps#lLeftPop(String)}类比即可, 不过是从list右侧移出元素
+         */
+        public static String lRightPop(String key) {
+            log.info("lRightPop(...) => key -> {}", key);
+            String item = redisTemplate.opsForList().rightPop(key);
+            log.info("lRightPop(...) => item -> {}", item);
+            return item;
+        }
+
+        /**
+         * 与{@link ListOps#lLeftPop(String, long, TimeUnit)}类比即可, 不过是从list右侧移出元素
+         */
+        public static String lRightPop(String key, long timeout, TimeUnit unit) {
+            log.info("lRightPop(...) => key -> {}, timeout -> {}, unit -> {}", key, timeout, unit);
+            String item = redisTemplate.opsForList().rightPop(key, timeout, unit);
+            log.info("lRightPop(...) => item -> {}", item);
+            return item;
+        }
+
+        /**
+         * 【非阻塞队列】 从sourceKey对应的sourceList右侧移出一个item, 并将这个item推
+         *              入(destinationKey对应的)destinationList的左侧
+         *
+         * 注: 若sourceKey对应的list中没有item了,则立马认为(从sourceKey对应的list中pop出来的)item为null,
+         *     null并不会往destinationKey对应的list中push。
+         *     追注: 此时,此方法的返回值是null。
+         *
+         * 注: 若将(sourceKey对应的)list中的所有元素都pop完了,那么该sourceKey会被删除。
+         *
+         * @param sourceKey
+         *            定位sourceList的key
+         * @param destinationKey
+         *            定位destinationList的key
+         *
+         * @return 移动的这个元素
+         * @date 2020/3/9 15:06:59
+         */
+        public static String lRightPopAndLeftPush(String sourceKey, String destinationKey) {
+            log.info("lRightPopAndLeftPush(...) => sourceKey -> {}, destinationKey -> {}",
+                    sourceKey, destinationKey);
+            String item = redisTemplate.opsForList().rightPopAndLeftPush(sourceKey, destinationKey);
+            log.info("lRightPopAndLeftPush(...) => item -> {}", item);
+            return item;
+        }
+
+        /**
+         * 【阻塞队列】 从sourceKey对应的sourceList右侧移出一个item, 并将这个item推
+         *            入(destinationKey对应的)destinationList的左侧
+         *
+         * 注: 若sourceKey对应的list中没有item了,则阻塞等待, 直到能从sourceList中移出一个非null的item(或等待时长超时);
+         *     case1: 等到了一个非null的item, 那么继续下面的push操作,并返回这个item。
+         *     case2: 超时了,还没等到非null的item, 那么pop出的结果就未null,此时并不会往destinationList进行push。
+         *            此时,此方法的返回值是null。
+         *
+         * 注: 若将(sourceKey对应的)list中的所有元素都pop完了,那么该sourceKey会被删除。
+         *
+         * @param sourceKey
+         *            定位sourceList的key
+         * @param destinationKey
+         *            定位destinationList的key
+         * @param timeout
+         *            超时时间
+         * @param unit
+         *            timeout的单位
+         *
+         * @return 移动的这个元素
+         * @date 2020/3/9 15:06:59
+         */
+        public static String lRightPopAndLeftPush(String sourceKey, String destinationKey, long timeout,
+                                                  TimeUnit unit) {
+            log.info("lRightPopAndLeftPush(...) => sourceKey -> {}, destinationKey -> {}, timeout -> {},"
+                    + " unit -> {}", sourceKey, destinationKey, timeout, unit);
+            String item = redisTemplate.opsForList().rightPopAndLeftPush(sourceKey, destinationKey, timeout, unit);
+            log.info("lRightPopAndLeftPush(...) => item -> {}", item);
+            return item;
+        }
+
+        /**
+         * 设置(key对应的)list中对应索引位置index处的元素为item
+         *
+         * 注: 若key不存在,则会抛出org.springframework.data.redis.RedisSystemException
+         * 注: 若索引越界,也会抛出org.springframework.data.redis.RedisSystemException
+         *
+         * @param key
+         *            定位list的key
+         * @param index
+         *            定位list中的元素的索引
+         * @param item
+         *            要替换成的值
+         * @date 2020/3/9 15:39:50
+         */
+        public static void lSet(String key, long index, String item) {
+            log.info("lSet(...) => key -> {}, index -> {}, item -> {}", key, index, item);
+            redisTemplate.opsForList().set(key, index, item);
+        }
+
+        /**
+         * 通过索引index, 获取(key对应的)list中的元素
+         *
+         * 注: 若key不存在 或 index超出(key对应的)list的索引范围,那么返回null
+         *
+         * @param key
+         *            定位list的key
+         * @param index
+         *            定位list中的item的索引
+         *
+         * @return  list中索引index对应的item
+         * @date 2020/3/10 0:27:23
+         */
+        public static String lIndex(String key, long index) {
+            log.info("lIndex(...) => key -> {}, index -> {}", key, index);
+            String item = redisTemplate.opsForList().index(key, index);
+            log.info("lIndex(...) => item -> {}", item);
+            return item;
+        }
+
+        /**
+         * 获取(key对应的)list中索引在[start, end]之间的item集
+         *
+         * 注: 含start、含end。
+         * 注: 当key不存在时,获取到的是空的集合。
+         * 注: 当获取的范围比list的范围还要大时,获取到的是这两个范围的交集。
+         *
+         * 提示: 可通过RedisUtil.ListOps.lRange(key, 0, -1)来获取到该key对应的整个list
+         *
+         * @param key
+         *            定位list的key
+         * @param start
+         *            起始元素的index
+         * @param end
+         *            结尾元素的index
+         *
+         * @return  对应的元素集合
+         * @date 2020/3/10 0:34:59
+         */
+        public static List<String> lRange(String key, long start, long end) {
+            log.info("lRange(...) => key -> {}, start -> {}, end -> {}", key, start, end);
+            List<String> result = redisTemplate.opsForList().range(key, start, end);
+            log.info("lRange(...) => result -> {}", result);
+            return result;
+        }
+
+        /**
+         * 获取(key对应的)list
+         *
+         * @see ListOps#lRange(String, long, long)
+         *
+         * @param key
+         *            定位list的key
+         * @return  (key对应的)list
+         * @date 2020/3/10 0:46:50
+         */
+        public static List<String> lWholeList(String key) {
+            log.info("lWholeList(...) => key -> {}", key);
+            List<String> result = redisTemplate.opsForList().range(key, 0, -1);
+            log.info("lWholeList(...) => result -> {}", result);
+            return result;
+        }
+
+        /**
+         * 获取(key对应的)list的size
+         *
+         * 注: 当key不存在时,获取到的size为0.
+         *
+         * @param key
+         *            定位list的key
+         *
+         * @return list的size。
+         *
+         * @date 2020/3/10 0:48:40
+         */
+        public static long lSize(String key) {
+            log.info("lSize(...) => key -> {}", key);
+            Long size = redisTemplate.opsForList().size(key);
+            log.info("lSize(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 删除(key对应的)list中,前expectCount个值等于item的项
+         *
+         * 注: 若expectCount == 0, 则表示删除list中所有的值等于item的项.
+         * 注: 若expectCount > 0,  则表示删除从左往右进行
+         * 注: 若expectCount < 0,  则表示删除从右往左进行
+         *
+         * 注: 若list中,值等于item的项的个数少于expectCount时,那么会删除list中所有的值等于item的项。
+         * 注: 当key不存在时, 返回0。
+         * 注: 若lRemove后, 将(key对应的)list中没有任何元素了,那么该key会被删除。
+         *
+         * @param key
+         *            定位list的key
+         * @param expectCount
+         *            要删除的item的个数
+         * @param item
+         *            要删除的item
+         *
+         * @return  实际删除了的item的个数
+         * @date 2020/3/10 0:52:57
+         */
+        public static long lRemove(String key, long expectCount, String item) {
+            log.info("lRemove(...) => key -> {}, expectCount -> {}, item -> {}", key, expectCount, item);
+            Long actualCount = redisTemplate.opsForList().remove(key, expectCount, item);
+            log.info("lRemove(...) => actualCount -> {}", actualCount);
+            if (actualCount == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return actualCount;
+        }
+
+        /**
+         * 裁剪(即: 对list中的元素取交集。)
+         *
+         * 举例说明: list中的元素索引范围是[0, 8], 而这个方法传入的[start, end]为 [3, 10],
+         *          那么裁剪就是对[0, 8]和[3, 10]进行取交集, 得到[3, 8], 那么裁剪后
+         *          的list中,只剩下(原来裁剪前)索引在[3, 8]之间的元素了。
+         *
+         * 注: 若裁剪后的(key对应的)list就是空的,那么该key会被删除。
+         *
+         * @param key
+         *            定位list的key
+         * @param start
+         *            要删除的item集的起始项的索引
+         * @param end
+         *            要删除的item集的结尾项的索引
+         * @date 2020/3/10 1:16:58
+         */
+        public static void lTrim(String key, long start, long end) {
+            log.info("lTrim(...) => key -> {}, start -> {}, end -> {}", key, start, end);
+            redisTemplate.opsForList().trim(key, start, end);
+        }
+
+    }
+
+    /**
+     * set相关操作
+     *
+     * 提示: set中的元素,不可以重复。
+     * 提示: set是无序的。
+     * 提示: redis中String的数据结构可参考resources/data-structure/Set(集合)的数据结构(示例一).png
+     *      redis中String的数据结构可参考resources/data-structure/Set(集合)的数据结构(示例二).png
+     *
+     * @author JustryDeng
+     * @date 2020/3/9 11:30:48
+     */
+    public static class SetOps {
+
+        /**
+         * 向(key对应的)set中添加items
+         *
+         * 注: 若key不存在,则会自动创建。
+         * 注: set中的元素会去重。
+         *
+         * @param key
+         *            定位set的key
+         * @param items
+         *            要向(key对应的)set中添加的items
+         *
+         * @return 此次添加操作,添加到set中的元素的个数
+         * @date 2020/3/11 8:16:00
+         */
+        public static long sAdd(String key, String... items) {
+            log.info("sAdd(...) => key -> {}, items -> {}", key, items);
+            Long count = redisTemplate.opsForSet().add(key, items);
+            log.info("sAdd(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 从(key对应的)set中删除items
+         *
+         * 注: 若key不存在, 则返回0。
+         * 注: 若已经将(key对应的)set中的项删除完了,那么对应的key也会被删除。
+         *
+         * @param key
+         *            定位set的key
+         * @param items
+         *            要移除的items
+         *
+         * @return 实际删除了的个数
+         * @date 2020/3/11 8:26:43
+         */
+        public static long sRemove(String key, Object... items) {
+            log.info("sRemove(...) => key -> {}, items -> {}", key, items);
+            Long count = redisTemplate.opsForSet().remove(key, items);
+            log.info("sRemove(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 从(key对应的)set中随机移出一个item, 并返回这个item
+         *
+         * 注: 因为set是无序的,所以移出的这个item,是随机的; 并且,哪怕
+         *     是数据一样的set,多次测试移出操作,移除的元素也是随机的。
+         *
+         * 注: 若已经将(key对应的)set中的项pop完了,那么对应的key会被删除。
+         *
+         * @param key
+         *            定位set的key
+         *
+         * @return  移出的项
+         * @date 2020/3/11 8:32:40
+         */
+        public static String sPop(String key) {
+            log.info("sPop(...) => key -> {}", key);
+            String popItem = redisTemplate.opsForSet().pop(key);
+            log.info("sPop(...) => popItem -> {}", popItem);
+            return popItem;
+        }
+
+        /**
+         * 将(sourceKey对应的)sourceSet中的元素item, 移动到(destinationKey对应的)destinationSet中
+         *
+         * 注: 当sourceKey不存在时, 返回false
+         * 注: 当item不存在时, 返回false
+         * 注: 若destinationKey不存在, 那么在移动时会自动创建
+         * 注: 若已经将(sourceKey对应的)set中的项move出去完了,那么对应的sourceKey会被删除。
+         *
+         * @param sourceKey
+         *            定位sourceSet的key
+         * @param item
+         *            要移动的项目
+         * @param destinationKey
+         *            定位destinationSet的key
+         *
+         * @return  移动成功与否
+         * @date 2020/3/11 8:43:32
+         */
+        public static boolean sMove(String sourceKey, String item, String destinationKey) {
+            Boolean result = redisTemplate.opsForSet().move(sourceKey, item, destinationKey);
+            log.info("sMove(...) => sourceKey -> {}, destinationKey -> {}, item -> {}",
+                    sourceKey, destinationKey, item);
+            log.info("sMove(...) =>  result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 获取(key对应的)set中的元素个数
+         *
+         * 注: 若key不存在,则返回0
+         *
+         * @param key
+         *            定位set的key
+         *
+         * @return  (key对应的)set中的元素个数
+         * @date 2020/3/11 8:57:19
+         */
+        public static long sSize(String key) {
+            log.info("sSize(...) => key -> {}", key);
+            Long size = redisTemplate.opsForSet().size(key);
+            log.info("sSize(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 判断(key对应的)set中是否含有item
+         *
+         * 注: 若key不存在,则返回false。
+         *
+         * @param key
+         *            定位set的key
+         * @param item
+         *            被查找的项
+         *
+         * @return  (key对应的)set中是否含有item
+         * @date 2020/3/11 9:03:29
+         */
+        public static boolean sIsMember(String key, Object item) {
+            log.info("sSize(...) => key -> {}, size -> {}", key, item);
+            Boolean result = redisTemplate.opsForSet().isMember(key, item);
+            log.info("sSize(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 获取两个(key对应的)Set的交集
+         *
+         * 注: 若不存在任何交集,那么返回空的集合(, 而不是null)
+         * 注: 若其中一个key不存在(或两个key都不存在),那么返回空的集合(, 而不是null)
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKey
+         *            定位其中另一个set的键
+         *
+         * @return  item交集
+         * @date 2020/3/11 9:31:25
+         */
+        public static Set<String> sIntersect(String key, String otherKey) {
+            log.info("sIntersect(...) => key -> {}, otherKey -> {}", key, otherKey);
+            Set<String> intersectResult = redisTemplate.opsForSet().intersect(key, otherKey);
+            log.info("sIntersect(...) => intersectResult -> {}", intersectResult);
+            return intersectResult;
+        }
+
+        /**
+         * 获取多个(key对应的)Set的交集
+         *
+         * 注: 若不存在任何交集,那么返回空的集合(, 而不是null)
+         * 注: 若>=1个key不存在,那么返回空的集合(, 而不是null)
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKeys
+         *            定位其它set的键集
+         *
+         * @return  item交集
+         * @date 2020/3/11 9:39:23
+         */
+        public static Set<String> sIntersect(String key, Collection<String> otherKeys) {
+            log.info("sIntersect(...) => key -> {}, otherKeys -> {}", key, otherKeys);
+            Set<String> intersectResult = redisTemplate.opsForSet().intersect(key, otherKeys);
+            log.info("sIntersect(...) => intersectResult -> {}", intersectResult);
+            return intersectResult;
+        }
+
+        /**
+         * 获取两个(key对应的)Set的交集, 并将结果add到storeKey对应的Set中。
+         *
+         * case1: 交集不为空, storeKey不存在, 则 会创建对应的storeKey,并将交集添加到(storeKey对应的)set中
+         * case2: 交集不为空, storeKey已存在, 则 会清除原(storeKey对应的)set中所有的项,然后将交集添加到(storeKey对应的)set中
+         * case3: 交集为空, 则不进行下面的操作, 直接返回0
+         *
+         * 注: 求交集的部分,详见{@link SetOps#sIntersect(String, String)}
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKey
+         *            定位其中另一个set的键
+         * @param storeKey
+         *            定位(要把交集添加到哪个)set的key
+         *
+         * @return  add到(storeKey对应的)Set后, 该set对应的size
+         * @date 2020/3/11 9:46:46
+         */
+        public static long sIntersectAndStore(String key, String otherKey, String storeKey) {
+            log.info("sIntersectAndStore(...) => key -> {}, otherKey -> {}, storeKey -> {}",
+                    key, otherKey, storeKey);
+            Long size = redisTemplate.opsForSet().intersectAndStore(key, otherKey, storeKey);
+            log.info("sIntersectAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取多个(key对应的)Set的交集, 并将结果add到storeKey对应的Set中。
+         *
+         * case1: 交集不为空, storeKey不存在, 则 会创建对应的storeKey,并将交集添加到(storeKey对应的)set中
+         * case2: 交集不为空, storeKey已存在, 则 会清除原(storeKey对应的)set中所有的项,然后将交集添加到(storeKey对应的)set中
+         * case3: 交集为空, 则不进行下面的操作, 直接返回0
+         *
+         * 注: 求交集的部分,详见{@link SetOps#sIntersect(String, Collection)}
+         *
+         * @date 2020/3/11 11:04:29
+         */
+        public static long sIntersectAndStore(String key, Collection<String> otherKeys, String storeKey) {
+            log.info("sIntersectAndStore(...) => key -> {}, otherKeys -> {}, storeKey -> {}", key, otherKeys, storeKey);
+            Long size = redisTemplate.opsForSet().intersectAndStore(key, otherKeys, storeKey);
+            log.info("sIntersectAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取两个(key对应的)Set的并集
+         *
+         * 注: 并集中的元素也是唯一的,这是Set保证的。
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKey
+         *            定位其中另一个set的键
+         *
+         * @return item并集
+         * @date 2020/3/11 11:18:35
+         */
+        public static Set<String> sUnion(String key, String otherKey) {
+            log.info("sUnion(...) => key -> {}, otherKey -> {}", key, otherKey);
+            Set<String> unionResult = redisTemplate.opsForSet().union(key, otherKey);
+            log.info("sUnion(...) => unionResult -> {}", unionResult);
+            return unionResult;
+        }
+
+        /**
+         * 获取两个(key对应的)Set的并集
+         *
+         * 注: 并集中的元素也是唯一的,这是Set保证的。
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKeys
+         *            定位其它set的键集
+         *
+         * @return item并集
+         * @date 2020/3/11 11:18:35
+         */
+        public static Set<String> sUnion(String key, Collection<String> otherKeys) {
+            log.info("sUnion(...) => key -> {}, otherKeys -> {}", key, otherKeys);
+            Set<String> unionResult = redisTemplate.opsForSet().union(key, otherKeys);
+            log.info("sUnion(...) => unionResult -> {}", unionResult);
+            return unionResult;
+        }
+
+        /**
+         * 获取两个(key对应的)Set的并集, 并将结果add到storeKey对应的Set中。
+         *
+         * case1: 并集不为空, storeKey不存在, 则 会创建对应的storeKey,并将并集添加到(storeKey对应的)set中
+         * case2: 并集不为空, storeKey已存在, 则 会清除原(storeKey对应的)set中所有的项,然后将并集添加到(storeKey对应的)set中
+         * case3: 并集为空, 则不进行下面的操作, 直接返回0
+         *
+         * 注: 求并集的部分,详见{@link SetOps#sUnion(String, String)}
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKey
+         *            定位其中另一个set的键
+         * @param storeKey
+         *            定位(要把并集添加到哪个)set的key
+         *
+         * @return  add到(storeKey对应的)Set后, 该set对应的size
+         * @date 2020/3/11 12:26:24
+         */
+        public static long sUnionAndStore(String key, String otherKey, String storeKey) {
+            log.info("sUnionAndStore(...) => key -> {}, otherKey -> {}, storeKey -> {}",
+                    key, otherKey, storeKey);
+            Long size = redisTemplate.opsForSet().unionAndStore(key, otherKey, storeKey);
+            log.info("sUnionAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取两个(key对应的)Set的并集, 并将结果add到storeKey对应的Set中。
+         *
+         * case1: 并集不为空, storeKey不存在, 则 会创建对应的storeKey,并将并集添加到(storeKey对应的)set中
+         * case2: 并集不为空, storeKey已存在, 则 会清除原(storeKey对应的)set中所有的项,然后将并集添加到(storeKey对应的)set中
+         * case3: 并集为空, 则不进行下面的操作, 直接返回0
+         *
+         * 注: 求并集的部分,详见{@link SetOps#sUnion(String, Collection)}
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKeys
+         *            定位其它set的键集
+         * @param storeKey
+         *            定位(要把并集添加到哪个)set的key
+         *
+         * @return  add到(storeKey对应的)Set后, 该set对应的size
+         * @date 2020/3/11 12:26:24
+         */
+        public static long sUnionAndStore(String key, Collection<String> otherKeys, String storeKey) {
+            log.info("sUnionAndStore(...) => key -> {}, otherKeys -> {}, storeKey -> {}",
+                    key, otherKeys, storeKey);
+            Long size = redisTemplate.opsForSet().unionAndStore(key, otherKeys, storeKey);
+            log.info("sUnionAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取 (key对应的)Set 减去 (otherKey对应的)Set 的差集
+         *
+         * 注: 如果被减数key不存在, 那么结果为空的集合(,而不是null)
+         * 注: 如果被减数key存在,但减数key不存在, 那么结果即为(被减数key对应的)Set
+         *
+         * @param key
+         *            定位"被减数set"的键
+         * @param otherKey
+         *            定位"减数set"的键
+         *
+         * @return item差集
+         * @date 2020/3/11 14:03:57
+         */
+        public static Set<String> sDifference(String key, String otherKey) {
+            log.info("sDifference(...) => key -> {}, otherKey -> {}",
+                    key, otherKey);
+            Set<String> differenceResult = redisTemplate.opsForSet().difference(key, otherKey);
+            log.info("sDifference(...) => differenceResult -> {}", differenceResult);
+            return differenceResult;
+        }
+
+        /**
+         * 获取 (key对应的)Set 减去 (otherKeys对应的)Sets 的差集
+         *
+         * 注: 如果被减数key不存在, 那么结果为空的集合(,而不是null)
+         * 注: 如果被减数key存在,但减数key不存在, 那么结果即为(被减数key对应的)Set
+         *
+         * 提示: 当有多个减数时, 被减数先减去哪一个减数,后减去哪一个减数,是无所谓的,是不影响最终结果的。
+         *
+         * @param key
+         *            定位"被减数set"的键
+         * @param otherKeys
+         *            定位"减数集sets"的键集
+         *
+         * @return item差集
+         * @date 2020/3/11 14:03:57
+         */
+        public static Set<String> sDifference(String key, Collection<String> otherKeys) {
+            log.info("sDifference(...) => key -> {}, otherKeys -> {}", key, otherKeys);
+            Set<String> differenceResult = redisTemplate.opsForSet().difference(key, otherKeys);
+            log.info("sDifference(...) => differenceResult -> {}", differenceResult);
+            return differenceResult;
+        }
+
+        /**
+         * 获取 (key对应的)Set 减去 (otherKey对应的)Set 的差集, 并将结果add到storeKey对应的Set中。
+         *
+         * case1: 差集不为空, storeKey不存在, 则 会创建对应的storeKey,并将差集添加到(storeKey对应的)set中
+         * case2: 差集不为空, storeKey已存在, 则 会清除原(storeKey对应的)set中所有的项,然后将差集添加到(storeKey对应的)set中
+         * case3: 差集为空, 则不进行下面的操作, 直接返回0
+         *
+         * 注: 求并集的部分,详见{@link SetOps#sDifference(String, String)}
+         *
+         * @param key
+         *            定位"被减数set"的键
+         * @param otherKey
+         *            定位"减数set"的键
+         * @param storeKey
+         *            定位(要把差集添加到哪个)set的key
+         *
+         * @return  add到(storeKey对应的)Set后, 该set对应的size
+         * @date 2020/3/11 14:33:36
+         */
+        public static long sDifferenceAndStore(String key, String otherKey, String storeKey) {
+            log.info("sDifferenceAndStore(...) => key -> {}, otherKey -> {}, storeKey -> {}",
+                    key, otherKey, storeKey);
+            Long size = redisTemplate.opsForSet().differenceAndStore(key, otherKey, storeKey);
+            log.info("sDifferenceAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取 (key对应的)Set 减去 (otherKey对应的)Set 的差集, 并将结果add到storeKey对应的Set中。
+         *
+         * case1: 差集不为空, storeKey不存在, 则 会创建对应的storeKey,并将差集添加到(storeKey对应的)set中
+         * case2: 差集不为空, storeKey已存在, 则 会清除原(storeKey对应的)set中所有的项,然后将差集添加到(storeKey对应的)set中
+         * case3: 差集为空, 则不进行下面的操作, 直接返回0
+         *
+         * 注: 求并集的部分,详见{@link SetOps#sDifference(String, String)}
+         *
+         * @param key
+         *            定位"被减数set"的键
+         * @param otherKeys
+         *            定位"减数集sets"的键集
+         * @param storeKey
+         *            定位(要把差集添加到哪个)set的key
+         *
+         * @return  add到(storeKey对应的)Set后, 该set对应的size
+         * @date 2020/3/11 14:33:36
+         */
+        public static long sDifferenceAndStore(String key, Collection<String> otherKeys, String storeKey) {
+            log.info("sDifferenceAndStore(...) => key -> {}, otherKeys -> {}, storeKey -> {}",
+                    key, otherKeys, storeKey);
+            Long size = redisTemplate.opsForSet().differenceAndStore(key, otherKeys, storeKey);
+            log.info("sDifferenceAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取key对应的set
+         *
+         * 注: 若key不存在, 则返回的是空的set(, 而不是null)
+         *
+         * @param key
+         *            定位set的key
+         * @return  (key对应的)set
+         * @date 2020/3/11 14:49:39
+         */
+        public static Set<String> sMembers(String key) {
+            log.info("sMembers(...) => key -> {}", key);
+            Set<String> members = redisTemplate.opsForSet().members(key);
+            log.info("sMembers(...) => members -> {}", members);
+            return members;
+        }
+
+        /**
+         * 从key对应的set中随机获取一项
+         *
+         * @param key
+         *            定位set的key
+         * @return  随机获取到的项
+         * @date 2020/3/11 14:54:58
+         */
+        public static String sRandomMember(String key) {
+            log.info("sRandomMember(...) => key -> {}", key);
+            String randomItem = redisTemplate.opsForSet().randomMember(key);
+            log.info("sRandomMember(...) => randomItem -> {}", randomItem);
+            return randomItem;
+        }
+
+        /**
+         * 从key对应的set中获取count次随机项(, set中的同一个项可能被多次获取)
+         *
+         * 注: count可大于set的size。
+         * 注: 取出来的结果里可能存在相同的值。
+         *
+         * @param key
+         *            定位set的key
+         * @param count
+         *            要取多少项
+         *
+         * @return  随机获取到的项集
+         * @date 2020/3/11 14:54:58
+         */
+        public static List<String> sRandomMembers(String key, long count) {
+            log.info("sRandomMembers(...) => key -> {}, count -> {}", key, count);
+            List<String> randomItems = redisTemplate.opsForSet().randomMembers(key, count);
+            log.info("sRandomMembers(...) => randomItems -> {}", randomItems);
+            return randomItems;
+        }
+
+        /**
+         * 从key对应的set中随机获取count个项
+         *
+         * 注: 若count >= set的size, 那么返回的即为这个key对应的set。
+         * 注: 取出来的结果里没有重复的项。
+         *
+         * @param key
+         *            定位set的key
+         * @param count
+         *            要取多少项
+         *
+         * @return  随机获取到的项集
+         * @date 2020/3/11 14:54:58
+         */
+        public static Set<String> sDistinctRandomMembers(String key, long count) {
+            log.info("sDistinctRandomMembers(...) => key -> {}, count -> {}", key, count);
+            Set<String> distinctRandomItems = redisTemplate.opsForSet().distinctRandomMembers(key, count);
+            log.info("sDistinctRandomMembers(...) => distinctRandomItems -> {}", distinctRandomItems);
+            return distinctRandomItems;
+        }
+
+        /**
+         * 根据options匹配到(key对应的)set中的对应的item, 并返回对应的item集
+         *
+         *
+         * 注: ScanOptions实例的创建方式举例:
+         *     1、ScanOptions.NONE
+         *     2、ScanOptions.scanOptions().match("n??e").build()
+         *
+         * @param key
+         *            定位set的key
+         * @param options
+         *            匹配set中的item的条件
+         *            注: ScanOptions.NONE表示全部匹配。
+         *            注: ScanOptions.scanOptions().match(pattern).build()表示按照pattern匹配,
+         *                其中pattern中可以使用通配符 * ? 等,
+         *                * 表示>=0个字符
+         *                ? 表示有且只有一个字符
+         *                此处的匹配规则与{@link KeyOps#keys(String)}处的一样。
+         *
+         * @return  匹配到的(key对应的)set中的项
+         * @date 2020/3/9 10:49:27
+         */
+        public static Cursor<String> sScan(String key, ScanOptions options) {
+            log.info("sScan(...) => key -> {}, options -> {}", key, JSON.toJSONString(options));
+            Cursor<String> cursor = redisTemplate.opsForSet().scan(key, options);
+            log.info("sScan(...) => cursor -> {}", JSON.toJSONString(cursor));
+            return cursor;
+        }
+    }
+
+    /**
+     * ZSet相关操作
+     *
+     * 特别说明: ZSet是有序的,
+     *             不仅体现在: redis中的存储上有序。
+     *             还体现在:   此工具类ZSetOps中返回值类型为Set<?>的方法, 实际返回类型是LinkedHashSet<?>
+     *
+     * 提示: redis中的ZSet, 一定程度等于redis中的Set + redis中的Hash的结合体。
+     * 提示: redis中String的数据结构可参考resources/data-structure/ZSet(有序集合)的数据结构(示例一).png
+     *      redis中String的数据结构可参考resources/data-structure/ZSet(有序集合)的数据结构(示例二).png
+     * 提示: ZSet中的entryKey即为成员项, entryValue即为这个成员项的分值, ZSet根据成员的分值,来堆成员进行排序。
+     *
+     * @author JustryDeng
+     * @date 2020/3/11 15:28:48
+     */
+    public static class ZSetOps {
+
+        /**
+         * 向(key对应的)zset中添加(item, score)
+         *
+         * 注: item为entryKey成员项, score为entryValue分数值。
+         *
+         * 注: 若(key对应的)zset中已存在(与此次要添加的项)相同的item项,那么此次添加操作会失败,返回false;
+         *     但是!!! zset中原item的score会被更新为此次add的相同item项的score。
+         *     所以, 也可以通过zAdd达到更新item对应score的目的。
+         *
+         * 注: score可为正、可为负、可为0; 总之, double范围内都可以。
+         *
+         * 注: 若score的值一样,则按照item排序。
+         *
+         * @param key
+         *            定位set的key
+         * @param item
+         *            要往(key对应的)zset中添加的成员项
+         * @param score
+         *            item的分值
+         *
+         * @return 是否添加成功
+         * @date 2020/3/11 15:35:30
+         */
+        public static boolean zAdd(String key, String item, double score) {
+            log.info("zAdd(...) => key -> {}, item -> {}, score -> {}", key, item, score);
+            Boolean result = redisTemplate.opsForZSet().add(key, item, score);
+            log.info("zAdd(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 批量添加entry<item, score>
+         *
+         * 注: 若entry<item, score>集中存在item相同的项(, score不一样),那么redis在执行真正的批量add操作前,会
+         *     将其中一个item过滤掉。
+         * 注: 同样的,若(key对应的)zset中已存在(与此次要添加的项)相同的item项,那么此次批量添加操作中,
+         *    对该item项的添加会失败,会失败,成功计数器不会加1;但是!!! zset中原item的score会被更新为此
+         *    次add的相同item项的score。所以, 也可以通过zAdd达到更新item对应score的目的。
+         *
+         * @param key
+         *            定位set的key
+         * @param entries
+         *            要添加的entry<item, score>集
+         *
+         * @return 本次添加进(key对应的)zset中的entry的个数
+         * @date 2020/3/11 16:45:45
+         */
+        public static long zAdd(String key, Set<TypedTuple<String>> entries) {
+            log.info("zAdd(...) => key -> {}, entries -> {}", key, JSON.toJSONString(entries));
+            Long count = redisTemplate.opsForZSet().add(key, entries);
+            log.info("zAdd(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 从(key对应的)zset中移除项
+         *
+         * 注:若key不存在,则返回0
+         *
+         * @param key
+         *            定位set的key
+         * @param items
+         *            要移除的项集
+         *
+         * @return  实际移除了的项的个数
+         * @date 2020/3/11 17:20:12
+         */
+        public static long zRemove(String key, Object... items) {
+            log.info("zRemove(...) => key -> {}, items -> {}", key, items);
+            Long count = redisTemplate.opsForZSet().remove(key, items);
+            log.info("zRemove(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 移除(key对应的)zset中, 排名范围在[startIndex, endIndex]内的item
+         *
+         * 注:默认的,按score.item升序排名, 排名从0开始
+         *
+         * 注: 类似于List中的索引, 排名可以分为多个方式:
+         *     从前到后(正向)的排名: 0、1、2...
+         *     从后到前(反向)的排名: -1、-2、-3...
+         *
+         * 注: 不论是使用正向排名,还是使用反向排名, 使用此方法时, 应保证 startRange代表的元素的位置
+         *     在endRange代表的元素的位置的前面, 如:
+         *      示例一: RedisUtil.ZSetOps.zRemoveRange("name", 0, 2);
+         *      示例二: RedisUtil.ZSetOps.zRemoveRange("site", -2, -1);
+         *      示例三: RedisUtil.ZSetOps.zRemoveRange("foo", 0, -1);
+         *
+         * 注:若key不存在,则返回0
+         *
+         * @param key
+         *            定位set的key
+         * @param startRange
+         *            开始项的排名
+         * @param endRange
+         *            结尾项的排名
+         *
+         * @return  实际移除了的项的个数
+         * @date 2020/3/11 17:20:12
+         */
+        public static long zRemoveRange(String key, long startRange, long endRange) {
+            log.info("zRemoveRange(...) => key -> {}, startRange -> {}, endRange -> {}",
+                    key, startRange, endRange);
+            Long count = redisTemplate.opsForZSet().removeRange(key, startRange, endRange);
+            log.info("zRemoveRange(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 移除(key对应的)zset中, score范围在[minScore, maxScore]内的item
+         *
+         * 提示: 虽然删除范围包含两侧的端点(即:包含minScore和maxScore), 但是由于double存在精度问题,所以建议:
+         *          设置值时,minScore应该设置得比要删除的项里,最小的score还小一点
+         *                   maxScore应该设置得比要删除的项里,最大的score还大一点
+         *          追注: 本人简单测试了几组数据,暂未出现精度问题。
+         *
+         * 注:若key不存在,则返回0
+         *
+         * @param key
+         *            定位set的key
+         * @param minScore
+         *            score下限(含这个值)
+         * @param maxScore
+         *            score上限(含这个值)
+         *
+         * @return  实际移除了的项的个数
+         * @date 2020/3/11 17:20:12
+         */
+        public static long zRemoveRangeByScore(String key, double minScore, double maxScore) {
+            log.info("zRemoveRangeByScore(...) => key -> {}, startIndex -> {}, startIndex -> {}",
+                    key, minScore, maxScore);
+            Long count = redisTemplate.opsForZSet().removeRangeByScore(key, minScore, maxScore);
+            log.info("zRemoveRangeByScore(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 增/减 (key对应的zset中,)item的分数值
+         *
+         * @param key
+         *            定位zset的key
+         * @param item
+         *            项
+         * @param delta
+         *            变化量(正 - 增, 负 - 减)
+         * @return 修改后的score值
+         * @date 2020/3/12 8:55:38
+         */
+        public static double zIncrementScore(String key, String item, double delta) {
+            log.info("zIncrementScore(...) => key -> {}, item -> {}, delta -> {}", key, item, delta);
+            Double scoreValue = redisTemplate.opsForZSet().incrementScore(key, item, delta);
+            log.info("zIncrementScore(...) => scoreValue -> {}", scoreValue);
+            if (scoreValue == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return scoreValue;
+        }
+
+        /**
+         * 返回item在(key对应的)zset中的(按score从小到大的)排名
+         *
+         * 注: 排名从0开始。 即意味着,此方法等价于: 返回item在(key对应的)zset中的位置索引。
+         * 注: 若key或item不存在, 返回null。
+         * 注: 排序规则是score,item, 即:优先以score排序,若score相同,则再按item排序。
+         *
+         * @param key
+         *            定位zset的key
+         * @param item
+         *            项
+         *
+         * @return 排名(等价于: 索引)
+         * @date 2020/3/12 9:14:09
+         */
+        public static long zRank(String key, Object item) {
+            log.info("zRank(...) => key -> {}, item -> {}", key, item);
+            Long rank = redisTemplate.opsForZSet().rank(key, item);
+            log.info("zRank(...) => rank -> {}", rank);
+            if (rank == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return rank;
+        }
+
+        /**
+         * 返回item在(key对应的)zset中的(按score从大到小的)排名
+         *
+         * 注: 排名从0开始。补充: 因为是按score从大到小排序的, 所以最大score对应的item的排名为0。
+         * 注: 若key或item不存在, 返回null。
+         * 注: 排序规则是score,item, 即:优先以score排序,若score相同,则再按item排序。
+         *
+         * @param key
+         *            定位zset的key
+         * @param item
+         *            项
+         *
+         * @return 排名(等价于: 索引)
+         * @date 2020/3/12 9:14:09
+         */
+        public static long zReverseRank(String key, Object item) {
+            log.info("zReverseRank(...) => key -> {}, item -> {}", key, item);
+            Long reverseRank = redisTemplate.opsForZSet().reverseRank(key, item);
+            log.info("zReverseRank(...) => reverseRank -> {}", reverseRank);
+            if (reverseRank == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return reverseRank;
+        }
+
+        /**
+         * 根据索引位置, 获取(key对应的)zset中排名处于[start, end]中的item项集
+         *
+         * 注: 不论是使用正向排名,还是使用反向排名, 使用此方法时, 应保证 startIndex代表的元素的
+         *      位置在endIndex代表的元素的位置的前面, 如:
+         *      示例一: RedisUtil.ZSetOps.zRange("name", 0, 2);
+         *      示例二: RedisUtil.ZSetOps.zRange("site", -2, -1);
+         *      示例三: RedisUtil.ZSetOps.zRange("foo", 0, -1);
+         *
+         * 注: 若key不存在, 则返回空的集合。
+         *
+         * 注: 当[start, end]的范围比实际zset的范围大时, 返回范围上"交集"对应的项集合。
+         *
+         * @param key
+         *            定位zset的key
+         * @param start
+         *            排名开始位置
+         * @param end
+         *            排名结束位置
+         *
+         * @return  对应的item项集
+         * @date 2020/3/12 9:50:40
+         */
+        public static Set<String> zRange(String key, long start, long end) {
+            log.info("zRange(...) => key -> {}, start -> {}, end -> {}", key, start, end);
+            Set<String> result = redisTemplate.opsForZSet().range(key, start, end);
+            log.info("zRange(...) => result -> {}", result);
+            return result;
+        }
+
+        /**
+         * 获取(key对应的)zset中的所有item项
+         *
+         * @see ZSetOps#zRange(String, long, long)
+         *
+         * @param key
+         *            定位zset的键
+         *
+         * @return  (key对应的)zset中的所有item项
+         * @date 2020/3/12 10:02:07
+         */
+        public static Set<String> zWholeZSetItem(String key) {
+            log.info("zWholeZSetItem(...) => key -> {}", key);
+            Set<String> result = redisTemplate.opsForZSet().range(key, 0, -1);
+            log.info("zWholeZSetItem(...) =>result -> {}", result);
+            return result;
+        }
+
+        /**
+         * 根据索引位置, 获取(key对应的)zset中排名处于[start, end]中的entry集
+         *
+         * 注: 不论是使用正向排名,还是使用反向排名, 使用此方法时, 应保证 startIndex代表的元素的
+         *      位置在endIndex代表的元素的位置的前面, 如:
+         *      示例一: RedisUtil.ZSetOps.zRange("name", 0, 2);
+         *      示例二: RedisUtil.ZSetOps.zRange("site", -2, -1);
+         *      示例三: RedisUtil.ZSetOps.zRange("foo", 0, -1);
+         *
+         * 注: 若key不存在, 则返回空的集合。
+         *
+         * 注: 当[start, end]的范围比实际zset的范围大时, 返回范围上"交集"对应的项集合。
+         *
+         * 注: 此方法和{@link ZSetOps#zRange(String, long, long)}类似,不过此方法返回的不是item集, 而是entry集
+         *
+         * @param key
+         *            定位zset的key
+         * @param start
+         *            排名开始位置
+         * @param end
+         *            排名结束位置
+         *
+         * @return  对应的entry集
+         * @date 2020/3/12 9:50:40
+         */
+        public static Set<TypedTuple<String>> zRangeWithScores(String key, long start, long end) {
+            log.info("zRangeWithScores(...) => key -> {}, start -> {}, end -> {}", key, start, end);
+            Set<TypedTuple<String>> entries = redisTemplate.opsForZSet().rangeWithScores(key, start, end);
+            log.info("zRangeWithScores(...) => entries -> {}", JSON.toJSONString(entries));
+            return entries;
+        }
+
+        /**
+         * 获取(key对应的)zset中的所有entry
+         *
+         * @see ZSetOps#zRangeWithScores(String, long, long)
+         *
+         * @param key
+         *            定位zset的键
+         *
+         * @return  (key对应的)zset中的所有entry
+         * @date 2020/3/12 10:02:07
+         */
+        public static Set<TypedTuple<String>> zWholeZSetEntry(String key) {
+            log.info("zWholeZSetEntry(...) => key -> {}", key);
+            Set<TypedTuple<String>> entries = redisTemplate.opsForZSet().rangeWithScores(key, 0, -1);
+            log.info("zWholeZSetEntry(...) => entries -> {}", key, JSON.toJSONString(entries));
+            return entries;
+        }
+
+        /**
+         * 根据score, 获取(key对应的)zset中分数值处于[minScore, maxScore]中的item项集
+         *
+         * 注: 若key不存在, 则返回空的集合。
+         * 注: 当[minScore, maxScore]的范围比实际zset中score的范围大时, 返回范围上"交集"对应的项集合。
+         *
+         * 提示: 虽然删除范围包含两侧的端点(即:包含minScore和maxScore), 但是由于double存在精度问题,所以建议:
+         *          设置值时,minScore应该设置得比要删除的项里,最小的score还小一点
+         *                   maxScore应该设置得比要删除的项里,最大的score还大一点
+         *          追注: 本人简单测试了几组数据,暂未出现精度问题。
+         *
+         * @param key
+         *            定位zset的key
+         * @param minScore
+         *            score下限
+         * @param maxScore
+         *            score上限
+         *
+         * @return  对应的item项集
+         * @date 2020/3/12 9:50:40
+         */
+        public static Set<String> zRangeByScore(String key, double minScore, double maxScore) {
+            log.info("zRangeByScore(...) => key -> {}, minScore -> {}, maxScore -> {}", key, minScore, maxScore);
+            Set<String> items = redisTemplate.opsForZSet().rangeByScore(key, minScore, maxScore);
+            log.info("zRangeByScore(...) => items -> {}", items);
+            return items;
+        }
+
+        /**
+         * 根据score, 获取(key对应的)zset中分数值处于[minScore, maxScore]中的, score处于[minScore,
+         * 排名大于等于offset的count个item项
+         *
+         * 特别注意: 对于不是特别熟悉redis的人来说, offset 和 count最好都使用正数, 避免引起理解上的歧义。
+         *
+         * 注: 若key不存在, 则返回空的集合。
+         *
+         * 提示: 虽然删除范围包含两侧的端点(即:包含minScore和maxScore), 但是由于double存在精度问题,所以建议:
+         *          设置值时,minScore应该设置得比要删除的项里,最小的score还小一点
+         *                   maxScore应该设置得比要删除的项里,最大的score还大一点
+         *          追注: 本人简单测试了几组数据,暂未出现精度问题。
+         *
+         * @param key
+         *            定位zset的key
+         * @param minScore
+         *            score下限
+         * @param maxScore
+         *            score上限
+         * @param offset
+         *            偏移量(即:排名下限)
+         * @param count
+         *            期望获取到的元素个数
+         *
+         * @return  对应的item项集
+         * @date 2020/3/12 9:50:40
+         */
+        public static Set<String> zRangeByScore(String key, double minScore, double maxScore,
+                                                long offset, long count) {
+            log.info("zRangeByScore(...) => key -> {}, minScore -> {}, maxScore -> {}, offset -> {}, "
+                    + "count -> {}", key, minScore, maxScore, offset, count);
+            Set<String> items = redisTemplate.opsForZSet().rangeByScore(key, minScore, maxScore, offset, count);
+            log.info("zRangeByScore(...) => items -> {}", items);
+            return items;
+        }
+
+        /**
+         * 获取(key对应的)zset中的所有score处于[minScore, maxScore]中的entry
+         *
+         * @see ZSetOps#zRangeByScore(String, double, double)
+         *
+         * 注: 若key不存在, 则返回空的集合。
+         * 注: 当[minScore, maxScore]的范围比实际zset中score的范围大时, 返回范围上"交集"对应的项集合。
+         *
+         * @param key
+         *            定位zset的键
+         * @param minScore
+         *            score下限
+         * @param maxScore
+         *            score上限
+         *
+         * @return  (key对应的)zset中的所有score处于[minScore, maxScore]中的entry
+         * @date 2020/3/12 10:02:07
+         */
+        public static Set<TypedTuple<String>> zRangeByScoreWithScores(String key, double minScore, double maxScore) {
+            log.info("zRangeByScoreWithScores(...) => key -> {}, minScore -> {}, maxScore -> {}",
+                    key, minScore, maxScore);
+            Set<TypedTuple<String>> entries = redisTemplate.opsForZSet().rangeByScoreWithScores(key, minScore, maxScore);
+            log.info("zRangeByScoreWithScores(...) => entries -> {}", JSON.toJSONString(entries));
+            return entries;
+        }
+
+        /**
+         * 获取(key对应的)zset中, score处于[minScore, maxScore]里的、排名大于等于offset的count个entry
+         *
+         * 特别注意: 对于不是特别熟悉redis的人来说, offset 和 count最好都使用正数, 避免引起理解上的歧义。
+         *
+         * @param key
+         *            定位zset的键
+         * @param minScore
+         *            score下限
+         * @param maxScore
+         *            score上限
+         * @param offset
+         *            偏移量(即:排名下限)
+         * @param count
+         *            期望获取到的元素个数
+         *
+         * @return [startIndex, endIndex] & [minScore, maxScore]里的entry
+         * @date 2020/3/12 11:09:06
+         */
+        public static Set<TypedTuple<String>> zRangeByScoreWithScores(String key, double minScore,
+                                                                      double maxScore, long offset,
+                                                                      long count) {
+            log.info("zRangeByScoreWithScores(...) => key -> {}, minScore -> {}, maxScore -> {},"
+                            + " offset -> {}, count -> {}",
+                    key, minScore, maxScore, offset, count);
+            Set<TypedTuple<String>> entries = redisTemplate.opsForZSet().rangeByScoreWithScores(key, minScore,
+                    maxScore, offset, count);
+            log.info("zRangeByScoreWithScores(...) => entries -> {}", JSON.toJSONString(entries));
+            return entries;
+        }
+
+
+        /**
+         * 获取时, 先按score倒序, 然后根据索引位置, 获取(key对应的)zset中排名处于[start, end]中的item项集
+         *
+         * @see ZSetOps#zRange(String, long, long)。 只是zReverseRange这里会提前多一个倒序。
+         */
+        public static Set<String> zReverseRange(String key, long start, long end) {
+            log.info("zReverseRange(...) => key -> {}, start -> {}, end -> {}", key, start, end);
+            Set<String> entries = redisTemplate.opsForZSet().reverseRange(key, start, end);
+            log.info("zReverseRange(...) => entries -> {}", entries);
+            return entries;
+        }
+
+        /**
+         * 获取时, 先按score倒序, 然后根据索引位置, 获取(key对应的)zset中排名处于[start, end]中的entry集
+         *
+         * @see ZSetOps#zRangeWithScores(String, long, long)。 只是zReverseRangeWithScores这里会提前多一个倒序。
+         */
+        public static Set<TypedTuple<String>> zReverseRangeWithScores(String key, long start, long end) {
+            log.info("zReverseRangeWithScores(...) => key -> {}, start -> {}, end -> {}", key, start, end);
+            Set<TypedTuple<String>> entries = redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
+            log.info("zReverseRangeWithScores(...) => entries -> {}", JSON.toJSONString(entries));
+            return entries;
+        }
+
+        /**
+         * 获取时, 先按score倒序, 然后根据score, 获取(key对应的)zset中分数值处于[minScore, maxScore]中的item项集
+         *
+         * @see ZSetOps#zRangeByScore(String, double, double)。 只是zReverseRangeByScore这里会提前多一个倒序。
+         */
+        public static Set<String> zReverseRangeByScore(String key, double minScore, double maxScore) {
+            log.info("zReverseRangeByScore(...) => key -> {}, minScore -> {}, maxScore -> {}",
+                    key, minScore, maxScore);
+            Set<String> items = redisTemplate.opsForZSet().reverseRangeByScore(key, minScore, maxScore);
+            log.info("zReverseRangeByScore(...) => items -> {}", items);
+            return items;
+        }
+
+        /**
+         * 获取时, 先按score倒序, 然后获取(key对应的)zset中的所有score处于[minScore, maxScore]中的entry
+         *
+         * @see ZSetOps#zRangeByScoreWithScores(String, double, double)。 只是zReverseRangeByScoreWithScores这里会提前多一个倒序。
+         */
+        public static Set<TypedTuple<String>> zReverseRangeByScoreWithScores(String key, double minScore, double maxScore) {
+            log.info("zReverseRangeByScoreWithScores(...) => key -> {}, minScore -> {}, maxScore -> {}",
+                    key, minScore, maxScore);
+            Set<TypedTuple<String>> entries = redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,
+                    minScore, maxScore);
+            log.info("zReverseRangeByScoreWithScores(...) => entries -> {}", JSON.toJSONString(entries));
+            return entries;
+        }
+
+        /**
+         * 获取时, 先按score倒序, 然后根据score, 获取(key对应的)zset中分数值处于[minScore, maxScore]中的,
+         * score处于[minScore,排名大于等于offset的count个item项
+         *
+         * @see ZSetOps#zRangeByScore(String, double, double, long, long)。 只是zReverseRangeByScore这里会提前多一个倒序。
+         */
+        public static Set<String> zReverseRangeByScore(String key, double minScore, double maxScore, long offset, long count) {
+            log.info("zReverseRangeByScore(...) => key -> {}, minScore -> {}, maxScore -> {}, offset -> {}, "
+                    + "count -> {}", key, minScore, maxScore, offset, count);
+            Set<String> items = redisTemplate.opsForZSet().reverseRangeByScore(key, minScore, maxScore, offset, count);
+            log.info("items -> {}", items);
+            return items;
+        }
+
+        /**
+         * 统计(key对应的zset中)score处于[minScore, maxScore]中的item的个数
+         *
+         * @param key
+         *            定位zset的key
+         * @param minScore
+         *            score下限
+         * @param maxScore
+         *            score上限
+         *
+         * @return  [minScore, maxScore]中item的个数
+         * @date 2020/3/13 12:20:43
+         */
+        public static long zCount(String key, double minScore, double maxScore) {
+            log.info("zCount(...) => key -> {}, minScore -> {}, maxScore -> {}",key, minScore, maxScore);
+            Long count = redisTemplate.opsForZSet().count(key, minScore, maxScore);
+            log.info("zCount(...) => count -> {}", count);
+            if (count == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return count;
+        }
+
+        /**
+         * 统计(key对应的)zset中item的个数
+         *
+         * 注: 此方法等价于{@link ZSetOps#zZCard(String)}
+         *
+         * @param key
+         *            定位zset的key
+         *
+         * @return  zset中item的个数
+         * @date 2020/3/13 12:20:43
+         */
+        public static long zSize(String key) {
+            log.info("zSize(...) => key -> {}", key);
+            Long size = redisTemplate.opsForZSet().size(key);
+            log.info("zSize(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 统计(key对应的)zset中item的个数
+         *
+         * 注: 此方法等价于{@link ZSetOps#zSize(String)}
+         *
+         * @param key
+         *            定位zset的key
+         *
+         * @return  zset中item的个数
+         * @date 2020/3/13 12:20:43
+         */
+        public static long zZCard(String key) {
+            log.info("zZCard(...) => key -> {}", key);
+            Long size = redisTemplate.opsForZSet().zCard(key);
+            log.info("zZCard(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 统计(key对应的)zset中指定item的score
+         *
+         * @param key
+         *            定位zset的key
+         * @param item
+         *            zset中的item
+         *
+         * @return  item的score
+         * @date 2020/3/13 14:51:43
+         */
+        public static double zScore(String key, Object item) {
+            log.info("zScore(...) => key -> {}, item -> {}", key, item);
+            Double score = redisTemplate.opsForZSet().score(key, item);
+            log.info("zScore(...) => score -> {}", score);
+            if (score == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return score;
+        }
+
+        /**
+         * 获取两个(key对应的)ZSet的并集, 并将结果add到storeKey对应的ZSet中。
+         *
+         * 注: 和set一样,zset中item是唯一的, 在多个zset进行Union时, 处理相同的item时, score的值会变为对应的score之和,如:
+         *         RedisUtil.ZSetOps.zAdd("name1", "a", 1);和RedisUtil.ZSetOps.zAdd("name2", "a", 2);
+         *         对(name1和name2对应的)zset进行zUnionAndStore之后,新的zset中的项a,对应的score值为3
+         *
+         * case1: 交集不为空, storeKey不存在, 则 会创建对应的storeKey,并将并集添加到(storeKey对应的)ZSet中
+         * case2: 交集不为空, storeKey已存在, 则 会清除原(storeKey对应的)ZSet中所有的项,然后将并集添加到(storeKey对应的)ZSet中
+         * case3: 交集为空, 则不进行下面的操作, 直接返回0
+         *
+         * @param key
+         *            定位其中一个zset的键
+         * @param otherKey
+         *            定位另外的zset的键
+         * @param storeKey
+         *            定位(要把交集添加到哪个)set的key
+         *
+         * @return  add到(storeKey对应的)ZSet后, 该ZSet对应的size
+         * @date 2020/3/11 12:26:24
+         */
+        public static long zUnionAndStore(String key, String otherKey, String storeKey) {
+            log.info("zUnionAndStore(...) => key -> {}, otherKey -> {}, storeKey -> {}", key, otherKey, storeKey);
+            Long size = redisTemplate.opsForZSet().unionAndStore(key, otherKey, storeKey);
+            log.info("zUnionAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取两个(key对应的)ZSet的并集, 并将结果add到storeKey对应的ZSet中。
+         *
+         * 注: 和set一样,zset中item是唯一的, 在多个zset进行Union时, 处理相同的item时, score的值会变为对应的score之和,如:
+         *         RedisUtil.ZSetOps.zAdd("name1", "a", 1);和RedisUtil.ZSetOps.zAdd("name2", "a", 2);
+         *         对(name1和name2对应的)zset进行zUnionAndStore之后,新的zset中的项a,对应的score值为3
+         *
+         * case1: 并集不为空, storeKey不存在, 则 会创建对应的storeKey,并将并集添加到(storeKey对应的)ZSet中
+         * case2: 并集不为空, storeKey已存在, 则 会清除原(storeKey对应的)ZSet中所有的项,然后将并集添加到(storeKey对应的)ZSet中
+         * case3: 并集为空, 则不进行下面的操作, 直接返回0
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKeys
+         *            定位其它set的键集
+         * @param storeKey
+         *            定位(要把并集添加到哪个)set的key
+         *
+         * @return  add到(storeKey对应的)ZSet后, 该ZSet对应的size
+         * @date 2020/3/11 12:26:24
+         */
+        public static long zUnionAndStore(String key, Collection<String> otherKeys, String storeKey) {
+            log.info("zUnionAndStore(...) => key -> {}, otherKeys -> {}, storeKey -> {}", key, otherKeys, storeKey);
+            Long size = redisTemplate.opsForZSet().unionAndStore(key, otherKeys, storeKey);
+            log.info("zUnionAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取两个(key对应的)ZSet的交集, 并将结果add到storeKey对应的ZSet中。
+         *
+         * 注: 和set一样,zset中item是唯一的, 在多个zset进行Intersect时, 处理相同的item时, score的值会变为对应的score之和,如:
+         *         RedisUtil.ZSetOps.zAdd("name1", "a", 1);
+         *         RedisUtil.ZSetOps.zAdd("name1", "b", 100);
+         *         和R
+         *         edisUtil.ZSetOps.zAdd("name2", "a", 2);
+         *         edisUtil.ZSetOps.zAdd("name2", "c", 200);
+         *         对(name1和name2对应的)zset进行zIntersectAndStore之后,新的zset中的项a,对应的score值为3
+         *
+         * case1: 交集不为空, storeKey不存在, 则 会创建对应的storeKey,并将交集添加到(storeKey对应的)ZSet中
+         * case2: 交集不为空, storeKey已存在, 则 会清除原(storeKey对应的)ZSet中所有的项,然后将交集添加到(storeKey对应的)ZSet中
+         * case3: 交集为空, 则不进行下面的操作, 直接返回0
+         *
+         * @param key
+         *            定位其中一个ZSet的键
+         * @param otherKey
+         *            定位其中另一个ZSet的键
+         * @param storeKey
+         *            定位(要把交集添加到哪个)ZSet的key
+         *
+         * @return  add到(storeKey对应的)ZSet后, 该ZSet对应的size
+         * @date 2020/3/11 9:46:46
+         */
+        public static long zIntersectAndStore(String key, String otherKey, String storeKey) {
+            log.info("zIntersectAndStore(...) => key -> {}, otherKey -> {}, storeKey -> {}", key, otherKey, storeKey);
+            Long size = redisTemplate.opsForZSet().intersectAndStore(key, otherKey, storeKey);
+            log.info("zIntersectAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+
+        /**
+         * 获取多个(key对应的)ZSet的交集, 并将结果add到storeKey对应的ZSet中。
+         *
+         * case1: 交集不为空, storeKey不存在, 则 会创建对应的storeKey,并将交集添加到(storeKey对应的)ZSet中
+         * case2: 交集不为空, storeKey已存在, 则 会清除原(storeKey对应的)ZSet中所有的项,然后将交集添加到(storeKey对应的)ZSet中
+         * case3: 交集为空, 则不进行下面的操作, 直接返回0
+         *
+         * @param key
+         *            定位其中一个set的键
+         * @param otherKeys
+         *            定位其它set的键集
+         * @param storeKey
+         *            定位(要把并集添加到哪个)set的key
+         *
+         * @return  add到(storeKey对应的)ZSet后, 该ZSet对应的size
+         * @date 2020/3/11 11:04:29
+         */
+        public static long zIntersectAndStore(String key, Collection<String> otherKeys, String storeKey) {
+            log.info("zIntersectAndStore(...) => key -> {}, otherKeys -> {}, storeKey -> {}",
+                    key, otherKeys, storeKey);
+            Long size = redisTemplate.opsForZSet().intersectAndStore(key, otherKeys, storeKey);
+            log.info("zIntersectAndStore(...) => size -> {}", size);
+            if (size == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return size;
+        }
+    }
+
+    /**
+     * redis分布式锁(单机版).
+     *
+     * 使用方式(示例):
+     * 			boolean flag = false;
+     * 			String lockName = "sichuan:mianyang:fucheng:ds";
+     * 			String lockValue = UUID.randomUUID().toString();
+     * 			try {
+     * 		        //	非阻塞获取(锁的最大存活时间采用默认值)
+     * 				flag = RedisUtil.LockOps.getLock(lockName, lockValue);
+     * 				//	非阻塞获取e.g.
+     * 				flag = RedisUtil.LockOps.getLock(lockName, lockValue, 3, TimeUnit.SECONDS);
+     * 			    // 阻塞获取(锁的最大存活时间采用默认值)
+     * 		        flag = RedisUtil.LockOps.getLockUntilTimeout(lockName, lockValue, 2000);
+     * 		        // 阻塞获取e.g.
+     * 		        flag = RedisUtil.LockOps.getLockUntilTimeout(lockName, lockValue, 2, TimeUnit.SECONDS, 2000);
+     * 				if (!flag) {
+     * 				    throw new RuntimeException(" obtain redis-lock[" + lockName + "] fail");
+     * 				}
+     * 		     	// your logic
+     * 			    //	...
+     *          } finally {
+     * 				if (flag) {
+     * 					RedisUtil.LockOps.releaseLock(lockName, lockValue);
+     *              }
+     *          }
+     *
+     * |--------------------------------------------------------------------------------------------------------------------|
+     * |单机版分布式锁、集群版分布式锁,特别说明:                                                                                 |
+     * |   - 此锁是针对单机Redis的分布式锁;                                                                                    |
+     * |   - 对于Redis集群而言, 此锁可能存在失效的情况。考虑如下情况:                                                              |
+     * |         首先,当客户端A通过key-value(假设为key名为key123)在Master上获取到一个锁。                                        |
+     * |         然后,Master试着把这个数据同步到Slave的时候突然挂了(此时Slave上没有该分布式锁的key123)。                            |
+     * |         接着,Slave变成了Master。                                                                                    |
+     * |         不巧的是,客户端B此时也一以相同的key去获取分布式锁;                                                              |
+     * |                 因为现在的Master上没有key123代表的分布式锁,                                                            |
+     * |                 所以客户端B此时再通过key123去获取分布式锁时,                                                            |
+     * |                 就能获取成功。                                                                                       |
+     * |         那么此时,客户端A和客户端B同时获取到了同一把分布式锁,分布式锁失效。                                                 |
+     * |   - 在Redis集群模式下,如果需要严格的分布式锁的话,可使用Redlock算法来实现。Redlock算法原理简述:                              |
+     * |     - 获取分布式锁:                                                                                                 |
+     * |           1. 客户端获取服务器当前的的时间t0。                                                                           |
+     * |           2. 使用相同的key和value依次向5个实例获取锁。                                                                  |
+     * |              注:为了避免在某个redis节点耗时太久而影响到对后面的Redis节点的锁的获取;                                         |
+     * |                 客户端在获取每一个Redis节点的锁的时候,自身需要设置一个较小的等待获取锁超时的时间,                             |
+     * |                 一旦都在某个节点获取分布式锁的时间超过了超时时间,那么就认为在这个节点获取分布式锁失败,                        |
+     * |                 (不把时间浪费在这一个节点上),继续获取下一个节点的分布式锁。                                              |
+     * |           3. 客户端通过当前时间(t1)减去t0,计算(从所有redis节点)获取锁所消耗的总时间t2(注:t2=t1-t0)。                      |
+     * |              只有t2小于锁本身的锁定时长(注:若锁的锁定时长是1小时, 假设下午一点开始上锁,那么锁会在下午两点                     |
+     * |              的时候失效, 而你却在两点后才获取到锁,这个时候已经没意义了),并且,客户端在至少在多半Redis                        |
+     * |              节点上获取到锁, 我们才认为分布式锁获取成功。                                                                |
+     * |           5. 如果锁已经获取,那么  锁的实际有效时长 = 锁的总有效时长 - 获取分布式锁所消耗的时长; 锁的实际有效时长 应保证 > 0。    |
+     * |              注: 也就是说, 如果获取锁失败,那么                                                                        |
+     * |                  A. 可能是   获取到的锁的个数,不满足大多数原则。                                                         |
+     * |                  B. 也可能是 锁的实际有效时长不大于0。                                                                  |
+     * |      - 释放分布式锁: 在每个redis节点上试着删除锁(, 不论有没有在该节点上获取到锁)。                                          |
+     * |   - 集群下的分布式锁,可直接使用现有类库<a href="https://github.com/redisson/redisson"/>                                |
+     * |                                                                                                                    |
+     * |   注: 如果Redis集群项目能够容忍master宕机导致单机版分布式锁失效的情况的话,那么是直接使用单机版分布式锁在Redis集群的项目中的;     |
+     * |       如果Redis集群项目不能容忍单机版分布式锁失效的情况的话,那么请使用基于RedLock算法的集群版分布式锁;                        |
+     * |--------------------------------------------------------------------------------------------------------------------|
+     *
+     * @author JustryDeng
+     * @date 2020/3/14 19:23:26
+     */
+    public static class LockOps {
+
+        /** lua脚本, 保证 释放锁脚本 的原子性(以避免, 并发场景下, 释放了别人的锁) */
+        private static final String RELEASE_LOCK_LUA;
+
+        /** 分布式锁默认(最大)存活时长 */
+        public static final long DEFAULT_LOCK_TIMEOUT = 3;
+
+        /** DEFAULT_LOCK_TIMEOUT的单位 */
+        public static final TimeUnit DEFAULT_TIMEOUT_UNIT = TimeUnit.SECONDS;
+
+        static {
+            // 不论lua中0是否代表失败; 对于java的Boolean而言, 返回0, 则会被解析为false
+            RELEASE_LOCK_LUA = "if redis.call('get',KEYS[1]) == ARGV[1] "
+                    + "then "
+                    + "    return redis.call('del',KEYS[1]) "
+                    + "else "
+                    + "    return 0 "
+                    + "end ";
+        }
+
+        /**
+         * 获取(分布式)锁.
+         *
+         * 注: 获取结果是即时返回的、是非阻塞的。
+         *
+         * @see LockOps#getLock(String, String, long, TimeUnit)
+         */
+        public static boolean getLock(final String key, final String value) {
+            return getLock(key, value, DEFAULT_LOCK_TIMEOUT, DEFAULT_TIMEOUT_UNIT);
+        }
+
+        /**
+         * 获取(分布式)锁。
+         * 若成功, 则直接返回;
+         * 若失败, 则进行重试, 直到成功 或 超时为止。
+         *
+         * 注: 获取结果是阻塞的, 要么成功, 要么超时, 才返回。
+         *
+         * @param retryTimeoutLimit
+         *            重试的超时时长(ms)
+         * 其它参数可详见:
+         *    @see LockOps#getLock(String, String, long, TimeUnit)
+         *
+         * @return 是否成功
+         */
+        public static boolean getLockUntilTimeout(final String key, final String value,
+                                                  final long retryTimeoutLimit) {
+            return getLockUntilTimeout(key, value, DEFAULT_LOCK_TIMEOUT, DEFAULT_TIMEOUT_UNIT, retryTimeoutLimit);
+        }
+
+        /**
+         * 获取(分布式)锁。
+         * 若成功, 则直接返回;
+         * 若失败, 则进行重试, 直到成功 或 超时为止。
+         *
+         * 注: 获取结果是阻塞的, 要么成功, 要么超时, 才返回。
+         *
+         * @param retryTimeoutLimit
+         *            重试的超时时长(ms)
+         * 其它参数可详见:
+         *    @see LockOps#getLock(String, String, long, TimeUnit, boolean)
+         *
+         * @return 是否成功
+         */
+        public static boolean getLockUntilTimeout(final String key, final String value,
+                                                  final long timeout, final TimeUnit unit,
+                                                  final long retryTimeoutLimit) {
+            log.info("getLockUntilTimeout(...) => key -> {}, value -> {}, timeout -> {}, unit -> {}, "
+                    + "retryTimeoutLimit -> {}ms", key, value, timeout, unit, retryTimeoutLimit);
+            long startTime = Instant.now().toEpochMilli();
+            long now = startTime;
+            do {
+                try {
+                    boolean alreadyGotLock = getLock(key, value, timeout, unit, false);
+                    if (alreadyGotLock) {
+                        log.info("getLockUntilTimeout(...) => consume time -> {}ms, result -> true", now - startTime);
+                        return true;
+                    }
+                } catch (Exception e) {
+                    log.warn("getLockUntilTimeout(...) => try to get lock failure! e.getMessage -> {}",
+                            e.getMessage());
+                }
+                now = Instant.now().toEpochMilli();
+            } while (now < startTime + retryTimeoutLimit);
+            log.info("getLockUntilTimeout(...) => consume time -> {}ms, result -> false", now - startTime);
+            return false;
+        }
+
+        /**
+         * 获取(分布式)锁
+         *
+         * 注: 获取结果是即时返回的、是非阻塞的。
+         *
+         * @see LockOps#getLock(String, String, long, TimeUnit, boolean)
+         */
+        public static boolean getLock(final String key, final String value,
+                                      final long timeout, final TimeUnit unit) {
+            return getLock(key, value, timeout, unit, true);
+        }
+
+        /**
+         * 获取(分布式)锁
+         *
+         * 注: 获取结果是即时返回的、是非阻塞的。
+         *
+         * @param key
+         *            锁名
+         * @param value
+         *            锁名对应的value
+         *            注: value一般采用全局唯一的值, 如: requestId、uuid等。
+         *               这样, 释放锁的时候, 可以再次验证value值,
+         *               保证自己上的锁只能被自己释放, 而不会被别人释放。
+         *               当然, 如果锁超时时, 会被redis自动删除释放。
+         * @param timeout
+         *            锁的(最大)存活时长
+         *            注: 一般的, 获取锁与释放锁 都是成对使用的, 在锁在达到(最大)存活时长之前,都会被主动释放。
+         *                但是在某些情况下(如:程序获取锁后,释放锁前,崩了),锁得不到释放, 这时就需要等锁过
+         *                了(最大)存活时长后,被redis自动删除清理了。这样就能保证redis中不会留下死数据。
+         * @param unit
+         *            timeout的单位
+         * @param recordLog
+         *            是否记录日志
+         *
+         * @return 是否成功
+         */
+        public static boolean getLock(final String key, final String value,
+                                      final long timeout, final TimeUnit unit,
+                                      boolean recordLog) {
+            if (recordLog) {
+                log.info("getLock(...) => key -> {}, value -> {}, timeout -> {}, unit -> {}, recordLog -> {}",
+                        key, value, timeout, unit, recordLog);
+            }
+            Boolean result = redisTemplate.execute((RedisConnection connection) ->
+                    connection.set(key.getBytes(StandardCharsets.UTF_8),
+                            value.getBytes(StandardCharsets.UTF_8),
+                            Expiration.seconds(unit.toSeconds(timeout)),
+                            RedisStringCommands.SetOption.SET_IF_ABSENT)
+            );
+            if (recordLog) {
+                log.info("getLock(...) => result -> {}", result);
+            }
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 释放(分布式)锁
+         *
+         * 注: 此方式能(通过value的唯一性)保证: 自己加的锁, 只能被自己释放。
+         * 注: 锁超时时, 也会被redis自动删除释放。
+         *
+         * @param key
+         *            锁名
+         * @param value
+         *            锁名对应的value
+         *
+         * @return 释放锁是否成功
+         * @date 2020/3/15 17:00:45
+         */
+        public static boolean releaseLock(final String key, final String value) {
+            log.info("releaseLock(...) => key -> {}, lockValue -> {}", key, value);
+            Boolean result = redisTemplate.execute((RedisConnection connection) ->
+                    connection.eval(RELEASE_LOCK_LUA.getBytes(),
+                            ReturnType.BOOLEAN ,1,
+                            key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8))
+            );
+            log.info("releaseLock(...) => result -> {}", result);
+            if (result == null) {
+                throw new RedisOpsResultIsNullException();
+            }
+            return result;
+        }
+
+        /**
+         * 释放锁, 不校验该key对应的value值
+         *
+         * 注: 此方式释放锁,可能导致: 自己加的锁, 结果被别人释放了。
+         *     所以不建议使用此方式释放锁。
+         *
+         * @param key
+         *            锁名
+         * @date 2020/3/15 18:56:59
+         */
+        @Deprecated
+        public static void releaseLock(final String key) {
+            KeyOps.delete(key);
+        }
+    }
+
+    /**
+     * 当使用Pipeline 或 Transaction操作redis时, (不论redis中实际操作是否成功, 这里)结果(都)会返回null。
+     * 此时,如果试着将null转换为基本类型的数据时,会抛出此异常。
+     *
+     * 即: 此工具类中的某些方法, 希望不要使用Pipeline或Transaction操作redis。
+     *
+     * 注: Pipeline 或 Transaction默认是不启用的, 可详见源码:
+     *     @see LettuceConnection#isPipelined()
+     *     @see LettuceConnection#isQueueing()
+     *     @see JedisConnection#isPipelined()
+     *     @see JedisConnection#isQueueing()
+     *
+     * @author JustryDeng
+     * @date 2020/3/14 21:22:39
+     */
+    public static class RedisOpsResultIsNullException extends NullPointerException {
+
+        public RedisOpsResultIsNullException() {
+            super();
+        }
+
+        public RedisOpsResultIsNullException(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * 提供一些基础功能支持
+     *
+     * @author JustryDeng
+     * @date 2020/3/16 0:48:14
+     */
+    public static class Helper {
+
+        /** 默认拼接符 */
+        public static final String DEFAULT_SYMBOL = ":";
+
+        /**
+         * 拼接args
+         *
+         * @see Helper#joinBySymbol(String, String...)
+         */
+        public static String join(String... args) {
+            return Helper.joinBySymbol(DEFAULT_SYMBOL, args);
+        }
+
+        /**
+         * 使用symbol拼接args
+         *
+         * @param symbol
+         *            分隔符, 如: 【:】
+         * @param args
+         *            要拼接的元素数组, 如: 【a b c】
+         *
+         * @return  拼接后的字符串, 如  【a:b:c】
+         * @date 2019/9/8 16:11
+         */
+        public static String joinBySymbol(String symbol, String... args) {
+            if (symbol == null || symbol.trim().length() == 0) {
+                throw new RuntimeException(" symbol must not be empty!");
+            }
+            if (args == null || args.length == 0) {
+                throw new RuntimeException(" args must not be empty!");
+            }
+            StringBuilder sb = new StringBuilder(16);
+            for (String arg : args) {
+                sb.append(arg).append(symbol);
+            }
+            sb.replace(sb.length() - symbol.length(), sb.length(), "");
+            return sb.toString();
+        }
+
+    }
+}

+ 45 - 0
src/main/java/com/loan/system/utils/ResultUtil.java

@@ -0,0 +1,45 @@
+package com.loan.system.utils;
+
+import com.loan.system.domain.enums.ExceptionEnum;
+import com.loan.system.domain.pojo.Result;
+
+import java.util.HashMap;
+
+/**
+ * @author EdwinXu
+ * @date 2020/9/2 - 19:53
+ * @Description 响应工具类
+ */
+public class ResultUtil {
+    private static Result result = Result.getInstance();
+
+    public static Result success(String msg){
+        return result.setCode(200).setMsg(msg).setData(null);
+    }
+
+    public static Result success(String msg, Object obj){
+        return result.setCode(200).setMsg(msg).setData(obj);
+    }
+
+    public static Result success(Long total, Object obj){
+        final HashMap<String, Object> map = new HashMap<>(2);
+        map.put("list", obj);
+        map.put("total", total);
+        return result.setCode(200).setMsg("success").setData(map);
+    }
+
+    public static Result success(){
+        return success("success");
+    }
+
+    public static Result error(Integer code, String msg){
+        return result.setCode(code).setMsg(msg).setData(null);
+    }
+
+    public static Result error(ExceptionEnum exceptionEnum){
+        return result.setCode(exceptionEnum.getCode()).setMsg(exceptionEnum.getMsg()).setData(null);
+    }
+    public static Result error(ExceptionEnum exceptionEnum, Object obj){
+        return result.setCode(exceptionEnum.getCode()).setMsg(exceptionEnum.getMsg()).setData(obj);
+    }
+}

+ 45 - 0
src/main/java/com/loan/system/utils/TreeUtil.java

@@ -0,0 +1,45 @@
+package com.loan.system.utils;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+/**
+ * 将列表转换为树形结构
+ * @author : EdwinXu
+ * @date : 2020/10/2 23:54
+ * https://www.cnblogs.com/wkaca7114/p/tree.html 还需要比较
+ */
+public class TreeUtil {
+    public static JSONArray listToTree(JSONArray arr,String id,String pid,String child){
+        JSONArray r = new JSONArray();
+        JSONObject hash = new JSONObject();
+        //将数组转为Object的形式,key为数组中的id
+        for (Object o : arr) {
+            JSONObject json = (JSONObject) o;
+            hash.put(json.getString(id), json);
+        }
+        //遍历结果集
+        for (Object o : arr) {
+            //单条记录
+            JSONObject aVal = (JSONObject) o;
+            //在hash中取出key为单条记录中pid的值
+            JSONObject hashVP = (JSONObject) hash.get(aVal.get(pid).toString());
+            //如果记录的pid存在,则说明它有父节点,将她添加到孩子节点的集合中
+            if (hashVP != null) {
+                //检查是否有child属性
+                if (hashVP.get(child) != null) {
+                    JSONArray ch = (JSONArray) hashVP.get(child);
+                    ch.add(aVal);
+                    hashVP.put(child, ch);
+                } else {
+                    JSONArray ch = new JSONArray();
+                    ch.add(aVal);
+                    hashVP.put(child, ch);
+                }
+            } else {
+                r.add(aVal);
+            }
+        }
+        return r;
+    }
+}

+ 122 - 0
src/main/java/com/loan/system/utils/WeChatUtil.java

@@ -0,0 +1,122 @@
+package com.loan.system.utils;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.codec.binary.Base64;
+import org.codehaus.xfire.client.Client;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author raptor
+ * @description WeChatUtil
+ * @date 2021/4/1 13:21
+ */
+public class WeChatUtil {
+    private static ObjectMapper mapper = new ObjectMapper();
+    private static ApplicationContext appContext;
+
+    /**
+     *
+     * @param person_info
+     * 接收人信息,每个人员信息格式为:名字|学工号|部门ID|部门名称|对应微信账号,
+     * 人与人之间用“^@^”隔开(如果数据无法提供,可以写空,但是“|”不可省略),必须
+     *                      "raptor|202050235|11644008804352|杭州电子科技大学|18758159060"
+     * @param priority  发送优先级(1:紧急通知;2:验证码;3:立即发送;4:发送),必须
+     * @param wechat_info   内容,必须
+     * @return  {"result":true,"msg_id":"22961055561793536","msg":"发布成功"}
+     */
+    public static Object sendWeChatMessage(String person_info, String priority, String wechat_info){
+        Map<String, Object> map = new HashMap<String, Object>(16);
+        try {
+            appContext = new ClassPathXmlApplicationContext(new String[] {});
+            // webservice 地址由生产环境决定
+            org.springframework.core.io.Resource resource = appContext
+                    .getResource("http://i.hdu.edu.cn/tp_mp/service/WechatService?wsdl");
+
+            /** start 协议参数 start **/
+            // 调用接口的第三方系统协议名称。由双方协议确定。(*必填项)
+            map.put("tp_name", "Document");
+            // 提供接口服务的系统名称(sys/up/mp…)。由统一门户系统提供。(*必填项)
+            map.put("sys_id", "mp");
+            // 接口方法所属模块。由统一门户系统提供。(*必填项)
+            map.put("module_id", "wechat");
+            // SHA 加密后第三方系统的权限密钥值。权限密钥由统一门户系统提供。(*必填项)
+            map.put("secret_key", getSHA("xWpDa+fGlqkXGUZLNjawGT+9MHw="));
+            // 对应的接口方法名称,由统一门户系统提供。(*必填项)
+            map.put("interface_method", "saveWechatInfo");
+            /** end 协议参数 end **/
+
+            map.put("person_info", person_info);
+
+            map.put("send_priority", priority);
+
+            /** start 业务参数 start **/
+            //发送微信发布类型(text:⽂本;news:图⽂;image:图⽚;file:⽂件;video:视频;voice:⾳频),不可为空,参数不可省略
+            map.put("wechat_type", "text");
+            //内容,必须
+            map.put("wechat_info", wechat_info);
+            //发送⼈ID_NUMBER,需要发送回执的时候不可为空,参数不可省略
+            map.put("operator_id_number", "09901");
+            //发送微信附件(包括图⽚,⽂件,视频和⾳频的附件路径),为空,参数不可	省略
+            map.put("wechat_attachment", "");
+            //发送附件名称,为空,参数不可省略
+            map.put("attachment_name", "");
+            //发送附件标题,为空,参数不可省略
+            map.put("attachment_title", "");
+            //发送附件描述,为空,参数不可省略
+            map.put("attachment_des", "");
+            //发送附件时⻓(视频和⾳频),为空,参数不可省略
+            map.put("attachment_time", "");
+            //发送模板选择,不发送模板值为”0”,参数不可省略
+            map.put("templet_id", "0");
+            //发送回执选择,不发送回执值为”0”,参数不可省略
+            map.put("receipt_id", "0");
+            //发送⼈签名,根据模板⽽定,选择的模板有“发送⼈签名”标签的需要写值,其他为空,参数不可省略
+            map.put("person_send", "公管处");
+            /** end 业务参数 end **/
+
+
+
+            String json = mapper.writeValueAsString(map);
+            // 请求参数
+            System.out.println("json:" + json);
+
+            Client client = new Client(resource.getInputStream(), null);
+            Object[] result = client.invoke("saveWechatInfo", new Object[] { json });
+
+            // 返回结果
+            System.out.println(result[0]);
+
+            client.close();
+            return result[0];
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+    /**
+     * 获取 SHA 值  
+     *
+     * @author neusoft
+     * @since V7.0
+     * @param password
+     * @return SHA(password)
+     */
+    private static String getSHA(String password) {
+        try {
+            MessageDigest sha = MessageDigest.getInstance("SHA");
+            sha.update(password.getBytes());
+            byte[] hash = sha.digest();
+            return new String(Base64.encodeBase64(hash));
+        } catch (NoSuchAlgorithmException e1) {
+            e1.printStackTrace();
+        }
+        return "";
+    }
+}

+ 70 - 0
src/main/java/com/loan/system/utils/logUtils/IPUtils.java

@@ -0,0 +1,70 @@
+package com.loan.system.utils.logUtils;
+
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.*;
+import java.util.Enumeration;
+
+/*
+ * ip工具类
+ *
+ * @author chen
+ * @date 2019/10/01
+ * */
+public class IPUtils {
+    public static String getLocalIP() throws SocketException {
+        String localIP = null;
+        Enumeration allNetInterfaces = NetworkInterface.getNetworkInterfaces();
+        InetAddress ip = null;
+        while (allNetInterfaces.hasMoreElements()) {
+            NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
+            Enumeration addresses = netInterface.getInetAddresses();
+            while (addresses.hasMoreElements()) {
+                ip = (InetAddress) addresses.nextElement();
+                if (ip != null && ip instanceof Inet4Address) {
+                    localIP = ip.getHostAddress();
+                    if (!"127.0.0.1".equalsIgnoreCase(localIP)) {
+                        return localIP;
+                    }
+                }
+            }
+        }
+        return localIP;
+    }
+
+    /**
+     * 获取当前网络ip
+     *
+     * @param request
+     * @return
+     */
+    public static  String getIpAddr(HttpServletRequest request) {
+        String ipAddress = request.getHeader("x-forwarded-for");
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
+            ipAddress = request.getRemoteAddr();
+            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
+                //根据网卡取本机配置的IP
+                InetAddress inet = null;
+                try {
+                    inet = InetAddress.getLocalHost();
+                } catch (UnknownHostException e) {
+                    e.printStackTrace();
+                }
+                ipAddress = inet.getHostAddress();
+            }
+        }
+        //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
+        if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
+            if (ipAddress.indexOf(",") > 0) {
+                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
+            }
+        }
+        return ipAddress;
+    }
+}

+ 108 - 0
src/main/java/com/loan/system/utils/logUtils/OperLogAspect.java

@@ -0,0 +1,108 @@
+package com.loan.system.utils.logUtils;
+
+
+import com.alibaba.fastjson.JSON;
+import com.loan.system.domain.entity.ExceptionLog;
+import com.loan.system.service.ExceptionLogService;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.util.Date;
+
+/**
+ * 操作日志、异常日志处理
+ *
+ * @author chen
+ * @date 2019/10/01
+ */
+@Aspect
+@Component
+public class OperLogAspect {
+    
+    @Autowired
+    ExceptionLogService exceptionService;
+
+    /**
+     * 异常日志切入点
+     */
+    @Pointcut("execution(* com.hdu.maintenance.controller..*.*(..))")
+    public void exceptionLogPoint() {
+    }
+
+    @AfterThrowing(pointcut = "exceptionLogPoint()", throwing = "e")
+    public void exceptionLog(JoinPoint joinPoint, Throwable e) {
+        // 获取RequestAttributes
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        // 从获取RequestAttributes中获取HttpServletRequest的信息
+        HttpServletRequest request = (HttpServletRequest) requestAttributes
+                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
+
+        ExceptionLog exceptionLog = new ExceptionLog();
+        try {
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            Method method = signature.getMethod();
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = method.getName();
+            methodName = className + "." + methodName;
+
+            String[] paraNames = signature.getParameterNames();
+            Object[] args = joinPoint.getArgs();
+            StringBuilder sb = new StringBuilder();
+            sb.append("{");
+            if (paraNames != null && paraNames.length > 0 && args != null && args.length > 0) {
+                for (int i = 0; i < paraNames.length; i++) {
+                    sb.append("\"" + paraNames[i] + "\":" + JSON.toJSONString(args[i]) + ",");
+                }
+            }
+            sb.delete(sb.length() - 1, sb.length() + 1);
+            sb.append("}");
+            String me_param = sb.toString();
+            exceptionLog.setMethodParam(me_param);
+            exceptionLog.setExceptionMethod(methodName); // 请求方法名
+            exceptionLog.setExceptionName(e.getClass().getName()); // 异常名称
+            exceptionLog.setExceptionMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace())); // 异常信息
+            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+            if (!(authentication instanceof AnonymousAuthenticationToken)) {
+                String currentUserName = authentication.getName();
+                exceptionLog.setOperUserName(currentUserName); // 用户名
+            }
+            exceptionLog.setOperUri(request.getRequestURI()); // 操作URI
+            exceptionLog.setOperIp(IPUtils.getIpAddr(request)); // 操作员IP
+            exceptionLog.setOperTime(new Date());
+            exceptionService.insertExceptionLog(exceptionLog);
+        } catch (Exception exception) {
+            e.printStackTrace();
+        }
+    }
+
+
+    /**
+     * 转换异常信息为字符串
+     *
+     * @param exceptionName    异常名称
+     * @param exceptionMessage 异常信息
+     * @param elements         堆栈信息
+     */
+    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
+        StringBuffer strbuff = new StringBuffer();
+        for (StackTraceElement stet : elements) {
+            strbuff.append(stet + "\n");
+        }
+        String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
+        return message;
+    }
+
+
+}

+ 42 - 0
src/main/java/com/loan/system/utils/logUtils/ServiceHelper.java

@@ -0,0 +1,42 @@
+package com.loan.system.utils.logUtils;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/*
+ * 反射工具类
+ *
+ * @author chen
+ * @date 2019/10/01
+ * */
+@Component
+public class ServiceHelper {
+
+    @Autowired
+    ApplicationContext applicationContext;
+
+    public Object invoke(String sname, String mname) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+        Object gteBean = applicationContext.getBean(sname);
+        Class objClass = gteBean.getClass();
+        Method getMethod = objClass.getMethod(mname, Integer.class);
+        return getMethod.invoke(gteBean, 8);
+    }
+
+    /*
+     * 动态获取bean,反射执行service的方法
+     *
+     * @param sname 被获取的bean
+     * @param mname bean中的方法名
+     * @param id    查询条件
+     * */
+    public Object getBeanById(String sname, String mname, Long id) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+        Object gteBean = applicationContext.getBean(sname);
+        Class objClass = gteBean.getClass();
+        Method getMethod = objClass.getMethod(mname, Long.class);
+        return getMethod.invoke(gteBean, id);
+    }
+}

+ 75 - 0
src/main/resources/application-dev.yaml

@@ -0,0 +1,75 @@
+server:
+  port: 8080
+
+spring:
+#  servlet:
+#    multipart:
+#      enabled: true
+#      max-file-size: 10MB
+#      max-request-size: 30MB
+  aop:
+    proxy-target-class: true
+    auto: true
+  jpa:
+    database: mysql
+    hibernate:
+      ddl-auto: update
+    show-sql: false
+    # 延续 session 到返回视图层,但是在测试类中无效
+    open-in-view: true
+    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
+    properties:
+      hibernate:
+        format_sql: true
+      generate_statistics: true
+      jdbc:
+        #每批500条提交
+        batch_size: 500
+        batch_versioned_data: true
+      order_inserts: true
+      order_updates: true
+  jackson:
+    time-zone: GMT+8
+    date-format: yyyy-MM-dd HH:mm:ss
+  redis:
+#    port: 6379
+#    database: 0
+#    host: 127.0.0.1
+#    password: 12345
+    jedis:
+      pool:
+        max-active: 8
+        max-wait: -1ms
+        max-idle: 8
+        min-idle: 0
+    timeout: 5000ms
+#    port: ${REDIS_PORT}
+#    host: ${REDIS_HOME}
+  data:
+    redis:
+      repositories:
+        enabled: false
+
+  datasource:
+#    url: jdbc:mysql://${MYSQL_URL}/${MYSQL_DATABASE}?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
+#    username: ${MYSQL_USERNAME}
+#    password: ${MYSQL_PASSWORD}
+#    driver-class-name: com.mysql.cj.jdbc.Driver
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://localhost:3306/loan_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai #改
+    username: root
+    password: wgw20030620 #改
+mybatis:
+  mapper-locations: classpath*:/mapping/*Mapper.xml
+ # type-aliases-package: com.he.common.entity.domain
+
+upload:
+  host: "http://tczbus.natappfree.cc/uploads"
+  location: "C:\\Users\\45528\\project\\java\\maintenance\\maintenance\\test" #改
+  extensions: "pdf,doc,docx,xlsx,JPG,jpg,bmp,BMP,gif,GIF,BMP,png,PNG,bmp,jpeg,JPEG,svg,txt"
+  #改
+
+system:
+  wechat:
+    appid: wx9c0acf979455549f
+    secret: 37452088548ea9cc6e244dc7817d938d

+ 57 - 0
src/main/resources/application-prod.yaml

@@ -0,0 +1,57 @@
+server: #部署
+  port: 8080
+  servlet:
+    context-path: /api/admin
+spring:
+  servlet:
+    multipart:
+      enabled: true
+      max-file-size: 10MB
+      max-request-size: 30MB
+  aop:
+    proxy-target-class: true
+    auto: true
+  jpa:
+    database: mysql
+    hibernate:
+      ddl-auto: update
+    show-sql: false
+    open-in-view: true
+    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
+    properties:
+      generate_statistics: true
+      jdbc:
+        #每批500条提交
+        batch_size: 500
+        batch_versioned_data: true
+      order_inserts: true
+      order_updates: true
+  jackson:
+    time-zone: GMT+8
+    date-format: yyyy-MM-dd HH:mm:ss
+  redis:
+    port: 6379
+    database: 0
+    host: 127.0.0.1
+    password: Hdu505...
+    jedis:
+      pool:
+        max-active: 8
+        max-wait: -1ms
+        max-idle: 8
+        min-idle: 0
+    timeout: 5000ms
+  data:
+    redis:
+      repositories:
+        enabled: false
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://localhost:9898/maintenance?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
+    username: root
+    password: Hdu505...
+
+upload:
+  host: "http://hqzxprojects.hdu.edu.cn/uploads"
+  location: "/maintenance/uploads"
+  extensions: "pdf,doc,docx,xlsx,JPG,jpg,bmp,BMP,gif,GIF,BMP,png,PNG,bmp,jpeg,JPEG,svg,txt"

+ 61 - 0
src/main/resources/application-test.yaml

@@ -0,0 +1,61 @@
+server:
+  port: 8080
+  servlet:
+    context-path: /api/admin
+spring:
+  servlet:
+    multipart:
+      enabled: true
+      max-file-size: 10MB
+      max-request-size: 30MB
+  aop:
+    proxy-target-class: true
+    auto: true
+  jpa:
+    database: mysql
+    hibernate:
+      ddl-auto: update
+    show-sql: true
+    open-in-view: true
+    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
+    properties:
+      generate_statistics: true
+      jdbc:
+        #每批500条提交
+        batch_size: 500
+        batch_versioned_data: true
+      order_inserts: true
+      order_updates: true
+  jackson:
+    time-zone: GMT+8
+    date-format: yyyy-MM-dd HH:mm:ss
+  redis:
+    port: 6379
+    database: 0
+#    host: 127.0.0.1
+#    password: Hdu505...
+    jedis:
+      pool:
+        max-active: 8
+        max-wait: -1ms
+        max-idle: 8
+        min-idle: 0
+    timeout: 5000ms
+  data:
+    redis:
+      repositories:
+        enabled: false
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://159.138.147.147:3306/maintenance?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
+    username: root
+    password: hzq123456
+
+upload:
+  host: "http://159.138.147.147:8888/uploads"
+  location: "/maintenance/uploads"
+  extensions: "pdf,doc,docx,xlsx,JPG,jpg,bmp,BMP,gif,GIF,BMP,png,PNG,bmp,jpeg,JPEG,svg,txt"
+
+
+
+  # 服务器密码:

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

@@ -0,0 +1,46 @@
+spring:
+  profiles:
+    active: dev
+  servlet:
+    multipart:
+      enabled: true
+      max-file-size: 50MB
+      max-request-size: 50MB
+#  mail:
+#    host: mail2010.hollysys.net
+#    username: fczf@hollysys.net
+#    password: Hls123456
+#    port: 443
+#    default-encoding: utf-8
+#    properties:
+#      mail:
+#        debug: true
+##        smtp:
+##          socketFactory:
+##            class: javax.net.ssl.SSLSocketFactory
+#    protocol: smtp
+
+JWT:
+  LOGIN_URL: "/login"
+  SECRET: "HDU-JUAasSDSAds*87ASud/A?D(G+KbPeShVmYq3s6v9y$B&E)H@McQfTjWnZr4u7w"
+  HEADER: "Authorization"
+  EXPIRATION: 604800
+  TOKEN_PREFIX: "HDU"
+  ISSUER: "GongGuanChu"
+
+logging:
+  level:
+    root:
+      info
+
+system:
+  jwt:
+    user-secret-key: HDU-JUAasSDSAds*87ASud/A?D(G+KbPeShVmYq3s6v9y$B&E)H@McQfTjWnZr4u7w
+    user-ttl: 604800000
+    user-token-name: Authorization
+    admin-secret-key: HDU-JUAasSDSAds*87ASud/A?D(G+KbPeShVmYq3s6v9y$B&E)H@McQfTjWnZr4u7w
+    admin-ttl: 604800000
+    admin-token-name: Authorization
+  wechat:
+    appid: ${system.wechat.appid}
+    secret: ${system.wechat.secret}

+ 95 - 0
src/main/resources/logback.xml

@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration debug="false">
+    <!--定义日志文件的存储地址 -->
+    <property name="LOG_HOME" value="/maintenance/logs" />
+
+    <property name="COLOR_PATTERN" value="%black(%contextName-) %red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta( %replace(%caller{1}){'\t|Caller.{1}0|\r\n', ''})- %gray(%msg%xEx%n)" />
+    <!-- 控制台输出 -->
+    <appender name="STDOUT"
+              class="ch.qos.logback.core.ConsoleAppender">
+        <encoder
+                class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <!-- 按照每天生成日志文件 -->
+    <appender name="FILE"
+              class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <rollingPolicy
+                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!--日志文件输出的文件名 -->
+            <FileNamePattern>${LOG_HOME}/%d{yyyy-MM-dd}.log
+            </FileNamePattern>
+            <!--日志文件保留天数 -->
+            <MaxHistory>30</MaxHistory>
+        </rollingPolicy>
+        <encoder
+                class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern>
+        </encoder>
+        <!--日志文件最大的大小 -->
+        <triggeringPolicy
+                class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+            <MaxFileSize>10MB</MaxFileSize>
+        </triggeringPolicy>
+    </appender>
+
+    <!-- 生成html格式日志开始 -->
+    <appender name="HTML" class="ch.qos.logback.core.FileAppender">
+        <!-- 过滤器,只记录WARN级别的日志 -->
+        <!-- <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>info</level>
+            <onMatch>ACCEPT</onMatch> <onMismatch>DENY</onMismatch> </filter> -->
+
+        <encoder
+                class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+            <layout class="ch.qos.logback.classic.html.HTMLLayout">
+                <pattern>%p%d%msg%M%F{32}%L</pattern>
+            </layout>
+        </encoder>
+        <file>${LOG_HOME}/error-log.html</file>
+    </appender>
+    <!-- 生成html格式日志结束 -->
+
+    <!-- 每天生成一个html格式的日志开始 -->
+    <appender name="FILE_HTML"
+              class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <rollingPolicy
+                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!--日志文件输出的文件名 -->
+            <FileNamePattern>${LOG_HOME}/maintenance.%d{yyyy-MM-dd}.html
+            </FileNamePattern>
+            <!--日志文件保留天数 -->
+            <MaxHistory>30</MaxHistory>
+        </rollingPolicy>
+        <encoder
+                class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+            <layout class="ch.qos.logback.classic.html.HTMLLayout">
+                <pattern>%p%d%msg%M%F{32}%L</pattern>
+            </layout>
+        </encoder>
+        <!--日志文件最大的大小 -->
+        <triggeringPolicy
+                class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+            <MaxFileSize>10MB</MaxFileSize>
+        </triggeringPolicy>
+    </appender>
+    <!-- 每天生成一个html格式的日志结束 -->
+
+    <!--myibatis log configure -->
+    <logger name="com.apache.ibatis" level="TRACE" />
+    <logger name="java.sql.Connection" level="DEBUG" />
+    <logger name="java.sql.Statement" level="DEBUG" />
+    <logger name="java.sql.PreparedStatement" level="DEBUG" />
+
+    <!-- 日志输出级别 -->
+    <root level="INFO">
+        <appender-ref ref="STDOUT" />
+        <appender-ref ref="FILE" />
+        <appender-ref ref="HTML" />
+        <appender-ref ref="FILE_HTML" />
+    </root>
+
+</configuration>

+ 0 - 0
src/main/resources/rebel.xml


Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio