浏览代码

initial repository

lhm 4 年之前
父节点
当前提交
789a2c1286
共有 61 个文件被更改,包括 4200 次插入0 次删除
  1. 39 0
      .gitignore
  2. 31 0
      build.gradle
  3. 二进制
      gradle/wrapper/gradle-wrapper.jar
  4. 5 0
      gradle/wrapper/gradle-wrapper.properties
  5. 185 0
      gradlew
  6. 89 0
      gradlew.bat
  7. 1 0
      settings.gradle
  8. 13 0
      src/main/java/com/ematou/wxservice/WechatServiceApplication.java
  9. 52 0
      src/main/java/com/ematou/wxservice/aop/LogAspect.java
  10. 65 0
      src/main/java/com/ematou/wxservice/api/WeChatApi.java
  11. 124 0
      src/main/java/com/ematou/wxservice/api/WeChatApiRestTemplate.java
  12. 14 0
      src/main/java/com/ematou/wxservice/common/constant/ResponseCodeConstant.java
  13. 177 0
      src/main/java/com/ematou/wxservice/common/constant/WeChatConstant.java
  14. 64 0
      src/main/java/com/ematou/wxservice/common/utils/CapturePackageClasses.java
  15. 40 0
      src/main/java/com/ematou/wxservice/common/utils/DateUtil.java
  16. 70 0
      src/main/java/com/ematou/wxservice/common/utils/SignUtil.java
  17. 74 0
      src/main/java/com/ematou/wxservice/common/utils/StringUtil.java
  18. 92 0
      src/main/java/com/ematou/wxservice/common/utils/XStreamInitializer.java
  19. 100 0
      src/main/java/com/ematou/wxservice/common/utils/XStreamTransformer.java
  20. 71 0
      src/main/java/com/ematou/wxservice/common/web/R.java
  21. 39 0
      src/main/java/com/ematou/wxservice/common/xml/builder/BaseBuilder.java
  22. 38 0
      src/main/java/com/ematou/wxservice/common/xml/builder/NewsBuilder.java
  23. 28 0
      src/main/java/com/ematou/wxservice/common/xml/builder/TextBuilder.java
  24. 17 0
      src/main/java/com/ematou/wxservice/common/xml/converter/XStreamCDataConverter.java
  25. 36 0
      src/main/java/com/ematou/wxservice/config/AppConfig.java
  26. 33 0
      src/main/java/com/ematou/wxservice/config/WeChatGeneralConfig.java
  27. 19 0
      src/main/java/com/ematou/wxservice/controller/UserInfoController.java
  28. 32 0
      src/main/java/com/ematou/wxservice/controller/WeChatController.java
  29. 65 0
      src/main/java/com/ematou/wxservice/controller/WeChatMessageController.java
  30. 21 0
      src/main/java/com/ematou/wxservice/controller/WeChatViewController.java
  31. 144 0
      src/main/java/com/ematou/wxservice/entity/dto/AccessToken.java
  32. 112 0
      src/main/java/com/ematou/wxservice/entity/dto/TemplateMessage.java
  33. 238 0
      src/main/java/com/ematou/wxservice/entity/pojo/UserInfo.java
  34. 120 0
      src/main/java/com/ematou/wxservice/interceptor/MybatisInsertAndUpdateInterceptor.java
  35. 19 0
      src/main/java/com/ematou/wxservice/mapper/UserInfoMapper.java
  36. 81 0
      src/main/java/com/ematou/wxservice/mp/handler/WeChatClickEventMessageHandler.java
  37. 22 0
      src/main/java/com/ematou/wxservice/mp/handler/WeChatMessageHandler.java
  38. 26 0
      src/main/java/com/ematou/wxservice/mp/handler/WeChatNewsMessageHandler.java
  39. 30 0
      src/main/java/com/ematou/wxservice/mp/handler/WeChatTemplateMessageEventHandler.java
  40. 40 0
      src/main/java/com/ematou/wxservice/mp/handler/WeChatTextMessageHandler.java
  41. 26 0
      src/main/java/com/ematou/wxservice/mp/handler/WeChatViewEventMessageHandler.java
  42. 39 0
      src/main/java/com/ematou/wxservice/mp/menu/Button.java
  43. 28 0
      src/main/java/com/ematou/wxservice/mp/menu/ClickButton.java
  44. 94 0
      src/main/java/com/ematou/wxservice/mp/menu/Matchrule.java
  45. 32 0
      src/main/java/com/ematou/wxservice/mp/menu/MenuButton.java
  46. 113 0
      src/main/java/com/ematou/wxservice/mp/menu/MenuButtonManager.java
  47. 50 0
      src/main/java/com/ematou/wxservice/mp/menu/MiniProgramButton.java
  48. 40 0
      src/main/java/com/ematou/wxservice/mp/menu/ViewButton.java
  49. 369 0
      src/main/java/com/ematou/wxservice/mp/message/WeChatMessage.java
  50. 93 0
      src/main/java/com/ematou/wxservice/mp/message/WeChatMpXmlOutMessage.java
  51. 122 0
      src/main/java/com/ematou/wxservice/mp/message/WeChatMpXmlOutNewsMessage.java
  52. 37 0
      src/main/java/com/ematou/wxservice/mp/message/WeChatMpXmlOutTextMessage.java
  53. 48 0
      src/main/java/com/ematou/wxservice/mp/router/WeChatMessageHandlerRouter.java
  54. 44 0
      src/main/java/com/ematou/wxservice/service/WeChatMessageService.java
  55. 17 0
      src/main/java/com/ematou/wxservice/service/WeChatOAuth2Service.java
  56. 170 0
      src/main/java/com/ematou/wxservice/service/WeChatService.java
  57. 30 0
      src/main/resources/application.yml
  58. 56 0
      src/main/resources/logback.xml
  59. 20 0
      src/main/resources/mybatis/UserInfoMapper.xml
  60. 245 0
      src/main/resources/templates/hello.html
  61. 61 0
      src/test/java/com/ematou/wxservice/wechatservice/WechatserviceApplicationTests.java

+ 39 - 0
.gitignore

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

+ 31 - 0
build.gradle

@@ -0,0 +1,31 @@
+plugins {
+    id 'org.springframework.boot' version '2.3.10.RELEASE'
+    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
+    id 'java'
+}
+
+group = 'com.ematou.wechatservice'
+version = '0.0.1-SNAPSHOT'
+sourceCompatibility = '1.8'
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    implementation 'org.springframework.boot:spring-boot-starter-web'
+    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
+    implementation 'org.springframework.boot:spring-boot-starter-aop'
+    implementation 'org.springframework.boot:spring-boot-starter-logging'
+    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+    implementation group: 'com.alibaba', name: 'fastjson', version: '1.2.75'
+    implementation group: 'com.thoughtworks.xstream', name: 'xstream', version: '1.4.15'
+    runtimeOnly 'mysql:mysql-connector-java'
+    testImplementation('org.springframework.boot:spring-boot-starter-test') {
+        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
+    }
+}
+
+test {
+    useJUnitPlatform()
+}

二进制
gradle/wrapper/gradle-wrapper.jar


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

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
gradlew

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

+ 89 - 0
gradlew.bat

@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem 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, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 1 - 0
settings.gradle

@@ -0,0 +1 @@
+rootProject.name = 'wxservice'

+ 13 - 0
src/main/java/com/ematou/wxservice/WechatServiceApplication.java

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

+ 52 - 0
src/main/java/com/ematou/wxservice/aop/LogAspect.java

@@ -0,0 +1,52 @@
+package com.ematou.wxservice.aop;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 16:30
+ */
+@Aspect
+@Component
+public class LogAspect {
+
+    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);
+
+    // TODO
+    @Pointcut("execution(* com.ematou.wxservice.*.*Service||*RestTemplate||*Controller||*Handler.*(..))")
+    public void targetMethod() {}
+
+    @Around("targetMethod()")
+    public Object around(ProceedingJoinPoint joinPoint) {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature ms = (MethodSignature) signature;
+        String targetMethodName = ms.getMethod().getName();
+        String className = joinPoint.getTarget().getClass().getName();
+
+        ObjectMapper objectMapper = new ObjectMapper();
+
+        Object proceed = null;
+        try {
+            logger.info("----------------方法: " + className + "." + targetMethodName + "的参数为: " + objectMapper.writeValueAsString(joinPoint.getArgs()) + "----------------");
+            long start = System.currentTimeMillis();
+            proceed = joinPoint.proceed();
+            long end = System.currentTimeMillis();
+            logger.info("----------------方法: " + className + "." + targetMethodName + "执行的返回值为: " + proceed + "----------------");
+            logger.info("----------------方法: " + className + "." + targetMethodName + "执行的时间为: " + (end-start) + "毫秒----------------");
+        } catch (Throwable throwable) {
+            logger.error("----------------方法: " + className + "." + targetMethodName + "执行出现异常,错误信息为:" + throwable + "----------------");
+        }
+        return proceed;
+    }
+
+}

+ 65 - 0
src/main/java/com/ematou/wxservice/api/WeChatApi.java

