最近因爲小程序的火爆,再加上老闆的要求。需要搭建並將部分公衆號功能開發到小程序,所以自己着手瞭解並搭建了小程序。(其中跳過很多坑,看過很多博客。希望這個博客可以幫助到同樣需求的童鞋把)
目錄:
- 微信小程序和公衆號的區別、關聯
- 封裝wx.request並且保持登陸的session
- 前端獲取用戶授權請求(wx.getUserInfo()無法彈窗後更新版)
- 後端處理小程序、公衆號新增、關聯,並使用後臺自己的session保持登陸
- 小程序和公衆號支付、退款
微信小程序和公衆號的區別、關聯
說起小程序和公衆號,其實基本上差不多,都有微信需要的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
});
}
}
當然,需要做判斷,不能每次讓用戶點擊按鈕。這邊我的處理方式是:
- wx.getSetting獲取配置是否用戶已經授權
- 已授權則直接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);
}