微信支付官方文档:https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F
在前面的业务中,我们已经完成了:
接下来,我们需要做的是:
在微信支付文档中,可以查询得到以下信息:
支付路径
URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数
第一部分如上图所示,第二部分封装到config中,这个config是我们自己写的,实现了官方的接口WXPayConfig:
PayConfig:
官方的接口WXPayConfig:
请求方式
返回结果
不一定执行了就一定成功,要做 通信和业务是否成功的校验 和 签名是否有效的校验,所以返回结果要做一个判断:
编写自定义配置,读取属性
payconfig:
@Data
public class PayConfig implements WXPayConfig {
private String appID; // 公众账号ID
private String mchID;// 商户号
private String key;// 生成签名的密钥
private int httpConnectTimeoutMs; // 连接超时时间
private int httpReadTimeoutMs;// 读取超时时间
private String notifyUrl;
@Override
public InputStream getCertStream() {
return null;
}
}
将PayConfig注册到Spring容器中
WXPayConfiguraturation:
@Configuration
public class WXPayConfiguration {
@Bean
@ConfigurationProperties(prefix = "ly.pay")
public PayConfig payConfig(){
return new PayConfig();
}
@Bean
public WXPay wxPay(PayConfig payConfig){
return new WXPay(payConfig, WXPayConstants.SignType.HMACSHA256);
}
}
现在工具类都准备好了,接下来我们生成预交易链接:
public String createOrder(Long orderId, Long totalPay, String desc){
try {
Map<String, String> data = new HashMap<>();
// 商品描述
data.put("body", desc);
// 订单号
data.put("out_trade_no", orderId.toString());
//金额,单位是分
data.put("total_fee", totalPay.toString());
//调用微信支付的终端IP
data.put("spbill_create_ip", "127.0.0.1");
//回调地址
data.put("notify_url", payConfig.getNotifyUrl());
// 交易类型为扫码支付
data.put("trade_type", "NATIVE");
// 利用wxPay工具,完成下单
Map<String, String> result = wxPay.unifiedOrder(data);
// 判断通信和业务标示
isSuccess(result);
// 校验签名
isValidSign(result);
// 下单成功,获取支付链接
String url = result.get("code_url");
return url;
} catch (Exception e) {
log.error("[微信下单] 创建预交易订单异常失败", e);
return null;
}
}
public void isSuccess(Map<String, String> result) {
// 判断通信标示
String returnCode = result.get("return_code");
if("FAIL".equals(returnCode)){
// 通信失败
log.error("[微信下单] 微信下单通信失败,失败原因:{}", result.get("return_msg"));
throw new LyException(ExceptionEnum.WX_PAY_ORDER_FAIL);
}
// 判断业务标示
String resultCode = result.get("result_code");
if("FAIL".equals(resultCode)){
// 通信失败
log.error("[微信下单] 微信下单业务失败,错误码:{}, 错误原因:{}",
result.get("err_code"), result.get("err_code_des"));
throw new LyException(ExceptionEnum.WX_PAY_ORDER_FAIL);
}
}
public void isValidSign(Map<String, String> result) {
// 校验签名
try {
String sign1 = WXPayUtil.generateSignature(result, payConfig.getKey(), WXPayConstants.SignType.HMACSHA256);
String sign2 = WXPayUtil.generateSignature(result, payConfig.getKey(), WXPayConstants.SignType.MD5);
String sign = result.get("sign");
if (!StringUtils.equals(sign, sign1) && !StringUtils.equals(sign, sign2)) {
throw new LyException(ExceptionEnum.WX_PAY_ORDER_FAIL);
}
} catch (Exception e) {
log.error("[微信支付] 校验签名失败,数据:{}", result);
throw new LyException(ExceptionEnum.WX_PAY_ORDER_FAIL);
}
}
}
@GetMapping("url/{id}")
public ResponseEntity<String> createPayUrl(@PathVariable("id") Long orderId){
return ResponseEntity.ok(orderService.createPayUrl(orderId));
}
public String createPayUrl(Long orderId) {
// 查询订单获得总金额
Order order = queryOrderById(orderId);
// 判断订单状态,如果订单已支付,下面的查询就很多余
OrderStatus orderStatus = order.getOrderStatus();
Integer status = orderStatus.getStatus();
if(status != OrderStatusEnum.UN_PAY.value()){
throw new LyException(ExceptionEnum.ORDER_STATUS_ERROE);
}
Long actualPay = 1L/*order.getActualPay()*/;
OrderDetail detail = order.getOrderDetails().get(0);//订单中可能有多件商品,获取第一件商品的标题作为订单的描述
String desc = detail.getTitle();
return payHelper.createOrder(orderId, actualPay, desc);
}
这里我们使用一个生成二维码的JS插件:qrcode,官网:https://github.com/davidshimjs/qrcodejs
我们把这个js脚本引入到项目中:
然后在页面引用:
页面定义一个div,用于展示二维码:
然后获取到付款链接后,根据链接生成二维码:
刷新页面查看效果:
此时,客户用手机扫描二维码,可以看到付款页面
当用户扫描上述二维码完成付款,微信通知服务端用户支付成功,此时我们来修改订单状态,我们需要做的也就是以下几步:
但是有一个问题,微信什么时候会通知服务端用户支付状态呢?我们注意到,我们统一下单接口有一个参数:
到时微信会自动调用notify_url的链接来查询用户的支付结果,一个URL对应一个controller,用来接收异步通知,我们完成这个功能就可以,但是有一个问题我们需要注意到,那就是通知URL必须为外网可以访问的URL,而我们的项目没有发布,只提供局域网访问,如何获得一个能够供外网访问的域名呢?不要紧,我们接下来学习一个小工具
名词解释:
简单来说内网穿透的目的就是:让外网能访问你本地的应用,例如在外网打开你本地http://127.0.0.1指向的web站点
我们使用的工具为:NatApp,网址:https://natapp.cn/ ,使用起来非常简单
简单使用见官方文档:
@Slf4j
@RestController
@RequestMapping("notify")
public class NotifyController {
@Autowired
private OrderService orderService;
@PostMapping(value = "pay",produces = "application/xml")//声明返回结果为xml类型
public Map<String,String> hello(@RequestBody Map<String,String> result){
//在pom文件中引入了xml解析器
orderService.handleNotify(result);
log.info("[支付回调] 接收微信支付回调, 结果:{}", result);
Map<String,String> msg = new HashMap<>();
msg.put("return_code", "SUCCESS");
msg.put("return_msg", "OK");
return msg;
}
}
public void handleNotify(Map<String, String> result) {
// 1 数据校验
payHelper.isSuccess(result);
// 2 签名校验
payHelper.isValidSign(result);
// 3 金额校验
String totalFeeStr = result.get("total_fee");
String tradeNo = result.get("out_trade_no");
if(StringUtils.isEmpty(totalFeeStr) || StringUtils.isEmpty(tradeNo)){
throw new LyException(ExceptionEnum.INVALID_ORDER_PARAM);
}
Long totalFee = Long.valueOf(totalFeeStr);
Long orderId = Long.valueOf(tradeNo);
Order order = orderMapper.selectByPrimaryKey(orderId);
if(totalFee != /*order.getActualPay()*/ 1){
// 金额不符
throw new LyException(ExceptionEnum.INVALID_ORDER_PARAM);
}
// 4 修改订单状态
OrderStatus status = new OrderStatus();
status.setStatus(OrderStatusEnum.PAYED.value());
status.setOrderId(orderId);
status.setPaymentTime(new Date());
int count = orderStatusMapper.updateByPrimaryKeySelective(status);
if(count != 1){
throw new LyException(ExceptionEnum.UPDATE_ORDER_STATUS_ERROR);
}
log.info("[订单回调], 订单支付成功! 订单编号:{}", orderId);
}
以上工作我们完成以后去页面重新购物并付款,此时发现我们虽然付款成功,查看数据库,数据库中的订单状态也已经改变,但是页面怎么不跳转呢?原因是我们只在后端改了,但是前端并不知道,所以就算付款完成我们前端也依旧不跳转
前端应该是出现了二维码界面之后一直发起查询订单状态的请求,否则查询一次失败了不再进行查询,避免出现我们订单状态有延迟而订单状态也确实发生了改变的情况下,接下来我们来实现查询订单状态的接口
我们查询完成后,把支付交易状态分为三种情况:
我们定义一个枚举来表示:
public enum PayState {
NOT_PAY(0),
SUCCESS(1),
FAIL(2);
PayState(int value) {
this.value = value;
}
int value;
public int getValue() {
return value;
}
}
查询订单状态有两种可能,一种是已支付,一种是未支付,如果是已支付那就说明真的已经支付过了;如果是未支付,那有可能是异步通知有延时,必须去微信支付查询支付状态,我们在payHelper中补充查询的API,该API具体应用场景及接口等详见官方文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_2
public PayState queryPayState(Long orderId) {
try {
// 组织请求参数
Map<String, String> data = new HashMap<>();
// 订单号
data.put("out_trade_no", orderId.toString());
// 查询状态
Map<String, String> result = wxPay.orderQuery(data);
// 校验状态
isSuccess(result);
// 校验签名
isValidSign(result);
// 校验金额
String totalFeeStr = result.get("total_fee");
String tradeNo = result.get("out_trade_no");
if(StringUtils.isEmpty(totalFeeStr) || StringUtils.isEmpty(tradeNo)){
throw new LyException(ExceptionEnum.INVALID_ORDER_PARAM);
}
// 3.1 获取结果中的金额
Long totalFee = Long.valueOf(totalFeeStr);
// 3.2 获取订单金额
Order order = orderMapper.selectByPrimaryKey(orderId);
if(totalFee != /*order.getActualPay()*/ 1){
// 金额不符
throw new LyException(ExceptionEnum.INVALID_ORDER_PARAM);
}
String state = result.get("trade_state");
if(SUCCESS.equals(state)){
// 支付成功
// 修改订单状态
OrderStatus status = new OrderStatus();
status.setStatus(OrderStatusEnum.PAYED.value());
status.setOrderId(orderId);
status.setPaymentTime(new Date());
int count = orderStatusMapper.updateByPrimaryKeySelective(status);
if(count != 1){
throw new LyException(ExceptionEnum.UPDATE_ORDER_STATUS_ERROR);
}
// 返回成功
return PayState.SUCCESS;
}
if("NOTPAY".equals(state) || "USERPAYING".equals(state)){
return PayState.NOT_PAY;
}
return PayState.FAIL;
}catch (Exception e){
return PayState.NOT_PAY;// 并不知道是否支付,再去发起请求申请一次
}
}
@GetMapping("state/{id}")
public ResponseEntity<Integer> queryOrderState(@PathVariable("id")Long orederId){
return ResponseEntity.ok(orderService.queryOrderState(orederId).getValue());
}
public PayState queryOrderState(Long orderId) {
OrderStatus orderStatus = orderStatusMapper.selectByPrimaryKey(orderId);
Integer status = orderStatus.getStatus();
if(!status.equals(OrderStatusEnum.UN_PAY.value())){
return PayState.SUCCESS;// 如果是已支付,则是真的已支付
}
// 如果未支付,但其实不一定是未支付,必须去微信查询支付状态
return payHelper.queryPayState(orderId);
}
我们再次来刷新前台页面测试一次:
支付成功后跳转到支付成功页面:
用户的移动支付端可以收到支付详情:
到此为止,乐优商城的主线业务就全部完成了~~~