公众号基础的小程序搭建

最近因为小程序的火爆,再加上老板的要求。需要搭建并将部分公众号功能开发到小程序,所以自己着手了解并搭建了小程序。(其中跳过很多坑,看过很多博客。希望这个博客可以帮助到同样需求的童鞋把)

目录:

  1. 微信小程序和公众号的区别、关联
  2. 封装wx.request并且保持登陆的session
  3. 前端获取用户授权请求(wx.getUserInfo()无法弹窗后更新版)
  4. 后端处理小程序、公众号新增、关联,并使用后台自己的session保持登陆
  5. 小程序和公众号支付、退款

微信小程序和公众号的区别、关联

说起小程序和公众号,其实基本上差不多,都有微信需要的openid,和对应的处理的微信接口(如推送模板消息、支付等都需要对应的openid)最主要的还是unionId(这个需要在开放平台上关联公众号和小程序,才能将两个独立的openId识别出相同的用户,具体可以百度一下,这里不做太多赘述)

封装wx.request并且保持登陆的session,前提:用户已经授权

虽然小程序可以直接获取用户openid作为用户,之后获取用户授权关联unionId的时候再同步数据。为了不操作麻烦,我的设计是在授权之后才能使用对应的功能。
好处:
方便统一处理某些情况,如请求session过期时自动刷新,接口调用失败显示错误信息。简化写法等。这边根据我们的后台业务做了一定的封装,可以借鉴一下

/**
   * 封装wx.request
   * param  obj 正常ajax内对象url,data等
   * param acFail方法,当活动返回错误-1时做的操作 func,这边是后台特定业务的返回值,做特殊处理
   */
  wxRequest: function(obj, acFail) {
    const that = this;
    let method = "POST"
    //更换方式
    if (obj.method) {
      method = obj.method
    }
    let header
    //根据请求方式,切换content-type类型
    if (method.toUpperCase() == "GET") {
      header = {
        'content-type': 'application/json'
      }
    } else {
      header = {
        'content-type': 'application/x-www-form-urlencoded'
      }
    }
    //放入服务端session,这个在登陆成功的接口中放置,作用:保持服务端session
    const sessionId = wx.getStorageSync("sessionId")
    if (sessionId) {
      header.cookie = 'SESSION=' + sessionId
    }
    // 封装request
    wx.request({
      url: getApp().config.apiServer + obj.url,
      data: obj.data,
      method: method,
      header: header,
      success: function(res) {
        if (res.statusCode != 200) {//这边是做一些请求验证,200就是正常请求,可做扩展
          if (obj.fail && typeof obj.fail === "function") {
            obj.fail(res);
          } else {
            wx.showToast({
              title: '请求失败,错误' + res.statusCode,
              icon: "none"
            })
          }
          return
        }
        const data = res.data
        if (data.status == 2) {//后台登陆session过期或者未登陆特定返回值
          //这个方法用作用户登陆,并且保存对应的缓存(包括sessionId)
          //session过期超时,需要刷新session,并重新调用请求方法
          getApp().wxLogin(null, obj)
        } else if (data.status == 1) {//后台普通错误返回值
          wx.showToast({
            title: data.msg,
            icon: "none",
            duration: 2000
          })
          console.log("error_url:", obj.url)
          console.log("err_msg:", data.msg)
        } else if (data.status == -1) {
          //定义活动失败操作,活动这块可以无视掉
          if (acFail && typeof acFail === 'function') {
            acFail()
          }
        } else {
        	if (obj.success&& typeof obj.success === "function") {
          		obj.success(data);
          	}
        }
      },
      fail: function(res) {
        if (obj.fail && typeof obj.fail === "function") {
          obj.fail(res);
        }
      }
    })
  },

其中有对错误情况的统一处理,重要的还是对session过期的处理,在下面会讲到方法wxLogin:(这里比较懒,有一种方式直接后台解密通过wx.getUserInfo()获取到用户的encryptedData获取用户信息,但是业务没那么精确,只需要unionId和用户头像暱称等,所以这边直接将基础信息userInfo传到后端,有需要的童鞋可以自己研究一下,顺便分享给我 d=====( ̄▽ ̄*)b)

