最近因为小程序的火爆,再加上老板的要求。需要搭建并将部分公众号功能开发到小程序,所以自己着手了解并搭建了小程序。(其中跳过很多坑,看过很多博客。希望这个博客可以帮助到同样需求的童鞋把),博主做了一次更新2019.2.18日(解决关于小程序获取不到unionid的问题)
微信小程序和公众号的区别、关联
说起小程序和公众号,其实基本上差不多,都有微信需要的openid,和对应的处理的微信接口(如推送模板消息、支付等都需要对应的openid)最主要的还是unionId(这个需要在开放平台上关联公众号和小程序,才能将两个独立的openId识别出相同的用户,具体可以百度一下,这里不做太多赘述)
微信小程序最重要的一点(!!!)
因为这边博主自己遇到,巨坑的一点。小程序通过公众平台关联公众号的时候。必须使用encryptedData获取,否则只通过code再在后端通过接口直接获取用户信息时,未关注、登录过公众号的小程序获取不到unionid。
获取unionid必须要通过解密encryptedData方式获取到加密的用户数据。
简单说流程:客户端获取使用wx.login+wx.getUserInfo[参数withCredentials: true]再将code(wx.login的返回值)、encryptedData、iv发送到后端;后端操作:使用code获取临时的session_key,再用session_key,encryptedData、iv。解密获取到unionid(这个一定不会空!!!)
封装wx.request并且保持登陆的session,前提:用户已经授权
虽然小程序可以直接获取用户openid作为用户,之后获取用户授权关联unionId的时候再同步数据。为了不操作麻烦,我的设计是在授权之后才能使用对应的功能。
好处:
方便统一处理某些情况,如请求session过期时自动刷新,接口调用失败显示错误信息。简化写法等。这边根据我们的后台业务做了一定的封装,可以借鉴一下
/**
* 初始化方法,包含获取用户信息和登陆后台(项目中所有请求都必须先通过该方法初始化)
* callback 只有用户授权成功并且登陆后才处理回调函数
*/
wxInit: function(callback) {
// 如果还没监测配置成功
if (!this.globalData.hasUserInfo) {
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo获取头像暱称,不会弹框
//微信登录
this.wxLogin(null, callback)
} else {
wx.navigateTo({
url: '../userInfo/index'
});
}
}
})
} else {
if (callback && typeof callback === "function") {
callback()
}
}
}
/**
* 封装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
//根据请求方式,切换内容类型
if (method.toUpperCase() == "GET") {
header = {
'content-type': 'application/json'
}
} else {
header = {
'content-type': 'application/x-www-form-urlencoded'
}
}
//放入服务端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) {
//请求状态不是200现实错误
if (res.statusCode != 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过期超时,需要刷新session,并重新调用方法
//判断当前是否是未授权前请求,如果是,则直接返回
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
getApp().wxLogin(obj)
} else {
//未授权请求
console.log("未授权请求" + obj.url)
}
}
})
} 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 {
obj.success(data);
}
},
fail: function(res) {
if (obj.fail && typeof obj.fail === "function") {
obj.fail(res);
}
}
})
}
其中有对错误情况的统一处理,重要的还是对session过期的处理,d=====( ̄▽ ̄*)b
/**
* 微信登录方法,获得code更新后台session
* userInfo 用户信息对象
* callbackObj 请求过期时需要重新执行的请求参数
* action 登录成功之后要做的事情
*/
wxLogin: function(callbackObj, action) {
wx.login({
success: res => {
const code = res.code
wx.getUserInfo({
withCredentials: true,
lang: "zh_CN",
success: res => {
getApp().globalData.hasUserInfo = true
const userInfo = res.userInfo
getApp().globalData.userInfo = res.userInfo
res.rawData = ""
const jsonStr = JSON.stringify(res)
//正式登陆
getApp().wxRequest({
url: "tokenHandle/smallProgramLogin",
data: {
code: code,
loginType: 1,
jsonStr: jsonStr
},
success: function(res) {
if (res.status == 0) {
wx.setStorageSync("sessionId", res.data.sessionId)
wx.setStorageSync("isMember", res.data.isMember)
wx.setStorageSync("memberNum", res.data.memberNum)
if (res.data.isMember == "true") {
wx.setStorageSync("user", res.data.unionid)
} else {
wx.removeStorageSync("user")
}
wx.setStorageSync("userSign", res.data.unionid)
//如果有该对象,表示之前session过期,并且需要重新执行请求
if (callbackObj) {
getApp().wxRequest(callbackObj)
}
if (action && typeof action === 'function') {
action()
}
}
console.log(res)
}
})
// 所以此处加入 callback 以防止这种情况
if (this.userInfoReadyCallback) {
this.userInfoReadyCallback(res)
}
}
})
}
})
}
知识点:
1.wx.login()
调用接口获取登录凭证(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(null, function() {
wx.showToast({
title: '授权成功!',
icon: "none",
duration: 500
})
setTimeout(function() {
var route = getCurrentPages()[getCurrentPages().length - 2].route
if (route == "pages/gssIndex/index") {
let options = getCurrentPages()[getCurrentPages().length - 2].options
if (options && options.togame) {
wx.setStorageSync('togame', 'true')
}
}
wx.navigateBack()
}, 1000)
})
} else {
console.log(333, '执行到这里,说明拒绝了授权')
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, HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "0");
response.setHeader("Access-Control-Allow-Headers",
"Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token");
response.setHeader("Access-Control-Allow-Credentials", "true");
try {
WxMpOAuth2AccessToken wmoat = (WxMpOAuth2AccessToken) getSessionAttribute(
WeConstants.WEB_SESSION_ACCESS_TOKEN_KEY);
if (wmoat == null && !StringUtils.isEmpty(code)) {
wmoat = wxMpService.oauth2getAccessToken(code);
setSessionAttribute(WeConstants.WEB_SESSION_ACCESS_TOKEN_KEY, wmoat);
}
BaseUser user = null;
try {
user = initUser(wmoat, code, 0, null);
} catch (Exception e) {
logger.info("init user fail ", e);
}
if ("1".equals(mustLogin) && user == null && (loginType == null || loginType == 0)) {// 只有公众号才能url验证
String loginUrl = null;
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) {//暂时删除,不然小程序web-view是没有鉴权的 throw new Exception("未鉴权"); }
*/
if (pageUrl != null && pageUrl.indexOf("#") != -1) {
pageUrl = pageUrl.substring(0, pageUrl.indexOf("#"));
}
WxJsapiSignature wxJsapiSignature = null;
wxJsapiSignature = wxMpService.createJsapiSignature(pageUrl);
wxJsapiSignature.setUnionId(user.getId());
wxJsapiSignature.setIsMember(user.isMember());
wxJsapiSignature.setSessionId(request.getSession().getId());
wxJsapiSignature.setName(user.getNickName());
return new AccessSuccessResult(wxJsapiSignature);
} catch (Exception e) {
return checkWechatError(e);
}
}
private ResultTO checkWechatError(Exception e) {
try {
String message = e.getMessage();
if (StringUtils.isNotBlank(message) && message.indexOf("json:") >= 0) {// 判断是否是微信的错误格式
String errorMsg = message.split("json:")[1];
Object parse = JSON.parse(errorMsg);
if (parse instanceof Map) {
@SuppressWarnings("rawtypes")
Map map = (Map) parse;
Object object = map.get("errcode");
if (object instanceof Integer) {
Integer error = (Integer) object;
if (error.intValue() == 40163) {// code使用过或者code过期(不合法的 oauth_code)则返回过期
AccessErrorResult accessErrorResult = new AccessErrorResult(e.getMessage());
accessErrorResult.wechatOverdue(e.getMessage());
return accessErrorResult;
}
}
}
}
} catch (Exception e1) {
}
return new AccessErrorResult(e.getMessage());
}
/**
* 小程序/小游戏登陆
*
* @param code
* @param loginType 1小程序,2小游戏
* @param jsonStr 包含code、encryptedData、iv的json串
* @param response
* @return
*/
@SuppressWarnings("static-access")
@RequestMapping("/smallProgramLogin")
@ResponseBody
public ResultTO smallProgramLogin(String code, Integer loginType, String jsonStr, HttpServletResponse response) {
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "0");
response.setHeader("Access-Control-Allow-Headers",
"Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token");
response.setHeader("Access-Control-Allow-Credentials", "true");
WxMpOAuth2AccessToken wmoat = (WxMpOAuth2AccessToken) getSessionAttribute(
WeConstants.WEB_SESSION_ACCESS_TOKEN_KEY);
try {
WechatUserInfo userInfo = null;
if (wmoat == null) {
String encryptedData = null;
String iv = null;
String session_key = null;
JSONObject data = JSONObject.parseObject(jsonStr);
encryptedData = data.getString("encryptedData");
iv = data.getString("iv");
if (loginType != null && loginType == 1) {// 小程序
wmoat = smallProgramService.jscode2session(code);
session_key = wmoat.getSession_key();
} else if (loginType != null && loginType == 2) {// 小游戏
wmoat = smallGameService.jscode2session(code);
session_key = wmoat.getSession_key();
}
if (wmoat == null) {
setSessionAttribute(WeConstants.WEB_SESSION_ACCESS_TOKEN_KEY, wmoat);
}
String userStr = getUserInfo(encryptedData, session_key, iv);
JSONObject user = JSONObject.parseObject(userStr);
String unionId = user.getString("unionId");
userInfo = user.parseObject(userStr, WechatUserInfo.class);
if (StringUtils.isNotBlank(unionId) && wmoat != null) {
wmoat.setUnionid(unionId);
}
}
BaseUser user = null;
try {
user = initUser(wmoat, code, loginType, userInfo);
} catch (Exception e) {
logger.info("init user fail ", e);
}
String sessionId = request.getSession().getId();
Map<String, Object> map = new HashMap<>();
map.put("unionid", user.getId());
map.put("isMember", user.isMember());
map.put("sessionId", sessionId);
map.put("memberNum", user.getMemberNum());
map.put("name", user.getNickName());
return new AccessSuccessResult(map);
} catch (WxErrorException e) {
return checkWechatError(e);
} catch (Exception e) {
return checkWechatError(e);
}
}
/**
* 解密encryptedData,获取用户信息方法
*/
public String getUserInfo(String encryptedData, String sessionkey, String iv) {
// 被加密的数据
byte[] dataByte = Base64.decode(encryptedData);
// 加密秘钥
byte[] keyByte = Base64.decode(sessionkey);
// 偏移量
byte[] ivByte = Base64.decode(iv);
try {
// 如果密钥不足16位,那么就补足. 这个if 中的内容很重要
int base = 16;
if (keyByte.length % base != 0) {
int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0);
byte[] temp = new byte[groups * base];
Arrays.fill(temp, (byte) 0);
System.arraycopy(keyByte, 0, temp, 0, keyByte.length);
keyByte = temp;
}
// 初始化
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC");
SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
parameters.init(new IvParameterSpec(ivByte));
cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化
byte[] resultByte = cipher.doFinal(dataByte);
if (null != resultByte && resultByte.length > 0) {
String result = new String(resultByte, "UTF-8");
return result;
}
} catch (NoSuchAlgorithmException e) {
logger.error("NoSuchAlgorithmException :", e);
} catch (NoSuchPaddingException e) {
logger.error("NoSuchPaddingException :", e);
} catch (InvalidParameterSpecException e) {
logger.error("InvalidParameterSpecException :", e);
} catch (IllegalBlockSizeException e) {
logger.error("IllegalBlockSizeException :", e);
} catch (BadPaddingException e) {
logger.error("BadPaddingException :", e);
} catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncodingException :", e);
} catch (InvalidKeyException e) {
logger.error("InvalidKeyException :", e);
} catch (InvalidAlgorithmParameterException e) {
logger.error("InvalidAlgorithmParameterException :", e);
} catch (NoSuchProviderException e) {
logger.error("NoSuchProviderException :", e);
}
return null;
}
//小程序特有的获取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);
}