乐优商城:笔记(十六):实现微信支付功能

文章目录

  • 1 介绍
  • 2 开发流程
  • 3 下单并生成支付链接
    • 3.1 API说明
    • 3.2 统一下单工具类
    • 3.3 生成预交易链接
      • 3.3.1 controller
      • 3.3.2 service
    • 3.4 生成二维码
  • 4 付款状态
    • 4.1 内网穿透
    • 4.2 接收回调
      • 4.2.1 引入依赖
      • 4.2.2 controller
      • 4.2.3 service
    • 4.3 付款状态
      • 4.3.1 工具类
      • 4.3.2 查询订单
      • 4.3.3 controller
      • 4.3.4 service

1 介绍

微信支付官方文档:https://pay.weixin.qq.com/index.php/core/home/login?return_url=%2F

我们选择的是扫码支付:
乐优商城:笔记(十六):实现微信支付功能_第1张图片
扫码支付有两种模式,我们选择流程二

2 开发流程

流程图:
乐优商城:笔记(十六):实现微信支付功能_第2张图片
这里我们把商户要做的事情总结一下:

  • 1、商户生成订单
  • 2、商户调用微信下单接口,获取预交易的链接
  • 3、商户将链接生成二维码图片,展示给用户;
  • 4、用户支付并确认
  • 5、支付结果通知:
    • 微信异步通知商户支付结果,商户告知微信支付接收情况
    • 商户如果没有收到通知,可以调用接口,查询支付状态
  • 6、如果支付成功,发货,修改订单状态

在前面的业务中,我们已经完成了:

  • 1、生成订单

接下来,我们需要做的是:

  • 2、调用微信接口,生成链接。
  • 3、并且根据链接生成二维码图片
  • 4、支付成功后修改订单状态

3 下单并生成支付链接

3.1 API说明

在微信支付文档中,可以查询得到以下信息:

支付路径

URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder

请求参数

乐优商城:笔记(十六):实现微信支付功能_第3张图片

  • 通知地址
    乐优商城:笔记(十六):实现微信支付功能_第4张图片
  • 在后面的代码中,我们将请求参数分成两部分填充:
    乐优商城:笔记(十六):实现微信支付功能_第5张图片

第一部分如上图所示,第二部分封装到config中,这个config是我们自己写的,实现了官方的接口WXPayConfig:
PayConfig:
乐优商城:笔记(十六):实现微信支付功能_第6张图片
官方的接口WXPayConfig:
乐优商城:笔记(十六):实现微信支付功能_第7张图片

然后在WXPay中的unifiedOrder方法中填充:
乐优商城:笔记(十六):实现微信支付功能_第8张图片
乐优商城:笔记(十六):实现微信支付功能_第9张图片

请求方式

post,源码如下:
乐优商城:笔记(十六):实现微信支付功能_第10张图片

返回结果

乐优商城:笔记(十六):实现微信支付功能_第11张图片
不一定执行了就一定成功,要做 通信和业务是否成功的校验 和 签名是否有效的校验,所以返回结果要做一个判断:
乐优商城:笔记(十六):实现微信支付功能_第12张图片
乐优商城:笔记(十六):实现微信支付功能_第13张图片

3.2 统一下单工具类

将属性定义到yml文件中
yml:
乐优商城:笔记(十六):实现微信支付功能_第14张图片

编写自定义配置,读取属性
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);
    }
}
}

3.3 生成预交易链接

3.3.1 controller

@GetMapping("url/{id}")
public ResponseEntity<String> createPayUrl(@PathVariable("id") Long orderId){
     
    return ResponseEntity.ok(orderService.createPayUrl(orderId));
}

3.3.2 service

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);
}

此时刷新页面,可以看到已经成功产生了URL:
乐优商城:笔记(十六):实现微信支付功能_第15张图片
乐优商城:笔记(十六):实现微信支付功能_第16张图片

3.4 生成二维码

这里我们使用一个生成二维码的JS插件:qrcode,官网:https://github.com/davidshimjs/qrcodejs

我们把这个js脚本引入到项目中:
乐优商城:笔记(十六):实现微信支付功能_第17张图片
然后在页面引用:
乐优商城:笔记(十六):实现微信支付功能_第18张图片
页面定义一个div,用于展示二维码:
乐优商城:笔记(十六):实现微信支付功能_第19张图片
然后获取到付款链接后,根据链接生成二维码:
乐优商城:笔记(十六):实现微信支付功能_第20张图片
刷新页面查看效果:
乐优商城:笔记(十六):实现微信支付功能_第21张图片
此时,客户用手机扫描二维码,可以看到付款页面

