一、前言
方便要开发微信小程序的朋友们,可以快速将服务搭建起来,不要把时间浪费在服务的搭建上,专心写我们的业务代码。
你需要了解的知识:
1.微信小程序大概的开发流程
2.注册小程序(个人测试账号)
3.服务器的配置
4.内网穿透(我用的是花生壳)
5.小程序开发文档先大概看一遍
废话不多说了,你懂得,直接讲重点…
源码下载地址(服务端):下载
源码下载地址(小程序demo):下载
二、脚手架预览
2.1 项目结构
2.2 小程序demo
整体小程序上就实现上面这些功能。
三、怎么快速把项目跑起来(服务端)
3.1 修改配置 application.xml
logging:
level:
org.springframework.web: info
com.lxh.miniapp: debug
cn.binarywang.wx.miniapp: debug
server:
port: 80
servlet:
context-path: /
wx:
miniapp:
configs:
- appid: wx2d88824e64axxxxxx
secret: f1e1d8785bfbe9d36d538xxxxxxxxx
token: miniapp
aesKey: K1ewVmypZKPTl2BIB8ySTY9C5rpteZxxxxxxxxx
msgDataFormat: JSON
3.2 启动服务
WxMiniAppDemoApplication.java
四、核心代码
本项目依赖WxJava - 微信开发 Java SDK。https://github.com/Wechat-Group/WxJava
由于本项目支持多个小程序,在调用接口时,要先调用下面接口切换一下小程序。
public static WxMaService switchover(String appid){
if (StringUtils.isBlank(appid)){
throw new IllegalArgumentException("appId为空");
}
WxMaService wxMaService = WxMiniAppConfiguration.getMaService(appid);
if (wxMaService == null) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}
return wxMaService;
}
4.1 WxMiniAppConfiguration.java
配置消息处理,将WxJava SDK进行初始化
package com.lxh.miniapp.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.bean.*;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import cn.binarywang.wx.miniapp.message.WxMaMessageHandler;
import cn.binarywang.wx.miniapp.message.WxMaMessageRouter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.Data;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
import me.chanjar.weixin.common.error.WxErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* created by lanxinghua@2dfire.com on 2020/2/23
*/
@Configuration
@Data
@EnableConfigurationProperties(WxMiniAppProperties.class)
public class WxMiniAppConfiguration {
private static final Logger logger = LoggerFactory.getLogger(WxMiniAppConfiguration.class);
private WxMiniAppProperties miniAppProperties;
private static Map<String/*appid*/, WxMaMessageRouter> routers = Maps.newHashMap();
private static Map<String/*appid*/, WxMaService> maServices = Maps.newHashMap();
public WxMiniAppConfiguration(WxMiniAppProperties miniAppProperties) {
this.miniAppProperties = miniAppProperties;
}
public static WxMaService getMaService(String appid) {
WxMaService wxService = maServices.get(appid);
if (wxService == null) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}
return wxService;
}
public static WxMaMessageRouter getRouter(String appid) {
return routers.get(appid);
}
/**
* 优先进行初始化
*/
@PostConstruct
public void init(){
// 1.获取到所有小程序的配置
List<WxMiniAppProperties.Config> configs = miniAppProperties.getConfigs();
if (configs == null) {
throw new RuntimeException("请添加下相关配置,注意别配错了!");
}
// 2.初始化maService和消息路由
maServices = configs.stream().map(e -> {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
config.setAppid(e.getAppid());
config.setSecret(e.getSecret());
config.setToken(e.getToken());
config.setAesKey(e.getAesKey());
config.setMsgDataFormat(e.getMsgDataFormat());
WxMaService maService = new WxMaServiceImpl();
maService.setWxMaConfig(config);
routers.put(e.getAppid(), this.newRouter(maService));
return maService;
}).collect(Collectors.toMap(a-> a.getWxMaConfig().getAppid(), a-> a));
}
/**
* 新建路由
* @param maService
* @return
*/
private WxMaMessageRouter newRouter(WxMaService maService){
final WxMaMessageRouter router = new WxMaMessageRouter(maService);
// 配置路由规则
router.rule().handler(LogHandler).next()
.rule().async(false).msgType(WxConsts.KefuMsgType.TEXT).handler(textHandler).end();
return router;
}
private final WxMaMessageHandler LogHandler = (message, context, service, sessionManager) -> {
logger.info("【消息日志】:" + message.toString());
return null;
};
private final WxMaMessageHandler textHandler = (message, context, service, sessionManager) -> {
logger.info("textHandler:" + message.toString());
String msg = message.getContent();
switch (msg){
case "图片":{
sendPic(service, message);
break;
}
case "模板":{
sendTemplate(service, message);
break;
}
case "订阅":{
sendSubscribeMsg(service, message);
break;
}
case "二维码":{
sendQrCode(service, message);
break;
}
default:{
sendText(service, message);
break;
}
}
return null;
};
private void sendText(WxMaService service, WxMaMessage message){
try {
service.getMsgService().sendKefuMsg(WxMaKefuMessage.newTextBuilder().content("回复文本消息")
.toUser(message.getFromUser()).build());
}catch (Exception e){
logger.error(e.getMessage());
}
}
private void sendPic(WxMaService service, WxMaMessage message){
try {
WxMediaUploadResult uploadResult = service.getMediaService()
.uploadMedia("image", "jpg", ClassLoader.getSystemResourceAsStream("lxh.jpg"));
service.getMsgService().sendKefuMsg(
WxMaKefuMessage
.newImageBuilder()
.mediaId(uploadResult.getMediaId())
.toUser(message.getFromUser())
.build());
} catch (WxErrorException e) {
logger.error(e.getMessage());
}
}
// 由于“模板消息”将下线,已不再支持添加模板,请尽快接入“订阅消息”。
private void sendTemplate(WxMaService service, WxMaMessage message){
try {
service.getMsgService().sendTemplateMsg(WxMaTemplateMessage.builder()
.templateId("2KwZNrdSs78NToB2CBzNKMfwoDN4fAggC7jJAweA4hM")
.formId("自己替换可用的formid")
.data(Lists.newArrayList(
new WxMaTemplateData("keyword1", "模板替换值。。。", "#173177")))
.toUser(message.getFromUser())
.build());
}catch (Exception e){
logger.error(e.getMessage());
}
}
private void sendSubscribeMsg(WxMaService service, WxMaMessage message){
try {
service.getMsgService().sendSubscribeMsg(WxMaSubscribeMessage.builder()
.templateId("2KwZNrdSs78NToB2CBzNKMfwoDN4fAggC7jJAweA4hM")
.data(Lists.newArrayList(
new WxMaSubscribeData("thing1", "一起学习呀", "#173177"),
new WxMaSubscribeData("thing2", "今晚一起去图书馆学习啊", "#173177")
))
.toUser(message.getFromUser())
.build());
}catch (Exception e){
logger.error(e.getMessage());
}
}
private void sendQrCode(WxMaService service, WxMaMessage message){
try {
final File file = service.getQrcodeService().createQrcode("qrCode", 430);
WxMediaUploadResult uploadResult = service.getMediaService().uploadMedia("image", file);
service.getMsgService().sendKefuMsg(
WxMaKefuMessage
.newImageBuilder()
.mediaId(uploadResult.getMediaId())
.toUser(message.getFromUser())
.build());
} catch (WxErrorException e) {
logger.error(e.getMessage());
}
}
}
4.2 WxPortalController.java
package com.lxh.miniapp.controller;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.bean.WxMaMessage;
import cn.binarywang.wx.miniapp.constant.WxMaConstants;
import cn.binarywang.wx.miniapp.message.WxMaMessageRouter;
import com.lxh.miniapp.utils.MiniAppUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
/**
* created by lanxinghua@2dfire.com on 2020/2/21
* 门户网站
*/
@Slf4j
@RestController
@RequestMapping("/wx/portal/{appid}")
public class WxPortalController {
private static final Logger logger = LoggerFactory.getLogger(WxPortalController.class);
private WxMaService wxMaService;
@GetMapping(produces = "text/plain;charset=utf-8")
public String authGet(@PathVariable String appid,
@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) {
log.info("\n接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature, timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
// 切换小程序
wxMaService = MiniAppUtils.switchover(appid);
if (wxMaService.checkSignature(timestamp, nonce, signature)) {
return echostr;
}
return "非法请求";
}
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(@PathVariable String appid,
@RequestBody String requestBody,
@RequestParam(name = "msg_signature", required = false) String msgSignature,
@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "encrypt_type", required = false) String encryptType,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce) {
logger.info("\n接收微信请求:[msg_signature=[{}], encrypt_type=[{}], signature=[{}]," + " timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
msgSignature, encryptType, signature, timestamp, nonce, requestBody);
wxMaService = MiniAppUtils.switchover(appid);
final boolean isJson = Objects.equals(wxMaService.getWxMaConfig().getMsgDataFormat(), WxMaConstants.MsgDataFormat.JSON);
// 明文传输的消息
if (StringUtils.isBlank(encryptType)) {
WxMaMessage inMessage;
if (isJson) {
inMessage = WxMaMessage.fromJson(requestBody);
} else {//xml
inMessage = WxMaMessage.fromXml(requestBody);
}
this.route(inMessage, appid);
return "success";
}
// aes加密的消息
if ("aes".equals(encryptType)) {
WxMaMessage inMessage;
if (isJson) {
inMessage = WxMaMessage.fromEncryptedJson(requestBody, wxMaService.getWxMaConfig());
} else {//xml
inMessage = WxMaMessage.fromEncryptedXml(requestBody, wxMaService.getWxMaConfig(), timestamp, nonce, msgSignature);
}
this.route(inMessage, appid);
return "success";
}
throw new RuntimeException("不可识别的加密类型:" + encryptType);
}
private void route(WxMaMessage message, String appid) {
try {
WxMaMessageRouter router = MiniAppUtils.getRouter(appid);
router.route(message);
} catch (Exception e) {
this.logger.error(e.getMessage(), e);
}
}
}
五、小程序核心代码
5.1 通过code到后台换取 openId, sessionKey, unionId
// 登录
wx.login({
success: res => {
if(this.globalData.openId != null){
console.log("openId:" + this.globalData.openId)
return;
}
// 发送 res.code 到后台换取 openId, sessionKey, unionId
console.log("登录:"+res.code)
var oauthUrl = this.globalData.baseUrl + "wx/user/" + this.globalData.appId + "/login";
if (res.code) {
wx.request({
url: oauthUrl,
data: {
code: res.code
},
success: function (res) {
console.log(res)
const self = this
//获取到用户凭证 存儲 3rd_session
var json = JSON.parse(res.data.Data)
wx.setStorage({
key: "third_Session",
data: json.third_Session
})
},
fail: function (res) {
}
})
}
},
fail: function (res) {
}
})
},
5.2 全局变量设置app.js
globalData: {
userInfo: null,
openId: 'o0aPU5JJUSr1DoNtkJXqSFa1CqoU',
unionId: null,
baseUrl: 'http://chenxingxing.51vip.biz/',
appId: 'wx2d88824e64aacf4d'
}
5.3 获取用户信息
onLoad() {
var that = this;
wx.login({
success: function (e) {
// 获取用户信息
var code = e.code;
wx.getSetting({
success: res => {
console.log(res.authSetting);
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
wx.getUserInfo({
success: res => {
console.log(res)
// 可以将 res 发送给后台解码出 unionId
that.setData({ userInfo: res.userInfo })
that.setData({ isAuth: true })
var userObj = {};
userObj.encryptedData = res.encryptedData;
userObj.iv = res.iv;
userObj.expires_in = Date.now() + 24 * 7 * 60 * 60;
wx.setStorageSync('userInfo', res.userInfo);
wx.setStorageSync('userObj', userObj);
app.globalData.userInfo = res.userInfo;
that.thirdLogin(code,userObj);
// 所以此处加入 callback 以防止这种情况
if (that.userInfoReadyCallback) {
that.userInfoReadyCallback(res)
}
}
})
}else{
that.setData({ isAuth: false })
}
}
})
}
});
},
5.4 web-view
建立一个专门跳网页page,通过动态传递url,进行跳转
jump: function(){
var type = "baidu";
wx.navigateTo({
url: "/pages/webview/book?type="+ type,
})
console.log(this.data.userInfo)
}
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
if (options.type = "baidu") {
this.setData({
url: "www.baidu.com"
})
}
}
<web-view src="https://{{url}}"></web-view>
5.5 用户订阅
subscribeMessage: function(e) {
console.log(e)
wx.requestSubscribeMessage({
tmplIds: ['2KwZNrdSs78NToB2CBzNKMfwoDN4fAggC7jJAweA4hM', 'fl9rdF50TxnOujwzCbl6KytQJrj3dKCsDTEz2O8gpGQ'],
success(res) {
console.log(res)
}
})
}
5.6 用户授权后刷新页面
<button open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo">授权登录</button>
bindGetUserInfo: function (e) {
console.log(e.detail.userInfo)
if (e.detail.userInfo) {
//用户按了允许授权按钮
this.onLoad();
} else {
//用户按了拒绝按钮
}
}
5.7 拨打电话
call:function(){
wx.makePhoneCall({
phoneNumber: phone//仅为示例,并非真实的电话号码
})
}
5.8 分享小程序
onShareAppMessage: function (res) {
if (res.from === 'button') {
console.log(res.target)
}
return {
title: '小程序标题',
path: 'pages/index/index'
}
}
5.9 预览图片
不支持本地图片
previewImage: function (e) {
wx.previewImage({
current: this.data.url, // 当前显示图片的http链接
urls: [this.data.url] // 需要预览的图片http链接列表
})
}
5.10 地图
<map id="map" longitude="112.984954" latitude="28.174734" scale="14" controls="{{controls}}" bindcontroltap="controltap" markers="{{markers}}" bindmarkertap="markertap" bindregionchange="regionchange" show-location style="width: 100%; height: 300px;"></map>
Page({
data: {
title: '',
markers: [{
iconPath: "../../images/location.png",
id: 0,
latitude: 28.18116,
longitude: 112.991562,
width: 50,
height: 50,
}],
},
regionchange(e) {
console.log(e.type)
},
markertap(e) {
console.log(e.markerId)
},
controltap(e) {
console.log(e.controlId)
}
到这里就差不多了,恭喜你,兄弟…