畅购商城(十二):接入微信扫码支付

好好学习,天天向上

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star

  • 畅购商城(一):环境搭建
  • 畅购商城(二):分布式文件系统FastDFS
  • 畅购商城(三):商品管理
  • 畅购商城(四):Lua、OpenResty、Canal实现广告缓存与同步
  • 畅购商城(五):Elasticsearch实现商品搜索
  • 畅购商城(六):商品搜索
  • 畅购商城(七):Thymeleaf实现静态页
  • 畅购商城(八):微服务网关和JWT令牌
  • 畅购商城(九):Spring Security Oauth2
  • 畅购商城(十):购物车
  • 畅购商城(十一):订单
  • 畅购商城(十二):接入微信支付

支付流程

image

为了实现支付的功能,这里选择接入微信支付。流程就是我们通过订单系统下单,然后订单系统调用支付系统去向微信支付的服务器发送请求,然后获取二维码返回给用户,然后订单系统就开始监听MQ。用户扫码支付后,支付系统将支付状态存进MQ中。订单系统检测到用户已经付钱了,就将订单设为已支付,然后存进MySQL中。可能会因为网络问题导致订单系统获取不到支付状态,所以订单系统会定时向微信支付服务器发送请求去查询订单状态。

微信支付简介

要想接入微信支付,就得有认证过的服务号,这个我没有,所以申请不了。就用黑马提供的账号吧,我试了一下,可以用。

appid(公众账号ID):wx8397f8696b538317
mch_id(商户号):1473426802
key(商户密钥):T6m9iK73b0kn9g5v426MKfHQH7X8rKwb

微信支付的开发文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1

在这个官方的开发文档里面介绍了微信支付相关的API并且提供了SDK。

image

这个SDK的内容不多,只有几个类,我稍微介绍一下:

  • WXPayConfig:这是个抽象类,里面有几个方法,是用来获取核心参数的,比如公众号id,商户号,密钥等,所以在使用的时候要先去实例化这个类将几个重要参数配置进去。

  • WXPay:和订单相关的方法都封装在这个类里面,比如下单,查询订单,取消订单等。里面有个方法fillRequestData(),每次执行下单等操作的时候都会去调用这个方法将WXPayConfig中的几个核心参数封装到请求参数的Map集合里。

  • WXPayRequest:这个是负责请求服务器的,WXPay也都是通过调用这个类中的方法去请求服务器的,执行相应方法的时候,会将WXPay传过来的Map集合转换成XML格式的字符串,然后使用HttpClient向服务器发送请求。没错,微信支付是通过XML进行数据传输的。

  • WXPayUtil:这是个工具类,封装了一些常用方法,比如Map转XML,XML转Map等。

在这个项目中用到的微信的Native支付,也就是扫码支付,有两种模式,我们用到的是模式二。

image

首先在畅购的订单系统中生成订单,然后将一些必要的参数传入到微信支付的后台,然后就会产生一个预支付的订单,将支付链接返回给我们,我们再根据支付链接生成二维码传给用户。用户扫码支付后再将支付结果传到我们的后台,这样整个支付的流程就结束了。

准备工作

介绍完了微信支付后,就来说一下项目中该怎么去集成微信支付。视频中用的是第三方的依赖,我用的是官方的。微信支付的SDK在Maven的远程仓库里是没有的,所以需要自己下载然后手动导入。这里面有两个坑有必要说一下,前面不是提到WXPayConfig是个抽象类么,那么用的时候肯定得去继承才能实例化吧。但是里面的抽象方法都没有权限修饰符,所以默认是包访问权限,我们既然是Maven依赖这个SDK,那么我们写的代码自然不会和它在同一个包下,所以要先在这几个抽象方法前面添加public修饰符。而且,微信提供的sdk文档里还写成了implements抽象类,真搞不懂微信怎么会犯这种错~~~

现在就可以将这个SDK添加到我们本地的Maven仓库里了,在解压后的sdk的根目录下执行mvn install命令。

image

当出现BUILD SUCCESS的字样的时候,就说明已经成功添加到本地的Maven仓库了。这时候第二个坑就来了,如果就这么添加到我们的项目中就有可能会出现Maven依赖冲突:

