微信公众号支付的完整流程,首先需要微信授权,获取openId,因为openid是微信用户在公众号appid下的唯一用户标识(appid不同,则获取到的openid就不同),可用于永久标记一个用户,同时也是微信JSAPI支付的必传参数。
首先解释一下微信公众号中的一些概念,想要完成支付,需要被认证的公众号外,还需要商户号。这两个都需要有一定资质才能申请。单纯拥有公众号,只能进行微信授权操作,需要公众号和商户号绑定后才能完成支付操作。
用个不够恰当的例子来解释:公众号就类比于银行的前台,商户号就类比于银行,前台不绑定银行的话那她就是一个普通人,不能完成银行的各类经济业务,前台和银行绑定后才可以操作款项。而这个前台也要通过有一定的资质认证,才能和银行绑定。
注:微信支付的接口不只有公众号支付一种,但是无论哪种支付接口,都需要绑定商户号才能进行支付操作。
获取OpenId
获取OpenId,有两种方式,“手工方式”和“利用第三方API”,最终目的都是一样的,但是在实际开发中还是用轮子比较容易。手工方式最主要的是一步一步的了解获取OpenId的过程,如果以使用为主,可以直接跳过“手工方式”,查看“利用第三方API”。
手工方式
首先,很重要也是很多人懒得去做的事情就是仔细看看【微信支付】商户接入文档,内容很多,因为是微信公众号,所以我选择JSAPI支付
打开链接可以看到JSAPI支付的详细内容,接下来的操作都是根据JSAPI支付中的“业务流程”逐步完成。
PS.其实下图链接网页授权获取用户openid接口文档也是真的写的很清楚了。当trade_type=JSAPI时(即公众号支付),openId必传。
1. 设置网页授权域名
按照文档来,在公众号中【设置网页授权域名】,这里接入的是外网地址,通俗但不够准确的讲,就是你程序所在的域名。
填写完域名之后,记得下载文件,因为规定很多,还需要ICP备案什么的,作为调试,我选择使用了https://natapp.cn/穿透内网,就可以使得微信这边访问到自己的电脑。
在NATAPP中,注册/登录,购买隧道,免费的只能临时用一下,还会随便换域名/端口,所以我购买了9/月的。
查看购买到的隧道,这个域名是你在购买过程中,他让你自己输入的。
查看教程NATAPP 1分钟快速图文教程,启动NATAPP。启动后会看到域名映射到当前本地端口。
此时通过在浏览器中,输入localhost:8080/myself
和 http://my.natapp.com/myself
访问的是同一个界面则表示成功。
再将之前在微信【网页授权域名】中下载的 MP_verify_nxxxxxx.txt文件放到源码文件中。
注意:这里要求文件的位置,必须是在域名的根目录下,在本例中也就是在浏览器中输入http://my.natapp.com/MP_verify_nxxxxxx.txt
后,页面不报错时,在点击【确认】才能在微信网页授权域名中添加成功。
当然,添加成功域名之后,这个MP_verify_nxxxxxx.txt文件也可以从源码文件中删除。
2. 获取code
可以查看微信文档,并了解相应参数说明。以下是微信文档的相应截图,我们只需要了解的就是替换掉链接中的appId和redirect_uri。用户同意授权后,跳转到redirect_uri并返回code。
3. 换取access_token
获取了code之后,以code作为票据再换access_token。以下是微信文档的相应截图,我们只需要了解的就是替换掉链接中的appId为自己的appId和code为刚刚后台获取的code(注:code时效只有5min)。4. 得到openId
在上一个步骤中,获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。请求上述链接时,正确时会返回如下JSON数据包。其中就包含了openId。更多使用,可以仔细查看微信文档!!文档写的很细致的!!
以我自己为例,当我手机微信访问了下面这个地址(网页授权URL)之后
https://open.weixin.qq.com/connect/oauth2/authorize?appid=myappId&redirect_uri=http://my.natapp.com/myRedirectURL&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
手机微信会自动跳转到http://my.natapp.com/myRedirectURL/code=xxcodexx
,虽然手机页面是空白,但是后台已经获得了code的信息。后台可以通过拼接字符串等操作,再发起请求https://api.weixin.qq.com/sns/oauth2/access_token?appid=myappId&secret=SECRET&code=xxcodexx&grant_type=authorization_code
,之后获得一个包含了openId信息的JSON数据包。
利用第三方API
直接看Github上的SDK:https://github.com/Wechat-Group/WxJava。里面文档、工具都非常详细。
因为我本地程序用的Maven,所以直接引用。[pom代码1]
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>3.5.0</version>
</dependency>
在本地函数中的使用,主要查看文档https://github.com/Wechat-Group/WxJava/wiki/MP_OAuth2网页授权
根据文档逐步完成本地代码。
新建WechatController.class,控制网络授权。[授权代码1]
@Controller
@RequestMapping("/wechat")
@Slf4j
public class WechatController {
@Autowired
WxMpService wxMpService = new WxMpServiceImpl();
@GetMapping("/authorize")
public String authorize(@RequestParam("returnUrl") String returnUrl) {
// 1.配置,项目中配置应该是进行一个统一配置,供程序各个部分使用。
// 2.调用方法,下面这个回调地址 是我自己的地址,你需要用你自己的
String url = "http://sell35.natapp1.cc/sell/wechat/userInfo";
String redirectUrl = wxMpService.oauth2buildAuthorizationUrl(url, WxConsts.OAUTH2_SCOPE_BASE, URLEncoder.encode(returnUrl));
log.info("【微信网页授权】获取code, result={}", redirectUrl);
return "redirect:" + redirectUrl;
}
// 获取用户信息
@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());
}
String openId = wxMpOAuth2AccessToken.getOpenId();
return "redirect:" + returnUrl + "?openid=" + openId;
}
}
建立config文件夹,并在下面新建WeChatMpConfig.class。将Service作为一个Bean、配置也作为Bean。 其中的AppId和AppSecret我们可以从配置文件中读取。[授权代码2]
@Component
public class WechatMpConfig {
@Autowired
private WechatAccountConfig accountConfig;
@Bean
public WxMpService wxMpService(){
WxMpService wxMpService = new WxMpServiceImpl();
wxMpService.setWxMpConfigStorage(wxMpConfigStorage());
return wxMpService;
}
@Bean
public WxMpConfigStorage wxMpConfigStorage(){
WxMpInMemoryConfigStorage wxMpConfigStorage =new WxMpInMemoryConfigStorage();
wxMpConfigStorage.setAppId(accountConfig.getMpAppId());
wxMpConfigStorage.setSecret(accountConfig.getMpAppSecret());
return wxMpConfigStorage;
}
}
配置文件微信账号相关的部分先写一个配置文件。WechatAccountConfig.class[授权代码3]
@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {
private String mpAppId;
private String mpAppSecret;
}
前端调试
首先先讲一下请求过程,微信访问sell.com,前端回会重定向到 /sell/wechat/authorize,并携带returnUrl:http://sell.com/abc。 通过上一步骤的授权操作获取openid,最后后端返回给前端 :http://sell.com/abc?openid=oxfjhaojdnsjcos
所以需要先在前端重定向,进入虚拟机(前端部署部分)
cd /opt/
cd code/
cd sell_fe_buyer/
cd config/
配置文件
vim index.js
在配置文件中
sellUrl
对应的是项目地址:http://sell.com
openidUrl
获取openId的地址:http://sell35.natapp.cc/sell/wechat/authorize
wechatPayUrl
支付地址(当前主要是配置授权,先无需配置这一项)
配置完成后,回到前端项目的根目录(cd ..
)。
再构建一下(npm run build
)
构建好的文件,在dist目录下,所以需要将构建好的文件copy到前端的根目录下,语句如下。
cp -r dist/* /opt/data/wwwroot/sell
但是此时我们通过手机访问"sell.com"是访问不了的。这是因为当前目标网址“sell.com”是在电脑端,电脑之所以能访问,是因为本机设置了host,他将域名直接指向虚拟机地址,所以可以成功访问,但是由于手机无法更改host。所以需要用代理解决这个问题。将手机的所有请求转发到电脑上,此时就可以访问了。Mac下可以使用Charles,Windows下可以使用fiddler,
通过终端输入ifconfig
,得到当前电脑ip为 192.168.1.103.
再通过手机查询当前手机的ip为 192.168.1.105.
最好二者接通前在terminal中ping一下。
ping通之后,在手机中设置手动代理
服务器中输入:电脑ip(103)
端口输入:8888(因为Charles的默认端口就8888)
此时再在手机端访问sell.com,就可以通过电脑访问到公众号网站。
微信支付
支付业务流程:生成商户订单(开发者生成的订单) —> 调用统一下单API —> 生成预付单后会返回一个预付单信息 —> 通过JSAPI页面调用的支付参数并签名(此时才会唤起支付) —> 支付完成后等待一个异步通知结果 —> 依据这个结果通知更改订单状态为已支付 —> 调用查询API,查询支付结果(用于对账)
选择SDK,可以选择之前的那个SDK,这里我选择的是Best Pay SDK。
请求过程:
- 重定向到
/sell/pay/create
,携带参数(orderId:123456
和returnUrl:http://xxx.com/abc/order/123456
) - 最后返回到
http://xxx.com/abc/order/123456
。
这里需要注意的是,支付过程中,只需要传过来订单ID即可,至于需要支付多少钱,可以通过订单ID去数据库查看。不能将支付金额作为参数往后传递,因为这样即便金额不对,也能够支付成功,或者后台再校验一边金额,无论怎样,都是多此一举。
引入SDK,在Pom文件中添加依赖。[pom代码2]
<dependency>
<groupId>cn.springboot</groupId>
<artifactId>best-pay-sdk</artifactId>
<version>1.1.0</version>
</dependency>
新建一个PayController Class,主要完成订单查询和支付操作。[该段代码非最终代码,至于此处以便与思考] 。
@Controller
@RequestMapping("/pay")
public class PayController {
@Autowired
private OrderService orderService;
// PayService之后作为服务新建,当前并不存在。
@Autowired
private PayService payService;
// 为了重定向,完成请求过程的第一步
@GetMapping("/create")
public void create(@RequestParam("orderId") String orderId,
@RequestParam("returnUrl") String returnUrl) {
//1. 查询订单
OrderDTO orderDTO = orderService.findOne(orderId);
if (orderDTO == null) {
throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
}
//2. 发起支付
PayResponse payResponse = payService.create(orderDTO);
}
}
根据SDK规则,微信账户相关内容需要配置,配置在WechatAccountConfig中。[代码3]
@Data
@Component
@ConfigurationProperties(prefix = "wechat")
public class WechatAccountConfig {
private String mpAppId;
private String mpAppSecret;
// 商户号
private String mchId;
// 商户密钥
private String mchKey;
// 商户证书路径
private String keyPath;
// 微信支付异步通知地址
private String notifyUrl;
}
同时修改给配置文件增加相应内容配置一下WechatPayConfig(),并把service作为Bean配置进去。[代码4]
@Component
public class WechatPayConfig {
@Autowired
private WechatAccountConfig accountConfig;
@Bean
public BestPayServiceImpl bestPayService(){
WxPayH5Config wxPayH5Config=new WxPayH5Config();
wxPayH5Config.setAppId(accountConfig.getMpAppId());
wxPayH5Config.setAppSecret(accountConfig.getMpAppSecret());
wxPayH5Config.setMchId(accountConfig.getMchId());
wxPayH5Config.setMchKey(accountConfig.getMchKey());
wxPayH5Config.setKeyPath(accountConfig.getKeyPath());
wxPayH5Config.setNotifyUrl(accountConfig.getNotifyUrl());
BestPayServiceImpl bestPayService=new BestPayServiceImpl();
bestPayService.setWxPayH5Config(wxPayH5Config);
return bestPayService;
}
支付操作作为一个服务,新建PayService,并建立该方法的实现PayServiceImpl。[代码6] 并且将之前BestPayServiceImpl配置好的注入进Service。上述的JsonUtil是个JSON格式化工具类,已附追在文章末尾。
@Service
@Slf4j
public class PayServiceImpl implements PayService {
private static final String ORDER_NAME = "微信点单订餐";
@Autowired
private BestPayServiceImpl bestPayService;
@Override
public PayResponse create(OrderDTO orderDTO) {
PayRequest payRequest = new PayRequest();
payRequest.setOpenid(orderDTO.getBuyerOpenid());
payRequest.setOrderAmount(orderDTO.getOrderAmount().doubleValue());
payRequest.setOrderId(orderDTO.getOrderId());
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;
}
}
最后返回的 response 内容包含了 "appId"
、"timeStamp"
、"nonceStr"
、"packAge"
、"signType"
、"paySign"
的值。
此时完成了业务流程中的:调用统一下单API,并且返回预付单信息(prepay_id
在"packAge"
对应的值中)。
下一步我们要做的就是发起支付。
从网页发起支付
支付操作的详细内容先仔细阅读文档JSAPI支付开发者文档
需要向后端先传递这些参数。
所以在代码部分,我们接下来的工作就是动态构造如上图所示的代码。
这里我们选择模版技术,用到了freemarker这个组件,现在pom文件中引入dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
完善之前的PayController Class[代码5] 。将返回的参数从void改成ModelAndView,最后return返回的“pay/create”路径下的create实际上就是一个create.flt文件(模版文件)。
@Controller
@RequestMapping("/pay")
public class PayController {
@Autowired
private OrderService orderService;
@Autowired
private PayService payService;
// 为了重定向,完成请求过程的第一步
@GetMapping("/create")
public ModelAndView create(@RequestParam("orderId") String orderId,
@RequestParam("returnUrl") String returnUrl,
Map<String, Object> map) {
//1. 查询订单
OrderDTO orderDTO = orderService.findOne(orderId);
if (orderDTO == null) {
throw new SellException(ResultEnum.PRODUCT_NOT_EXIST);
}
//2. 发起支付
PayResponse payResponse = payService.create(orderDTO);
map.put("payResponse", payResponse);
map.put("returnUrl", returnUrl);
return new ModelAndView("pay/create");
}
}
create.flt中放的就是微信内H5调起支付
文档中返回的代码格式
<script>
function onBridgeReady() {
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":"${payResponse.appId}", //公众号名称,由商户传入
"timeStamp":"${payResponse.timeStamp}", //时间戳,自1970年以来的秒数
"nonceStr":"${payResponse.nonceStr}", //随机串
"package":"${payResponse.packAge}",
"signType": "MD5", //微信签名方式:
"paySign":"${payResponse.paySign}" //微信签名
},
function (res) {
// if (res.err_msg == "get_brand_wcpay_request:ok") {
// } // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
location.href="${returnUrl}"
}
);
}
if (typeof WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}
</script>
此时已经完成动态注入参数了,但是完成支付还需要我们在前端文件中配置一下,参考之前的【前端调试】模块。记得改完之后,build和拷贝文件。
此时再去支付,支付完成后发现并没有得到“支付成功”的通知,这是因为我们没有修改订单状。在微信的支付业务流程中,我们还没有做处理微信异步通知结果这一步。
所以我们下一步的工作就是:接受微信的异步通知结果,并根据结果更改订单的支付状态。
微信异步通知
在微信内H5调起支付时,前端也可以接收到一个是否成功的标志。注意这行注释,我们知道不能通过get_brand_wcpay_request的值去判断是否支付成功。因为在前端,该代码是有可能被篡改的。更安全的方式是根据后端的异步通知来确定是否支付成功。
在PayController Class中加入一个接受微信异步通知的方法notify。直接使用SDK中的notify处理方法。[代码7]
@PostMapping("/notify")
public void notify(@RequestBody String notifyData){
payService.notify(notifyData);
}
将异步通知的逻辑写入PayService、PayServiceImpl。[代码8]
@Override
public PayResponse notify(String notifyData) {
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】异步通知,payResponse={}", payResponse);
return payResponse;
}
同时需要在配置文件application.yml
文件中配置notify地址。
支付成功后,需要修改订单的支付状态。也就是更改一下代码8中的内容。[代码9]
@Override
public PayResponse notify(String notifyData) {
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】异步通知,payResponse={}", payResponse);
// 修改订单支付状态
// 1 先查询一下当前订单状态
OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());
// 2 修改订单状态
orderService.paid(orderDTO);
return payResponse;
}
此时,我们可以发现代码安全性不足。在微信异步通知中,有几方面需要注意:
- 验证签名(验证一下这个签名是不是真正来自于微信,不然别人模拟一个微信验证请求,我们也会傻fufu的通过)
- 支付的状态(虽然会得到异步通知,但是消息的内容不一定是支付成功,也有失败等多种情况)
- 支付金额(有可能程序错误,导致微信回调之后的金额不够统一,所以需要校验金额)
- 付款人(下单人 == 支付人)(根据业务需要确定下单人和支付人是否一直,所以根据情况可以校验确认一下)
由于使用了SDK,所以第1、2点是不需要我们去做的。代码中我们还需要做第3步。
在判断金额中,要判断微信返回金额与系统金额是否一致,不仅需要保证二者的数据类型相同,也需要精度一致。所以把判断金额这个部分写入了单独的utils
MathUtil.class [代码10]
public class MathUtil {
private static final Double Money_Range = 0.01;
public static Boolean equals(Double d1, Double d2){
Double result = Math.abs(d1 - d2);
if (result < Money_Range){
return true;
} else {
return false;
}
}
}
完成MathUtil.class之后,我们也需要相应的更改代码9。[代码11]
@Override
public PayResponse notify(String notifyData) {
PayResponse payResponse = bestPayService.asyncNotify(notifyData);
log.info("【微信支付】异步通知,payResponse={}", payResponse);
// 修改订单支付状态
// 1 先查询一下当前订单
OrderDTO orderDTO = orderService.findOne(payResponse.getOrderId());
// 2 判断订单是否存在
if (orderDTO == null) {
log.error("【微信支付】异步通知,订单不存在。orderId={}", payResponse.getOrderId());
throw new SellException(ResultEnum.ORDER_NOT_EXIST);
}
// 3 判断金额是否一致(因为很多判断中,由于精度的不同,会判断两个金额不一致,比如0.10和0.1;所以采用相减的方式,写在util工具类中)
if (!MathUtil.equals(payResponse.getOrderAmount(), orderDTO.getOrderAmount().doubleValue())) {
log.error("【微信支付】异步通知,订单不存在。orderId={}, 微信通知金额={}, 系统金额 ={}", payResponse.getOrderId(), payResponse.getOrderAmount(), orderDTO.getOrderAmount());
throw new SellException(ResultEnum.WXPAY_NOTIFY_MONEY_VERIFY_ERROR);
}
// 4 2、3步都通过后,再修改订单状态
orderService.paid(orderDTO);
return payResponse;
}
根据微信支付业务流程,在支付成功后需要给微信返回“支付通知”,否则将会一直回调PayService 中的notify
。如图是微信支付成功的API文档。
在PayController Class中,同发起支付一样,选择返回
ModelAndView
模版,完成微信异步通知,也就是完善代码5中的代码。[代码12]
@PostMapping("/notify")
public ModelAndView notify(@RequestBody String notifyData) {
payService.notify(notifyData);
// 返回给微信处理结果
return new ModelAndView("pay/success");
}
到这里,微信公众号支付的流程就全部结束了。
代码:JsonUtil
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class JsonUtil {
public static String toJson(Object object) {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setPrettyPrinting();
Gson gson = gsonBuilder.create();
return gson.toJson(object);
}
}