diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..34292da
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/target/
+/.settings/
+*.project
+*.classpath
+application.properties
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..4d30df2
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.4.1.RELEASE
+
+
+ com.github.binarywang.wechat
+ 1.0.0-SNAPSHOT
+ weixin-mp-demo-springboot
+ jar
+
+ spring-boot-demo-for-wechat-mp
+ Spring Boot Demo with wechat MP
+
+
+ UTF-8
+ zh_CN
+ 1.7
+ 2.2.4
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ com.github.binarywang
+ weixin-java-mp
+ ${weixin-java-mp.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ ${project.build.jdk}
+ ${project.build.sourceEncoding}
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ com.testwithspring.starter.springboot.UnitTest
+
+
+
+
+
+
diff --git a/src/main/java/com/github/binarywang/demo/wechat/WechatMpDemoApplication.java b/src/main/java/com/github/binarywang/demo/wechat/WechatMpDemoApplication.java
new file mode 100644
index 0000000..6ccebde
--- /dev/null
+++ b/src/main/java/com/github/binarywang/demo/wechat/WechatMpDemoApplication.java
@@ -0,0 +1,16 @@
+package com.github.binarywang.demo.wechat;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ *
+ * @author Binary Wang
+ */
+@SpringBootApplication
+public class WechatMpDemoApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(WechatMpDemoApplication.class, args);
+ }
+}
diff --git a/src/main/java/com/github/binarywang/demo/wechat/builder/AbstractBuilder.java b/src/main/java/com/github/binarywang/demo/wechat/builder/AbstractBuilder.java
new file mode 100644
index 0000000..33461d9
--- /dev/null
+++ b/src/main/java/com/github/binarywang/demo/wechat/builder/AbstractBuilder.java
@@ -0,0 +1,20 @@
+package com.github.binarywang.demo.wechat.builder;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+
+/**
+ *
+ * @author Binary Wang
+ *
+ */
+public abstract class AbstractBuilder {
+ protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+ public abstract WxMpXmlOutMessage build(String content,
+ WxMpXmlMessage wxMessage, WxMpService service);
+}
diff --git a/src/main/java/com/github/binarywang/demo/wechat/builder/ImageBuilder.java b/src/main/java/com/github/binarywang/demo/wechat/builder/ImageBuilder.java
new file mode 100644
index 0000000..628d525
--- /dev/null
+++ b/src/main/java/com/github/binarywang/demo/wechat/builder/ImageBuilder.java
@@ -0,0 +1,26 @@
+package com.github.binarywang.demo.wechat.builder;
+
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.WxMpXmlOutImageMessage;
+import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+
+/**
+ *
+ * @author Binary Wang
+ *
+ */
+public class ImageBuilder extends AbstractBuilder {
+
+ @Override
+ public WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage,
+ WxMpService service) {
+
+ WxMpXmlOutImageMessage m = WxMpXmlOutMessage.IMAGE().mediaId(content)
+ .fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
+ .build();
+
+ return m;
+ }
+
+}
diff --git a/src/main/java/com/github/binarywang/demo/wechat/builder/TextBuilder.java b/src/main/java/com/github/binarywang/demo/wechat/builder/TextBuilder.java
new file mode 100644
index 0000000..a8baf54
--- /dev/null
+++ b/src/main/java/com/github/binarywang/demo/wechat/builder/TextBuilder.java
@@ -0,0 +1,24 @@
+package com.github.binarywang.demo.wechat.builder;
+
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+import me.chanjar.weixin.mp.bean.WxMpXmlOutTextMessage;
+
+/**
+ *
+ * @author Binary Wang
+ *
+ */
+public class TextBuilder extends AbstractBuilder {
+
+ @Override
+ public WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage,
+ WxMpService service) {
+ WxMpXmlOutTextMessage m = WxMpXmlOutMessage.TEXT().content(content)
+ .fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
+ .build();
+ return m;
+ }
+
+}
diff --git a/src/main/java/com/github/binarywang/demo/wechat/config/WechatMpConfiguration.java b/src/main/java/com/github/binarywang/demo/wechat/config/WechatMpConfiguration.java
new file mode 100644
index 0000000..cd04918
--- /dev/null
+++ b/src/main/java/com/github/binarywang/demo/wechat/config/WechatMpConfiguration.java
@@ -0,0 +1,172 @@
+package com.github.binarywang.demo.wechat.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.github.binarywang.demo.wechat.handler.AbstractHandler;
+import com.github.binarywang.demo.wechat.handler.KfSessionHandler;
+import com.github.binarywang.demo.wechat.handler.LocationHandler;
+import com.github.binarywang.demo.wechat.handler.LogHandler;
+import com.github.binarywang.demo.wechat.handler.MenuHandler;
+import com.github.binarywang.demo.wechat.handler.MsgHandler;
+import com.github.binarywang.demo.wechat.handler.NullHandler;
+import com.github.binarywang.demo.wechat.handler.StoreCheckNotifyHandler;
+import com.github.binarywang.demo.wechat.handler.SubscribeHandler;
+import com.github.binarywang.demo.wechat.handler.UnsubscribeHandler;
+
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.mp.api.WxMpConfigStorage;
+import me.chanjar.weixin.mp.api.WxMpInMemoryConfigStorage;
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
+
+/**
+ * wechat mp configuration
+ *
+ * @author Binary Wang
+ */
+@Configuration
+@ConditionalOnClass(WxMpService.class)
+@EnableConfigurationProperties(WechatMpProperties.class)
+public class WechatMpConfiguration {
+ @Autowired
+ private WechatMpProperties properties;
+
+ @Bean
+ @ConditionalOnMissingBean
+ public WxMpConfigStorage configStorage() {
+ WxMpInMemoryConfigStorage configStorage = new WxMpInMemoryConfigStorage();
+ configStorage.setAppId(this.properties.getAppId());
+ configStorage.setSecret(this.properties.getSecret());
+ configStorage.setToken(this.properties.getToken());
+ configStorage.setAesKey(this.properties.getAesKey());
+ configStorage.setPartnerId(this.properties.getPartnerId());
+ configStorage.setPartnerKey(this.properties.getPartnerKey());
+ return configStorage;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public WxMpService wxMpService(WxMpConfigStorage configStorage) {
+ WxMpService wxMpService = new WxMpServiceImpl();
+ wxMpService.setWxMpConfigStorage(configStorage);
+ return wxMpService;
+ }
+
+ @Bean
+ public WxMpMessageRouter router(WxMpService wxMpService) {
+ final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
+
+ // 记录所有事件的日志
+ newRouter.rule().handler(this.logHandler).next();
+
+ // 接收客服会话管理事件
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.EVT_KF_CREATE_SESSION)
+ .handler(this.kfSessionHandler).end();
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.EVT_KF_CLOSE_SESSION).handler(this.kfSessionHandler)
+ .end();
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.EVT_KF_SWITCH_SESSION)
+ .handler(this.kfSessionHandler).end();
+
+ // 门店审核事件
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.EVT_POI_CHECK_NOTIFY)
+ .handler(this.storeCheckNotifyHandler).end();
+
+ // 自定义菜单事件
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.BUTTON_CLICK).handler(this.getMenuHandler()).end();
+
+ // 点击菜单连接事件
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.BUTTON_VIEW).handler(this.nullHandler).end();
+
+ // 关注事件
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.EVT_SUBSCRIBE).handler(this.getSubscribeHandler())
+ .end();
+
+ // 取消关注事件
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.EVT_UNSUBSCRIBE)
+ .handler(this.getUnsubscribeHandler()).end();
+
+ // 上报地理位置事件
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.EVT_LOCATION).handler(this.getLocationHandler())
+ .end();
+
+ // 接收地理位置消息
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_LOCATION)
+ .handler(this.getLocationHandler()).end();
+
+ // 扫码事件
+ newRouter.rule().async(false).msgType(WxConsts.XML_MSG_EVENT)
+ .event(WxConsts.EVT_SCAN).handler(this.getScanHandler()).end();
+
+ // 默认
+ newRouter.rule().async(false).handler(this.getMsgHandler()).end();
+
+ return newRouter;
+ }
+
+ @Autowired
+ private LocationHandler locationHandler;
+
+ @Autowired
+ private MenuHandler menuHandler;
+
+ @Autowired
+ private MsgHandler msgHandler;
+
+ @Autowired
+ protected LogHandler logHandler;
+
+ @Autowired
+ protected NullHandler nullHandler;
+
+ @Autowired
+ protected KfSessionHandler kfSessionHandler;
+
+ @Autowired
+ protected StoreCheckNotifyHandler storeCheckNotifyHandler;
+
+ @Autowired
+ private UnsubscribeHandler unsubscribeHandler;
+
+ @Autowired
+ private SubscribeHandler subscribeHandler;
+
+ protected MenuHandler getMenuHandler() {
+ return this.menuHandler;
+ }
+
+ protected SubscribeHandler getSubscribeHandler() {
+ return this.subscribeHandler;
+ }
+
+ protected UnsubscribeHandler getUnsubscribeHandler() {
+ return this.unsubscribeHandler;
+ }
+
+ protected AbstractHandler getLocationHandler() {
+ return this.locationHandler;
+ }
+
+ protected MsgHandler getMsgHandler() {
+ return this.msgHandler;
+ }
+
+ protected AbstractHandler getScanHandler() {
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/github/binarywang/demo/wechat/config/WechatMpProperties.java b/src/main/java/com/github/binarywang/demo/wechat/config/WechatMpProperties.java
new file mode 100644
index 0000000..83da5a7
--- /dev/null
+++ b/src/main/java/com/github/binarywang/demo/wechat/config/WechatMpProperties.java
@@ -0,0 +1,97 @@
+package com.github.binarywang.demo.wechat.config;
+
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * wechat mp properties
+ *
+ * @author Binary Wang
+ */
+@ConfigurationProperties(prefix = "wechat.mp")
+public class WechatMpProperties {
+ /**
+ * 设置微信公众号的appid
+ */
+ private String appId;
+
+ /**
+ * 设置微信公众号的app secret
+ */
+ private String secret;
+
+ /**
+ * 微信支付partner id
+ */
+ private String partnerId;
+
+ /**
+ * 微信支付partner key
+ */
+ private String partnerKey;
+
+ /**
+ * 设置微信公众号的token
+ */
+ private String token;
+
+ /**
+ * 设置微信公众号的EncodingAESKey
+ */
+ private String aesKey;
+
+ public String getAppId() {
+ return this.appId;
+ }
+
+ public void setAppId(String appId) {
+ this.appId = appId;
+ }
+
+ public String getSecret() {
+ return this.secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+
+ public String getPartnerId() {
+ return this.partnerId;
+ }
+
+ public void setPartnerId(String partnerId) {
+ this.partnerId = partnerId;
+ }
+
+ public String getPartnerKey() {
+ return this.partnerKey;
+ }
+
+ public void setPartnerKey(String partnerKey) {
+ this.partnerKey = partnerKey;
+ }
+
+ public String getToken() {
+ return this.token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public String getAesKey() {
+ return this.aesKey;
+ }
+
+ public void setAesKey(String aesKey) {
+ this.aesKey = aesKey;
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this,
+ ToStringStyle.MULTI_LINE_STYLE);
+ }
+}
diff --git a/src/main/java/com/github/binarywang/demo/wechat/controller/WechatController.java b/src/main/java/com/github/binarywang/demo/wechat/controller/WechatController.java
new file mode 100644
index 0000000..00210c5
--- /dev/null
+++ b/src/main/java/com/github/binarywang/demo/wechat/controller/WechatController.java
@@ -0,0 +1,110 @@
+package com.github.binarywang.demo.wechat.controller;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import me.chanjar.weixin.mp.api.WxMpMessageRouter;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.WxMpXmlOutMessage;
+
+/**
+ * @author Binary Wang
+ */
+@RestController
+@RequestMapping("/wechat/portal")
+public class WechatController {
+ private final Logger logger = LoggerFactory.getLogger(this.getClass());
+
+ @Autowired
+ private WxMpService wxService;
+
+ @Autowired
+ private WxMpMessageRouter router;
+
+ @RequestMapping(method = RequestMethod.GET,
+ produces = "text/plain;charset=utf-8")
+ public @ResponseBody String authGet(
+ @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) {
+
+ this.logger.info("\n接收到来自微信服务器的认证消息:[{},{},{},{}]", signature,
+ timestamp, nonce, echostr);
+
+ if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
+ throw new IllegalArgumentException("请求参数非法,请核实~");
+ }
+
+ if (this.wxService.checkSignature(timestamp, nonce, signature)) {
+ return echostr;
+ }
+
+ return "非法请求";
+ }
+
+ @RequestMapping(method = RequestMethod.POST,
+ produces = "application/xml; charset=UTF-8")
+ public @ResponseBody String post(@RequestBody String requestBody,
+ @RequestParam("signature") String signature,
+ @RequestParam("timestamp") String timestamp,
+ @RequestParam("nonce") String nonce,
+ @RequestParam(name = "encrypt_type",
+ required = false) String encType,
+ @RequestParam(name = "msg_signature",
+ required = false) String msgSignature) {
+ this.logger.info("\n接收微信请求:[{},{},{},{},{}]\n{} ", signature, encType,
+ msgSignature, timestamp, nonce, requestBody);
+
+ String out = null;
+ if (encType == null) {
+ // 明文传输的消息
+ WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
+ WxMpXmlOutMessage outMessage = this.route(inMessage);
+ if (outMessage == null) {
+ return "";
+ }
+
+ out = outMessage.toXml();
+ } else if ("aes".equals(encType)) {
+ // aes加密的消息
+ WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(
+ requestBody, this.wxService.getWxMpConfigStorage(), timestamp,
+ nonce, msgSignature);
+ this.logger.debug("\n消息解密后内容为:\n{} ", inMessage.toString());
+ WxMpXmlOutMessage outMessage = this.route(inMessage);
+ if (outMessage == null) {
+ return "";
+ }
+
+ out = outMessage
+ .toEncryptedXml(this.wxService.getWxMpConfigStorage());
+ }
+
+ this.logger.debug("\n组装回复信息:{}", out);
+
+ return out;
+ }
+
+ private WxMpXmlOutMessage route(WxMpXmlMessage message) {
+ try {
+ return this.router.route(message);
+ } catch (Exception e) {
+ this.logger.error(e.getMessage(), e);
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/github/binarywang/demo/wechat/controller/WechatErrorController.java b/src/main/java/com/github/binarywang/demo/wechat/controller/WechatErrorController.java
new file mode 100644
index 0000000..5d47e93
--- /dev/null
+++ b/src/main/java/com/github/binarywang/demo/wechat/controller/WechatErrorController.java
@@ -0,0 +1,121 @@
+package com.github.binarywang.demo.wechat.controller;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.web.ErrorAttributes;
+import org.springframework.boot.autoconfigure.web.ErrorController;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * @author 王彬 (Binary Wang)
+ *
+ */
+@Controller
+public class WechatErrorController implements ErrorController {
+
+ private static final Logger logger = LoggerFactory
+ .getLogger(WechatErrorController.class);
+
+ private static WechatErrorController appErrorController;
+
+ /**
+ * Error Attributes in the Application
+ */
+ @Autowired
+ private ErrorAttributes errorAttributes;
+
+ private final static String ERROR_PATH = "/error";
+
+ /**
+ * Controller for the Error Controller
+ *
+ * @param errorAttributes
+ */
+
+ public WechatErrorController(ErrorAttributes errorAttributes) {
+ this.errorAttributes = errorAttributes;
+ }
+
+ public WechatErrorController() {
+ if (appErrorController == null) {
+ appErrorController = new WechatErrorController(this.errorAttributes);
+ }
+ }
+
+ /**
+ * Supports the HTML Error View
+ * @param request
+ */
+ @RequestMapping(value = ERROR_PATH, produces = "text/html")
+ public ModelAndView errorHtml(HttpServletRequest request) {
+ return new ModelAndView("error",
+ this.getErrorAttributes(request, false));
+ }
+
+ /**
+ * Supports other formats like JSON, XML
+ * @param request
+ */
+ @RequestMapping(value = ERROR_PATH)
+ @ResponseBody
+ public ResponseEntity