image

可以看到,出现冲突的包是slf4j-simple,而微信支付sdk恰好依赖了这个包,所以在导入微信支付的时候把这个依赖排除掉即可。


    com.github.wxpay
    wxpay-sdk
    3.0.9
    
        
            org.slf4j
            slf4j-simple
        
    

然后就可以创建支付的工程了。在changgou-service下创建一个Moudle名为changgou-service-wechatpay,然后将微信支付的依赖添加到这个工程下,启动类没啥好说的,配置文件如下:

server:
  port: 18090
spring:
  application:
    name: wechatpay
  main:
    allow-bean-definition-overriding: true
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        timeout:
          #如果enabled设置为false,则请求超时交给ribbon控制
          enabled: true
        isolation:
          strategy: SEMAPHORE

#微信支付信息配置
wechat:
  # 应用id
  app_id: wx8397f8696b538317
  # 商户号id
  mch_id: 1473426802
  # 密钥
  key: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
  # 支付回调地址
  notify_url: http://www.itcast.cn

这里配置了appid,mch_id,key,notify_url这几个参数,用的时候直接读取就可以了。这样支付微服务就搭建好了。

微信支付二维码生成

添加一个Controller层的类WeChatPayController,然后再创建一个入口方法,因为二维码支付叫做Native支付,所以这里就起名为createNative了,接收一个Order参数,视频中用的是Map接收参数。但是既然是创建订单,直接用Order作为参数可读性不是好很多吗。

//创建二维码
@RequestMapping(value = "/create/native")
public Result createNative(@RequestBody Order order){
    Map resultMap = weChatPayService.createNative(order);
    return new Result(true, StatusCode.OK,"创建二维码预付订单成功!",resultMap);
}

写完了Controller就可以写Service层了,视频中是用HttpClient调用微信支付的远程接口,既然微信已经提供了sdk,为什么还要重复造轮子呢?所以我就没和视频中写的一样,而是直接使用微信的sdk,虽然底层用的也是HttpClient。

首先需要将WXPayConfig给实现一下。

public class MyWXPayConfig extends WXPayConfig {

    private String appId;

    private String mchId;

    private String key;

    public MyWXPayConfig(String appId, String mchId, String key) {
        this.appId = appId;
        this.mchId = mchId;
        this.key = key;
    }

    @Override
    public String getAppID() {
        return appId;
    }

    @Override
    public String getMchID() {
        return mchId;
    }

    @Override
    public String getKey() {
        return key;
    }

    @Override
    public InputStream getCertStream() {
        return null;
    }

    @Override
    public int getHttpConnectTimeoutMs() {
        return 8000;
    }

    @Override
    public int getHttpReadTimeoutMs() {
        return 10000;
    }

    @Override
    public IWXPayDomain getWXPayDomain() {
        IWXPayDomain iwxPayDomain = new IWXPayDomain() {
            @Override
            public void report(String domain, long elapsedTimeMillis, Exception ex) {
            }
            @Override
            public DomainInfo getDomain(WXPayConfig config) {
                return new IWXPayDomain.DomainInfo(WXPayConstants.DOMAIN_API, true);
            }
        };
        return iwxPayDomain;
    }

}

然后在WeChatPayServiceImpl编写相应的代码。

@Value("${wechat.appid}")
private String appId;

@Value("${wechat.mch_id}")
private String mcnId;

@Value("${wechat.key}")
private String key;

@Value("${wechat.notify_url}")
private String notifyUrl;