@@ -0,0 +1,65 @@
+package com.ematou.wxservice.api;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 11:54
+ */
+public enum WeChatApi {
+
+    /**
+     * 微信基础服务获取AccessToken
+     */
+    GET_TOKEN("http://61.144.244.114:4040/wxbase/token", "GET"),
+    /**
+     * 微信基础服务生成AccessToken
+     */
+    GENERAL_TOKEN("http://61.144.244.114:4040/wxbase/generate/token", "GET"),
+    /**
+     * 获取用户信息,只能获取到openId,如需要使用unionId,需要将公众号绑定到微信开放平台帐号
+     */
+    GET_USER_INFO("https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN", "GET"),
+    /**
+     * 获取微信OAuth2的AccessToken
+     */
+    WECHAT_OAUTH_TOKEN(" https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", "GET"),
+    /**
+     * 创建菜单
+     * https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
+     */
+    CREATE_MENU("https://api.weixin.qq.com/cgi-bin/menu/create?access_token=%s", "POST"),
+    /**
+     * 获取模板消息ID,请求体参考:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html
+     */
+    GET_TEMPLATE_MSGID("https://api.weixin.qq.com/cgi-bin/template/api_add_template?access_token=%s", "POST"),
+    /**
+     * 发送模板消息,请求体参考:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html
+     * 发送成功和失败都有事件的推送
+     */
+    SEND_TEMPLATE_MSG("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", "POST");
+
+
+    private String url;
+    private String method;
+
+    WeChatApi(String url, String method) {
+        this.url = url;
+        this.method = method;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getMethod() {
+        return method;
+    }
+
+    public void setMethod(String method) {
+        this.method = method;
+    }
+}

+ 124 - 0
src/main/java/com/ematou/wxservice/api/WeChatApiRestTemplate.java

@@ -0,0 +1,124 @@
+package com.ematou.wxservice.api;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.ematou.wxservice.common.constant.WeChatConstant;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Arrays;
+
+/**
+ * 微信API请求工具类
+ * @author lhm
+ * @version 1.0
+ * 2021-05-13 11:10
+ */
+@Component
+public class WeChatApiRestTemplate {
+
+    private static final Logger logger = LoggerFactory.getLogger(WeChatApiRestTemplate.class);
+
+    @Autowired
+    RestTemplate restTemplate;
+
+    /**
+     * 请求微信API,返回体带有errcode和errmsg字段的调这个方法
+     * @param url       url地址
+     * @param params    参数
+     * @return          响应信息
+     */
+    public String getForRest(String url, Object...params) {
+        String res;
+        Integer errcode;
+        String errmsg;
+        try {
+            res = restTemplate.getForObject(url, String.class, params);
+
+            JSONObject response = JSON.parseObject(res);
+            errcode = response.getObject(WeChatConstant.RESPONSE_ERROR_CODE, Integer.class);
+            errmsg = response.getObject(WeChatConstant.RESPONSE_ERROR_MSG, String.class);
+            if (errcode != 0 || !errmsg.equalsIgnoreCase(WeChatConstant.RESPONSE_OK)) {
+                logger.error("请求微信API失败,请求方法:GET\nurl:\n" + url + "\n请求参数:" + Arrays.toString(params) + "\nerror code:" + errcode + "\nerror message:" + errmsg);
+            }
+        } catch (Exception e) {
+            logger.error("请求微信API失败,请求方法:GET\nurl:\n" + url + "\n请求参数:" + Arrays.toString(params) + "\n异常信息:" + e.getMessage());
+            throw new RuntimeException("请求微信API失败,请求方法:GET\nurl:\n" + url + "\n请求参数:" + Arrays.toString(params) + "\n异常信息:" + e.getMessage());
+        }
+        return res;
+    }
+
+    /**
+     * 返回体不带有errcode和errmsg字段
+     * @param url       url地址
+     * @param params    参数
+     * @return          响应信息
+     */
+    public String getForOther(String url, Object...params) {
+        String res;
+        try {
+            res = restTemplate.getForObject(url, String.class, params);
+        } catch (Exception e) {
+            logger.error("请求微信API失败,请求方法:GET\nurl:\n" + url + "\n请求参数:" + Arrays.toString(params) + "\n异常信息:" + e.getMessage());
+            throw new RuntimeException("请求微信API失败,请求方法:GET\nurl:\n" + url + "\n请求参数:" + Arrays.toString(params) + "\n异常信息:" + e.getMessage());
+        }
+        return res;
+    }
+
+    /**
+     * 请求微信API,返回体带有errcode和errmsg字段的调这个方法
+     * @param url       url地址
+     * @param body      请求体
+     * @return          响应信息
+     */
+    public String postForRest(String url, Object body) {
+        String res;
+        Integer errcode;
+        String errmsg;
+        try {
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
+            HttpEntity<Object> request = new HttpEntity<>(body, headers);
+            res = restTemplate.postForObject(url, request, String.class);
+
+            JSONObject response = JSON.parseObject(res);
+            errcode = response.getObject(WeChatConstant.RESPONSE_ERROR_CODE, Integer.class);
+            errmsg = response.getObject(WeChatConstant.RESPONSE_ERROR_MSG, String.class);
+            if (errcode != 0 || !errmsg.equalsIgnoreCase(WeChatConstant.RESPONSE_OK)) {
+                logger.error("请求微信API失败,请求方法:POST\nurl:\n" + url + "\n请求体:" + body + "\nerror code:" + errcode + "\nerror message:" + errmsg);
+            }
+        } catch (Exception e) {
+            logger.error("请求微信API失败,请求方法:POST\nurl:\n" + url + "\n请求体:" + body + "\n异常信息:" + e.getMessage());
+            throw new RuntimeException("请求微信API失败,请求方法:POST\nurl:\n" + url + "\n请求体:" + body + "\n异常信息:" + e.getMessage());
+        }
+        return res;
+    }
+
+
+    /**
+     * 返回体不带有errcode和errmsg字段
+     * @param url       url地址
+     * @param body      请求体
+     * @return          响应信息
+     */
+    public String postForOther(String url, Object body) {
+        String res;
+        try {
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
+            HttpEntity<Object> request = new HttpEntity<>(body, headers);
+            res = restTemplate.postForObject(url, request, String.class);
+        } catch (Exception e) {
+            logger.error("请求微信API失败,请求方法:POST\nurl:\n" + url + "\n请求体:" + body + "\n异常信息:" + e.getMessage());
+            throw new RuntimeException("请求微信API失败,请求方法:POST\nurl:\n" + url + "\n请求体:" + body + "\n异常信息:" + e.getMessage());
+        }
+        return res;
+    }
+
+}

+ 14 - 0
src/main/java/com/ematou/wxservice/common/constant/ResponseCodeConstant.java

@@ -0,0 +1,14 @@
+package com.ematou.wxservice.common.constant;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 14:39
+ */
+public class ResponseCodeConstant {
+
+    public static final int code_Success = 0;
+
+    public static final int code_100 = 100;
+
+}

+ 177 - 0
src/main/java/com/ematou/wxservice/common/constant/WeChatConstant.java

@@ -0,0 +1,177 @@
+package com.ematou.wxservice.common.constant;
+
+/**
+ * 微信开发中需要用到的常量
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 15:10
+ */
+public class WeChatConstant {
+
+    public static final String RESPONSE_OK = "ok";
+    public static final String RESPONSE_ERROR_CODE = "errcode";
+    public static final String RESPONSE_ERROR_MSG = "errmsg";
+
+
+    /**
+     * 自定义的模板的编号,可通过模板编号获取模板消息id
+     */
+    public static class TemplateNumber {
+        public static final String TAKE_PARCEL_CODE_TEMPLATE = "暂无模板";
+    }
+
+    /**
+     * 自定义菜单事件的key
+     */
+    public static class CustomEventKey {
+        public static final String CALL_COMPANY = "call_company_button";        // 联系我们
+        public static final String MY_TAKE_PARCEL_CODE = "my_code_button";      // 我的取件码
+        public static final String MY_HISTORY_RECORD = "my_history_button";     // 取件历史
+        public static final String POLICY_SUPPORT = "policy_support_button";    // 政策支持
+    }
+
+    /**
+     * 消息类型
+     */
+    public static class XmlMsgType {
+        public static final String TEXT = "text";
+        public static final String IMAGE = "image";
+        public static final String VOICE = "voice";
+        public static final String SHORTVIDEO = "shortvideo";
+        public static final String VIDEO = "video";
+        public static final String NEWS = "news";
+        public static final String MUSIC = "music";
+        public static final String LOCATION = "location";
+        public static final String LINK = "link";
+        public static final String EVENT = "event";
+        public static final String DEVICE_TEXT = "device_text";
+        public static final String DEVICE_EVENT = "device_event";
+        public static final String DEVICE_STATUS = "device_status";
+        public static final String HARDWARE = "hardware";
+        public static final String TRANSFER_CUSTOMER_SERVICE = "transfer_customer_service";
+    }
+
+    /**
+     * 微信端推送过来的事件类型.
+     */
+    public static class EventType {
+        public static final String SUBSCRIBE = "subscribe";
+        public static final String UNSUBSCRIBE = "unsubscribe";
+        public static final String SCAN = "SCAN";
+        public static final String LOCATION = "LOCATION";
+        public static final String CLICK = "CLICK";
+        public static final String VIEW = "VIEW";
+        public static final String MASS_SEND_JOB_FINISH = "MASSSENDJOBFINISH";
+        /**
+         * 扫码推事件的事件推送
+         */
+        public static final String SCANCODE_PUSH = "scancode_push";
+        /**
+         * 扫码推事件且弹出“消息接收中”提示框的事件推送.
+         */
+        public static final String SCANCODE_WAITMSG = "scancode_waitmsg";
+        /**
+         * 弹出系统拍照发图的事件推送.
+         */
+        public static final String PIC_SYSPHOTO = "pic_sysphoto";
+        /**
+         * 弹出拍照或者相册发图的事件推送.
+         */
+        public static final String PIC_PHOTO_OR_ALBUM = "pic_photo_or_album";
+        /**
+         * 弹出微信相册发图器的事件推送.
+         */
+        public static final String PIC_WEIXIN = "pic_weixin";
+        /**
+         * 弹出地理位置选择器的事件推送.
+         */
+        public static final String LOCATION_SELECT = "location_select";
+
+        public static final String TEMPLATE_SEND_JOB_FINISH = "TEMPLATESENDJOBFINISH";
+        /**
+         * 微信小店 订单付款通知.
+         */
+        public static final String MERCHANT_ORDER = "merchant_order";
+
+        /**
+         * 卡券事件:卡券通过审核
+         */
+        public static final String CARD_PASS_CHECK = "card_pass_check";
+
+        /**
+         * 卡券事件:卡券未通过审核
+         */
+        public static final String CARD_NOT_PASS_CHECK = "card_not_pass_check";
+
+        /**
+         * 卡券事件:用户领取卡券
+         */
+        public static final String CARD_USER_GET_CARD = "user_get_card";
+
+        /**
+         * 卡券事件:用户转赠卡券
+         */
+        public static final String CARD_USER_GIFTING_CARD = "user_gifting_card";
+
+
+        /**
+         * 卡券事件:用户核销卡券
+         */
+        public static final String CARD_USER_CONSUME_CARD = "user_consume_card";
+
+
+        /**
+         * 卡券事件:用户通过卡券的微信买单完成时推送
+         */
+        public static final String CARD_USER_PAY_FROM_PAY_CELL = "user_pay_from_pay_cell";
+
+
+        /**
+         * 卡券事件:用户提交会员卡开卡信息
+         */
+        public static final String CARD_SUBMIT_MEMBERCARD_USER_INFO = "submit_membercard_user_info";
+
+        /**
+         * 卡券事件:用户打开查看卡券
+         */
+        public static final String CARD_USER_VIEW_CARD = "user_view_card";
+
+        /**
+         * 卡券事件:用户删除卡券
+         */
+        public static final String CARD_USER_DEL_CARD = "user_del_card";
+
+        /**
+         * 卡券事件:用户在卡券里点击查看公众号进入会话时(需要用户已经关注公众号)
+         */
+        public static final String CARD_USER_ENTER_SESSION_FROM_CARD = "user_enter_session_from_card";
+
+        /**
+         * 卡券事件:当用户的会员卡积分余额发生变动时
+         */
+        public static final String CARD_UPDATE_MEMBER_CARD = "update_member_card";
+
+        /**
+         * 卡券事件:当某个card_id的初始库存数大于200且当前库存小于等于100时,用户尝试领券会触发发送事件给商户,事件每隔12h发送一次
+         */
+        public static final String CARD_SKU_REMIND = "card_sku_remind";
+
+        /**
+         * 卡券事件:当商户朋友的券券点发生变动时
+         */
+        public static final String CARD_PAY_ORDER = "card_pay_order";
+
+        /**
+         * 小程序审核事件:审核通过
+         */
+        public static final String WEAPP_AUDIT_SUCCESS = "weapp_audit_success";
+
+        /**
+         * 小程序审核事件:审核不通过
+         */
+        public static final String WEAPP_AUDIT_FAIL = "weapp_audit_fail";
+
+    }
+
+
+}

+ 64 - 0
src/main/java/com/ematou/wxservice/common/utils/CapturePackageClasses.java

@@ -0,0 +1,64 @@
+package com.ematou.wxservice.common.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ResourceLoaderAware;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternUtils;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 15:54
+ */
+@Component
+public class CapturePackageClasses implements ResourceLoaderAware {
+
+    private ResourceLoader resourceLoader;
+
+    private static final Logger logger = LoggerFactory.getLogger(CapturePackageClasses.class);
+
+    public List<Class<?>> getClass(String packageName) {
+
+        List<Class<?>> classes = new ArrayList<>();
+
+        packageName = packageName.replace('.' ,'/');
+
+        ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
+        MetadataReaderFactory metaReader = new CachingMetadataReaderFactory(resourceLoader);
+        Resource[] resources = new Resource[0];
+        try {
+            resources = resolver.getResources("classpath*:" + packageName + "/**/*.class");
+        } catch (IOException e) {
+            logger.error("包名格式不对!");
+        }
+
+        for (Resource resource : resources) {
+            MetadataReader reader = null;
+            try {
+                reader = metaReader.getMetadataReader(resource);
+            } catch (IOException e) {
+                logger.error(e.getMessage());
+            }
+            assert reader != null : "reader为null!";
+            classes.add(reader.getClass());
+        }
+
+        return classes;
+    }
+
+    @Override
+    public void setResourceLoader(ResourceLoader resourceLoader) {
+        this.resourceLoader = resourceLoader;
+    }
+}

+ 40 - 0
src/main/java/com/ematou/wxservice/common/utils/DateUtil.java

@@ -0,0 +1,40 @@
+package com.ematou.wxservice.common.utils;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 15:23
+ */
+public class DateUtil {
+
+    /**
+     * 时间类型转字符串
+     * @param date 时间类型
+     * @return 时间字符串
+     */
+    public static String formatDate(Date date){
+
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+        return sdf.format(date);
+
+    }
+
+    /**
+     * 将时间字符串转为时间类型
+     * @param str 时间字符串
+     * @return 时间类型
+     */
+    public static Date parseString(String str) throws ParseException {
+
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+        return sdf.parse(str);
+
+    }
+
+}

+ 70 - 0
src/main/java/com/ematou/wxservice/common/utils/SignUtil.java

@@ -0,0 +1,70 @@
+package com.ematou.wxservice.common.utils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * 根据服务器配置的令牌校验签名
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 11:49
+ */
+public class SignUtil {
+    private static final String token = "rv978qwd";// 服务器配置中的令牌
+
+    /**
+     * 校验签名
+     * @param signature  签名
+     * @param timestamp 时间戳
+     * @param nonce 随机数
+     * @return true 成功,false 失败
+     */
+    public static boolean checkSignature(String signature,String timestamp, String nonce){
+
+        String checktext = null;
+        if(null != signature){
+            // 对Token,timestamp nonce 按字典排序
+            String [] paramArr = new String[] {token, timestamp, nonce};
+            Arrays.sort(paramArr);
+            // 将排序后的结果拼成一个字符串
+            String content = paramArr[0].concat(paramArr[1]).concat(paramArr[2]);
+            try {
+                MessageDigest md = MessageDigest.getInstance("SHA-1");
+                // 对接后的字符串进行sha1加密
+                byte[] digest = md.digest(content.toString().getBytes());
+                checktext = byteToStr(digest);
+            } catch (NoSuchAlgorithmException e) {
+                e.printStackTrace();
+            }
+        }
+        // 将加密后的字符串与signature进行对比
+        return checktext != null && checktext.matches(signature.toUpperCase());
+    }
+
+    /**
+     * 将字节数组转化为16进制字符串
+     * @return 字符串
+     */
+    private static String byteToStr(byte[] byteArrays) {
+        StringBuilder str= new StringBuilder();
+        for (byte byteArray : byteArrays) {
+            str.append(byteToHexStr(byteArray));
+        }
+        return str.toString();
+    }
+
+    /**
+     * 将字节转化为十六进制字符串
+     * @param myByte 字节
+     * @return 字符串
+     */
+    private static String byteToHexStr(byte myByte) {
+
+        char[] Digit = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
+        char[] tempArr = new char[2];
+        tempArr[0] = Digit[(myByte >>> 4)&0X0F];
+        tempArr[1] = Digit[myByte & 0x0F];
+        return new String(tempArr);
+    }
+}

+ 74 - 0
src/main/java/com/ematou/wxservice/common/utils/StringUtil.java

@@ -0,0 +1,74 @@
+package com.ematou.wxservice.common.utils;
+
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 10:35
+ */
+public class StringUtil {
+
+    public static String encodeUrl(String url){
+
+        return getRealUrl(url);
+
+    }
+
+
+    private static String getRealUrl(String str) {
+        try {
+            int index = str.indexOf("?");
+            if (index < 0) return str;
+            String query = str.substring(0, index);
+            String params = str.substring(index + 1);
+            Map map = getArgs(params);
+            String encodeParams = transMapToString(map);
+            return query + "?" + encodeParams;
+        } catch (Exception ex) {
+            System.out.println(ex.getMessage());
+        }
+        return "";
+    }
+
+    //将url参数格式转化为map
+    private static Map getArgs(String params) throws Exception {
+        Map map = new HashMap();
+        String[] pairs = params.split("&");
+        for (int i = 0; i < pairs.length; i++) {
+            int pos = pairs[i].indexOf("=");
+            if (pos == -1) continue;
+            String argname = pairs[i].substring(0, pos);
+            String value = pairs[i].substring(pos + 1);
+            value = URLEncoder.encode(value, "utf-8");
+            map.put(argname, value);
+        }
+        return map;
+    }
+
+    //将map转化为指定的String类型
+    private static String transMapToString(Map map) {
+        java.util.Map.Entry entry;
+        StringBuffer sb = new StringBuffer();
+        for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext(); ) {
+            entry = (java.util.Map.Entry) iterator.next();
+            sb.append(entry.getKey().toString()).append("=").append(null == entry.getValue() ? "" :
+                    entry.getValue().toString()).append(iterator.hasNext() ? "&" : "");
+        }
+        return sb.toString();
+    }
+
+    //将String类型按一定规则转换为Map
+    private static Map transStringToMap(String mapString) {
+        Map map = new HashMap();
+        java.util.StringTokenizer items;
+        for (StringTokenizer entrys = new StringTokenizer(mapString, "&"); entrys.hasMoreTokens();
+             map.put(items.nextToken(), items.hasMoreTokens() ? ((Object) (items.nextToken())) : null))
+            items = new StringTokenizer(entrys.nextToken(), "=");
+        return map;
+    }
+}

+ 92 - 0
src/main/java/com/ematou/wxservice/common/utils/XStreamInitializer.java

