├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── README.md ├── commit.sh ├── pom.xml └── src ├── main ├── docker │ └── Dockerfile ├── java │ └── com │ │ └── github │ │ └── binarywang │ │ └── demo │ │ └── wx │ │ └── mp │ │ ├── WxMpDemoApplication.java │ │ ├── builder │ │ ├── AbstractBuilder.java │ │ ├── ImageBuilder.java │ │ └── TextBuilder.java │ │ ├── config │ │ ├── WxMpConfiguration.java │ │ └── WxMpProperties.java │ │ ├── controller │ │ ├── WxJsapiController.java │ │ ├── WxMenuController.java │ │ ├── WxPortalController.java │ │ └── WxRedirectController.java │ │ ├── error │ │ ├── ErrorController.java │ │ └── ErrorPageConfiguration.java │ │ ├── handler │ │ ├── AbstractHandler.java │ │ ├── KfSessionHandler.java │ │ ├── LocationHandler.java │ │ ├── LogHandler.java │ │ ├── MenuHandler.java │ │ ├── MsgHandler.java │ │ ├── NullHandler.java │ │ ├── ScanHandler.java │ │ ├── StoreCheckNotifyHandler.java │ │ ├── SubscribeHandler.java │ │ └── UnsubscribeHandler.java │ │ └── utils │ │ └── JsonUtils.java └── resources │ ├── META-INF │ └── additional-spring-configuration-metadata.json │ ├── application.yml.template │ └── templates │ ├── error.html │ └── greet_user.html └── test └── java └── com └── github └── binarywang └── demo └── wx └── mp └── controller ├── BaseControllerTest.java ├── WxJsapiControllerTest.java └── WxMenuControllerTest.java /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.yml] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ binarywang ] 4 | custom: https://github.com/Wechat-Group/WxJava/blob/master/images/qrcodes/wepay.jpg?raw=true 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /.settings/ 3 | *.project 4 | *.classpath 5 | application.yml 6 | /.idea/ 7 | *.iml 8 | /ngrok/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | 5 | script: "mvn clean package -Dmaven.test.skip=true" 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | notifications: 12 | email: 13 | - binarywang@vip.qq.com 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [](https://gitee.com/binary/weixin-java-mp-demo-springboot) 2 | [](https://github.com/binarywang/weixin-java-mp-demo-springboot) 3 | [](https://travis-ci.org/binarywang/weixin-java-mp-demo-springboot) 4 | ----------------------- 5 | 6 | ### 本Demo基于Spring Boot构建,实现微信公众号后端开发功能。 7 | ### 本项目为WxJava的Demo演示程序,更多Demo请[查阅此处](https://github.com/Wechat-Group/WxJava/blob/master/demo.md)。 8 | #### 如有问题请[【在此提问】](https://github.com/binarywang/weixin-java-mp-demo-springboot/issues),谢谢配合。 9 | 10 | 11 |
15 |
16 | ![]() |
19 |
20 |
21 | ![]() |
24 |
32 | * 自定义菜单创建接口 33 | * 详情请见:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013&token=&lang=zh_CN 34 | * 如果要创建个性化菜单,请设置matchrule属性 35 | * 详情请见:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1455782296&token=&lang=zh_CN 36 | *37 | * 38 | * @return 如果是个性化菜单,则返回menuid,否则返回null 39 | */ 40 | @PostMapping("/create") 41 | public String menuCreate(@PathVariable String appid, @RequestBody WxMenu menu) throws WxErrorException { 42 | return this.wxService.switchoverTo(appid).getMenuService().menuCreate(menu); 43 | } 44 | 45 | @GetMapping("/create") 46 | public String menuCreateSample(@PathVariable String appid) throws WxErrorException, MalformedURLException { 47 | WxMenu menu = new WxMenu(); 48 | WxMenuButton button1 = new WxMenuButton(); 49 | button1.setType(MenuButtonType.CLICK); 50 | button1.setName("今日歌曲"); 51 | button1.setKey("V1001_TODAY_MUSIC"); 52 | 53 | // WxMenuButton button2 = new WxMenuButton(); 54 | // button2.setType(WxConsts.BUTTON_MINIPROGRAM); 55 | // button2.setName("小程序"); 56 | // button2.setAppId("wx286b93c14bbf93aa"); 57 | // button2.setPagePath("pages/lunar/index.html"); 58 | // button2.setUrl("http://mp.weixin.qq.com"); 59 | 60 | WxMenuButton button3 = new WxMenuButton(); 61 | button3.setName("菜单"); 62 | 63 | menu.getButtons().add(button1); 64 | // menu.getButtons().add(button2); 65 | menu.getButtons().add(button3); 66 | 67 | WxMenuButton button31 = new WxMenuButton(); 68 | button31.setType(MenuButtonType.VIEW); 69 | button31.setName("搜索"); 70 | button31.setUrl("http://www.soso.com/"); 71 | 72 | WxMenuButton button32 = new WxMenuButton(); 73 | button32.setType(MenuButtonType.VIEW); 74 | button32.setName("视频"); 75 | button32.setUrl("http://v.qq.com/"); 76 | 77 | WxMenuButton button33 = new WxMenuButton(); 78 | button33.setType(MenuButtonType.CLICK); 79 | button33.setName("赞一下我们"); 80 | button33.setKey("V1001_GOOD"); 81 | 82 | WxMenuButton button34 = new WxMenuButton(); 83 | button34.setType(MenuButtonType.VIEW); 84 | button34.setName("获取用户信息"); 85 | 86 | ServletRequestAttributes servletRequestAttributes = 87 | (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); 88 | if (servletRequestAttributes != null) { 89 | HttpServletRequest request = servletRequestAttributes.getRequest(); 90 | URL requestURL = new URL(request.getRequestURL().toString()); 91 | String url = this.wxService.switchoverTo(appid).getOAuth2Service().buildAuthorizationUrl( 92 | String.format("%s://%s/wx/redirect/%s/greet", requestURL.getProtocol(), requestURL.getHost(), appid), 93 | WxConsts.OAuth2Scope.SNSAPI_USERINFO, null); 94 | button34.setUrl(url); 95 | } 96 | 97 | button3.getSubButtons().add(button31); 98 | button3.getSubButtons().add(button32); 99 | button3.getSubButtons().add(button33); 100 | button3.getSubButtons().add(button34); 101 | 102 | this.wxService.switchover(appid); 103 | return this.wxService.getMenuService().menuCreate(menu); 104 | } 105 | 106 | /** 107 | *
108 | * 自定义菜单创建接口 109 | * 详情请见: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013&token=&lang=zh_CN 110 | * 如果要创建个性化菜单,请设置matchrule属性 111 | * 详情请见:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1455782296&token=&lang=zh_CN 112 | *113 | * 114 | * @return 如果是个性化菜单,则返回menuid,否则返回null 115 | */ 116 | @PostMapping("/createByJson") 117 | public String menuCreate(@PathVariable String appid, @RequestBody String json) throws WxErrorException { 118 | return this.wxService.switchoverTo(appid).getMenuService().menuCreate(json); 119 | } 120 | 121 | /** 122 | *
123 | * 自定义菜单删除接口 124 | * 详情请见: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141015&token=&lang=zh_CN 125 | *126 | */ 127 | @GetMapping("/delete") 128 | public void menuDelete(@PathVariable String appid) throws WxErrorException { 129 | this.wxService.switchoverTo(appid).getMenuService().menuDelete(); 130 | } 131 | 132 | /** 133 | *
134 | * 删除个性化菜单接口 135 | * 详情请见: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1455782296&token=&lang=zh_CN 136 | *137 | * 138 | * @param menuId 个性化菜单的menuid 139 | */ 140 | @GetMapping("/delete/{menuId}") 141 | public void menuDelete(@PathVariable String appid, @PathVariable String menuId) throws WxErrorException { 142 | this.wxService.switchoverTo(appid).getMenuService().menuDelete(menuId); 143 | } 144 | 145 | /** 146 | *
147 | * 自定义菜单查询接口 148 | * 详情请见: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141014&token=&lang=zh_CN 149 | *150 | */ 151 | @GetMapping("/get") 152 | public WxMpMenu menuGet(@PathVariable String appid) throws WxErrorException { 153 | return this.wxService.switchoverTo(appid).getMenuService().menuGet(); 154 | } 155 | 156 | /** 157 | *
158 | * 测试个性化菜单匹配结果 159 | * 详情请见: http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html 160 | *161 | * 162 | * @param userid 可以是粉丝的OpenID,也可以是粉丝的微信号。 163 | */ 164 | @GetMapping("/menuTryMatch/{userid}") 165 | public WxMenu menuTryMatch(@PathVariable String appid, @PathVariable String userid) throws WxErrorException { 166 | return this.wxService.switchoverTo(appid).getMenuService().menuTryMatch(userid); 167 | } 168 | 169 | /** 170 | *
171 | * 获取自定义菜单配置接口 172 | * 本接口将会提供公众号当前使用的自定义菜单的配置,如果公众号是通过API调用设置的菜单,则返回菜单的开发配置,而如果公众号是在公众平台官网通过网站功能发布菜单,则本接口返回运营者设置的菜单配置。 173 | * 请注意: 174 | * 1、第三方平台开发者可以通过本接口,在旗下公众号将业务授权给你后,立即通过本接口检测公众号的自定义菜单配置,并通过接口再次给公众号设置好自动回复规则,以提升公众号运营者的业务体验。 175 | * 2、本接口与自定义菜单查询接口的不同之处在于,本接口无论公众号的接口是如何设置的,都能查询到接口,而自定义菜单查询接口则仅能查询到使用API设置的菜单配置。 176 | * 3、认证/未认证的服务号/订阅号,以及接口测试号,均拥有该接口权限。 177 | * 4、从第三方平台的公众号登录授权机制上来说,该接口从属于消息与菜单权限集。 178 | * 5、本接口中返回的图片/语音/视频为临时素材(临时素材每次获取都不同,3天内有效,通过素材管理-获取临时素材接口来获取这些素材),本接口返回的图文消息为永久素材素材(通过素材管理-获取永久素材接口来获取这些素材)。 179 | * 接口调用请求说明: 180 | * http请求方式: GET(请使用https协议) 181 | * https://api.weixin.qq.com/cgi-bin/get_current_selfmenu_info?access_token=ACCESS_TOKEN 182 | *183 | */ 184 | @GetMapping("/getSelfMenuInfo") 185 | public WxMpGetSelfMenuInfoResult getSelfMenuInfo(@PathVariable String appid) throws WxErrorException { 186 | return this.wxService.switchoverTo(appid).getMenuService().getSelfMenuInfo(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/com/github/binarywang/demo/wx/mp/controller/WxPortalController.java: -------------------------------------------------------------------------------- 1 | package com.github.binarywang.demo.wx.mp.controller; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.extern.log4j.Log4j; 5 | import lombok.extern.slf4j.Slf4j; 6 | import me.chanjar.weixin.mp.api.WxMpMessageRouter; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestBody; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RequestParam; 17 | import org.springframework.web.bind.annotation.RestController; 18 | 19 | import me.chanjar.weixin.mp.api.WxMpService; 20 | import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; 21 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; 22 | 23 | /** 24 | * @author Binary Wang 25 | */ 26 | @Slf4j 27 | @AllArgsConstructor 28 | @RestController 29 | @RequestMapping("/wx/portal/{appid}") 30 | public class WxPortalController { 31 | private final WxMpService wxService; 32 | private final WxMpMessageRouter messageRouter; 33 | 34 | @GetMapping(produces = "text/plain;charset=utf-8") 35 | public String authGet(@PathVariable String appid, 36 | @RequestParam(name = "signature", required = false) String signature, 37 | @RequestParam(name = "timestamp", required = false) String timestamp, 38 | @RequestParam(name = "nonce", required = false) String nonce, 39 | @RequestParam(name = "echostr", required = false) String echostr) { 40 | 41 | log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature, 42 | timestamp, nonce, echostr); 43 | if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) { 44 | throw new IllegalArgumentException("请求参数非法,请核实!"); 45 | } 46 | 47 | if (!this.wxService.switchover(appid)) { 48 | throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid)); 49 | } 50 | 51 | if (wxService.checkSignature(timestamp, nonce, signature)) { 52 | return echostr; 53 | } 54 | 55 | return "非法请求"; 56 | } 57 | 58 | @PostMapping(produces = "application/xml; charset=UTF-8") 59 | public String post(@PathVariable String appid, 60 | @RequestBody String requestBody, 61 | @RequestParam("signature") String signature, 62 | @RequestParam("timestamp") String timestamp, 63 | @RequestParam("nonce") String nonce, 64 | @RequestParam("openid") String openid, 65 | @RequestParam(name = "encrypt_type", required = false) String encType, 66 | @RequestParam(name = "msg_signature", required = false) String msgSignature) { 67 | log.info("\n接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}]," 68 | + " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ", 69 | openid, signature, encType, msgSignature, timestamp, nonce, requestBody); 70 | 71 | if (!this.wxService.switchover(appid)) { 72 | throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid)); 73 | } 74 | 75 | if (!wxService.checkSignature(timestamp, nonce, signature)) { 76 | throw new IllegalArgumentException("非法请求,可能属于伪造的请求!"); 77 | } 78 | 79 | String out = null; 80 | if (encType == null) { 81 | // 明文传输的消息 82 | WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody); 83 | WxMpXmlOutMessage outMessage = this.route(inMessage); 84 | if (outMessage == null) { 85 | return ""; 86 | } 87 | 88 | out = outMessage.toXml(); 89 | } else if ("aes".equalsIgnoreCase(encType)) { 90 | // aes加密的消息 91 | WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxService.getWxMpConfigStorage(), 92 | timestamp, nonce, msgSignature); 93 | log.debug("\n消息解密后内容为:\n{} ", inMessage.toString()); 94 | WxMpXmlOutMessage outMessage = this.route(inMessage); 95 | if (outMessage == null) { 96 | return ""; 97 | } 98 | 99 | out = outMessage.toEncryptedXml(wxService.getWxMpConfigStorage()); 100 | } 101 | 102 | log.debug("\n组装回复信息:{}", out); 103 | return out; 104 | } 105 | 106 | private WxMpXmlOutMessage route(WxMpXmlMessage message) { 107 | try { 108 | return this.messageRouter.route(message); 109 | } catch (Exception e) { 110 | log.error("路由消息时出现异常!", e); 111 | } 112 | 113 | return null; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/github/binarywang/demo/wx/mp/controller/WxRedirectController.java: -------------------------------------------------------------------------------- 1 | package com.github.binarywang.demo.wx.mp.controller; 2 | 3 | import lombok.AllArgsConstructor; 4 | import me.chanjar.weixin.common.bean.WxOAuth2UserInfo; 5 | import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken; 6 | import me.chanjar.weixin.common.error.WxErrorException; 7 | import me.chanjar.weixin.mp.api.WxMpService; 8 | import me.chanjar.weixin.mp.bean.result.WxMpUser; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.ModelMap; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | 15 | /** 16 | * @author Edward 17 | */ 18 | @AllArgsConstructor 19 | @Controller 20 | @RequestMapping("/wx/redirect/{appid}") 21 | public class WxRedirectController { 22 | private final WxMpService wxService; 23 | 24 | @RequestMapping("/greet") 25 | public String greetUser(@PathVariable String appid, @RequestParam String code, ModelMap map) { 26 | if (!this.wxService.switchover(appid)) { 27 | throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid)); 28 | } 29 | 30 | try { 31 | WxOAuth2AccessToken accessToken = wxService.getOAuth2Service().getAccessToken(code); 32 | WxOAuth2UserInfo user = wxService.getOAuth2Service().getUserInfo(accessToken, null); 33 | map.put("user", user); 34 | } catch (WxErrorException e) { 35 | e.printStackTrace(); 36 | } 37 | 38 | return "greet_user"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/binarywang/demo/wx/mp/error/ErrorController.java: -------------------------------------------------------------------------------- 1 | package com.github.binarywang.demo.wx.mp.error; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | 7 | /** 8 | *
9 | * 出错页面控制器 10 | * Created by Binary Wang on 2018/8/25. 11 | *12 | * 13 | * @author Binary Wang 14 | */ 15 | @Controller 16 | @RequestMapping("/error") 17 | public class ErrorController { 18 | 19 | @GetMapping(value = "/404") 20 | public String error404() { 21 | return "error"; 22 | } 23 | 24 | @GetMapping(value = "/500") 25 | public String error500() { 26 | return "error"; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/binarywang/demo/wx/mp/error/ErrorPageConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.binarywang.demo.wx.mp.error; 2 | 3 | import org.springframework.boot.web.server.ErrorPage; 4 | import org.springframework.boot.web.server.ErrorPageRegistrar; 5 | import org.springframework.boot.web.server.ErrorPageRegistry; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | *
11 | * 配置错误状态与对应访问路径 12 | * Created by Binary Wang on 2018/8/25. 13 | *14 | * 15 | * @author Binary Wang 16 | */ 17 | @Component 18 | public class ErrorPageConfiguration implements ErrorPageRegistrar { 19 | @Override 20 | public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { 21 | errorPageRegistry.addErrorPages( 22 | new ErrorPage(HttpStatus.NOT_FOUND, "/error/404"), 23 | new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500") 24 | ); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/binarywang/demo/wx/mp/handler/AbstractHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.binarywang.demo.wx.mp.handler; 2 | 3 | import me.chanjar.weixin.mp.api.WxMpMessageHandler; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | /** 8 | * @author Binary Wang 9 | */ 10 | public abstract class AbstractHandler implements WxMpMessageHandler { 11 | protected Logger logger = LoggerFactory.getLogger(getClass()); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/binarywang/demo/wx/mp/handler/KfSessionHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.binarywang.demo.wx.mp.handler; 2 | 3 | import me.chanjar.weixin.common.session.WxSessionManager; 4 | import me.chanjar.weixin.mp.api.WxMpService; 5 | import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; 6 | import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * @author Binary Wang 13 | */ 14 | @Component 15 | public class KfSessionHandler extends AbstractHandler { 16 | 17 | @Override 18 | public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, 19 | Map
性别:
23 |城市:
24 |