@Override
public Map createNative(Order order) {
    try {
        Map map = new HashMap<>(16);
        map.put("body", "腾讯充值中心-QQ会员充值");    //商品描述
        map.put("out_trade_no", order.getId());      //商户订单号
        map.put("total_fee", String.valueOf((int)(order.getTotalMoney() * 100))); //标价金额,单位为分
        map.put("spbill_create_ip", "127.0.0.1");    //终端IP
        map.put("trade_type", "NATIVE ");    //交易类型,JSAPI -JSAPI支付,NATIVE -Native支付,APP -APP支付

        Map response = wxpay.unifiedOrder(map);
        if (response == null || response.size() == 0) {
            throw new RuntimeException("下单失败");
        }
        return response;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

这里用到了@Value注解将配置文件中的几个参数注入了进来。然后将创建订单的几个参数添加到Map集合中,键的名称在文档中有说明,不能改变,金额的单位是分,我们传过来的是元,所以需要乘100,微信要求金额是整数,所以再强转成int。然后就去创建MyWXPayConfig的实例,将三个参数传入进入,然后去创建WXPay的对象,将config和notifyUrl传入进去,然后调用unifiedOrder()方法把map传入进去就可以创建订单了,其实内部就是把appid这几个参数放在了我们传入的map中,所以在这里放入到map中也是OK的。

image

测试一下,OK了,成功创建了订单,也拿到了二维码的地址,接下来只要把code_url转换成二维码图片就可以了。

如果是做测试,随便在网上找个二维码生成器,然后把code_url复制过去就行了。在项目中是用qrious生成。



二维码入门小demo

  






查询订单状态

有时候可能因为网络原因导致支付状态没有及时返回到我们的服务器中,这个时候就要手动地去查询了,代码很简单,我就不多说了。

//WeChatPayController
@GetMapping(value = "/status/query")
public Result> queryPayStatus(@RequestParam String outTradeNo) {
    Map resultMap = weChatPayService.queryPayStatus(outTradeNo);
    return new Result<>(true, StatusCode.OK,"订单查询成功",resultMap);
}
--------------------------------------------------------------------------------------
//WeChatPayServiceImpl
@Override
public Map queryPayStatus(String outTradeNo) {
    try {
        Map map = new HashMap<>();
        map.put("out_trade_no", outTradeNo);      //商户订单号
        MyWXPayConfig config = new MyWXPayConfig(appId,mcnId, key);
        WXPay wxpay = new WXPay(config,notifyUrl);
        Map response = wxpay.orderQuery(map);
        if (response == null || response.size() == 0) {
            throw new RuntimeException("订单查询失败");
        }
        return response;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

测试一下:

image

支付结果回调通知及RabbitMQ监听

要实现支付结果回调通知,首先得告诉微信支付的服务器我们的支付结果回调通知的地址,因为我们没有公网IP,所以微信服务器没办法主动发消息到我们的服务器,所以可以使用内网穿透。我选择的是uTools的一个内网穿透插件,不用注册账号就可以直接使用,很方便。

image

然后将内网穿透的地址配置到之前配置notify_url的地方就可以了。

# 支付回调地址
notify_url: http://robod123.cn1.utools.club/wechat/pay/notify/url

接下来就该配置RabbitMQ了,首先得在虚拟机中安装RabbitMQ

docker pull docker.io/rabbitmq:3.7-management           # 下载rabbitmq的镜像
docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 4b23cfb64730  # 安装rabbitmq,最后面的id是docker images查出来的
docker exec -it rabbitmq /bin/bash                      # 进入到rabbitmq中
rabbitmqctl add_user root 123456                        # 添加一个名为root的用户,密码为123456
rabbitmqctl set_permissions -p / root ".*" ".*" ".*"    # 赋予root用户所有权限
rabbitmqctl set_user_tags root administrator            # 赋予root用户administrator角色

因为支付微服务和订单微服务都用到了RabbitMQ,所以在这两个微服务中添加mq的依赖


    org.springframework.boot
    spring-boot-starter-amqp

然后在这两个微服务中添加rabbitmq的配置信息:

spring:         # 配置rabbitmq的ip和端口
  rabbitmq:
    host: 192.168.31.200
    port: 5672
    username: root
    password: 123456

接着就去添加exchange和queue了,视频中是用程序自动创建的,但是我遇到了一个问题,就是订单微服务启动的时候报了一个错,导致服务启动不了:

org.springframework.amqp.rabbit.listener.QueuesNotAvailableException: Cannot prepare queue for listener. Either the queue doesn't exist or the broker will not allow us to use it.

所以我就在网页端手动创建exchange和queue了,并指定routing key将exchange和queue绑定起来。

image

配置的部分到这里就已经完成了,接下来就可以去写代码了,为了方便理解,我画了一张流程图:

image

代码我就不去解释了,直接贴了,对照着这张流程图应该就能看懂了。视频中提到的两个小作业我也写了。

WeChatPayController

/**
 * 支付结果回调通知
 * @param request
 * @return
 * @throws Exception
 */
@RequestMapping("/notify/url")
public String notifyUrl(HttpServletRequest request) throws Exception {
    ServletInputStream inputStream = request.getInputStream();
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int len = 0;
    while ((len = inputStream.read(buffer))!=-1) {
        outputStream.write(buffer,0,len);
    }
    String xmlString = outputStream.toString("UTF-8");

    //将java对象转换成amqp消息发送出去,调用的是send方法
    rabbitTemplate.convertAndSend("exchange.order","routing.order", xmlString);

    return  "";
}

OrderMessageListener

@Component
@RabbitListener(queues = {"queue.order"})
public class OrderMessageListener {

    private final static String SUCCESS = "SUCCESS";

    @Autowired
    private OrderService orderService;

    @Autowired
    private WeChatPayFeign weChatPayFeign;

    /**
     * 监听mq的消息
     * @param message xml格式的消息
     */
    @RabbitHandler
    public void getMessage(String message) throws Exception {
        Map map = WXPayUtil.xmlToMap(message);
        String returnCode = map.getOrDefault("return_code","");     //返回状态码
        String resultCode = map.getOrDefault("result_code","");     //业务结果
        if (SUCCESS.equals(returnCode)) {   //支付成功,修改订单状态
            String outTradeNo = map.get("out_trade_no");    //商户订单号
            if (! SUCCESS.equals(resultCode)) { //交易失败,关闭订单,从数据库中将订单状态修改为支付失败,回滚库存
                Map closeResult = weChatPayFeign.closeOrder(outTradeNo).getData();  //关闭订单时服务器返回的数据
                //如果错误代码为ORDERPAID则说明订单已经支付,当作正常订单处理,反之 回滚库存
                if (!("FAIL".equals(closeResult.get("result_code")) && "ORDERPAID".equals(closeResult.get("err_code")))) {
                    orderService.deleteOrder(outTradeNo);
                    return;
                }
            }
            String transactionId = map.get("transaction_id");   //微信支付订单号
            String timeEnd = map.get("time_end");               //支付完成时间
            orderService.updateStatus(outTradeNo,timeEnd,transactionId);
        }
    }

}

OrderServiceImpl

@Override
public void updateStatus(String outTradeNo,String timeEnd,String transactionId) {
    Order order = orderMapper.findById(outTradeNo);
    LocalDateTime payTime = LocalDateTime.parse(timeEnd, DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));

    order.setPayStatus("1");                //支付状态修改为1表示已支付
    order.setTransactionId(transactionId);  //交易流水号
    order.setPayTime(payTime);              //交易时间

    orderMapper.updateByPrimaryKey(order);
}

@Override
public void deleteOrder(String outTradeNo) {
    Order order = orderMapper.findById(outTradeNo);
    LocalDateTime time = LocalDateTime.now();
    //修改状态
    order.setPayStatus("2");    //交易失败
    order.setUpdateTime(time);
    //提交到数据库
    orderMapper.updateByPrimaryKey(order);
    //回滚库存
    List orderItems = orderItemMapper.findByOrderId(order.getId());
    List skuIds = new ArrayList<>();
    for (OrderItem orderItem : orderItems) {
        skuIds.add(orderItem.getSkuId());
    }
    List skuList = skuFeign.findBySkuIds(order.getSkuIds()).getData(); //数据库中对应的sku集合
    Map skuMap = skuList.stream().collect(Collectors.toMap(Sku::getId, a -> a));
    for (OrderItem orderItem : orderItems) {
        Sku sku = skuMap.get(orderItem.getSkuId());
        sku.setNum(sku.getNum()+orderItem.getNum());    //加库存
    }
    skuFeign.updateMap(skuMap);
}

定时处理订单状态

当创建一个订单后,可能用户并不会去支付,或者是因为网络的原因导致我们获取不到用户支付的结果。所以可以采用定时处理的方式,具体的操作就是下单成功后就往mq的队列1中发送一条消息,设置30分钟过期,过期后将消息发送给队列2,然后我们监听队列2。这样一旦监听到了队列2的消息,则说明离下单已经过去了30分钟,这时候我们去查询一下订单状态,如果是已支付就不去管它,要是未支付的话就通知微信服务器关闭订单,然后从数据库中删除订单。为了方便理解,我画了一张流程图:

image

代码挺简单的,首先将添加一个配置类去把队列创建出来,直接在web页面上配置也是OK的,不过上一节是在web页面配置的,这个就在程序中添加吧。

@Configuration
public class MqConfig {

    //队列1,延时队列,消息过期后发送给队列2
    @Bean
    public Queue orderDelayQueue() {
        return QueueBuilder
                .durable("orderDelayQueue")
                //orderDelayQueue队列信息会过期,过期之后,进入到死信队列,死信队列数据绑定到其他交换机
                .withArgument("x-dead-letter-exchange","orderListenerExchange")
                .withArgument("x-dead-letter-routing-key","orderListenerRoutingKey") //绑定指定的routing-key
                .build();
    }

    //队列2
    @Bean(name = "orderListenerQueue")  //名称不写默认就是方法名
    public Queue orderListenerQueue() {
        return new Queue("orderListenerQueue",true);
    }

    //创建交换机
    @Bean
    public Exchange orderListenerExchange() {
        return new DirectExchange("orderListenerExchange");
    }

    //队列queue2绑定exchange
    @Bean
    public Binding orderListenerBinding(Queue orderListenerQueue,Exchange orderListenerExchange) {
        return BindingBuilder
                .bind(orderListenerQueue)
                .to(orderListenerExchange)
                .with("orderListenerRoutingKey")
                .noargs();
    }

}

在创建队列1的时候,配置了将死信队列的数据绑定到队列2的交换机上,这样数据过期后就会被发送到队列2中。

然后在创建订单的时候将订单号发送到队列1中

//OrderServiceImpl
@Override
public synchronized void add(Order order) {
    order.setId(String.valueOf(idWorker.nextId()));
    …………
    rabbitTemplate.convertAndSend("orderDelayQueue", (Object)order.getId(), new MessagePostProcessor() {
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            message.getMessageProperties().setExpiration("30*60*1000");   //定时30min后过时
            return message;
        }
    });
}

监听队列2然后执行相应的操作

@Component
@RabbitListener(queues = "orderListenerQueue")
public class DelayMessageQueueListener {

    @Autowired
    private OrderService orderService;

    @Autowired
    private WeChatPayFeign weChatPayFeign;

    @RabbitHandler
    public void getMessage(String orderId) throws Exception {
        Order order = orderService.findById(orderId);
        if ("0".equals(order.getPayStatus())) {
            //0表示未支付,通知微信服务器取消订单,从数据库中删除订单,回滚库存
            Map closeResult = weChatPayFeign.closeOrder(orderId).getData();
            //如果错误代码为ORDERPAID则说明订单已经支付,当作正常订单处理,反之 回滚库存
            if (!("FAIL".equals(closeResult.get("result_code")) && "ORDERPAID".equals(closeResult.get("err_code")))) {
                orderService.deleteOrder(orderId);
            }
        }
    }

}

OK,这样就可以定时去处理订单数据了。

总结

这篇文章主要就是实现了微信扫码支付,因为微信提供的sdk有点问题,还是花了一些时间的,不过最后还是完成了。然后又实现了支付结果回调通知和rabbitmq监听的功能,用到了内网穿透。最后使用两个队列来实现了定时处理订单。还有一个问题就是我没系统地学过RabbitMQ,所以对于很多操作都是一知半解的,都是参考着视频中讲的然后再自己摸索摸索web页面的配置,后期我会系统地学习一下RabbitMQ然后再总结出一篇文章。

如果觉得我写的还可以,请不要吝啬你的

代码:https://github.com/RobodLee/changgou

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star

你可能感兴趣的:(畅购商城(十二):接入微信扫码支付)