@@ -0,0 +1,92 @@
+package com.ematou.wxservice.common.utils;
+
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.converters.basic.*;
+import com.thoughtworks.xstream.converters.collections.CollectionConverter;
+import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
+import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
+import com.thoughtworks.xstream.core.util.QuickWriter;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
+import com.thoughtworks.xstream.io.xml.XppDriver;
+import com.thoughtworks.xstream.security.NoTypePermission;
+import com.thoughtworks.xstream.security.WildcardTypePermission;
+
+import java.io.Writer;
+
+/**
+ * XStream初始化器
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 16:16
+ */
+public class XStreamInitializer {
+
+    private static final XppDriver XPP_DRIVER = new XppDriver() {
+        @Override
+        public HierarchicalStreamWriter createWriter(Writer out) {
+            return new PrettyPrintWriter(out, getNameCoder()) {
+                private static final String PREFIX_CDATA = "<![CDATA[";
+                private static final String SUFFIX_CDATA = "]]>";
+                private static final String PREFIX_MEDIA_ID = "<MediaId>";
+                private static final String SUFFIX_MEDIA_ID = "</MediaId>";
+
+                @Override
+                protected void writeText(QuickWriter writer, String text) {
+                    if (text.startsWith(PREFIX_CDATA) && text.endsWith(SUFFIX_CDATA)) {
+                        writer.write(text);
+                    } else if (text.startsWith(PREFIX_MEDIA_ID) && text.endsWith(SUFFIX_MEDIA_ID)) {
+                        writer.write(text);
+                    } else {
+                        super.writeText(writer, text);
+                    }
+
+                }
+
+                @Override
+                public String encodeNode(String name) {
+                    //防止将_转换成__
+                    return name;
+                }
+            };
+        }
+    };
+
+    /**
+     * Gets instance.
+     *
+     * @return the instance
+     */
+    public static XStream getInstance() {
+        XStream xstream = new XStream(new PureJavaReflectionProvider(), XPP_DRIVER) {
+            // only register the converters we need; other converters generate a private access warning in the console on Java9+...
+            @Override
+            protected void setupConverters() {
+                registerConverter(new NullConverter(), PRIORITY_VERY_HIGH);
+                registerConverter(new IntConverter(), PRIORITY_NORMAL);
+                registerConverter(new FloatConverter(), PRIORITY_NORMAL);
+                registerConverter(new DoubleConverter(), PRIORITY_NORMAL);
+                registerConverter(new LongConverter(), PRIORITY_NORMAL);
+                registerConverter(new ShortConverter(), PRIORITY_NORMAL);
+                registerConverter(new BooleanConverter(), PRIORITY_NORMAL);
+                registerConverter(new ByteConverter(), PRIORITY_NORMAL);
+                registerConverter(new StringConverter(), PRIORITY_NORMAL);
+                registerConverter(new DateConverter(), PRIORITY_NORMAL);
+                registerConverter(new CollectionConverter(getMapper()), PRIORITY_NORMAL);
+                registerConverter(new ReflectionConverter(getMapper(), getReflectionProvider()), PRIORITY_VERY_LOW);
+            }
+        };
+        xstream.ignoreUnknownElements();
+        xstream.setMode(XStream.NO_REFERENCES);
+        XStream.setupDefaultSecurity(xstream);
+        xstream.autodetectAnnotations(true);
+
+        // 通过限制哪些类可以被XStream加载来设置适当的安全性
+        xstream.addPermission(NoTypePermission.NONE);
+        xstream.addPermission(new WildcardTypePermission(new String[]{
+                "com.ematou.wxservice.entity.dto.*", "com.ematou.wxservice.mp.message.xml.*"
+        }));
+        xstream.setClassLoader(Thread.currentThread().getContextClassLoader());
+        return xstream;
+    }
+}

+ 100 - 0
src/main/java/com/ematou/wxservice/common/utils/XStreamTransformer.java

@@ -0,0 +1,100 @@
+package com.ematou.wxservice.common.utils;
+
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutNewsMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutTextMessage;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.security.AnyTypePermission;
+
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ *  @author lhm
+ * @version 1.0
+ * 2021-05-11 16:14
+ */
+public class XStreamTransformer {
+
+    private static final Map<Class<?>, XStream> CLASS_2_XSTREAM_INSTANCE = new HashMap<>();
+
+    static {
+        // TODO 每新增一个响应方式,都需要注册
+        registerClass(WeChatMessage.class);
+        registerClass(WeChatMpXmlOutMessage.class);
+        registerClass(WeChatMpXmlOutTextMessage.class);
+        registerClass(WeChatMpXmlOutNewsMessage.class);
+    }
+
+    /**
+     * xml -> pojo.
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T fromXml(Class<T> clazz, String xml) {
+        T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(xml);
+        return object;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T fromXml(Class<T> clazz, InputStream is) {
+        T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(is);
+        return object;
+    }
+
+    /**
+     * pojo -> xml.
+     */
+    public static <T> String toXml(Class<T> clazz, T object) {
+        return CLASS_2_XSTREAM_INSTANCE.get(clazz).toXML(object);
+    }
+
+    /**
+     * 注册扩展消息的解析器.
+     *
+     * @param clz     类型
+     * @param xStream xml解析器
+     */
+    public static void register(Class<?> clz, XStream xStream) {
+        CLASS_2_XSTREAM_INSTANCE.put(clz, xStream);
+    }
+
+    /**
+     * 会自动注册该类及其子类.
+     *
+     * @param clz 要注册的类
+     */
+    private static void registerClass(Class<?> clz) {
+        XStream xstream = XStreamInitializer.getInstance();
+
+        xstream.addPermission(AnyTypePermission.ANY);
+        xstream.processAnnotations(clz);
+        xstream.processAnnotations(getInnerClasses(clz));
+        if (clz.equals(WeChatMessage.class)) {
+            // 操蛋的微信,模板消息推送成功的消息是MsgID,其他消息推送过来是MsgId
+            xstream.aliasField("MsgID", WeChatMessage.class, "msgId");
+        }
+
+        register(clz, xstream);
+    }
+
+    private static Class<?>[] getInnerClasses(Class<?> clz) {
+        Class<?>[] innerClasses = clz.getClasses();
+        if (innerClasses == null) {
+            return null;
+        }
+
+        List<Class<?>> result = new ArrayList<>();
+        result.addAll(Arrays.asList(innerClasses));
+        for (Class<?> inner : innerClasses) {
+            Class<?>[] innerClz = getInnerClasses(inner);
+            if (innerClz == null) {
+                continue;
+            }
+
+            result.addAll(Arrays.asList(innerClz));
+        }
+
+        return result.toArray(new Class<?>[0]);
+    }
+}

+ 71 - 0
src/main/java/com/ematou/wxservice/common/web/R.java