4 付款状态

当用户扫描上述二维码完成付款,微信通知服务端用户支付成功,此时我们来修改订单状态,我们需要做的也就是以下几步:
乐优商城:笔记(十六):实现微信支付功能_第22张图片
但是有一个问题,微信什么时候会通知服务端用户支付状态呢?我们注意到,我们统一下单接口有一个参数:
在这里插入图片描述
到时微信会自动调用notify_url的链接来查询用户的支付结果,一个URL对应一个controller,用来接收异步通知,我们完成这个功能就可以,但是有一个问题我们需要注意到,那就是通知URL必须为外网可以访问的URL,而我们的项目没有发布,只提供局域网访问,如何获得一个能够供外网访问的域名呢?不要紧,我们接下来学习一个小工具

4.1 内网穿透

名词解释:
乐优商城:笔记(十六):实现微信支付功能_第23张图片
简单来说内网穿透的目的就是:让外网能访问你本地的应用,例如在外网打开你本地http://127.0.0.1指向的web站点

我们使用的工具为:NatApp,网址:https://natapp.cn/ ,使用起来非常简单
乐优商城:笔记(十六):实现微信支付功能_第24张图片
简单使用见官方文档:
乐优商城:笔记(十六):实现微信支付功能_第25张图片

4.2 接收回调

官方文档:
乐优商城:笔记(十六):实现微信支付功能_第26张图片

通知参数(xml格式):
乐优商城:笔记(十六):实现微信支付功能_第27张图片

返回参数(也是xml格式):
乐优商城:笔记(十六):实现微信支付功能_第28张图片

4.2.1 引入依赖

在pom文件中引入了xml解析器:
在这里插入图片描述

4.2.2 controller

@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;
    }
}

4.2.3 service

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);
}

以上工作我们完成以后去页面重新购物并付款,此时发现我们虽然付款成功,查看数据库,数据库中的订单状态也已经改变,但是页面怎么不跳转呢?原因是我们只在后端改了,但是前端并不知道,所以就算付款完成我们前端也依旧不跳转
乐优商城:笔记(十六):实现微信支付功能_第29张图片
前端应该是出现了二维码界面之后一直发起查询订单状态的请求,否则查询一次失败了不再进行查询,避免出现我们订单状态有延迟而订单状态也确实发生了改变的情况下,接下来我们来实现查询订单状态的接口

4.3 付款状态

我们在前端代码中已经定义了查询的js代码:
乐优商城:笔记(十六):实现微信支付功能_第30张图片

4.3.1 工具类

我们查询完成后,把支付交易状态分为三种情况:

  • 0,通信失败或者未支付,需要重新查询
  • 1,支付成功
  • 2,支付失败

我们定义一个枚举来表示:

public enum PayState {
     
    NOT_PAY(0),
    SUCCESS(1),
    FAIL(2);

    PayState(int value) {
     
        this.value = value;
    }
    
    int value;
    
    public int getValue() {
     
        return value;
    }
}

4.3.2 查询订单

乐优商城:笔记(十六):实现微信支付功能_第31张图片
查询订单状态有两种可能,一种是已支付,一种是未支付,如果是已支付那就说明真的已经支付过了;如果是未支付,那有可能是异步通知有延时,必须去微信支付查询支付状态,我们在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;// 并不知道是否支付,再去发起请求申请一次
    }
}

4.3.3 controller

@GetMapping("state/{id}")
public ResponseEntity<Integer> queryOrderState(@PathVariable("id")Long orederId){
     
    return ResponseEntity.ok(orderService.queryOrderState(orederId).getValue());
}

4.3.4 service

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);
}

我们再次来刷新前台页面测试一次:

点击提交订单跳转到订单支付页面,并生成支付二维码:
乐优商城:笔记(十六):实现微信支付功能_第32张图片

支付成功后跳转到支付成功页面:
乐优商城:笔记(十六):实现微信支付功能_第33张图片
用户的移动支付端可以收到支付详情:
乐优商城:笔记(十六):实现微信支付功能_第34张图片

到此为止,乐优商城的主线业务就全部完成了~~~

你可能感兴趣的:(乐优商城,乐优商城,微信支付)