/**
   * 微信登录方法,获得code更新后台session
   * userInfo 用户信息对象
   * callbackObj 请求过期时需要重新执行的请求参数
   * action 登录成功之后要做的事情
   */
  wxLogin: function(userInfo, callbackObj, action) {
    const jsonStr = userInfo ? JSON.stringify(userInfo) : ""
    wx.login({
      success: res => {
        getApp().wxRequest({
          url: "tokenHandle/takeWxJsapiSignature",
          data: {
            code: res.code,
            loginType: 1,
            mustLogin: "1",
            jsonStr: jsonStr
          },
          success: function(res) {
            if (res.status == 0) {
            	//保持会话,后台需要传过来的(必须是验证登陆成功后才有)
              wx.setStorageSync("sessionId", res.data.sessionId)
              //如果有该对象,表示之前session过期,并且需要重新执行请求
              if (callbackObj) {
                getApp().wxRequest(callbackObj)
              }
              if (action && typeof action === 'function') {
                action()
              }
            }
            console.log(res)
          }
        })
      }
    })
  },

知识点:
1.wx.login(Object object)
调用接口获取登录凭证(code)进而换取用户登录态信息,包括用户的唯一标识(openid) 及本次登录的 会话密钥(session_key)等。
2.封装request中的回调函数的使用和其中:请求过期后重新登陆需要再次执行一次过期的request

前端获取用户授权请求

因为微信后台的升级,之后都不能使用wx.getUserInfo的方式直接调取获取用户信息的弹窗,需要我们手动写一个对应的button以点击按钮的形式来提示获取用户信息。

<button open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo" plain='true'>

bindGetUserInfo,用户点击成功/拒绝之后调用的方法:

/**
   * 用户触发登录操作
   */
  bindGetUserInfo: function(e) {
    if (e.detail.userInfo) {
      // 发送 res.code 到后台换取 openId, sessionKey, unionId
      const userInfo = e.detail.userInfo
      getApp().wxLogin(userInfo, null, function() {
        wx.showToast({
          title: '授权成功,即将返回',
          icon: "none",
          duration: 1000
        })
        setTimeout(function() {
          wx.navigateBack()
        }, 1000)
      })
    } else {
      console.log('执行到这里,说明拒绝了授权')
      wx.showToast({
        title: "为了您更好的体验,请先同意授权",
        icon: 'none',
        duration: 2000
      });
    }
  }

当然,需要做判断,不能每次让用户点击按钮。这边我的处理方式是:

  1. wx.getSetting获取配置是否用户已经授权
  2. 已授权则直接wx.getUserInfo获取用户信息,未授权则弹窗提示引导用户点击/直接弹出按钮提示用户点击

后端处理小程序、公众号使用项目自己的session+unionid登陆:

后台获取用户信息,关联用户的方法

/**
	 * 获取签名信息
	 * 
	 * @param code
	 *            微信参数
	 * @param pageUrl
	 *            重定向地址
	 * @param mustLogin
	 *            强制登陆
	 * @param loginType
	 *            登陆类型 0或者null公众号,1小程序
	 * @return
	 */
	@RequestMapping("/takeWxJsapiSignature")
	@ResponseBody
	public ResultTO takeWxJsapiSignature(String code, String pageUrl, String mustLogin, Integer loginType,
			String jsonStr) {
		WechatUserInfo userInfo = null;
		try {
			//获取AccessToken
			WxMpOAuth2AccessToken wmoat = (WxMpOAuth2AccessToken) getSessionAttribute(
					WeConstants.WEB_SESSION_ACCESS_TOKEN_KEY);
			if (wmoat == null && !StringUtils.isEmpty(code)) {
				if (loginType != null && loginType.intValue() == 1) {
					//如果是小程序,直接获取前端发过来的用户信息json串
					if (StringUtils.isNotBlank(jsonStr)) {
						ObjectMapper mapper = new ObjectMapper();
						userInfo = mapper.readValue(jsonStr, WechatUserInfo.class);
					}
					//小程序特有的获取openid,unionId方法
					wmoat = smallProgramService.jscode2session(code);
				} else {
					//公众号获取用户信息方法
					wmoat = wxMpService.oauth2getAccessToken(code);
				}
				setSessionAttribute(WeConstants.WEB_SESSION_ACCESS_TOKEN_KEY, wmoat);
			}
			BaseUser user = null;
			try {
				//最重要的,获取用户/关联用户的方法
				user = initUser(wmoat, code, loginType, userInfo);
			} catch (Exception e) {
				logger.info("init user fail ", e);
			}
			//公众号使用的,session过期必须重新获取用户信息
			if ("1".equals(mustLogin) && user == null) {
				String loginUrl = null;
				if (loginType != null && loginType.intValue() == 1) {
					loginUrl = smallProgramService.oauth2buildAuthorizationUrl(pageUrl,
							CommonConstants.OAUTH2_SCOPE_USER_INFO, null);
				} else {
					loginUrl = wxMpService.oauth2buildAuthorizationUrl(pageUrl, CommonConstants.OAUTH2_SCOPE_USER_INFO,
							null);
				}
				ResultTO res = new ResultTO();
				res.setStatus(3);// 未登录
				res.setData(loginUrl);
				return res;
			}
			if (wmoat == null) {
				throw new Exception("未鉴权");
			}
			if (pageUrl != null && pageUrl.indexOf("#") != -1) {
				pageUrl = pageUrl.substring(0, pageUrl.indexOf("#"));
			}
			WxJsapiSignature wxJsapiSignature = null;
			if (loginType != null && loginType.intValue() == 1) {
				String sessionId = request.getSession().getId();
				Map<String, Object> map = new HashMap<>();
				map.put("unionid", user.getWxUnionid());
				map.put("isMember", user.isMember());
				map.put("sessionId", sessionId);
				return new AccessSuccessResult(map);
			} else {
				wxJsapiSignature = wxMpService.createJsapiSignature(pageUrl);
				wxJsapiSignature.setUnionId(user.getWxUnionid());
				wxJsapiSignature.setIsMember(user.isMember());
				return new AccessSuccessResult(wxJsapiSignature);
			}
		} catch (Exception e) {
			e.printStackTrace();
			return new AccessErrorResult(e.getMessage());
		}

	}