@@ -0,0 +1,71 @@
+package com.ematou.wxservice.common.web;
+
+import com.ematou.wxservice.common.constant.ResponseCodeConstant;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 14:25
+ */
+public class R<T> {
+
+    private int code;
+
+    private String message;
+
+    private T data;
+
+    public R() {
+    }
+
+    public R(int code, String message, T data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public R<T> success(T data) {
+        this.setCode(ResponseCodeConstant.code_Success);
+        this.setMessage("success");
+        this.setData(data);
+        return this;
+    }
+
+    public R<T> success() {
+        this.setCode(ResponseCodeConstant.code_Success);
+        this.setMessage("success");
+        this.setData(null);
+        return this;
+    }
+
+    public R<?> error(String message) {
+        this.setCode(ResponseCodeConstant.code_100);
+        this.setMessage(message);
+        this.setData(null);
+        return this;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+}

+ 39 - 0
src/main/java/com/ematou/wxservice/common/xml/builder/BaseBuilder.java

@@ -0,0 +1,39 @@
+package com.ematou.wxservice.common.xml.builder;
+
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+
+/**
+ * 构建消息基类
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 15:01
+ */
+public abstract class BaseBuilder<BuilderType, ValueType> {
+
+    protected String toUserName;
+
+    protected String fromUserName;
+
+    @SuppressWarnings("unchecked")
+    public BuilderType toUser(String touser) {
+        this.toUserName = touser;
+        return (BuilderType) this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public BuilderType fromUser(String fromusername) {
+        this.fromUserName = fromusername;
+        return (BuilderType) this;
+    }
+
+    public abstract ValueType build();
+
+    public void setCommon(WeChatMpXmlOutMessage m) {
+        m.setToUserName(this.toUserName);
+        m.setFromUserName(this.fromUserName);
+        m.setCreateTime(System.currentTimeMillis() / 1000L);
+    }
+
+
+
+}

+ 38 - 0
src/main/java/com/ematou/wxservice/common/xml/builder/NewsBuilder.java

@@ -0,0 +1,38 @@
+package com.ematou.wxservice.common.xml.builder;
+
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutNewsMessage;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-12 11:52
+ */
+public class NewsBuilder extends BaseBuilder<NewsBuilder, WeChatMpXmlOutNewsMessage> {
+
+    private List<WeChatMpXmlOutNewsMessage.Item> articles = new ArrayList<>();
+
+    public NewsBuilder addArticle(WeChatMpXmlOutNewsMessage.Item... items) {
+        Collections.addAll(this.articles, items);
+        return this;
+    }
+
+    public NewsBuilder articles(List<WeChatMpXmlOutNewsMessage.Item> articles){
+        this.articles = articles;
+        return this;
+    }
+
+    @Override
+    public WeChatMpXmlOutNewsMessage build() {
+        WeChatMpXmlOutNewsMessage m = new WeChatMpXmlOutNewsMessage();
+        for (WeChatMpXmlOutNewsMessage.Item item : this.articles) {
+            m.addArticle(item);
+        }
+        setCommon(m);
+        return m;
+    }
+
+}

+ 28 - 0
src/main/java/com/ematou/wxservice/common/xml/builder/TextBuilder.java

@@ -0,0 +1,28 @@
+package com.ematou.wxservice.common.xml.builder;
+
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutTextMessage;
+
+/**
+ * 构建文本消息
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 15:03
+ */
+public class TextBuilder extends BaseBuilder<TextBuilder, WeChatMpXmlOutTextMessage> {
+
+    private String content;
+
+    public TextBuilder content(String content) {
+        this.content = content;
+        return this;
+    }
+
+    @Override
+    public WeChatMpXmlOutTextMessage build() {
+        WeChatMpXmlOutTextMessage weChatMpXmlOutTextMessage = new WeChatMpXmlOutTextMessage();
+        setCommon(weChatMpXmlOutTextMessage);
+        weChatMpXmlOutTextMessage.setContent(this.content);
+        return weChatMpXmlOutTextMessage;
+    }
+}

+ 17 - 0
src/main/java/com/ematou/wxservice/common/xml/converter/XStreamCDataConverter.java

@@ -0,0 +1,17 @@
+package com.ematou.wxservice.common.xml.converter;
+
+import com.thoughtworks.xstream.converters.basic.StringConverter;
+
+/**
+ * CDATA转换器,加上CDATA标签
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 14:58
+ */
+public class XStreamCDataConverter extends StringConverter {
+
+    @Override
+    public String toString(Object obj) {
+        return "<![CDATA[" + super.toString(obj) + "]]>";
+    }
+}

+ 36 - 0
src/main/java/com/ematou/wxservice/config/AppConfig.java

@@ -0,0 +1,36 @@
+package com.ematou.wxservice.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 14:02
+ */
+@Configuration
+@EnableAspectJAutoProxy
+@MapperScan("com.ematou.wxservice.mapper")
+@ComponentScan("com.ematou.wxservice.*")
+public class AppConfig {
+
+    @Bean
+    public WeChatGeneralConfig wxGeneralConfig() {
+        return new WeChatGeneralConfig();
+    }
+
+    @Bean
+    public RestTemplate restTemplate() {
+        // 可发送HTTPS请求的RestTemplate
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setReadTimeout(60000);
+        factory.setConnectTimeout(15000);
+        return new RestTemplate(factory);
+    }
+
+}

+ 33 - 0
src/main/java/com/ematou/wxservice/config/WeChatGeneralConfig.java

@@ -0,0 +1,33 @@
+package com.ematou.wxservice.config;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 11:22
+ */
+public class WeChatGeneralConfig {
+
+    @Value("${wx.general.appId}")
+    private String appId;
+
+    @Value("${wx.general.appSecret}")
+    private String appSecret;
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getAppSecret() {
+        return appSecret;
+    }
+
+    public void setAppSecret(String appSecret) {
+        this.appSecret = appSecret;
+    }
+}

+ 19 - 0
src/main/java/com/ematou/wxservice/controller/UserInfoController.java

@@ -0,0 +1,19 @@
+package com.ematou.wxservice.controller;
+
+import com.ematou.wxservice.common.web.R;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-10 19:55
+ */
+@RestController
+public class UserInfoController {
+
+
+}

+ 32 - 0
src/main/java/com/ematou/wxservice/controller/WeChatController.java

@@ -0,0 +1,32 @@
+package com.ematou.wxservice.controller;
+
+import com.ematou.wxservice.common.web.R;
+import com.ematou.wxservice.entity.dto.TemplateMessage;
+import com.ematou.wxservice.service.WeChatService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-13 11:56
+ */
+@RestController
+public class WeChatController {
+
+    @Autowired
+    WeChatService weChatService;
+
+    @PostMapping("/template/message")
+    public R<?> sendTemplateMsg(@RequestBody TemplateMessage templateMessage){
+        Integer i = weChatService.sendTemplateMsg(templateMessage);
+        if (i==0) {
+            return new R<String>().error("推送消息失败!");
+        } else {
+            return new R<String>().success();
+        }
+    }
+
+}

+ 65 - 0
src/main/java/com/ematou/wxservice/controller/WeChatMessageController.java

@@ -0,0 +1,65 @@
+package com.ematou.wxservice.controller;
+
+import com.ematou.wxservice.common.utils.SignUtil;
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutTextMessage;
+import com.ematou.wxservice.service.WeChatMessageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 11:48
+ */
+@RestController
+public class WeChatMessageController {
+
+    @Autowired
+    WeChatMessageService weChatMessageService;
+
+    /**
+     * 微信公众号消息回调 用于微信鉴权
+     * @param signature 微信的签名,需要跟自己的签名比对,相同才成功
+     * @param timestamp 时间戳
+     * @param nonce     随机数字
+     * @param echostr   随机字符串
+     * @param request   请求
+     * @param response  响应
+     * @return          字符串
+     */
+    @GetMapping("/message")
+    public String sign(@RequestParam(name = "signature",required = false) String signature,
+                       @RequestParam(name = "timestamp",required = false) String timestamp,
+                       @RequestParam(name = "nonce",required = false) String nonce,
+                       @RequestParam(name = "echostr",required = false) String echostr,
+                       HttpServletRequest request, HttpServletResponse response){
+        if(SignUtil.checkSignature(signature, timestamp, nonce)) {
+            return echostr;
+        }
+        return "";
+    }
+
+    /**
+     * 消息入口,由于微信平台在5s内无返回信息会进行重试,返回空字符串则不会重试
+     */
+    @PostMapping("/message")
+    public String handleMessage(HttpServletRequest request, HttpServletResponse response){
+        try {
+            ServletInputStream inputStream = request.getInputStream();
+
+            WeChatMessage weChatMessage = WeChatMessage.fromXml(inputStream);
+            WeChatMpXmlOutMessage outTextMessage = weChatMessageService.handleMessage(weChatMessage);
+            return null == outTextMessage ? "" : outTextMessage.toXml();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return "";
+    }
+}

+ 21 - 0
src/main/java/com/ematou/wxservice/controller/WeChatViewController.java

@@ -0,0 +1,21 @@
+package com.ematou.wxservice.controller;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-12 16:00
+ */
+@Controller
+public class WeChatViewController {
+
+    @GetMapping("/view/main")
+    public String main(){
+
+        return "hello";
+    }
+
+
+}

+ 144 - 0
src/main/java/com/ematou/wxservice/entity/dto/AccessToken.java

@@ -0,0 +1,144 @@
+package com.ematou.wxservice.entity.dto;
+
+import java.sql.Timestamp;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 11:45
+ */
+public class AccessToken {
+
+    private Integer id;
+
+    private String accessToken;
+
+    private String expiresTime;
+
+    private String effectTime;
+
+    private Integer expiresIn;
+
+    private Integer opId;
+
+    private Integer isValid;
+
+    private String createrSn;
+
+    private String createTime;
+
+    private String moderSn;
+
+    private String modTime;
+
+    private Timestamp tstm;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getAccessToken() {
+        return accessToken;
+    }
+
+    public void setAccessToken(String accessToken) {
+        this.accessToken = accessToken;
+    }
+
+    public String getExpiresTime() {
+        return expiresTime;
+    }
+
+    public void setExpiresTime(String expiresTime) {
+        this.expiresTime = expiresTime;
+    }
+
+    public String getEffectTime() {
+        return effectTime;
+    }
+
+    public void setEffectTime(String effectTime) {
+        this.effectTime = effectTime;
+    }
+
+    public Integer getExpiresIn() {
+        return expiresIn;
+    }
+
+    public void setExpiresIn(Integer expiresIn) {
+        this.expiresIn = expiresIn;
+    }
+
+    public Integer getOpId() {
+        return opId;
+    }
+
+    public void setOpId(Integer opId) {
+        this.opId = opId;
+    }
+
+    public Integer getIsValid() {
+        return isValid;
+    }
+
+    public void setIsValid(Integer isValid) {
+        this.isValid = isValid;
+    }
+
+    public String getCreaterSn() {
+        return createrSn;
+    }
+
+    public void setCreaterSn(String createrSn) {
+        this.createrSn = createrSn;
+    }
+
+    public String getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(String createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getModerSn() {
+        return moderSn;
+    }
+
+    public void setModerSn(String moderSn) {
+        this.moderSn = moderSn;
+    }
+
+    public String getModTime() {
+        return modTime;
+    }
+
+    public void setModTime(String modTime) {
+        this.modTime = modTime;
+    }
+
+    public Timestamp getTstm() {
+        return tstm;
+    }
+
+    public void setTstm(Timestamp tstm) {
+        this.tstm = tstm;
+    }
+
+    @Override
+    public String toString() {
+        return "AccessToken{" +
+                "id=" + id +
+                ", accessToken='" + accessToken + '\'' +
+                ", expiresTime='" + expiresTime + '\'' +
+                ", effectTime='" + effectTime + '\'' +
+                ", expiresIn=" + expiresIn +
+                ", opId=" + opId +
+                ", isValid=" + isValid +
+                '}';
+    }
+}

+ 112 - 0
src/main/java/com/ematou/wxservice/entity/dto/TemplateMessage.java

@@ -0,0 +1,112 @@
+package com.ematou.wxservice.entity.dto;
+
+import java.util.List;
+
+/**
+ * 需要发送的模板消息
+ * @author lhm
+ * @version 1.0
+ * 2021-05-13 09:45
+ */
+public class TemplateMessage {
+
+    private String openId;
+
+    private List<TemplateData> data;
+
+    /**
+     * 模板跳转链接(海外帐号没有跳转能力)
+     */
+    private String url;
+
+    /**
+     * 跳小程序所需数据,不需跳小程序可不用传该数据,非必填
+     */
+    private MiniProgramInfo miniProgram;
+
+
+    public static class TemplateData {
+
+        /**
+         * 模板数据内容
+         */
+        private String value;
+
+        /**
+         * 模板内容字体颜色(16进制值),不填默认为黑色
+         */
+        private String color;
+
+        public String getValue() {
+            return value;
+        }
+
+        public void setValue(String value) {
+            this.value = value;
+        }
+
+        public String getColor() {
+            return color;
+        }
+
+        public void setColor(String color) {
+            this.color = color;
+        }
+    }
+
+    public static class MiniProgramInfo {
+
+        private String appId;
+
+        private String pagePath;
+
+        public String getAppId() {
+            return appId;
+        }
+
+        public void setAppId(String appId) {
+            this.appId = appId;
+        }
+
+        public String getPagePath() {
+            return pagePath;
+        }
+
+        public void setPagePath(String pagePath) {
+            this.pagePath = pagePath;
+        }
+    }
+
+
+    public String getOpenId() {
+        return openId;
+    }
+
+    public void setOpenId(String openId) {
+        this.openId = openId;
+    }
+
+    public List<TemplateData> getData() {
+        return data;
+    }
+
+    public void setData(List<TemplateData> data) {
+        this.data = data;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public MiniProgramInfo getMiniProgram() {
+        return miniProgram;
+    }
+
+    public void setMiniProgram(MiniProgramInfo miniProgram) {
+        this.miniProgram = miniProgram;
+    }
+}

+ 238 - 0
src/main/java/com/ematou/wxservice/entity/pojo/UserInfo.java

@@ -0,0 +1,238 @@
+/*
+ * Copyright 2021 json.cn
+ */
+package com.ematou.wxservice.entity.pojo;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+/**
+ * Auto-generated: 2021-05-10 18:15:32
+ *
+ * @author json.cn (i@json.cn)
+ * @website http://www.json.cn/java2pojo/
+ */
+public class UserInfo {
+
+    private Integer subscribe;
+    private String openId;
+    private String nickName;
+    private Integer sex;
+    private String language;
+    private String city;
+    private String province;
+    private String country;
+    private String headimgurl;
+    private Long subscribeTime;
+    private String unionId;
+    private String remark;
+    private Integer groupId;
+    private List<Integer> tagIdList;
+    private String subscribeScene;
+    private Long qrScene;
+    private String qrSceneStr;
+    private Integer creatorId;
+    private String createTime;
+    private Integer moderId;
+    private String modTime;
+    private Timestamp tmst;
+
+    public Integer getSubscribe() {
+        return subscribe;
+    }
+
+    public void setSubscribe(Integer subscribe) {
+        this.subscribe = subscribe;
+    }
+
+    public String getOpenId() {
+        return openId;
+    }
+
+    public void setOpenId(String openId) {
+        this.openId = openId;
+    }
+
+    public String getNickName() {
+        return nickName;
+    }
+
+    public void setNickName(String nickName) {
+        this.nickName = nickName;
+    }
+
+    public Integer getSex() {
+        return sex;
+    }
+
+    public void setSex(Integer sex) {
+        this.sex = sex;
+    }
+
+    public String getLanguage() {
+        return language;
+    }
+
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+
+    public String getCity() {
+        return city;
+    }
+
+    public void setCity(String city) {
+        this.city = city;
+    }
+
+    public String getProvince() {
+        return province;
+    }
+
+    public void setProvince(String province) {
+        this.province = province;
+    }
+
+    public String getCountry() {
+        return country;
+    }
+
+    public void setCountry(String country) {
+        this.country = country;
+    }
+
+    public String getHeadimgurl() {
+        return headimgurl;
+    }
+
+    public void setHeadimgurl(String headimgurl) {
+        this.headimgurl = headimgurl;
+    }
+
+    public Long getSubscribeTime() {
+        return subscribeTime;
+    }
+
+    public void setSubscribeTime(Long subscribeTime) {
+        this.subscribeTime = subscribeTime;
+    }
+
+    public String getUnionId() {
+        return unionId;
+    }
+
+    public void setUnionId(String unionId) {
+        this.unionId = unionId;
+    }
+
+    public String getRemark() {
+        return remark;
+    }
+
+    public void setRemark(String remark) {
+        this.remark = remark;
+    }
+
+    public Integer getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(Integer groupId) {
+        this.groupId = groupId;
+    }
+
+    public List<Integer> getTagIdList() {
+        return tagIdList;
+    }
+
+    public void setTagIdList(List<Integer> tagIdList) {
+        this.tagIdList = tagIdList;
+    }
+
+    public String getSubscribeScene() {
+        return subscribeScene;
+    }
+
+    public void setSubscribeScene(String subscribeScene) {
+        this.subscribeScene = subscribeScene;
+    }
+
+    public Long getQrScene() {
+        return qrScene;
+    }
+
+    public void setQrScene(Long qrScene) {
+        this.qrScene = qrScene;
+    }
+
+    public String getQrSceneStr() {
+        return qrSceneStr;
+    }
+
+    public void setQrSceneStr(String qrSceneStr) {
+        this.qrSceneStr = qrSceneStr;
+    }
+
+    public Integer getCreatorId() {
+        return creatorId;
+    }
+
+    public void setCreatorId(Integer creatorId) {
+        this.creatorId = creatorId;
+    }
+
+    public String getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(String createTime) {
+        this.createTime = createTime;
+    }
+
+    public Integer getModerId() {
+        return moderId;
+    }
+
+    public void setModerId(Integer moderId) {
+        this.moderId = moderId;
+    }
+
+    public String getModTime() {
+        return modTime;
+    }
+
+    public void setModTime(String modTime) {
+        this.modTime = modTime;
+    }
+
+    public Timestamp getTmst() {
+        return tmst;
+    }
+
+    public void setTmst(Timestamp tmst) {
+        this.tmst = tmst;
+    }
+
+    @Override
+    public String toString() {
+        return "UserInfo{" +
+                "subscribe=" + subscribe +
+                ", openId='" + openId + '\'' +
+                ", nickName='" + nickName + '\'' +
+                ", sex=" + sex +
+                ", language='" + language + '\'' +
+                ", city='" + city + '\'' +
+                ", province='" + province + '\'' +
+                ", country='" + country + '\'' +
+                ", headimgurl='" + headimgurl + '\'' +
+                ", subscribeTime=" + subscribeTime +
+                ", unionId='" + unionId + '\'' +
+                ", remark='" + remark + '\'' +
+                ", groupId=" + groupId +
+                ", tagIdList='" + tagIdList + '\'' +
+                ", subscribeScene='" + subscribeScene + '\'' +
+                ", qrScene=" + qrScene +
+                ", qrSceneStr='" + qrSceneStr + '\'' +
+                '}';
+    }
+}

+ 120 - 0
src/main/java/com/ematou/wxservice/interceptor/MybatisInsertAndUpdateInterceptor.java

@@ -0,0 +1,120 @@
+package com.ematou.wxservice.interceptor;
+
+import com.ematou.wxservice.common.utils.DateUtil;
+import org.apache.ibatis.executor.Executor;
+import org.apache.ibatis.mapping.MappedStatement;
+import org.apache.ibatis.mapping.SqlCommandType;
+import org.apache.ibatis.plugin.*;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Field;
+import java.sql.Timestamp;
+import java.util.Date;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-04-30 15:30
+ */
+@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
+@Component
+public class MybatisInsertAndUpdateInterceptor implements Interceptor {
+
+
+    @Override
+    public Object intercept(Invocation invocation) throws Throwable {
+
+        Object[] args = invocation.getArgs();
+        MappedStatement mappedStatement = (MappedStatement) args[0];
+
+        if (mappedStatement.getSqlCommandType() == SqlCommandType.UPDATE) {
+            Object arg = args[1];
+            if (arg instanceof Map) {
+                Map<?, ?> map = (Map<?, ?>) arg;
+                for (Object key : map.keySet()) {
+                    if (key.toString().contains("param")) {
+                        handleUpdateData(map.get(key));
+                    }
+                }
+            } else {
+                handleInsertData(arg);
+            }
+        } else if (mappedStatement.getSqlCommandType() == SqlCommandType.INSERT) {
+            Object arg = args[1];
+            if (arg instanceof Map) {
+                Map<?, ?> map = (Map<?, ?>) arg;
+                for (Object key : map.keySet()) {
+                    if (key.toString().contains("param")) {
+                        handleInsertData(map.get(key));
+                    }
+                }
+            } else {
+                handleInsertData(arg);
+            }
+        }
+
+        return invocation.proceed();
+    }
+
+    private void handleInsertData(Object o) throws IllegalAccessException {
+        Field[] fields = o.getClass().getDeclaredFields();
+
+        for (Field field : fields) {
+            switch (field.getName()) {
+                case "createTime":
+                    field.setAccessible(true);
+                    field.set(o, DateUtil.formatDate(new Date()));
+                    break;
+                case "modTime":
+                    field.setAccessible(true);
+                    field.set(o, DateUtil.formatDate(new Date()));
+                    break;
+                case "tstm":
+                    field.setAccessible(true);
+                    field.set(o, new Timestamp(System.currentTimeMillis()));
+                    break;
+                case "createrSn":
+                    // TODO
+                    field.setAccessible(true);
+                    field.set(o, "admin");
+                    break;
+            }
+        }
+
+    }
+
+    private void handleUpdateData(Object o) throws IllegalAccessException {
+        Field[] fields = o.getClass().getDeclaredFields();
+
+        for (Field field : fields) {
+            switch (field.getName()) {
+                case "modTime":
+                    field.setAccessible(true);
+                    field.set(o, DateUtil.formatDate(new Date()));
+                    break;
+                case "tstm":
+                    field.setAccessible(true);
+                    field.set(o, new Timestamp(System.currentTimeMillis()));
+                    break;
+                case "moderSn":
+                    // TODO
+                    field.setAccessible(true);
+                    field.set(o, "admin");
+                    break;
+            }
+        }
+
+    }
+
+    @Override
+    public Object plugin(Object target) {
+        return Plugin.wrap(target, this);
+    }
+
+    @Override
+    public void setProperties(Properties properties) {
+
+    }
+}

+ 19 - 0
src/main/java/com/ematou/wxservice/mapper/UserInfoMapper.java

@@ -0,0 +1,19 @@
+package com.ematou.wxservice.mapper;
+
+import com.ematou.wxservice.entity.pojo.UserInfo;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-10 18:48
+ */
+@Mapper
+@Repository
+public interface UserInfoMapper {
+
+    int insertUserInfo(@Param("userInfo") UserInfo userInfo);
+
+}

+ 81 - 0
src/main/java/com/ematou/wxservice/mp/handler/WeChatClickEventMessageHandler.java

@@ -0,0 +1,81 @@
+package com.ematou.wxservice.mp.handler;
+
+import com.ematou.wxservice.common.constant.WeChatConstant;
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutNewsMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutTextMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * CLICK事件处理器
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 19:28
+ */
+@Component
+public class WeChatClickEventMessageHandler implements WeChatMessageHandler {
+
+    /**
+     * 处理消息,并封装返回消息
+     *
+     * @param weChatMessage 接收到的消息
+     * @return 响应消息
+     */
+    @Override
+    public WeChatMpXmlOutMessage handlerMessage(WeChatMessage weChatMessage) {
+        WeChatMpXmlOutMessage response = null;
+        // TODO 处理菜单点击事件
+        switch (weChatMessage.getEventKey()) {
+            case WeChatConstant.CustomEventKey.POLICY_SUPPORT:
+                response = handlerPolicySupportEvent(weChatMessage);
+                break;
+            case WeChatConstant.CustomEventKey.CALL_COMPANY:
+                response = handlerCallCompanyEvent(weChatMessage);
+                break;
+            case WeChatConstant.CustomEventKey.MY_TAKE_PARCEL_CODE:
+                // TODO 我的取件码
+                break;
+            case WeChatConstant.CustomEventKey.MY_HISTORY_RECORD:
+                // TODO 取件记录
+                break;
+            default:
+                break;
+        }
+
+        return response;
+    }
+
+    /**
+     * 处理联系我们按钮点击事件
+     * @param weChatMessage 事件
+     * @return 响应
+     */
+    private WeChatMpXmlOutTextMessage handlerCallCompanyEvent(WeChatMessage weChatMessage){
+
+        String content = "【深圳前海电商供应链管理有限公司】\n" +
+                "\n" +
+                "我们在这里!↓↓↓mo-[嘿哈]\n" +
+                "地址:深圳市南山区前海湾临海大道59号招商海运中心主塔楼17楼1701室\n" +
+                "\n" +
+                "或者直接拨打电话 0755-21628282mo-[握手]\n" +
+                "我们热情的等着您~";
+        return WeChatMpXmlOutMessage.TEXT().content(content).toUser(weChatMessage.getFromUser()).fromUser(weChatMessage.getToUser()).build();
+    }
+
+    /**
+     * 处理政策支持按钮点击事件
+     * @param weChatMessage 事件
+     * @return 响应
+     */
+    private WeChatMpXmlOutNewsMessage handlerPolicySupportEvent(WeChatMessage weChatMessage){
+
+        WeChatMpXmlOutNewsMessage.Item item = new WeChatMpXmlOutNewsMessage.Item();
+        item.setDescription("【11月8日上午,深圳前海国检领导和业务科长一行莅临e码头进行企业调研。】e码头董事长沈琦雅向来访领导介绍了");
+        item.setTitle("前海国检领导和业务科长莅临e码头 ——就企业创新需求及模式进行调研");
+        item.setPicUrl("http://mmbiz.qpic.cn/mmbiz_png/Vsr05SEyic67Jj1G2pnbuJSfc0I3qoZ6mV3KXKtrhjicK1jZvicHH431laAMWskiamd4KibLkTvk1I5YT8aEt6RPmfg/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1");
+        item.setUrl("https://mp.weixin.qq.com/s?__biz=MzIwMTI2MTg1MA==&tempkey=MTExM185eTh5aE1DSkRjRWFqa1FGRElaWmNGa1pZLWpTbG5va0tNTnJxaXE0SE04WWVLMmYzc1hteU82THN3Rm5QamtSQUtXS0N2Zm4ybTBBZnhaOUZOZnl2eTVNVjZ5MzRvSXFPZ3lEcFNnUVdpUzV5aVRHZHJocXNLTV9RNUJYSUhTeTBVdFcxM2pxZU9INWJHSDVOUmRDWTdHcmlQSVR4TGNSQ2I3bHpnfn4%3D&chksm=0ef9ac39398e252f2791f410f2c92def335d61bfb8d7bc8563ae4dc4d91eb6b9d1b5c72ed4f4#rd");
+        return WeChatMpXmlOutMessage.NEWS().addArticle(item).toUser(weChatMessage.getFromUser()).fromUser(weChatMessage.getToUser()).build();
+    }
+
+}

+ 22 - 0
src/main/java/com/ematou/wxservice/mp/handler/WeChatMessageHandler.java

@@ -0,0 +1,22 @@
+package com.ematou.wxservice.mp.handler;
+
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutTextMessage;
+
+/**
+ * 用户消息处理器接口
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 16:22
+ */
+public interface WeChatMessageHandler {
+
+    /**
+     * 处理消息,并封装返回消息
+     * @param weChatMessage 接收到的消息
+     * @return 响应消息
+     */
+    public WeChatMpXmlOutMessage handlerMessage(WeChatMessage weChatMessage);
+
+}

+ 26 - 0
src/main/java/com/ematou/wxservice/mp/handler/WeChatNewsMessageHandler.java

@@ -0,0 +1,26 @@
+package com.ematou.wxservice.mp.handler;
+
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * 图文消息处理器
+ * @author lhm
+ * @version 1.0
+ * 2021-05-12 11:26
+ */
+@Component
+public class WeChatNewsMessageHandler implements WeChatMessageHandler {
+
+    /**
+     * 处理消息,并封装返回消息
+     *
+     * @param weChatMessage 接收到的消息
+     * @return 响应消息
+     */
+    @Override
+    public WeChatMpXmlOutMessage handlerMessage(WeChatMessage weChatMessage) {
+        return null;
+    }
+}

+ 30 - 0
src/main/java/com/ematou/wxservice/mp/handler/WeChatTemplateMessageEventHandler.java

@@ -0,0 +1,30 @@
+package com.ematou.wxservice.mp.handler;
+
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+
+/**
+ * 模板消息推送事件
+ * @author lhm
+ * @version 1.0
+ * 2021-05-13 14:47
+ */
+public class WeChatTemplateMessageEventHandler implements WeChatMessageHandler {
+
+
+    /**
+     * 处理消息,并封装返回消息
+     *
+     * @param weChatMessage 接收到的消息
+     * @return 响应消息
+     */
+    @Override
+    public WeChatMpXmlOutMessage handlerMessage(WeChatMessage weChatMessage) {
+
+        String status = weChatMessage.getStatus();
+
+
+
+        return null;
+    }
+}

+ 40 - 0
src/main/java/com/ematou/wxservice/mp/handler/WeChatTextMessageHandler.java

@@ -0,0 +1,40 @@
+package com.ematou.wxservice.mp.handler;
+
+import com.alibaba.fastjson.JSON;
+import com.ematou.wxservice.api.WeChatApi;
+import com.ematou.wxservice.api.WeChatApiRestTemplate;
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.entity.pojo.UserInfo;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutTextMessage;
+import com.ematou.wxservice.service.WeChatService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 文本信息处理器
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 16:27
+ */
+@Component
+public class WeChatTextMessageHandler implements WeChatMessageHandler {
+
+    @Autowired
+    WeChatApiRestTemplate weChatApiRestTemplate;
+
+    @Autowired
+    WeChatService weChatService;
+
+    @Override
+    public WeChatMpXmlOutTextMessage handlerMessage(WeChatMessage weChatMessage) {
+
+        String openId = weChatMessage.getFromUser();
+
+        String response = weChatApiRestTemplate.getForOther(String.format(WeChatApi.GET_USER_INFO.getUrl(), weChatService.getAccessToken().getAccessToken(), openId));
+        // TODO 构建响应数据,需要修改响应数据,目前只是测试
+        UserInfo userInfo = JSON.parseObject(response, UserInfo.class);
+        System.out.println(userInfo.toString());
+        return WeChatMpXmlOutMessage.TEXT().content(response).toUser(weChatMessage.getFromUser()).fromUser(weChatMessage.getToUser()).build();
+    }
+}

+ 26 - 0
src/main/java/com/ematou/wxservice/mp/handler/WeChatViewEventMessageHandler.java

@@ -0,0 +1,26 @@
+package com.ematou.wxservice.mp.handler;
+
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * VIEW事件处理器
+ * @author lhm
+ * @version 1.0
+ * 2021-05-12 11:13
+ */
+@Component
+public class WeChatViewEventMessageHandler implements WeChatMessageHandler {
+
+    /**
+     * 处理消息,并封装返回消息
+     *
+     * @param weChatMessage 接收到的消息
+     * @return 响应消息
+     */
+    @Override
+    public WeChatMpXmlOutMessage handlerMessage(WeChatMessage weChatMessage) {
+        return null;
+    }
+}

+ 39 - 0
src/main/java/com/ematou/wxservice/mp/menu/Button.java

@@ -0,0 +1,39 @@
+package com.ematou.wxservice.mp.menu;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * @link https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Personalized_menu_interface.html
+ * 2021-05-11 19:39
+ */
+public class Button {
+
+    protected String name;
+
+    protected String type;
+
+    public Button(String name, String type) {
+        this.name = name;
+        this.type = type;
+    }
+
+    public Button() {
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+}

+ 28 - 0
src/main/java/com/ematou/wxservice/mp/menu/ClickButton.java

@@ -0,0 +1,28 @@
+package com.ematou.wxservice.mp.menu;
+
+/**
+ * 点击类型
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 19:39
+ */
+public class ClickButton extends Button {
+
+    private String key;
+
+    public ClickButton(String name, String type, String key) {
+        super(name, type);
+        this.key = key;
+    }
+
+    public ClickButton() {
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+}

+ 94 - 0
src/main/java/com/ematou/wxservice/mp/menu/Matchrule.java

@@ -0,0 +1,94 @@
+package com.ematou.wxservice.mp.menu;
+
+/**
+ * 匹配规则
+ * @author lhm
+ * @version 1.0
+ * @link https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Personalized_menu_interface.html
+ * 2021-05-11 20:18
+ */
+public class Matchrule {
+
+    private String tag_id;
+
+    private String sex;
+
+    private String country;
+
+    private String province;
+
+    private String city;
+
+    private String client_platform_type;
+
+    private String language;
+
+    public Matchrule(String tag_id, String sex, String country, String province, String city, String client_platform_type, String language) {
+        this.tag_id = tag_id;
+        this.sex = sex;
+        this.country = country;
+        this.province = province;
+        this.city = city;
+        this.client_platform_type = client_platform_type;
+        this.language = language;
+    }
+
+    public Matchrule() {
+    }
+
+    public String getTag_id() {
+        return tag_id;
+    }
+
+    public void setTag_id(String tag_id) {
+        this.tag_id = tag_id;
+    }
+
+    public String getSex() {
+        return sex;
+    }
+
+    public void setSex(String sex) {
+        this.sex = sex;
+    }
+
+    public String getCountry() {
+        return country;
+    }
+
+    public void setCountry(String country) {
+        this.country = country;
+    }
+
+    public String getProvince() {
+        return province;
+    }
+
+    public void setProvince(String province) {
+        this.province = province;
+    }
+
+    public String getCity() {
+        return city;
+    }
+
+    public void setCity(String city) {
+        this.city = city;
+    }
+
+    public String getClient_platform_type() {
+        return client_platform_type;
+    }
+
+    public void setClient_platform_type(String client_platform_type) {
+        this.client_platform_type = client_platform_type;
+    }
+
+    public String getLanguage() {
+        return language;
+    }
+
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+}

+ 32 - 0
src/main/java/com/ematou/wxservice/mp/menu/MenuButton.java

@@ -0,0 +1,32 @@
+package com.ematou.wxservice.mp.menu;
+
+import java.util.List;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 19:41
+ */
+public class MenuButton extends Button {
+
+    /**
+     * 子菜单列表
+     */
+    private List<Button> sub_button;
+
+    public MenuButton(String name){
+        super();
+        super.name = name;
+    }
+
+    public MenuButton() {
+    }
+
+    public List<Button> getSub_button() {
+        return sub_button;
+    }
+
+    public void setSub_button(List<Button> sub_button) {
+        this.sub_button = sub_button;
+    }
+}

+ 113 - 0
src/main/java/com/ematou/wxservice/mp/menu/MenuButtonManager.java

@@ -0,0 +1,113 @@
+package com.ematou.wxservice.mp.menu;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.ematou.wxservice.api.WeChatApi;
+import com.ematou.wxservice.api.WeChatApiRestTemplate;
+import com.ematou.wxservice.common.constant.WeChatConstant;
+import com.ematou.wxservice.service.WeChatService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 菜单按钮管理类
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 20:46
+ */
+@Component
+public class MenuButtonManager {
+
+    public static final Logger logger = LoggerFactory.getLogger(MenuButtonManager.class);
+
+    @Autowired
+    WeChatApiRestTemplate weChatApiRestTemplate;
+
+    @Autowired
+    WeChatService weChatService;
+
+    @PostConstruct
+    public void init() {
+        // TODO 初始化菜单按钮
+        ViewButton testBtn = new ViewButton("测试按钮", WeChatConstant.EventType.VIEW.toLowerCase(), "http://f3dhion.nat.ipyingshe.com/view/main");
+        ClickButton myCodeBtn = new ClickButton("我的取件码", WeChatConstant.EventType.CLICK.toLowerCase(), WeChatConstant.CustomEventKey.MY_TAKE_PARCEL_CODE);
+        ClickButton myHistoryBtn = new ClickButton("取件历史", WeChatConstant.EventType.CLICK.toLowerCase(), WeChatConstant.CustomEventKey.MY_HISTORY_RECORD);
+        MenuButton takeParcelMenuButton = new MenuButton("取件服务");
+        takeParcelMenuButton.setSub_button(Arrays.asList(myCodeBtn, myHistoryBtn, testBtn));
+
+        ViewButton companyState = new ViewButton("公司状态", WeChatConstant.EventType.VIEW.toLowerCase(), "http://mp.weixin.qq.com/mp/homepage?__biz=MzIwMTI2MTg1MA==&hid=3&sn=da0db13971baf7a27bf639f2f7eaebbe&scene=18#wechat_redirect");
+        ViewButton companyInfo = new ViewButton("公司简介", WeChatConstant.EventType.VIEW.toLowerCase(), "http://mp.weixin.qq.com/s?__biz=MzIwMTI2MTg1MA==&mid=503159833&idx=1&sn=fbddc8c56f960d7912cee60c959c42e8&chksm=0ef9aa6c398e237a862143c4f4c7314c0159a77e87ad4fd39a89d22181bcb05aedc9a816c8b2&scene=18#wechat_redirect");
+        ViewButton showNews = new ViewButton("展会快讯", WeChatConstant.EventType.VIEW.toLowerCase(), "http://mp.weixin.qq.com/mp/homepage?__biz=MzIwMTI2MTg1MA==&hid=4&sn=89a19c85a4742d13eb3e4ceab2179ba8&scene=18#wechat_redirect");
+        ViewButton industryReport = new ViewButton("行业周报", WeChatConstant.EventType.VIEW.toLowerCase(), "http://mp.weixin.qq.com/mp/homepage?__biz=MzIwMTI2MTg1MA==&hid=1&sn=f7becf0ef818d4a57a4ca4acfde72613&scene=18#wechat_redirect");
+        ClickButton policySupport = new ClickButton("政策支持", WeChatConstant.EventType.CLICK.toLowerCase(), WeChatConstant.CustomEventKey.POLICY_SUPPORT);
+        MenuButton companyMenuButton = new MenuButton("公司信息");
+        companyMenuButton.setSub_button(Arrays.asList(companyState, companyInfo, showNews, policySupport, industryReport));
+
+        ViewButton supplier = new ViewButton("供应商拓展", WeChatConstant.EventType.VIEW.toLowerCase(), "http://mp.weixin.qq.com/mp/homepage?__biz=MzIwMTI2MTg1MA==&hid=2&sn=cd51f61ada3218168fe3af6c3318b6ca&scene=18#wechat_redirect");
+        ViewButton showGoods = new ViewButton("货源展示区", WeChatConstant.EventType.VIEW.toLowerCase(), "https://mp.weixin.qq.com/bizmall/mallshelf?id=&t=mall/list&biz=MzIwMTI2MTg1MA==&shelf_id=5&showwxpaytitle=1#wechat_redirect");
+        ClickButton callCompanyBtn = new ClickButton("联系我们", WeChatConstant.EventType.CLICK.toLowerCase(), WeChatConstant.CustomEventKey.CALL_COMPANY);
+        MenuButton goodsMenuButton = new MenuButton("货源展示");
+        goodsMenuButton.setSub_button(Arrays.asList(supplier, showGoods, callCompanyBtn));
+
+        HashMap<String, List<Button>> map = new HashMap<>();
+        map.put("button", Arrays.asList(companyMenuButton, goodsMenuButton, takeParcelMenuButton));
+
+        String menu = JSON.toJSONString(map);
+
+        logger.info("初始化菜单信息:" + menu);
+
+        CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
+            int retry = 0;
+            while (retry < 3) {
+                try {
+                    String response = weChatApiRestTemplate.postForOther(String.format(WeChatApi.CREATE_MENU.getUrl(), weChatService.getAccessToken().getAccessToken()), menu);
+
+                    if (!StringUtils.hasLength(response)) {
+                        retry++;
+                        logger.error("创建菜单失败,返回为空!等待5秒继续重试中。。");
+                        if (retry == 3) {
+                            logger.error("创建菜单已经失败,请检查微信公众号服务器配置是否开启,或检查服务器配置是否正确!");
+                        }
+                        TimeUnit.SECONDS.sleep(5);
+                        continue;
+                    }
+
+                    JSONObject jsonObject = JSON.parseObject(response);
+                    logger.info("创建菜单结果:" + response);
+                    Integer errcode = jsonObject.getObject(WeChatConstant.RESPONSE_ERROR_CODE, Integer.class);
+                    String errmsg = jsonObject.getObject(WeChatConstant.RESPONSE_ERROR_MSG, String.class);
+                    if (errcode != 0 && !errmsg.equalsIgnoreCase(WeChatConstant.RESPONSE_OK)) {
+                        retry++;
+                        logger.error("创建菜单失败,error code:" + errcode + ",error message:" + errmsg);
+                        TimeUnit.SECONDS.sleep(5);
+                        continue;
+                    }
+                    break;
+                } catch (Throwable e) {
+                    retry++;
+                }
+            }
+        });
+
+        // 不阻塞主线程
+//        try {
+//            runAsync.get(20, TimeUnit.SECONDS);
+//        } catch (ExecutionException | TimeoutException e) {
+//            logger.error("等待创建菜单执行结果超时或执行出错:" + e.getMessage());
+//        }
+
+    }
+
+}

+ 50 - 0
src/main/java/com/ematou/wxservice/mp/menu/MiniProgramButton.java

@@ -0,0 +1,50 @@
+package com.ematou.wxservice.mp.menu;
+
+/**
+ * 小程序类型
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 19:50
+ */
+public class MiniProgramButton extends Button {
+
+    private String url;
+
+    private String appid;
+
+    private String pagepath;
+
+    public MiniProgramButton(String name, String type, String url, String appid, String pagepath) {
+        super(name, type);
+        this.url = url;
+        this.appid = appid;
+        this.pagepath = pagepath;
+    }
+
+    public MiniProgramButton() {
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getAppid() {
+        return appid;
+    }
+
+    public void setAppid(String appid) {
+        this.appid = appid;
+    }
+
+    public String getPagepath() {
+        return pagepath;
+    }
+
+    public void setPagepath(String pagepath) {
+        this.pagepath = pagepath;
+    }
+}

+ 40 - 0
src/main/java/com/ematou/wxservice/mp/menu/ViewButton.java

@@ -0,0 +1,40 @@
+package com.ematou.wxservice.mp.menu;
+
+import java.util.List;
+
+/**
+ * 网页类型
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 19:48
+ */
+public class ViewButton extends Button {
+
+    private String url;
+
+    private List<Button> sub_button;
+
+    public ViewButton(String name, String type, String url) {
+        super(name, type);
+        this.url = url;
+    }
+
+    public ViewButton() {
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public List<Button> getSub_button() {
+        return sub_button;
+    }
+
+    public void setSub_button(List<Button> sub_button) {
+        this.sub_button = sub_button;
+    }
+}

+ 369 - 0
src/main/java/com/ematou/wxservice/mp/message/WeChatMessage.java

@@ -0,0 +1,369 @@
+package com.ematou.wxservice.mp.message;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.ematou.wxservice.common.utils.XStreamTransformer;
+import com.ematou.wxservice.common.xml.converter.XStreamCDataConverter;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+
+import java.io.InputStream;
+import java.io.Serializable;
+
+/**
+ * 用户发过来的消息
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 16:08
+ */
+@XStreamAlias("xml")
+public class WeChatMessage implements Serializable {
+    private static final long serialVersionUID = -3586245291677274914L;
+
+    @JSONField(name = "Encrypt")
+    @XStreamAlias("Encrypt")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String encrypt;
+
+    @JSONField(name = "ToUserName")
+    @XStreamAlias("ToUserName")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String toUser;
+
+    @JSONField(name = "FromUserName")
+    @XStreamAlias("FromUserName")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String fromUser;
+
+    @JSONField(name = "CreateTime")
+    @XStreamAlias("CreateTime")
+    private Integer createTime;
+
+    @JSONField(name = "MsgType")
+    @XStreamAlias("MsgType")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String msgType;
+
+    @JSONField(name = "MsgDataFormat")
+    @XStreamAlias("MsgDataFormat")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String msgDataFormat;
+
+    @JSONField(name = "Content")
+    @XStreamAlias("Content")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String content;
+
+    @JSONField(name = "MsgId")
+    @XStreamAlias("MsgId")
+    private Long msgId;
+
+    @JSONField(name = "PicUrl")
+    @XStreamAlias("PicUrl")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String picUrl;
+
+    @JSONField(name = "MediaId")
+    @XStreamAlias("MediaId")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String mediaId;
+
+    @JSONField(name = "Event")
+    @XStreamAlias("Event")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String event;
+
+    @JSONField(name = "EventKey")
+    @XStreamAlias("EventKey")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String eventKey;
+
+    @JSONField(name = "Title")
+    @XStreamAlias("Title")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String title;
+
+    @JSONField(name = "AppId")
+    @XStreamAlias("AppId")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String appId;
+
+    @JSONField(name = "PagePath")
+    @XStreamAlias("PagePath")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String pagePath;
+
+    @JSONField(name = "ThumbUrl")
+    @XStreamAlias("ThumbUrl")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String thumbUrl;
+
+    @JSONField(name = "ThumbMediaId")
+    @XStreamAlias("ThumbMediaId")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String thumbMediaId;
+
+    @JSONField(name = "SessionFrom")
+    @XStreamAlias("SessionFrom")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String sessionFrom;
+
+    /**
+     * 以下是异步校验图片/音频是否含有违法违规内容的异步检测结果推送报文中的参数
+     */
+    @JSONField(name = "isrisky")
+    @XStreamAlias("isrisky")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String isRisky;
+
+    @JSONField(name = "extra_info_json")
+    @XStreamAlias("extra_info_json")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String extraInfoJson;
+
+    @JSONField(name = "appid")
+    @XStreamAlias("appid")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String appid;
+
+    @JSONField(name = "trace_id")
+    @XStreamAlias("trace_id")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String traceId;
+
+    @JSONField(name = "status_code")
+    @XStreamAlias("status_code")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String statusCode;
+
+    @JSONField(name = "Status")
+    @XStreamAlias("Status")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String status;
+
+    @JSONField(name = "Scene")
+    @XStreamAlias("Scene")
+    private Integer scene;
+
+    @JSONField(name = "Query")
+    @XStreamAlias("Query")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String query;
+
+
+    public static WeChatMessage fromXml(String xml) {
+        return XStreamTransformer.fromXml(WeChatMessage.class, xml);
+    }
+
+    public static WeChatMessage fromXml(InputStream is) {
+        return XStreamTransformer.fromXml(WeChatMessage.class, is);
+    }
+
+
+    public String getEncrypt() {
+        return encrypt;
+    }
+
+    public void setEncrypt(String encrypt) {
+        this.encrypt = encrypt;
+    }
+
+    public String getToUser() {
+        return toUser;
+    }
+
+    public void setToUser(String toUser) {
+        this.toUser = toUser;
+    }
+
+    public String getFromUser() {
+        return fromUser;
+    }
+
+    public void setFromUser(String fromUser) {
+        this.fromUser = fromUser;
+    }
+
+    public Integer getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Integer createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getMsgType() {
+        return msgType;
+    }
+
+    public void setMsgType(String msgType) {
+        this.msgType = msgType;
+    }
+
+    public String getMsgDataFormat() {
+        return msgDataFormat;
+    }
+
+    public void setMsgDataFormat(String msgDataFormat) {
+        this.msgDataFormat = msgDataFormat;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public Long getMsgId() {
+        return msgId;
+    }
+
+    public void setMsgId(Long msgId) {
+        this.msgId = msgId;
+    }
+
+    public String getPicUrl() {
+        return picUrl;
+    }
+
+    public void setPicUrl(String picUrl) {
+        this.picUrl = picUrl;
+    }
+
+    public String getMediaId() {
+        return mediaId;
+    }
+
+    public void setMediaId(String mediaId) {
+        this.mediaId = mediaId;
+    }
+
+    public String getEvent() {
+        return event;
+    }
+
+    public void setEvent(String event) {
+        this.event = event;
+    }
+
+    public String getEventKey() {
+        return eventKey;
+    }
+
+    public void setEventKey(String eventKey) {
+        this.eventKey = eventKey;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public String getPagePath() {
+        return pagePath;
+    }
+
+    public void setPagePath(String pagePath) {
+        this.pagePath = pagePath;
+    }
+
+    public String getThumbUrl() {
+        return thumbUrl;
+    }
+
+    public void setThumbUrl(String thumbUrl) {
+        this.thumbUrl = thumbUrl;
+    }
+
+    public String getThumbMediaId() {
+        return thumbMediaId;
+    }
+
+    public void setThumbMediaId(String thumbMediaId) {
+        this.thumbMediaId = thumbMediaId;
+    }
+
+    public String getSessionFrom() {
+        return sessionFrom;
+    }
+
+    public void setSessionFrom(String sessionFrom) {
+        this.sessionFrom = sessionFrom;
+    }
+
+    public String getIsRisky() {
+        return isRisky;
+    }
+
+    public void setIsRisky(String isRisky) {
+        this.isRisky = isRisky;
+    }
+
+    public String getExtraInfoJson() {
+        return extraInfoJson;
+    }
+
+    public void setExtraInfoJson(String extraInfoJson) {
+        this.extraInfoJson = extraInfoJson;
+    }
+
+    public String getAppid() {
+        return appid;
+    }
+
+    public void setAppid(String appid) {
+        this.appid = appid;
+    }
+
+    public String getTraceId() {
+        return traceId;
+    }
+
+    public void setTraceId(String traceId) {
+        this.traceId = traceId;
+    }
+
+    public String getStatusCode() {
+        return statusCode;
+    }
+
+    public void setStatusCode(String statusCode) {
+        this.statusCode = statusCode;
+    }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    public Integer getScene() {
+        return scene;
+    }
+
+    public void setScene(Integer scene) {
+        this.scene = scene;
+    }
+
+    public String getQuery() {
+        return query;
+    }
+
+    public void setQuery(String query) {
+        this.query = query;
+    }
+}

+ 93 - 0
src/main/java/com/ematou/wxservice/mp/message/WeChatMpXmlOutMessage.java

@@ -0,0 +1,93 @@
+package com.ematou.wxservice.mp.message;
+
+import com.ematou.wxservice.common.utils.XStreamTransformer;
+import com.ematou.wxservice.common.xml.builder.NewsBuilder;
+import com.ematou.wxservice.common.xml.builder.TextBuilder;
+import com.ematou.wxservice.common.xml.converter.XStreamCDataConverter;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+
+import java.io.Serializable;
+
+/**
+ * 响应消息基类,没新增一种响应消息类型,都需要将其注册到XStreamTransformer类的Map中。
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 14:51
+ */
+@XStreamAlias("xml")
+public abstract class WeChatMpXmlOutMessage implements Serializable {
+
+    private static final long serialVersionUID = -381382011286216263L;
+
+    @XStreamAlias("ToUserName")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    protected String toUserName;
+
+    @XStreamAlias("FromUserName")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    protected String fromUserName;
+
+    @XStreamAlias("CreateTime")
+    protected Long createTime;
+
+    @XStreamAlias("MsgType")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    protected String msgType;
+
+    @XStreamAlias("MsgId")
+    protected Long msgId;
+
+    /**
+     * 获得文本消息builder
+     */
+    public static TextBuilder TEXT() {
+        return new TextBuilder();
+    }
+
+    public static NewsBuilder NEWS() { return new NewsBuilder(); }
+
+    public String toXml() {
+        return XStreamTransformer.toXml((Class<WeChatMpXmlOutMessage>) this.getClass(), this);
+    }
+
+    public String getToUserName() {
+        return toUserName;
+    }
+
+    public void setToUserName(String toUserName) {
+        this.toUserName = toUserName;
+    }
+
+    public String getFromUserName() {
+        return fromUserName;
+    }
+
+    public void setFromUserName(String fromUserName) {
+        this.fromUserName = fromUserName;
+    }
+
+    public Long getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(Long createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getMsgType() {
+        return msgType;
+    }
+
+    public void setMsgType(String msgType) {
+        this.msgType = msgType;
+    }
+
+    public Long getMsgId() {
+        return msgId;
+    }
+
+    public void setMsgId(Long msgId) {
+        this.msgId = msgId;
+    }
+}

+ 122 - 0
src/main/java/com/ematou/wxservice/mp/message/WeChatMpXmlOutNewsMessage.java

@@ -0,0 +1,122 @@
+package com.ematou.wxservice.mp.message;
+
+import com.ematou.wxservice.common.constant.WeChatConstant;
+import com.ematou.wxservice.common.xml.converter.XStreamCDataConverter;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 图文信息响应
+ * @author lhm
+ * @version 1.0
+ * 2021-05-12 11:14
+ */
+@XStreamAlias("xml")
+public class WeChatMpXmlOutNewsMessage extends WeChatMpXmlOutMessage {
+
+    private static final long serialVersionUID = -7878689073807947544L;
+
+    /**
+     * 图文消息信息.
+     * 注意,如果图文数超过限制,则将只发限制内的条数
+     */
+    @XStreamAlias("Articles")
+    protected final List<Item> articles = new ArrayList<>();
+    /**
+     * 图文消息个数.
+     * 当用户发送文本、图片、视频、图文、地理位置这五种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
+     */
+    @XStreamAlias("ArticleCount")
+    protected int articleCount;
+
+    public WeChatMpXmlOutNewsMessage() {
+        this.msgType = WeChatConstant.XmlMsgType.NEWS;
+    }
+
+    public void addArticle(Item item) {
+        this.articles.add(item);
+        this.articleCount = this.articles.size();
+    }
+
+    @XStreamAlias("item")
+    public static class Item implements Serializable {
+        private static final long serialVersionUID = -4971456355028904754L;
+
+        /**
+         * 图文消息标题.
+         */
+        @XStreamAlias("Title")
+        @XStreamConverter(value = XStreamCDataConverter.class)
+        private String title;
+
+        /**
+         * 图文消息描述.
+         */
+        @XStreamAlias("Description")
+        @XStreamConverter(value = XStreamCDataConverter.class)
+        private String description;
+
+        /**
+         * 图片链接.
+         * 支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
+         */
+        @XStreamAlias("PicUrl")
+        @XStreamConverter(value = XStreamCDataConverter.class)
+        private String picUrl;
+
+        /**
+         * 点击图文消息跳转链接.
+         */
+        @XStreamAlias("Url")
+        @XStreamConverter(value = XStreamCDataConverter.class)
+        private String url;
+
+        public String getTitle() {
+            return title;
+        }
+
+        public void setTitle(String title) {
+            this.title = title;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        public void setDescription(String description) {
+            this.description = description;
+        }
+
+        public String getPicUrl() {
+            return picUrl;
+        }
+
+        public void setPicUrl(String picUrl) {
+            this.picUrl = picUrl;
+        }
+
+        public String getUrl() {
+            return url;
+        }
+
+        public void setUrl(String url) {
+            this.url = url;
+        }
+    }
+
+    public List<Item> getArticles() {
+        return articles;
+    }
+
+    public int getArticleCount() {
+        return articleCount;
+    }
+
+    public void setArticleCount(int articleCount) {
+        this.articleCount = articleCount;
+    }
+}

+ 37 - 0
src/main/java/com/ematou/wxservice/mp/message/WeChatMpXmlOutTextMessage.java

@@ -0,0 +1,37 @@
+package com.ematou.wxservice.mp.message;
+
+import com.ematou.wxservice.common.constant.WeChatConstant;
+import com.ematou.wxservice.common.xml.converter.XStreamCDataConverter;
+import com.thoughtworks.xstream.annotations.XStreamAlias;
+import com.thoughtworks.xstream.annotations.XStreamConverter;
+
+/**
+ * 文本消息响应
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 15:06
+ */
+@XStreamAlias("xml")
+public class WeChatMpXmlOutTextMessage extends WeChatMpXmlOutMessage {
+
+    private static final long serialVersionUID = 4348440573714051104L;
+
+    @XStreamAlias("Content")
+    @XStreamConverter(value = XStreamCDataConverter.class)
+    private String content;
+
+    public WeChatMpXmlOutTextMessage(){
+        this.msgType = WeChatConstant.XmlMsgType.TEXT;
+    }
+
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+
+}

+ 48 - 0
src/main/java/com/ematou/wxservice/mp/router/WeChatMessageHandlerRouter.java

@@ -0,0 +1,48 @@
+package com.ematou.wxservice.mp.router;
+
+import com.alibaba.fastjson.JSON;
+import com.ematou.wxservice.common.constant.WeChatConstant;
+import com.ematou.wxservice.mp.handler.*;
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 根据消息类型找到对应的处理类
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 16:23
+ */
+@Component
+public class WeChatMessageHandlerRouter {
+
+    private static final Map<String, WeChatMessageHandler> map = new ConcurrentHashMap<>();
+
+    public WeChatMessageHandlerRouter(
+            WeChatTextMessageHandler weChatTextMessageHandler,
+            WeChatClickEventMessageHandler weChatClickEventMessageHandler,
+            WeChatViewEventMessageHandler weChatViewEventMessageHandler,
+            WeChatNewsMessageHandler weChatNewsMessageHandler
+    ){
+        map.put(WeChatConstant.XmlMsgType.TEXT, weChatTextMessageHandler);
+        map.put(WeChatConstant.EventType.CLICK.toLowerCase(), weChatClickEventMessageHandler);
+        map.put(WeChatConstant.EventType.VIEW.toLowerCase(), weChatViewEventMessageHandler);
+        map.put(WeChatConstant.XmlMsgType.NEWS, weChatNewsMessageHandler);
+    }
+
+    public static WeChatMessageHandler router(WeChatMessage weChatMessage){
+
+        String msgType = weChatMessage.getMsgType();
+        // 如果是事件类型
+        if (WeChatConstant.XmlMsgType.EVENT.equalsIgnoreCase(msgType)) {
+            return map.get(weChatMessage.getEvent().toLowerCase());
+        }
+
+        return map.get(msgType);
+    }
+
+}

+ 44 - 0
src/main/java/com/ematou/wxservice/service/WeChatMessageService.java

@@ -0,0 +1,44 @@
+package com.ematou.wxservice.service;
+
+import com.alibaba.fastjson.JSON;
+import com.ematou.wxservice.mp.message.WeChatMessage;
+import com.ematou.wxservice.mp.handler.WeChatMessageHandler;
+import com.ematou.wxservice.mp.message.WeChatMpXmlOutMessage;
+import com.ematou.wxservice.mp.router.WeChatMessageHandlerRouter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-11 16:19
+ */
+@Service
+public class WeChatMessageService {
+
+    private static final Logger logger = LoggerFactory.getLogger(WeChatMessageHandlerRouter.class);
+
+    /**
+     * 处理消息
+     * @param weChatMessage 微信推送的消息
+     * @return  响应给用户的消息
+     */
+    public WeChatMpXmlOutMessage handleMessage(WeChatMessage weChatMessage) {
+        logger.info("wxservice接收到微信平台推送的消息:" + JSON.toJSONString(weChatMessage));
+
+        WeChatMessageHandler handler = WeChatMessageHandlerRouter.router(weChatMessage);
+
+        if (null == handler) {
+            logger.warn("暂不支持" + weChatMessage.getMsgType() + "类型或事件的处理");
+            return null;
+        }
+
+        WeChatMpXmlOutMessage outMessage = handler.handlerMessage(weChatMessage);
+
+        logger.info("wxservice处理完响应给用户的消息:" + JSON.toJSONString(outMessage));
+        return outMessage;
+
+    }
+
+}

+ 17 - 0
src/main/java/com/ematou/wxservice/service/WeChatOAuth2Service.java

@@ -0,0 +1,17 @@
+package com.ematou.wxservice.service;
+
+import org.springframework.stereotype.Service;
+
+/**
+ * 微信OAuth2.0认证
+ * @author lhm
+ * @version 1.0
+ * 2021-05-12 15:46
+ */
+@Service
+public class WeChatOAuth2Service {
+
+
+
+
+}

+ 170 - 0
src/main/java/com/ematou/wxservice/service/WeChatService.java

@@ -0,0 +1,170 @@
+package com.ematou.wxservice.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.ematou.wxservice.api.WeChatApi;
+import com.ematou.wxservice.api.WeChatApiRestTemplate;
+import com.ematou.wxservice.common.constant.WeChatConstant;
+import com.ematou.wxservice.config.WeChatGeneralConfig;
+import com.ematou.wxservice.entity.dto.AccessToken;
+import com.ematou.wxservice.entity.dto.TemplateMessage;
+import com.ematou.wxservice.entity.pojo.UserInfo;
+import com.ematou.wxservice.mapper.UserInfoMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+
+/**
+ * @author lhm
+ * @version 1.0
+ * 2021-05-10 18:04
+ */
+@Service
+public class WeChatService {
+
+    private final static Logger logger = LoggerFactory.getLogger(WeChatService.class);
+
+    @Autowired
+    WeChatGeneralConfig weChatGeneralConfig;
+
+    @Autowired
+    WeChatApiRestTemplate weChatApiRestTemplate;
+
+    @Autowired
+    UserInfoMapper userInfoMapper;
+
+    /**
+     * 获取普通AccessToken
+     * @return AccessToken
+     * @throws RuntimeException 异常
+     */
+    public AccessToken getAccessToken() throws RuntimeException {
+        String response = weChatApiRestTemplate.getForOther(WeChatApi.GET_TOKEN.getUrl());
+        AccessToken accessToken = JSON.parseObject(response).getObject("data", AccessToken.class);
+
+        if (null == accessToken) {
+            response = weChatApiRestTemplate.getForOther(WeChatApi.GENERAL_TOKEN.getUrl());
+            accessToken = JSON.parseObject(response).getObject("data", AccessToken.class);
+            if (null == accessToken) {
+                logger.error("微信基础服务可能已经宕机!response:" + response);
+                throw new RuntimeException("获取AccessToken失败,请稍后再试!");
+            }
+        }
+
+        return accessToken;
+    }
+
+    /**
+     * 获取用户信息
+     * @param openId openId
+     * @return 用户信息
+     * @throws RuntimeException 异常
+     */
+    public UserInfo getUserInfo(String openId) throws RuntimeException {
+        if (!StringUtils.hasLength(openId)) {
+            logger.error("获取用户信息的openId为空");
+            return null;
+        }
+        String accessToken = getAccessToken().getAccessToken();
+
+        UserInfo userInfo;
+        String response = null;
+        try {
+            response = weChatApiRestTemplate.getForOther(String.format(WeChatApi.GET_USER_INFO.getUrl(), accessToken, openId));
+            userInfo = JSON.parseObject(response, UserInfo.class);
+            userInfoMapper.insertUserInfo(userInfo);
+        } catch (RuntimeException e) {
+            logger.error("请求微信平台获取用户信息出错!response:" + response + ",error message:" + e.getMessage());
+            throw new RuntimeException("请求微信平台获取用户信息出错!");
+        }
+        return userInfo;
+    }
+
+    /**
+     * 根据模板编号获取模板消息ID
+     * @param templateNum 模板编号
+     * @see WeChatConstant.TemplateNumber
+     * @return 模板消息id
+     */
+    public String getTemplateMsgId(String templateNum){
+
+        String url = String.format(WeChatApi.GET_TEMPLATE_MSGID.getUrl(), getAccessToken().getAccessToken());
+
+        String requestBody = "{ \"template_id_short\": \"" + templateNum + "\" }";
+
+        String res = weChatApiRestTemplate.postForRest(url, requestBody);
+
+        if (StringUtils.hasLength(res)) {
+            JSONObject response = JSON.parseObject(res);
+            String templateId = response.getObject("template_id", String.class);
+            logger.info("获取模板消息ID成功,template_id:" + templateId);
+            return templateId;
+        }
+        return "";
+    }
+
+    /**
+     * 发送模板信息
+     * @param msg 模板信息数据
+     * @return 是否发送成功
+     */
+    public Integer sendTemplateMsg(TemplateMessage msg){
+
+        String accessToken = getAccessToken().getAccessToken();
+        if (!StringUtils.hasLength(accessToken)) {
+            logger.error("发送模板消息时获取AccessToken值为空");
+            return 0;
+        }
+        String templateMsgId = getTemplateMsgId(WeChatConstant.TemplateNumber.TAKE_PARCEL_CODE_TEMPLATE);
+        if (!StringUtils.hasLength(templateMsgId)) {
+            logger.error("发送模板消息时获取模板消息ID值为空");
+            return 0;
+        }
+        String url = String.format(WeChatApi.SEND_TEMPLATE_MSG.getUrl(), accessToken);
+
+        // 组装消息数据
+        HashMap<String, Object> body = new HashMap<>();
+        body.put("touser", msg.getOpenId());
+        body.put("template_id", templateMsgId);
+        if (StringUtils.hasLength(msg.getUrl())) {
+            body.put("url", msg.getUrl());
+        }
+        if (null != msg.getMiniProgram()) {
+            body.put("miniprogram", msg.getMiniProgram());
+        }
+        HashMap<String, Object> data = new HashMap<>();
+        // 组装data数据
+        for (int i = 0; i < msg.getData().size(); i++) {
+            TemplateMessage.TemplateData templateData = msg.getData().get(i);
+            if (i == 0) {
+                data.put("first", templateData);
+                continue;
+            }
+            if (i == msg.getData().size()-1) {
+                data.put("remark", templateData);
+                break;
+            }
+            data.put("keyword" + i, templateData);
+        }
+        body.put("data", data);
+
+        String res;
+        try {
+            res = weChatApiRestTemplate.postForRest(url, body);
+
+            if (!StringUtils.hasLength(res)) {
+                return 0;
+            }
+        } catch (Exception e) {
+            logger.error("推送模板消息失败,exception message:" + e.getMessage());
+            return 0;
+        }
+
+        return 1;
+    }
+
+}

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

@@ -0,0 +1,30 @@
+server:
+  port: 3030
+
+wx:
+  general:
+    appId: wxf9360d70bc1406ee
+    appSecret: 78413a82d0332ecbf7fdf475d0a8b08e
+#    appId: wx2215996b5fe1ec10
+#    appSecret: 8622b470fe3b779ffafe3baeb204fe41
+spring:
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: tuser
+    password: Qq!123
+    url: jdbc:mysql://120.76.84.45:3306/wx_base?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
+  thymeleaf:
+    cache: false
+    prefix: classpath:/templates/
+    suffix: .html
+    check-template-location: true
+    encoding: UTF-8
+    mode: HTML
+    servlet:
+      content-type: text/html
+mybatis:
+  mapper-locations: classpath:mybatis/*.xml
+  configuration:
+    map-underscore-to-camel-case: true
+logging:
+  config: classpath:logback.xml

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

@@ -0,0 +1,56 @@
+<configuration>
+    <!-- %m输出的信息, %p日志级别, %t线程名, %d日期, %c类的全名, %i索引 -->
+    <!-- appender是configuration的子节点,是负责写日志的组件 -->
+    <!-- ConsoleAppender把日志输出到控制台 -->
+    <!--    <property name="CONSOLE_LOG_PATTERN" -->
+    <!--               value="%date{yyyy-MM-dd HH:mm:ss} | %highlight(%-5level) | %boldYellow(%thread) | %boldGreen(%logger) | %msg%n"/> -->
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <!--<pattern>${CONSOLE_LOG_PATTERN}</pattern> -->
+            <pattern>%date{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) (%file:%line\)- %m%n</pattern>
+            <!-- 控制台也要使用utf-8,不要使用gbk -->
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
+    <!-- 1.先按日期存日志,日期变了,将前一天的日志文件名重命名为xxx%日期%索引,新的日志仍然是sys.log -->
+    <!-- 2.如果日期没有变化,但是当前日志文件的大小超过1kb时,对当前日志进行分割 重名名 -->
+    <appender name="syslog" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!--<File>${catalina.base}/mylog/sys.log</File>-->
+<!--        <File>/app/project/wx_base/logs/wxbase.log</File>-->
+        <File>logs/wxservice.log</File>
+        <!-- rollingPolicy:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。 -->
+        <!-- TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
+            <!--<fileNamePattern>${catalina.base}/mylog/sys.%d.%i.log</fileNamePattern>-->
+<!--            <fileNamePattern>${catalina.base}/%d/wxservice.%d.%i.log</fileNamePattern>-->
+            <fileNamePattern>logs/wxservice.%d.%i.log</fileNamePattern>
+            <!-- 每产生一个日志文件,该日志文件的保存期限为30天 -->
+            <maxHistory>30</maxHistory>
+            <timeBasedFileNamingAndTriggeringPolicy  class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <!-- maxFileSize:这是活动文件的大小,默认值是10MB,本篇设置为1KB,只是为了演示 -->
+                <maxFileSize>10MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+        </rollingPolicy>
+        <encoder>
+            <!-- pattern节点,用来设置日志的输入格式 -->
+            <pattern>
+                %d %p (%file:%line\)- %m%n
+            </pattern>
+            <!-- 记录日志的编码 -->
+            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
+        </encoder>
+    </appender>
+    <!-- 控制台日志输出级别 -->
+    <root level="INFO">
+        <appender-ref ref="STDOUT" />
+    </root>
+    <!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
+    <!-- com.appley为根包,也就是只要是发生在这个根包下面的所有日志操作行为的权限都是DEBUG -->
+    <!-- 级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE  -->
+    <logger name="com.ematou.wxservice" level="INFO">
+        <appender-ref ref="syslog" />
+    </logger>
+</configuration>

+ 20 - 0
src/main/resources/mybatis/UserInfoMapper.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.ematou.wxservice.mapper.UserInfoMapper">
+
+    <sql id="Base_Column_List">
+        open_id,union_id,group_id,nick_name,sex,city,country,province,language,head_img_url,subscribe_time,tagid_list,
+        subscribe,remark,subscribe_scene,qr_scene,qr_scene_str,creator_id,create_time,moder_id,mod_time,tmst
+    </sql>
+
+    <insert id="insertUserInfo" >
+        insert into user_info ( <include refid="Base_Column_List"/> )
+        values (#{userInfo.openId}, #{userInfo.unionId}, #{userInfo.groupId}, #{userInfo.nickName}, #{userInfo.sex}, #{userInfo.city},
+        #{userInfo.country}, #{userInfo.province}, #{userInfo.language}, #{userInfo.headImgUrl}, #{userInfo.subscribeTime}, #{userInfo.tagIdList},
+        #{userInfo.subscribe}, #{userInfo.remark}, #{userInfo.subscribeScene}, #{userInfo.qrScene}, #{userInfo.qrSceneStr}, #{userInfo.creatorId},
+        #{userInfo.createTime},  #{userInfo.moderId}, #{userInfo.modTime}, #{userInfo.tmst})
+    </insert>
+
+</mapper>

+ 245 - 0
src/main/resources/templates/hello.html

@@ -0,0 +1,245 @@
+<!DOCTYPE html>
+<html lang="en" >
+<head>
+    <meta charset="UTF-8">
+
+    <style type="text/css" >
+        .nav {
+            padding-left: 0;
+            margin-bottom: 0;
+            list-style: none;
+        }
+        .nav > li {
+            position: relative;
+            display: block;
+        }
+        .nav > li > a {
+            position: relative;
+            display: block;
+            padding: 10px 15px;
+        }
+        .nav > li > a:hover,
+        .nav > li > a:focus {
+            text-decoration: none;
+            background-color: #eeeeee;
+        }
+        .nav > li.disabled > a {
+            color: #777777;
+        }
+        .nav > li.disabled > a:hover,
+        .nav > li.disabled > a:focus {
+            color: #777777;
+            text-decoration: none;
+            cursor: not-allowed;
+            background-color: transparent;
+        }
+        .nav .open > a,
+        .nav .open > a:hover,
+        .nav .open > a:focus {
+            background-color: #eeeeee;
+            border-color: #337ab7;
+        }
+        .nav .nav-divider {
+            height: 1px;
+            margin: 9px 0;
+            overflow: hidden;
+            background-color: #e5e5e5;
+        }
+        .nav > li > a > img {
+            max-width: none;
+        }
+        .nav-tabs {
+            border-bottom: 1px solid #ddd;
+        }
+        .nav-tabs > li {
+            float: left;
+            margin-bottom: -1px;
+        }
+        .nav-tabs > li > a {
+            margin-right: 2px;
+            line-height: 1.42857143;
+            border: 1px solid transparent;
+            border-radius: 4px 4px 0 0;
+        }
+        .nav-tabs > li > a:hover {
+            border-color: #eeeeee #eeeeee #ddd;
+        }
+        .nav-tabs > li.active > a,
+        .nav-tabs > li.active > a:hover,
+        .nav-tabs > li.active > a:focus {
+            color: #555555;
+            cursor: default;
+            background-color: #fff;
+            border: 1px solid #ddd;
+            border-bottom-color: transparent;
+        }
+        .nav-tabs.nav-justified {
+            width: 100%;
+            border-bottom: 0;
+        }
+        .nav-tabs.nav-justified > li {
+            float: none;
+        }
+        .nav-tabs.nav-justified > li > a {
+            margin-bottom: 5px;
+            text-align: center;
+        }
+        .nav-tabs.nav-justified > .dropdown .dropdown-menu {
+            top: auto;
+            left: auto;
+        }
+        @media (min-width: 768px) {
+            .nav-tabs.nav-justified > li {
+                display: table-cell;
+                width: 1%;
+            }
+            .nav-tabs.nav-justified > li > a {
+                margin-bottom: 0;
+            }
+        }
+        .nav-tabs.nav-justified > li > a {
+            margin-right: 0;
+            border-radius: 4px;
+        }
+        .nav-tabs.nav-justified > .active > a,
+        .nav-tabs.nav-justified > .active > a:hover,
+        .nav-tabs.nav-justified > .active > a:focus {
+            border: 1px solid #ddd;
+        }
+        @media (min-width: 768px) {
+            .nav-tabs.nav-justified > li > a {
+                border-bottom: 1px solid #ddd;
+                border-radius: 4px 4px 0 0;
+            }
+            .nav-tabs.nav-justified > .active > a,
+            .nav-tabs.nav-justified > .active > a:hover,
+            .nav-tabs.nav-justified > .active > a:focus {
+                border-bottom-color: #fff;
+            }
+        }
+        .nav-pills > li {
+            float: left;
+        }
+        .nav-pills > li > a {
+            border-radius: 4px;
+        }
+        .nav-pills > li + li {
+            margin-left: 2px;
+        }
+        .nav-pills > li.active > a,
+        .nav-pills > li.active > a:hover,
+        .nav-pills > li.active > a:focus {
+            color: #fff;
+            background-color: #337ab7;
+        }
+        .nav-stacked > li {
+            float: none;
+        }
+        .nav-stacked > li + li {
+            margin-top: 2px;
+            margin-left: 0;
+        }
+        .nav-justified {
+            width: 100%;
+        }
+        .nav-justified > li {
+            float: none;
+        }
+        .nav-justified > li > a {
+            margin-bottom: 5px;
+            text-align: center;
+        }
+        .nav-justified > .dropdown .dropdown-menu {
+            top: auto;
+            left: auto;
+        }
+        @media (min-width: 768px) {
+            .nav-justified > li {
+                display: table-cell;
+                width: 1%;
+            }
+            .nav-justified > li > a {
+                margin-bottom: 0;
+            }
+        }
+        .nav-tabs-justified {
+            border-bottom: 0;
+        }
+        .nav-tabs-justified > li > a {
+            margin-right: 0;
+            border-radius: 4px;
+        }
+        .nav-tabs-justified > .active > a,
+        .nav-tabs-justified > .active > a:hover,
+        .nav-tabs-justified > .active > a:focus {
+            border: 1px solid #ddd;
+        }
+        @media (min-width: 768px) {
+            .nav-tabs-justified > li > a {
+                border-bottom: 1px solid #ddd;
+                border-radius: 4px 4px 0 0;
+            }
+            .nav-tabs-justified > .active > a,
+            .nav-tabs-justified > .active > a:hover,
+            .nav-tabs-justified > .active > a:focus {
+                border-bottom-color: #fff;
+            }
+        }
+        .nav:before,
+        .nav:after {
+            display: table;
+            content: " ";
+        }
+        .nav:after {
+            clear: both;
+        }
+        nav {
+            display: none;
+        }
+        .nav-tabs .dropdown-menu {
+            margin-top: -1px;
+            border-top-left-radius: 0;
+            border-top-right-radius: 0;
+        }
+        .nav-tabs-justified {
+            border-bottom: 0;
+        }
+        .nav-tabs-justified > li > a {
+            margin-right: 0;
+            border-radius: 4px;
+        }
+        .nav-tabs-justified > .active > a,
+        .nav-tabs-justified > .active > a:hover,
+        .nav-tabs-justified > .active > a:focus {
+            border: 1px solid #ddd;
+        }
+        @media (min-width: 768px) {
+            .nav-tabs-justified > li > a {
+                border-bottom: 1px solid #ddd;
+                border-radius: 4px 4px 0 0;
+            }
+            .nav-tabs-justified > .active > a,
+            .nav-tabs-justified > .active > a:hover,
+            .nav-tabs-justified > .active > a:focus {
+                border-bottom-color: #fff;
+            }
+        }
+        a {
+            text-decoration-line: none;
+        }
+        a:visited {
+            color: #c4e3f3;
+        }
+
+    </style>
+
+    <title>Title</title>
+</head>
+<body>
+<ul class="nav nav-tabs">
+    <li role="presentation" class="active"><a href="#">全部</a></li>
+    <li role="presentation"><a href="#">已取件</a></li>
+    <li role="presentation"><a href="#">未取件</a></li>
+</ul>
+</body>
+</html>

+ 61 - 0
src/test/java/com/ematou/wxservice/wechatservice/WechatserviceApplicationTests.java

@@ -0,0 +1,61 @@
+package com.ematou.wxservice.wechatservice;
+
+import com.ematou.wxservice.config.WeChatGeneralConfig;
+import com.ematou.wxservice.entity.dto.AccessToken;
+import com.ematou.wxservice.mp.menu.MenuButtonManager;
+import com.ematou.wxservice.service.WeChatService;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Map;
+
+@SpringBootTest
+class WechatserviceApplicationTests {
+
+    private static final Logger logger = LoggerFactory.getLogger(WechatserviceApplicationTests.class);
+
+    private static String getAccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
+
+    @Autowired
+    WeChatGeneralConfig weChatGeneralConfig;
+
+    @Autowired
+    RestTemplate restTemplate;
+
+    @Test
+    void contextLoads() {
+
+        // 调用接口获取access_token
+        Map map = restTemplate.getForObject(String.format(getAccessTokenUrl, weChatGeneralConfig.getAppId(), weChatGeneralConfig.getAppSecret()), Map.class);
+
+        System.out.println(map);
+
+
+
+    }
+
+
+    @Autowired
+    WeChatService weChatService;
+
+    @Test
+    void testGetGlobalAccessToken(){
+        AccessToken accessToken = weChatService.getAccessToken();
+        System.out.println(accessToken);
+    }
+
+    @Autowired
+    MenuButtonManager manager;
+
+    @Test
+    void testCreateMenu() throws InterruptedException {
+
+        manager.init();
+
+    }
+
+}