通过这几天的对微信公众号支付的学习,我知道了想要完成在微信内置浏览器访问第三方网站进行支付或者其他操作都首先要进行获取网页授权的操作,也就是要获取用户的openid,只有有了openid我们才有权限进行接下来的操作,我现在就来安步奏详细说明一下其中的流程和坑。
根据官方文档,我们可以得到以下步奏:
首先我们需要得到APPID和用户需要跳转的地址returnUrl
然后根据这两个信息组成一个新的url进行跳转
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String backUrl = "http://ynfywtq.hk1.mofasuidao.cn/Weixin/callBack";
//第一步:用户同意授权,获取code,会重定向到backUrl
String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid="+WeixinUtil.APPID
+ "&redirect_uri="+URLEncoder.encode(backUrl)
+ "&response_type=code"
+ "&scope=snsapi_userinfo"
+ "&state=STATE#wechat_redirect";
response.sendRedirect(url);
}
当跳转到这个url后,微信内置的浏览器就会解析这段代码,他会判断你是不是用微信浏览器进行的访问,如果不是就会显示必须用微信客户端登录,如果是客户端并且你的scope=snsapi_userinfo就会弹出一个授权页面让用户授权,授权之后页面将跳转至redirect_uri/?code=CODE&state=STATE
如果出现redirect_url参数错误,是因为在微信公众平台没有配置好,一定要把地址写上后,测试能不能访问到那个txt文件
获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
通过code换取网页授权access_token
通过access_token和openid拉取用户信息,如果为base请求的url会不同
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//接收code
String code = request.getParameter("code");
//第二步:通过code换取网页授权access_token
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+WeixinUtil.APPID
+ "&secret="+WeixinUtil.APPSECRET
+ "&code="+ code
+ "&grant_type=authorization_code";
//它会返回一个json数据包
JSONObject jsonObject = WeixinUtil.doGetStr(url);
String openid = jsonObject.getString("openid");
String token = jsonObject.getString("access_token");
//第三步:刷新access_token(如果需要)
//第四步:拉取用户信息(需scope为 snsapi_userinfo)
String infoUrl = "https://api.weixin.qq.com/sns/userinfo?access_token="+token
+ "&openid="+openid
+ "&lang=zh_CN";
JSONObject userInfo = WeixinUtil.doGetStr(infoUrl);
//1、使用微信用户信息直接登录,无需注册和绑定
//System.out.println(userInfo);
request.setAttribute("info", userInfo);
request.getRequestDispatcher("/index1.jsp").forward(request, response);
//2、有自己的账号体系就要再数据库事先创建好表单,然后与微信信息进行绑定
}
这里我们使用的是:
<dependency>
<groupId>com.github.binarywanggroupId>
<artifactId>weixin-java-mpartifactId>
<version>2.7.0version>
dependency>
首先我们需要配置基本信息
@Data
@Component//是一个泛化的概念,仅仅表示一个组件 (Bean) ,可以作用在任何层次。
/*(把普通pojo实例化到spring容器中,相当于配置文件中的 )
* 用这个注解注册之后就可以用@Autowired来进行调用了
* */
@ConfigurationProperties(prefix = "wechat")//这是调用配置文件
public class WechatAccountConfig {
private String mpAppId;
private String mpAppSecret;
//商户号
private String mchId;
//商户秘钥
private String mchKey;
//商户证书路径
private String keyPath;
//微信支付异步通知地址
private String notifyUrl;
}
然后把这些信息注册到service里面
@Component
public class WechatMpConfig {
@Autowired
private WechatAccountConfig accountConfig;
@Bean
public WxMpService wxMpService(){
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxMpConfigStorage());//这是根据官方文档知道的设置方法,设置了之后wxMpService就有了配置文件
return wxMpService;
}
@Bean
public WxMpConfigStorage wxMpConfigStorage(){
WxMpInMemoryConfigStorage wxMpInMemoryConfigStorage = new WxMpInMemoryConfigStorage();
wxMpInMemoryConfigStorage.setAppId(accountConfig.getMpAppId());
wxMpInMemoryConfigStorage.setSecret(accountConfig.getMpAppSecret());
return wxMpInMemoryConfigStorage;
}
}
然后就是在controller里面写具体的url跳转代码,通过下面两个方法
authorize可以实现普通方法中的第一二步
userInfo可以完成通过code获取token和openid了
@Controller
@RequestMapping("/wechat")
@Slf4j
public class WechatController {
@Autowired
private WxMpService wxMpService;
//这也是点击项目首页后第一个调用的方法,获取openid
@GetMapping("/authorize")
public String authorize(@RequestParam("returnUrl") String returnUrl){
//1.配置
//2.调用方法
String url = "http://ynfywtq.hk1.mofasuidao.cn/sell/wechat/userInfo";
//这里是根据配置的方法去重定向到下面一个方法得到返回值,这里主要是要为了获取code
String redirectUrl = wxMpService.oauth2buildAuthorizationUrl(url, WxConsts.OAUTH2_SCOPE_BASE, URLEncoder.encode(returnUrl));
//log.info("【微信网页授权】获取code,result={}", redirectUrl);
//重定向必须要加"redirect:"拼接,否则要像ssm一样配置好
return "redirect:" + redirectUrl;
}
//这里得到code和目标地址,如果不是用微信客户顿打开的话就会重定向到另一个地址,叫你用客户端打开
@GetMapping("/userInfo")
public String userInfo(@RequestParam("code") String code,
@RequestParam("state") String returnUrl){
WxMpOAuth2AccessToken wxMpOAuth2AccessToken = new WxMpOAuth2AccessToken();
try {
wxMpOAuth2AccessToken = wxMpService.oauth2getAccessToken(code);
} catch (WxErrorException e) {
log.error("【微信网页授权】{}", e);
throw new SellException(ResultEnum.WECHAT_MP_ERROR.getCode(), e.getError().getErrorMsg());
}
//我们是为了网页支付,而网页支付主要是要openid
String openId = wxMpOAuth2AccessToken.getOpenId();
return "redirect:" + returnUrl +"?openid=" + openId;
}
}
由于发起支付首先是需要一个订单的,所以我们要根据需求首先完成创建订单的操作,订单信息里面就会包含用户的openid的信息,之后我们在发起支付就可以通过订单里面openid来进行支付的操作
而支付这里我只用了第三方SDK完成:
<dependency>
<groupId>cn.springbootgroupId>
<artifactId>best-pay-sdkartifactId>
<version>1.1.0version>
dependency>
传入参数为orderid和returnUrl(支付成功后的回调地址)
//只需要接受orderid和回调地址
@GetMapping("/create")
public ModelAndView create(@RequestParam("orderId") String orderId,
@RequestParam("returnUrl") String returnUrl,
Map map) {
//1. 根据传过来的orderid查询订单
OrderDTO orderDTO = orderService.findOne(orderId);
if(orderDTO == null){
throw new SellException(ResultEnum.ORDER_NOT_EXIST);
}
//2. 发起支付
PayResponse payResponse = payService.create(orderDTO);
map.put("payResponse", payResponse);
map.put("returnUrl", returnUrl);
//返回到WeixinJSBridge内置对象在其他浏览器中无效
return new ModelAndView("pay/create", map);
}
具体发起操作
@Override
public PayResponse create(OrderDTO orderDTO) {
PayRequest payRequest = new PayRequest();
//发起支付需要传一些参数
payRequest.setOpenid(orderDTO.getBuyerOpenid());//用户openid
payRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());//订单总金额
payRequest.setOrderId(orderDTO.getOrderId());//订单orderid
payRequest.setOrderName(ORDER_NAME);//订单名字,自己随便起
payRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);//支付方式
log.info("【微信支付】发起支付,request={}", JsonUtil.toJson(payRequest));
PayResponse payResponse = bestPayService.pay(payRequest);//根据传参得到预付支付的参数
log.info("【微信支付】发起支付生成预付信息,response={}", JsonUtil.toJson(payResponse));
return payResponse;
}
2017-08-26 19:35:27.679 INFO 12268 --- [nio-8080-exec-1] com.akk.service.impl.PayServiceImpl : 【微信支付】发起支付,request={
"payTypeEnum": "WXPAY_H5",
"orderId": "1503747327509255028",
"orderAmount": 0.01,
"orderName": "微信点餐订单",
"openid": "oopqG1kXTv-S_NsiOlwFGjyofJZg"
}
2017-08-26 19:35:28.298 INFO 12268 --- [nio-8080-exec-1] com.akk.service.impl.PayServiceImpl : 【微信支付】发起支付生成预付信息,response={
"appId": "wx8b20c44179a091b4",
"timeStamp": "1503747328",
"nonceStr": "5utjFpjTlD5Tjxbn",
"packAge": "prepay_id\u003dwx20170826193526270d20a17b0967136475",
"signType": "MD5",
"paySign": "6C770BCB3057365BF76A56652C8BDBA8"
}
有了预付信息后按照微信官方文档的说法就是
生成JSAPI页面调用的支付参数并签名
而我发现我们这里的步奏似乎和官方的步奏不太一样
官方是
5.先同一调用下单API,生成预付单 6.生成JSAPI页面调用的支付参数并签名
7.用户点击支付8.微信支付系统验证参数的合法性和授权域权限
而这里的第三方SDK再用户点击支付后直接产生预付信息在生成JSAPI页面调用的支付参数并签名,就没有了统一下单的调用减少了一层逻辑
否则按照官方的标准,用户操作应该是
1.下单(产生预付单)
2.用户确认后点击支付
然后回到
9.用户输入密码的界面,之后系统会像微信支付系统验证授权 10.再异步通知商户后台支付结果(是否成功)(通过配置的notifyUrl来通知商户系统支付结果,然后商户在做进一步判断,防止中间被黑)
@Override
public PayResponse notify(String notifyData) {
//1. 验证签名
//2. 支付状态
//3. 支付金额
//4. 支付人(下单人 == 支付人)比如有代付,有的必须本人支付
//前两步SDK已经做了
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】异步通知,payResponse={}", JsonUtil.toJson(payResponse));
//查询订单
OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());
//判断订单是否存在
if(orderDTO == null) {
log.info("【微信支付】异步通知,订单不存在,orderId={}", payResponse.getOrderId());
throw new SellException(ResultEnum.ORDER_NOT_EXIST);
}
//判断金额是否一致,由于这里两个变量的类型不一致,数据库用的BigDecimal而SDK用的double,所以要做转换
//但是转换类型比较还是不行,因为转换后小数点后面会出现奇怪的数字,所以要用相减判断
if(!MathUtil.equals(payResponse.getOrderAmount(), orderDTO.getOrderAmount().doubleValue())) {
log.info("【微信支付】异步通知,订单金额不一致,orderId={},微信通知金额={},系统金额={}",
payResponse.getOrderId(),
payResponse.getOrderAmount(),
orderDTO.getOrderAmount());
throw new SellException(ResultEnum.WXPAY_NOTIFY_MONEY_VERIFY_ERROR);
}
//修改订单支付状态
orderService.paid(orderDTO);
return payResponse;
}
11.返回成功结果给微信支付系统
12.返回支付结果并发消息给用户。完成支付
基本上按照第三方SDK的操作进行编写就不会出错
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。
我这里只进行了退款操作,没有写判断的逻辑
@Override
public RefundResponse refund(OrderDTO orderDTO) {
RefundRequest refundRequest = new RefundRequest();
refundRequest.setOrderId(orderDTO.getOrderId());
refundRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());
refundRequest.setPayTypeEnum(BestPayTypeEnum.WXPAY_H5);
log.info("【微信退款】request={}", JsonUtil.toJson(refundRequest));
RefundResponse refundResponse = bestPayService.refund(refundRequest);
log.info("【微信退款】response={}", JsonUtil.toJson(refundResponse));
return refundResponse;
}
用第三方SDK完成操作的确很简单,但是我们还要能明白其中的详细原理最好,最好的方式是自己先完成一遍不用SDK的普通调,在去用别人的包的时候就不会一头雾水了。
这样在写属于自己特定的逻辑的时候就很清楚该怎样操作了。
比如统一下单和确定支付是否分开写。