//小程序特有的获取openid,unionId方法
public WxMpOAuth2AccessToken jscode2session(String code) throws WxErrorException {
		String url = "https://api.weixin.qq.com/sns/jscode2session?";
		url += "appid=" + wxMpConfigStorage.getAppId();
		url += "&secret=" + wxMpConfigStorage.getSecret();
		url += "&js_code=" + code;
		url += "&grant_type=authorization_code";
		CloseableHttpClient httpClient = getHttpclient();
		try {
			RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
			String responseText = executor.execute(httpClient, httpProxy, url, null);
			return WxMpOAuth2AccessToken.fromJson(responseText);
		} catch (ClientProtocolException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			if (httpClient != null) {
				try {
					httpClient.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}

公众号获取用户信息(这个做过公众号的都不用解释了):

public WxMpOAuth2AccessToken oauth2getAccessToken(String code) throws WxErrorException {
		String url = "https://api.weixin.qq.com/sns/oauth2/access_token?";
		url += "appid=" + wxMpConfigStorage.getAppId();
		url += "&secret=" + wxMpConfigStorage.getSecret();
		url += "&code=" + code;
		url += "&grant_type=authorization_code";
		CloseableHttpClient httpClient = getHttpclient();
		try {
			RequestExecutor<String, String> executor = new SimpleGetRequestExecutor();
			String responseText = executor.execute(httpClient, httpProxy, url, null);
			return WxMpOAuth2AccessToken.fromJson(responseText);
		} catch (ClientProtocolException e) {
			throw new RuntimeException(e);
		} catch (IOException e) {
			throw new RuntimeException(e);
		} finally {
			if (httpClient != null) {
				try {
					httpClient.close();
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
/**
	 * 初始化、关联用户的方法
	 * @param wmoat 微信返回构造的对象
	 * @param code 公众号通过跳转授权的code/小程序wx.login的code
	 * @param loginType 登陆类型 0/null公众号,1小程序
	 * @param userInfo 小程序获取过来的用户对象
	 * @return
	 * @throws Exception
	 */
private BaseUser initUser(WxMpOAuth2AccessToken wmoat, String code, Integer loginType, WechatUserInfo userInfo)
			throws Exception {
		//获取request中的用户信息
		BaseUser user = (BaseUser) getSessionAttribute(WeConstants.WEB_SESSION_LOGIN_USER);
		if (user == null) {
			// 测试公众号无法获取Unionid只能使用openid,测试用
			if (wmoat != null && StringUtils.isBlank(wmoat.getUnionid())) {
				wmoat.setUnionid(wmoat.getOpenId());
			}
			//查询数据库中的用户信息,判断是新增用户,还是应该关联用户
			user = this.baseUserService.findBaseUserWxUnionId(wmoat.getUnionid());
			//判断用户关联,如果某一个登陆方式不存在,并且现在已经获取信息了,就更新对应的数据并且跟新缓存
			if (user != null) {
				if (loginType != null && loginType.intValue() == 1) {
					if (StringUtils.isBlank(user.getOpenIdS())) {
						BaseUser modifyUser = new BaseUser();
						modifyUser.setId(user.getId());
						modifyUser.setOpenIdS(wmoat.getOpenId());
						baseUserService.saveBaseUser(modifyUser);
						user.setOpenIdS(wmoat.getOpenId());
						setSessionAttribute(WeConstants.WEB_SESSION_LOGIN_USER, user);
					}
				} else {
					if (StringUtils.isBlank(user.getOpenId())) {
						BaseUser modifyUser = new BaseUser();
						modifyUser.setId(user.getId());
						modifyUser.setOpenId(wmoat.getOpenId());
						baseUserService.saveBaseUser(modifyUser);
						user.setOpenId(wmoat.getOpenId());
						setSessionAttribute(WeConstants.WEB_SESSION_LOGIN_USER, user);
					}
				}
			}

		}
		if (user == null) {
			// 新添加用户
			BaseUser newUser = new BaseUser();
			if (loginType != null && loginType.intValue() == 1) {
				newUser.setWxUnionid(wmoat.getUnionid());
				newUser.setOpenIdS(wmoat.getOpenId());
				newUser.setNickName(userInfo.getNickName());
				newUser.setImage(userInfo.getAvatarUrl());
				newUser.setSex(userInfo.getGender().intValue() == 0 ? "女" : "男");
				newUser.setRegion(userInfo.getCountry() + " " + userInfo.getProvince() + " " + userInfo.getCity());
				newUser.setScore(50);
				newUser.setIsAdmin(false);
			} else {
			//公众号获取用户,这块代码需要详细的可以扣我
				WxMpUser wxMpUser = (WxMpUser) getSessionAttribute(WeConstants.WEB_SESSION_WXMP_USER_KEY);
				if (wxMpUser == null) {
					if (!StringUtils.isEmpty(code) && wmoat != null
							&& !CommonConstants.OAUTH2_SCOPE_USER_INFO.equalsIgnoreCase(wmoat.getScope())) {
						wmoat = wxMpService.oauth2getAccessToken(code);
						setSessionAttribute(WeConstants.WEB_SESSION_ACCESS_TOKEN_KEY, wmoat);
					}
					if (wmoat != null) {
						if (loginType != null && loginType.intValue() == 1) {
							wxMpUser = smallProgramService.oauth2getUserInfo(wmoat, null);
						} else {
							wxMpUser = wxMpService.oauth2getUserInfo(wmoat, null);
						}
						setSessionAttribute(WeConstants.WEB_SESSION_WXMP_USER_KEY, wxMpUser);
					}
				}
				if (wxMpUser == null) {
					throw new Exception("获取信息失败");
				}
				newUser.setWxUnionid(wmoat.getUnionid());
				newUser.setOpenId(wxMpUser.getOpenId());
				newUser.setNickName(wxMpUser.getNickname());
				newUser.setImage(wxMpUser.getHeadImgUrl());
				newUser.setSex(wxMpUser.getSex());
				newUser.setRegion(wxMpUser.getCountry() + " " + wxMpUser.getProvince() + " " + wxMpUser.getCity());
				newUser.setScore(50);
				newUser.setIsAdmin(false);
			}
			user = baseUserService.saveBaseUser(newUser);
		}
		setSessionAttribute(WeConstants.WEB_SESSION_LOGIN_USER, user);
		return user;
	}

这边主要讲一下initUser方法,这个方法首先获取session中存储的用户,当为空(就是第一次登陆,或者session过期),然后再查询数据库。如果数据库中的用户不为空,则校验关联。如果为空,则使用微信的接口跟loginType根据不同的方法获取对应的用户数据,做用户新增。

获取,删除会话属性

/**
		 * 
		 * 设置session属性
		 * @Method setSessionAttribute
		 * @param request
		 * @param key
		 * @param valueObj void
		 * @Author gonghb
		 * @Date 2018年9月25日下午2:26:37
		 */
	 protected void setSessionAttribute(String key, Object valueObj){
		 setSessionAttribute(request, key, valueObj);
	 }
	 protected void setSessionAttribute(HttpServletRequest request, String key, Object valueObj){
		 request.getSession().setAttribute(key,valueObj);
	}
	/**
	 * 
	 * 获取session属性
	 * @Method getSessionAttribute
	 * @param arg1
	 * @return Object
	 * @Author gonghb
	 * @Date 2018年9月25日下午2:22:57
	 */
    protected Object getSessionAttribute(String key){
		 return getSessionAttribute(request,key);
	 }
    
	 protected Object getSessionAttribute(HttpServletRequest request,String key){
		return request.getSession().getAttribute(key);
	}

小程序和公众号支付、退款

这边支付和退款,也就是对应的openid,appid,AppSecret的区别,这边默认后台都是使用的同一个,商户也都会使用同一个,一般也都这样操作。
对于支付和退款我先说一下思路把,我使用的是微信的支付sdk,所以我的做法是用两个不同的实现类实现WXPayConfig,其中的商户id,apikey,还有证书文件的读取都是一样的,在预支付订单下单的时候,先判断当前的登陆类型和接口也要加一个loginType来区分读取的是小程序的openid还是公众号的openid。
并且预支付id和商户内部id(我们自己生成的uuid)也都拆分开来,用于某些操作(如:公众号支付预支付订单之后不付款,转小程序付款之类的)另外,就是为了方便退款,也需要将成功时的类型记录下来,以便于后面退款使用正确的config实现类来退款


@Autowired
private MyConfig config;
@Autowired
private SmallProgramConfig spConfig;
/**
	 * 微信预订单生成
	 * 
	 * @param wxOrder
	 *            初始化基本数据
	 * @param wxNotifyUrl
	 *            如:/order/wxNotifyOrder.do 微信回调接口
	 * @return
	 * @throws Exception
	 */
	public String wechatPrepay(WxPayOrder wxOrder, String wxNotifyUrl, Integer loginType) throws Exception {
		String settleId = wxOrder.getOutTradeNo();
		String ipAddr = wxOrder.getClientIp();
		String openId = wxOrder.getOpenid();
		String totalFee = wxOrder.getTotalFee();
		String body = wxOrder.getBody();
		// 切换小程序环境
		WXPay pay = null;
		if (loginType != null && loginType.intValue() == 1) {
			pay = new WXPay(spConfig, autoReport, useSandbox);
		} else {
			pay = new WXPay(config, autoReport, useSandbox);
		}

		SortedMap<String, String> reqData = new TreeMap<String, String>();
		reqData.put("attach", loginType == null ? "0" : loginType.toString());
		reqData.put("body", body);
		reqData.put("openid", openId);
		reqData.put("out_trade_no", settleId);
		reqData.put("spbill_create_ip", ipAddr);
		reqData.put("total_fee", totalFee);
		reqData.put("trade_type", "JSAPI");
		reqData.put("notify_url", concatChatServerUrl() + wxNotifyUrl);
		log.info("预支付数据:" + reqData);
		Map<String, String> unifiedOrder = pay.unifiedOrder(reqData);
		String prepayId = unifiedOrder.get("prepay_id");
		if (StringUtils.isEmpty(prepayId)) {
			if (loginType != null && loginType.intValue() == 1) {
				throw new Exception("小程序生成预支付订单失败");
			} else {
				throw new Exception("微信生成预支付订单失败");
			}
		}
		return prepayId;
	}
/**
	 * 退款
	 * 
	 * @param settleId 支付id
	 * @param refundId 退款id
	 * @param totalMoney 订单总价
	 * @param refundMoney 退款价格
	 * @param refundDesc 退款备注
	 * @param wxRefundNotifyUrl 异步回调通知地址
	 * @return
	 * @throws Exception
	 */
	public Map<String, String> refund(String settleId, String refundId, double totalMoney, String wxRefundNotifyUrl,
			double refundMoney, String refundDesc, Integer loginType) throws Exception {
		String totalFee = String.valueOf(OrderUtils.moneyToFen(totalMoney));
		String refundFee = String.valueOf(OrderUtils.moneyToFen(refundMoney));
		WXPay pay = null;
		if (loginType != null && loginType.intValue() == 1) {
			pay = new WXPay(spConfig, wxRefundNotifyUrl, autoReport, useSandbox);
		} else {
			pay = new WXPay(config, wxRefundNotifyUrl, autoReport, useSandbox);
		}
		SortedMap<String, String> reqData = new TreeMap<String, String>();
		reqData.put("out_trade_no", settleId);
		reqData.put("out_refund_no", refundId);
		reqData.put("total_fee", totalFee);
		reqData.put("refund_fee", refundFee);
		if (StringUtils.isNotBlank(refundDesc)) {
			reqData.put("refund_desc", refundDesc);
		}
		return pay.refund(reqData);
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章